Files
Umbraco-CMS/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentValidationServiceTests.cs
Kenn Jacobsen aaf7075313 Property level validation for Management API (#15644)
* Property level validation for content - initial implementation

* Always succeed create/update regardless of property level validation errors

* Move old complex editor validation classes to Web.BackOffice so they will be deleted

* Include operation status and property validation errors in ProblemDetails

* Refactor property validation to its own service(s)

* Make the problem details builder a little more generic towards extensions

* Validation for item and branch publish

* Moved malplaced test

* Get rid of a TODO

* Integration tests for content validation service

* Simplify validation service

* Add missing response types to create and update for document and media

* Remove test that no longer applies

* Use "errors" for model validation errors (property validation errors)

* Split create/update and validation into their own endpoints

* Fix forward merge

* Correct wrong assumption for missing properties

* Remove localization from validation error messages - decreases dependencies, adds a lot of obsolete constructors

* Reuse existing validation service + support custom error messages

* Fix merge errors

* Review comments
2024-01-31 10:40:58 +01:00

375 lines
17 KiB
C#

using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Serialization;
using Umbraco.Cms.Tests.Common.Builders;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Testing;
namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services;
[TestFixture]
[UmbracoTest(
Database = UmbracoTestOptions.Database.NewSchemaPerTest,
PublishedRepositoryEvents = true,
WithApplication = true)]
public class ContentValidationServiceTests : UmbracoIntegrationTestWithContent
{
private IContentValidationService ContentValidationService => GetRequiredService<IContentValidationService>();
protected override void ConfigureTestServices(IServiceCollection services)
{
// block list requires System.Text.Json as serializer - currently we still perform fallback to Json.NET in tests
services.AddSingleton<IJsonSerializer, SystemTextJsonSerializer>();
services.AddSingleton<IConfigurationEditorJsonSerializer, SystemTextConfigurationEditorJsonSerializer>();
}
[Test]
public async Task Can_Validate_Block_List_Nested_In_Block_List()
{
var setup = await SetupBlockListTest();
var validationResult = await ContentValidationService.ValidatePropertiesAsync(
new ContentCreateModel
{
ContentTypeKey = setup.DocumentType.Key,
InvariantName = "Test Document",
InvariantProperties = new[]
{
new PropertyValueModel
{
Alias = "blocks",
Value = $$"""
{
"layout": {
"Umbraco.BlockList": [{
"contentUdi": "umb://element/9addc377c02c4db088c273b933704f7b",
"settingsUdi": "umb://element/65db1ecd78e041a584f07296123a0a73"
}, {
"contentUdi": "umb://element/3af93b5b5e404c64b1422564309fc4c7",
"settingsUdi": "umb://element/efb9583ce67043f282fb2a0cb0f3e736"
}
]
},
"contentData": [{
"contentTypeKey": "{{setup.ElementType.Key}}",
"udi": "umb://element/9addc377c02c4db088c273b933704f7b",
"title": "Valid root content",
"blocks": {
"layout": {
"Umbraco.BlockList": [{
"contentUdi": "umb://element/f36cebfad03b44519e604bf32c5b1e2f",
"settingsUdi": "umb://element/c9129a4671bb4b4e8f0ad525ad4a5de3"
}, {
"contentUdi": "umb://element/b8173e4a0618475c8277c3c6af68bee6",
"settingsUdi": "umb://element/77f7ea3507664395bf7f0c9df04530f7"
}
]
},
"contentData": [{
"contentTypeKey": "{{setup.ElementType.Key}}",
"udi": "umb://element/f36cebfad03b44519e604bf32c5b1e2f",
"title": "Invalid nested content"
}, {
"contentTypeKey": "{{setup.ElementType.Key}}",
"udi": "umb://element/b8173e4a0618475c8277c3c6af68bee6",
"title": "Valid nested content"
}
],
"settingsData": [{
"contentTypeKey": "{{setup.ElementType.Key}}",
"udi": "umb://element/c9129a4671bb4b4e8f0ad525ad4a5de3",
"title": "Valid nested setting"
}, {
"contentTypeKey": "{{setup.ElementType.Key}}",
"udi": "umb://element/77f7ea3507664395bf7f0c9df04530f7",
"title": "Invalid nested setting"
}
]
}
}, {
"contentTypeKey": "{{setup.ElementType.Key}}",
"udi": "umb://element/3af93b5b5e404c64b1422564309fc4c7",
"title": "Invalid root content"
}
],
"settingsData": [{
"contentTypeKey": "{{setup.ElementType.Key}}",
"udi": "umb://element/65db1ecd78e041a584f07296123a0a73",
"title": "Invalid root setting"
}, {
"contentTypeKey": "{{setup.ElementType.Key}}",
"udi": "umb://element/efb9583ce67043f282fb2a0cb0f3e736",
"title": "Valid root setting"
}
]
}
"""
}
}
},
setup.DocumentType);
Assert.AreEqual(4, validationResult.ValidationErrors.Count());
Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(r => r.Alias == "blocks" && r.JsonPath == ".contentData[0].blocks.contentData[0].title"));
Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(r => r.Alias == "blocks" && r.JsonPath == ".contentData[0].blocks.settingsData[1].title"));
Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(r => r.Alias == "blocks" && r.JsonPath == ".contentData[1].title"));
Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(r => r.Alias == "blocks" && r.JsonPath == ".settingsData[0].title"));
}
[TestCase(true)]
[TestCase(false)]
public async Task Can_Validate_RegEx_For_Simple_Property_On_Document(bool valid)
{
var contentType = SetupSimpleTest();
var validationResult = await ContentValidationService.ValidatePropertiesAsync(
new ContentCreateModel
{
ContentTypeKey = contentType.Key,
InvariantName = "Test Document",
InvariantProperties = new[]
{
new PropertyValueModel
{
Alias = "title",
Value = "The title value"
},
new PropertyValueModel
{
Alias = "author",
Value = valid ? "Valid value" : "Invalid value"
}
}
},
contentType);
if (valid)
{
Assert.IsEmpty(validationResult.ValidationErrors);
}
else
{
Assert.AreEqual(1, validationResult.ValidationErrors.Count());
Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(r => r.Alias == "author" && r.JsonPath == string.Empty));
}
}
[TestCase(true)]
[TestCase(false)]
public async Task Can_Validate_Mandatory_For_Simple_Property_On_Document(bool valid)
{
var contentType = SetupSimpleTest();
var validationResult = await ContentValidationService.ValidatePropertiesAsync(
new ContentCreateModel
{
ContentTypeKey = contentType.Key,
InvariantName = "Test Document",
InvariantProperties = new[]
{
new PropertyValueModel
{
Alias = "title",
Value = valid ? "A value" : string.Empty
},
new PropertyValueModel
{
Alias = "author",
Value = "Valid value"
}
}
},
contentType);
if (valid)
{
Assert.IsEmpty(validationResult.ValidationErrors);
}
else
{
Assert.AreEqual(1, validationResult.ValidationErrors.Count());
Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(r => r.Alias == "title" && r.JsonPath == string.Empty));
}
}
[Test]
public async Task Can_Validate_Mandatory_For_Property_Not_Present_In_Document()
{
var contentType = SetupSimpleTest();
var validationResult = await ContentValidationService.ValidatePropertiesAsync(
new ContentCreateModel
{
ContentTypeKey = contentType.Key,
InvariantName = "Test Document",
InvariantProperties = new[]
{
new PropertyValueModel
{
Alias = "author",
Value = "Valid value"
}
}
},
contentType);
Assert.AreEqual(1, validationResult.ValidationErrors.Count());
Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(r => r.Alias == "title" && r.JsonPath == string.Empty));
}
[Test]
public async Task Uses_Localizaton_Keys_For_Validation_Error_Messages()
{
var contentType = SetupSimpleTest();
var validationResult = await ContentValidationService.ValidatePropertiesAsync(
new ContentCreateModel
{
ContentTypeKey = contentType.Key,
InvariantName = "Test Document",
InvariantProperties = new[]
{
new PropertyValueModel
{
Alias = "author",
Value = "Invalid value"
}
}
},
contentType);
Assert.AreEqual(2, validationResult.ValidationErrors.Count());
Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(
r => r.Alias == "title"
&& r.ErrorMessages.Length == 1
&& r.ErrorMessages.First() == Constants.Validation.ErrorMessages.Properties.Missing));
Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(
r => r.Alias == "author"
&& r.ErrorMessages.Length == 1
&& r.ErrorMessages.First() == Constants.Validation.ErrorMessages.Properties.PatternMismatch));
}
[Test]
public async Task Custom_Validation_Error_Messages_Replaces_Localizaton_Keys()
{
var contentType = SetupSimpleTest();
contentType.PropertyTypes.First(pt => pt.Alias == "title").MandatoryMessage = "Custom mandatory message";
contentType.PropertyTypes.First(pt => pt.Alias == "author").ValidationRegExpMessage = "Custom regex message";
ContentTypeService.Save(contentType);
var validationResult = await ContentValidationService.ValidatePropertiesAsync(
new ContentCreateModel
{
ContentTypeKey = contentType.Key,
InvariantName = "Test Document",
InvariantProperties = new[]
{
new PropertyValueModel
{
Alias = "author",
Value = "Invalid value"
}
}
},
contentType);
Assert.AreEqual(2, validationResult.ValidationErrors.Count());
Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(
r => r.Alias == "title"
&& r.ErrorMessages.Length == 1
&& r.ErrorMessages.First() == "Custom mandatory message"));
Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(
r => r.Alias == "author"
&& r.ErrorMessages.Length == 1
&& r.ErrorMessages.First() == "Custom regex message"));
}
private async Task<(IContentType DocumentType, IContentType ElementType)> SetupBlockListTest()
{
var propertyEditorCollection = GetRequiredService<PropertyEditorCollection>();
if (propertyEditorCollection.TryGet(Constants.PropertyEditors.Aliases.BlockList, out IDataEditor dataEditor) is false)
{
Assert.Fail("Could not get the Block List data editor");
}
var elementType = new ContentType(ShortStringHelper, Constants.System.Root)
{
Name = "Test Element Type", Alias = "testElementType", IsElement = true
};
await ContentTypeService.SaveAsync(elementType, Constants.Security.SuperUserKey);
Assert.IsTrue(elementType.HasIdentity, "Could not create the element type");
var configurationEditorJsonSerializer = GetRequiredService<IConfigurationEditorJsonSerializer>();
IDataType blockListDataType = new DataType(dataEditor, configurationEditorJsonSerializer)
{
Name = "Test Block List",
ParentId = Constants.System.Root,
DatabaseType = ValueTypes.ToStorageType(dataEditor.GetValueEditor().ValueType),
ConfigurationData = new Dictionary<string, object>
{
{
nameof(BlockListConfiguration.Blocks).ToFirstLowerInvariant(),
new[]
{
new BlockListConfiguration
{
Blocks = new[]
{
new BlockListConfiguration.BlockConfiguration
{
ContentElementTypeKey = elementType.Key,
SettingsElementTypeKey = elementType.Key
}
}
}
}
}
}
};
var dataTypeService = GetRequiredService<IDataTypeService>();
var dataTypeCreateResult = await dataTypeService.CreateAsync(blockListDataType, Constants.Security.SuperUserKey);
Assert.IsTrue(dataTypeCreateResult.Success, "Could not create the block list data type");
blockListDataType = dataTypeCreateResult.Result;
// add the block list and a regex validated text box to the element type
elementType.AddPropertyType(new PropertyType(ShortStringHelper, blockListDataType, "blocks"));
var textBoxDataType = await dataTypeService.GetAsync("Textstring");
Assert.IsNotNull(textBoxDataType, "Could not get the default TextBox data type");
elementType.AddPropertyType(new PropertyType(ShortStringHelper, textBoxDataType, "title")
{
ValidationRegExp = "^Valid.*$"
});
await ContentTypeService.SaveAsync(elementType, Constants.Security.SuperUserKey);
// create a document type with the block list and a regex validated text box
var documentType = new ContentType(ShortStringHelper, Constants.System.Root)
{
Name = "Test Document Type", Alias = "testDocumentType", IsElement = false, AllowedAsRoot = true
};
documentType.AddPropertyType(new PropertyType(ShortStringHelper, blockListDataType, "blocks"));
await ContentTypeService.SaveAsync(documentType, Constants.Security.SuperUserKey);
Assert.IsTrue(documentType.HasIdentity, "Could not create the document type");
return (documentType, elementType);
}
private IContentType SetupSimpleTest()
{
var contentType = ContentTypeBuilder.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type");
contentType.PropertyTypes.First(pt => pt.Alias == "title").Mandatory = true;
contentType.PropertyTypes.First(pt => pt.Alias == "author").ValidationRegExp = "^Valid.*$";
contentType.AllowedAsRoot = true;
ContentTypeService.Save(contentType);
return contentType;
}
}