* Added Element <-> Document type switch validation * Apply HasElementconfigured to block grid and block list Fix smalle bug + optimization * Moved some of the logic into warnings trough notifcationhandlers and eventmessages * Cleanup * Update openApi spec (merge changes) * Add IsElement check between parent and child on creation * Typos * Transformed HasElementConfigured into HasElementConfigured * Typo Co-authored-by: Kenn Jacobsen <kja@umbraco.dk> * IsElement Validation refactor Moved validation logic regarding doctype IsElement switch into its own service as it will be consumed by more things down the line * commit missing services... * Naming improvements * Bugfix * First batch of integration tests for ElementSwitchValidator * More integration tests! * Little reformatting * Changed the default values of block based configuration to match expected values. --------- Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
359 lines
15 KiB
C#
359 lines
15 KiB
C#
using NUnit.Framework;
|
|
using Umbraco.Cms.Core;
|
|
using Umbraco.Cms.Core.Extensions;
|
|
using Umbraco.Cms.Core.Models;
|
|
using Umbraco.Cms.Core.PropertyEditors;
|
|
using Umbraco.Cms.Core.Services;
|
|
using Umbraco.Cms.Core.Services.ContentTypeEditing;
|
|
using Umbraco.Cms.Tests.Common.Attributes;
|
|
using Umbraco.Cms.Tests.Common.Builders;
|
|
using Umbraco.Cms.Tests.Common.Builders.Extensions;
|
|
using Umbraco.Cms.Tests.Common.Testing;
|
|
using Umbraco.Cms.Tests.Integration.Testing;
|
|
|
|
namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services;
|
|
|
|
[TestFixture]
|
|
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
|
|
public class ElementSwitchValidatorTests : UmbracoIntegrationTest
|
|
{
|
|
private IElementSwitchValidator ElementSwitchValidator => GetRequiredService<IElementSwitchValidator>();
|
|
|
|
private IContentTypeService ContentTypeService => GetRequiredService<IContentTypeService>();
|
|
|
|
private IContentService ContentService => GetRequiredService<IContentService>();
|
|
|
|
private IDataTypeService DataTypeService => GetRequiredService<IDataTypeService>();
|
|
|
|
[TestCase(new[] { true }, 0, true, true, TestName = "E=>E No Ancestor or children")]
|
|
[TestCase(new[] { false }, 0, false, true, TestName = "D=>D No Ancestor or children")]
|
|
[TestCase(new[] { true }, 0, false, true, TestName = "E=>D No Ancestor or children")]
|
|
[TestCase(new[] { false }, 0, true, true, TestName = "D=>E No Ancestor or children")]
|
|
[TestCase(new[] { true, true }, 1, true, true, TestName = "E Valid Parent")]
|
|
[TestCase(new[] { true, true }, 0, true, true, TestName = "E Valid Child")]
|
|
[TestCase(new[] { false, false }, 1, false, true, TestName = "D Valid Parent")]
|
|
[TestCase(new[] { false, false }, 0, false, true, TestName = "D Valid Child")]
|
|
[TestCase(new[] { false, false }, 1, true, false, TestName = "E InValid Parent")]
|
|
[TestCase(new[] { false, false }, 0, true, true, TestName = "E InValid Child")]
|
|
[TestCase(new[] { true, true }, 1, false, false, TestName = "D InValid Parent")]
|
|
[TestCase(new[] { true, true }, 0, false, true, TestName = "D InValid Child")]
|
|
[TestCase(new[] { true, false, false, true, false }, 2, true, false,
|
|
TestName = "D=>E InValid Child, Invalid Parent")]
|
|
[TestCase(new[] { false, true, false, true, false }, 2, true, false,
|
|
TestName = "D=>E InValid Child, Invalid Ancestor")]
|
|
[TestCase(new[] { true, false, false, true, true }, 2, true, false,
|
|
TestName = "D=>E Valid Children, Invalid Parent")]
|
|
[TestCase(new[] { false, true, false, true, true }, 2, true, false,
|
|
TestName = "D=>E Valid Children, Invalid Ancestor")]
|
|
[TestCase(new[] { false, false, false, false, false }, 2, true, false, TestName = "D=>E mismatch")]
|
|
[TestCase(new[] { false, false, true, false, false }, 2, false, true, TestName = "D=>E correction")]
|
|
[TestCase(new[] { true, true, true, true, true }, 2, false, false, TestName = "E=>D mismatch")]
|
|
[TestCase(new[] { true, true, false, true, true }, 2, true, true, TestName = "E=>D correction")]
|
|
[LongRunning]
|
|
public async Task AncestorsAreAligned(
|
|
bool[] isElementDoctypeChain,
|
|
int itemToTestIndex,
|
|
bool itemToTestNewIsElementValue,
|
|
bool validationShouldPass)
|
|
{
|
|
// Arrange
|
|
IContentType? parentItem = null;
|
|
IContentType? itemToTest = null;
|
|
for (var index = 0; index < isElementDoctypeChain.Length; index++)
|
|
{
|
|
var itemIsElement = isElementDoctypeChain[index];
|
|
var builder = new ContentTypeBuilder()
|
|
.WithIsElement(itemIsElement);
|
|
if (parentItem is not null)
|
|
{
|
|
builder.WithParentContentType(parentItem);
|
|
}
|
|
|
|
var contentType = builder.Build();
|
|
await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey);
|
|
parentItem = contentType;
|
|
if (index == itemToTestIndex)
|
|
{
|
|
itemToTest = contentType;
|
|
}
|
|
}
|
|
|
|
// Act
|
|
itemToTest!.IsElement = itemToTestNewIsElementValue;
|
|
var result = await ElementSwitchValidator.AncestorsAreAlignedAsync(itemToTest);
|
|
|
|
// Assert
|
|
Assert.AreEqual(result, validationShouldPass);
|
|
}
|
|
|
|
[TestCase(new[] { true }, 0, true, true, TestName = "E=>E No Ancestor or children")]
|
|
[TestCase(new[] { false }, 0, false, true, TestName = "D=>D No Ancestor or children")]
|
|
[TestCase(new[] { true }, 0, false, true, TestName = "E=>D No Ancestor or children")]
|
|
[TestCase(new[] { false }, 0, true, true, TestName = "D=>E No Ancestor or children")]
|
|
[TestCase(new[] { true, true }, 1, true, true, TestName = "E Valid Parent")]
|
|
[TestCase(new[] { true, true }, 0, true, true, TestName = "E Valid Child")]
|
|
[TestCase(new[] { false, false }, 1, false, true, TestName = "D Valid Parent")]
|
|
[TestCase(new[] { false, false }, 0, false, true, TestName = "D Valid Child")]
|
|
[TestCase(new[] { false, false }, 1, true, true, TestName = "E InValid Parent")]
|
|
[TestCase(new[] { false, false }, 0, true, false, TestName = "E InValid Child")]
|
|
[TestCase(new[] { true, true }, 1, false, true, TestName = "D InValid Parent")]
|
|
[TestCase(new[] { true, true }, 0, false, false, TestName = "D InValid Child")]
|
|
[TestCase(new[] { true, false, false, true, false }, 2, true, false,
|
|
TestName = "D=>E InValid Child, Invalid Parent")]
|
|
[TestCase(new[] { false, true, false, true, false }, 2, true, false,
|
|
TestName = "D=>E InValid Child, Invalid Ancestor")]
|
|
[TestCase(new[] { true, false, false, true, true }, 2, true, true,
|
|
TestName = "D=>E Valid Children, Invalid Parent")]
|
|
[TestCase(new[] { false, true, false, true, true }, 2, true, true,
|
|
TestName = "D=>E Valid Children, Invalid Ancestor")]
|
|
[TestCase(new[] { false, false, false, false, false }, 2, true, false, TestName = "D=>E mismatch")]
|
|
[TestCase(new[] { false, false, true, false, false }, 2, false, true, TestName = "D=>E correction")]
|
|
[TestCase(new[] { true, true, true, true, true }, 2, false, false, TestName = "E=>D mismatch")]
|
|
[TestCase(new[] { true, true, false, true, true }, 2, true, true, TestName = "E=>D correction")]
|
|
[LongRunning]
|
|
public async Task DescendantsAreAligned(
|
|
bool[] isElementDoctypeChain,
|
|
int itemToTestIndex,
|
|
bool itemToTestNewIsElementValue,
|
|
bool validationShouldPass)
|
|
{
|
|
// Arrange
|
|
IContentType? parentItem = null;
|
|
IContentType? itemToTest = null;
|
|
for (var index = 0; index < isElementDoctypeChain.Length; index++)
|
|
{
|
|
var itemIsElement = isElementDoctypeChain[index];
|
|
var builder = new ContentTypeBuilder()
|
|
.WithIsElement(itemIsElement);
|
|
if (parentItem is not null)
|
|
{
|
|
builder.WithParentContentType(parentItem);
|
|
}
|
|
|
|
var contentType = builder.Build();
|
|
await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey);
|
|
parentItem = contentType;
|
|
if (index == itemToTestIndex)
|
|
{
|
|
itemToTest = contentType;
|
|
}
|
|
}
|
|
|
|
// Act
|
|
itemToTest!.IsElement = itemToTestNewIsElementValue;
|
|
var result = await ElementSwitchValidator.DescendantsAreAlignedAsync(itemToTest);
|
|
|
|
// Assert
|
|
Assert.AreEqual(result, validationShouldPass);
|
|
}
|
|
|
|
[TestCase(0, true, TestName = "No Content")]
|
|
[TestCase(1, false, TestName = "One Content Item")]
|
|
[TestCase(5, false, TestName = "Many Content Items")]
|
|
public async Task DocumentToElementHasNoContent(int amountOfDocumentsCreated, bool validationShouldPass)
|
|
{
|
|
// Arrange
|
|
var contentType = await SetupContentType(false);
|
|
|
|
for (int i = 0; i < amountOfDocumentsCreated; i++)
|
|
{
|
|
var contentBuilder = new ContentBuilder().WithContentType(contentType);
|
|
var content = contentBuilder.Build();
|
|
ContentService.Save(content);
|
|
}
|
|
|
|
// Act
|
|
contentType.IsElement = true;
|
|
var result = await ElementSwitchValidator.DocumentToElementHasNoContentAsync(contentType);
|
|
|
|
// Assert
|
|
Assert.AreEqual(result, validationShouldPass);
|
|
}
|
|
|
|
// Since the full permutation table would result in 64 tests and more block editors might be added later,
|
|
// we will at least test each single failure and a few combinations
|
|
// used in none
|
|
[TestCase(false, false, false, false, false, false, true)]
|
|
// used in one
|
|
[TestCase(true, false, false, false, false, false, false)]
|
|
[TestCase(false, true, false, false, false, false, false)]
|
|
[TestCase(false, false, true, false, false, false, false)]
|
|
[TestCase(false, false, false, true, false, false, false)]
|
|
[TestCase(false, false, false, false, true, false, false)]
|
|
[TestCase(false, false, false, false, false, true, false)]
|
|
// used in selection and setting
|
|
[TestCase(true, true, false, false, false, false, false)]
|
|
// used in 2 selections
|
|
[TestCase(true, false, true, false, false, false, false)]
|
|
// used in 2 settings
|
|
[TestCase(false, true, false, false, false, true, false)]
|
|
// used in all
|
|
[TestCase(true, true, true, true, true, true, false)]
|
|
public async Task ElementToDocumentNotUsedInBlockStructures(
|
|
bool isUsedInBlockList,
|
|
bool isUsedInBlockListBlockSetting,
|
|
bool isUsedInBlockGrid,
|
|
bool isUsedInBlockGridBlockSetting,
|
|
bool isUsedInRte,
|
|
bool isUsedInRteBlockSetting,
|
|
bool validationShouldPass)
|
|
{
|
|
// Arrange
|
|
var elementType = await SetupContentType(true);
|
|
|
|
var otherElementType = await SetupContentType(true);
|
|
|
|
if (isUsedInBlockList)
|
|
{
|
|
await SetupDataType(Constants.PropertyEditors.Aliases.BlockList, elementType.Key, null);
|
|
}
|
|
|
|
if (isUsedInBlockListBlockSetting)
|
|
{
|
|
await SetupDataType(Constants.PropertyEditors.Aliases.BlockList, otherElementType.Key, elementType.Key);
|
|
}
|
|
|
|
if (isUsedInBlockGrid)
|
|
{
|
|
await SetupDataType(Constants.PropertyEditors.Aliases.BlockGrid, elementType.Key, null);
|
|
}
|
|
|
|
if (isUsedInBlockGridBlockSetting)
|
|
{
|
|
await SetupDataType(Constants.PropertyEditors.Aliases.BlockGrid, otherElementType.Key, elementType.Key);
|
|
}
|
|
|
|
if (isUsedInRte)
|
|
{
|
|
await SetupDataType(Constants.PropertyEditors.Aliases.RichText, elementType.Key, null);
|
|
}
|
|
|
|
if (isUsedInRteBlockSetting)
|
|
{
|
|
await SetupDataType(Constants.PropertyEditors.Aliases.RichText, otherElementType.Key, elementType.Key);
|
|
}
|
|
|
|
// Act
|
|
var result = await ElementSwitchValidator.ElementToDocumentNotUsedInBlockStructuresAsync(elementType);
|
|
|
|
// Assert
|
|
Assert.AreEqual(result, validationShouldPass);
|
|
}
|
|
|
|
private async Task<IContentType> SetupContentType(bool isElement)
|
|
{
|
|
var typeBuilder = new ContentTypeBuilder()
|
|
.WithIsElement(isElement);
|
|
var contentType = typeBuilder.Build();
|
|
await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey);
|
|
return contentType;
|
|
}
|
|
|
|
// Constants.PropertyEditors.Aliases.BlockGrid
|
|
private async Task SetupDataType(
|
|
string editorAlias,
|
|
Guid elementKey,
|
|
Guid? elementSettingKey)
|
|
{
|
|
Dictionary<string, object> configuration;
|
|
switch (editorAlias)
|
|
{
|
|
case Constants.PropertyEditors.Aliases.BlockGrid:
|
|
configuration = GetBlockGridBaseConfiguration();
|
|
break;
|
|
case Constants.PropertyEditors.Aliases.RichText:
|
|
configuration = GetRteBaseConfiguration();
|
|
break;
|
|
default:
|
|
configuration = new Dictionary<string, object>();
|
|
break;
|
|
}
|
|
|
|
SetBlockConfiguration(
|
|
configuration,
|
|
elementKey,
|
|
elementSettingKey,
|
|
editorAlias == Constants.PropertyEditors.Aliases.BlockGrid ? true : null);
|
|
|
|
|
|
var dataTypeBuilder = new DataTypeBuilder()
|
|
.WithId(0)
|
|
.WithDatabaseType(ValueStorageType.Nvarchar)
|
|
.AddEditor()
|
|
.WithAlias(editorAlias);
|
|
|
|
switch (editorAlias)
|
|
{
|
|
case Constants.PropertyEditors.Aliases.BlockGrid:
|
|
dataTypeBuilder.WithConfigurationEditor(
|
|
new BlockGridConfigurationEditor(IOHelper) { DefaultConfiguration = configuration });
|
|
break;
|
|
case Constants.PropertyEditors.Aliases.BlockList:
|
|
dataTypeBuilder.WithConfigurationEditor(
|
|
new BlockListConfigurationEditor(IOHelper) { DefaultConfiguration = configuration });
|
|
break;
|
|
case Constants.PropertyEditors.Aliases.RichText:
|
|
dataTypeBuilder.WithConfigurationEditor(
|
|
new RichTextConfigurationEditor(IOHelper) { DefaultConfiguration = configuration });
|
|
break;
|
|
}
|
|
|
|
var dataType = dataTypeBuilder.Done()
|
|
.Build();
|
|
|
|
await DataTypeService.CreateAsync(dataType, Constants.Security.SuperUserKey);
|
|
}
|
|
|
|
private void SetBlockConfiguration(
|
|
Dictionary<string, object> dictionary,
|
|
Guid? elementKey,
|
|
Guid? elementSettingKey,
|
|
bool? allowAtRoot)
|
|
{
|
|
if (elementKey is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
dictionary["blocks"] = new[] { BuildBlockConfiguration(elementKey.Value, elementSettingKey, allowAtRoot) };
|
|
}
|
|
|
|
private Dictionary<string, object> GetBlockGridBaseConfiguration()
|
|
=> new Dictionary<string, object> { ["gridColumns"] = 12 };
|
|
|
|
private Dictionary<string, object> GetRteBaseConfiguration()
|
|
{
|
|
var dictionary = new Dictionary<string, object>
|
|
{
|
|
["maxImageSize"] = 500,
|
|
["mode"] = "Classic",
|
|
["toolbar"] = new[]
|
|
{
|
|
"styles", "bold", "italic", "alignleft", "aligncenter", "alignright", "bullist", "numlist",
|
|
"outdent", "indent", "sourcecode", "link", "umbmediapicker", "umbembeddialog"
|
|
},
|
|
};
|
|
return dictionary;
|
|
}
|
|
|
|
private Dictionary<string, object> BuildBlockConfiguration(
|
|
Guid? elementKey,
|
|
Guid? elementSettingKey,
|
|
bool? allowAtRoot)
|
|
{
|
|
var dictionary = new Dictionary<string, object>();
|
|
if (allowAtRoot is not null)
|
|
{
|
|
dictionary.Add("allowAtRoot", allowAtRoot.Value);
|
|
}
|
|
|
|
dictionary.Add("contentElementTypeKey", elementKey.ToString());
|
|
if (elementSettingKey is not null)
|
|
{
|
|
dictionary.Add("settingsElementTypeKey", elementSettingKey.ToString());
|
|
}
|
|
|
|
return dictionary;
|
|
}
|
|
}
|