From 5fe91f837ddaf1d8ceaa855eb1ff29cf7f8970a4 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 31 Oct 2024 10:04:54 +0100 Subject: [PATCH] Validation for block level variation (#17355) * Validation for block level variation * Make the "missing property value" JSON path expression valid * Update src/Umbraco.Core/Services/ContentValidationServiceBase.cs Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> * Update src/Umbraco.Core/Services/ContentValidationServiceBase.cs Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> * Update src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> --------- Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> --- .../Content/ContentControllerBase.cs | 22 +- src/Umbraco.Core/Models/IDataValueEditor.cs | 4 +- .../Validation/PropertyValidationContext.cs | 22 + .../PropertyEditors/ConfigurationEditor.cs | 3 +- .../PropertyEditors/DataValueEditor.cs | 5 +- .../PropertyEditors/IValueValidator.cs | 4 +- .../Validation/JsonPathExpression.cs | 9 + .../Validators/DateTimeValidator.cs | 3 +- .../Validators/DecimalValidator.cs | 3 +- .../Validators/EmailValidator.cs | 3 +- .../Validators/IntegerValidator.cs | 3 +- .../Validators/RegexValidator.cs | 3 +- .../Validators/RequiredValidator.cs | 3 +- .../Services/ContentPublishingService.cs | 15 +- .../Services/ContentValidationServiceBase.cs | 59 +- .../Services/IPropertyValidationService.cs | 23 +- .../Services/PropertyValidationService.cs | 98 +++- .../BlockEditorMinMaxValidatorBase.cs | 3 +- .../PropertyEditors/BlockEditorValidator.cs | 5 +- .../BlockEditorValidatorBase.cs | 87 ++- .../BlockGridPropertyEditorBase.cs | 3 +- .../BlockListPropertyEditorBase.cs | 3 +- .../ColorPickerConfigurationEditor.cs | 3 +- .../PropertyEditors/ComplexEditorValidator.cs | 23 +- .../RichTextEditorBlockValidator.cs | 5 +- .../TemporaryFileUploadValidator.cs | 3 +- .../ValueListUniqueValueValidator.cs | 3 +- ...stElementLevelVariationTests.Publishing.cs | 328 +++++++++++ ...stElementLevelVariationTests.Validation.cs | 515 ++++++++++++++++++ .../BlockListElementLevelVariationTests.cs | 57 ++ .../Services/ContentValidationServiceTests.cs | 8 + .../Umbraco.Tests.Integration.csproj | 3 + .../PropertyEditors/ColorListValidatorTest.cs | 10 +- .../EnsureUniqueValuesValidatorTest.cs | 28 +- 34 files changed, 1246 insertions(+), 123 deletions(-) create mode 100644 src/Umbraco.Core/Models/Validation/PropertyValidationContext.cs create mode 100644 src/Umbraco.Core/PropertyEditors/Validation/JsonPathExpression.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Validation.cs diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentControllerBase.cs index c008dad102..0b527a9b39 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentControllerBase.cs @@ -4,6 +4,7 @@ using Umbraco.Cms.Api.Management.ViewModels.Content; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.ContentEditing.Validation; +using Umbraco.Cms.Core.PropertyEditors.Validation; using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.Extensions; @@ -100,7 +101,7 @@ public abstract class ContentControllerBase : ManagementApiControllerBase var errors = new SortedDictionary(); - var missingPropertyModels = new List(); + var validationErrorExpressionRoot = $"$.{nameof(ContentModelBase.Values).ToFirstLowerInvariant()}"; foreach (PropertyValidationError validationError in validationResult.ValidationErrors) { TValueModel? requestValue = requestModel.Values.FirstOrDefault(value => @@ -109,13 +110,16 @@ public abstract class ContentControllerBase : ManagementApiControllerBase && value.Segment == validationError.Segment); if (requestValue is null) { - missingPropertyModels.Add(MapMissingProperty(validationError)); + errors.Add( + $"{validationErrorExpressionRoot}[{JsonPathExpression.MissingPropertyValue(validationError.Alias, validationError.Culture, validationError.Segment)}].{nameof(ValueModelBase.Value)}", + validationError.ErrorMessages); continue; } var index = requestModel.Values.IndexOf(requestValue); - var key = $"$.{nameof(ContentModelBase.Values).ToFirstLowerInvariant()}[{index}].{nameof(ValueModelBase.Value).ToFirstLowerInvariant()}{validationError.JsonPath}"; - errors.Add(key, validationError.ErrorMessages); + errors.Add( + $"$.{nameof(ContentModelBase.Values).ToFirstLowerInvariant()}[{index}].{nameof(ValueModelBase.Value).ToFirstLowerInvariant()}{validationError.JsonPath}", + validationError.ErrorMessages); } return OperationStatusResult(status, problemDetailsBuilder @@ -123,16 +127,6 @@ public abstract class ContentControllerBase : ManagementApiControllerBase .WithTitle("Validation failed") .WithDetail("One or more properties did not pass validation") .WithRequestModelErrors(errors) - .WithExtension("missingValues", missingPropertyModels.ToArray()) .Build())); } - - private PropertyValidationResponseModel MapMissingProperty(PropertyValidationError source) => - new() - { - Alias = source.Alias, - Segment = source.Segment, - Culture = source.Culture, - Messages = source.ErrorMessages, - }; } diff --git a/src/Umbraco.Core/Models/IDataValueEditor.cs b/src/Umbraco.Core/Models/IDataValueEditor.cs index 691ee70bf1..f1599677d5 100644 --- a/src/Umbraco.Core/Models/IDataValueEditor.cs +++ b/src/Umbraco.Core/Models/IDataValueEditor.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations; using System.Xml.Linq; using Umbraco.Cms.Core.Models.Editors; +using Umbraco.Cms.Core.Models.Validation; using Umbraco.Cms.Core.PropertyEditors; namespace Umbraco.Cms.Core.Models; @@ -42,7 +43,8 @@ public interface IDataValueEditor /// The property value. /// A value indicating whether the property value is required. /// A specific format (regex) that the property value must respect. - IEnumerable Validate(object? value, bool required, string? format); + /// The context in which the property value is being validated. + IEnumerable Validate(object? value, bool required, string? format, PropertyValidationContext validationContext); /// /// Converts a value posted by the editor to a property value. diff --git a/src/Umbraco.Core/Models/Validation/PropertyValidationContext.cs b/src/Umbraco.Core/Models/Validation/PropertyValidationContext.cs new file mode 100644 index 0000000000..d10fa076b0 --- /dev/null +++ b/src/Umbraco.Core/Models/Validation/PropertyValidationContext.cs @@ -0,0 +1,22 @@ +namespace Umbraco.Cms.Core.Models.Validation; + +public sealed class PropertyValidationContext +{ + public required string? Culture { get; init; } + + public required string? Segment { get; init; } + + public required IEnumerable CulturesBeingValidated { get; init; } + + public required IEnumerable SegmentsBeingValidated { get; init; } + + public static PropertyValidationContext Empty() => new() + { + Culture = null, Segment = null, CulturesBeingValidated = [], SegmentsBeingValidated = [] + }; + + public static PropertyValidationContext CultureAndSegment(string? culture, string? segment) => new() + { + Culture = culture, Segment = segment, CulturesBeingValidated = [], SegmentsBeingValidated = [] + }; +} diff --git a/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs index 68ce85eac9..58412fa7a2 100644 --- a/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; +using Umbraco.Cms.Core.Models.Validation; using Umbraco.Cms.Core.Serialization; using Umbraco.Extensions; @@ -85,7 +86,7 @@ public class ConfigurationEditor : IConfigurationEditor => Fields .SelectMany(field => configuration.TryGetValue(field.Key, out var value) - ? field.Validators.SelectMany(validator => validator.Validate(value, null, null)) + ? field.Validators.SelectMany(validator => validator.Validate(value, null, null, PropertyValidationContext.Empty())) : Enumerable.Empty()) .ToArray(); diff --git a/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs b/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs index e9d131c75a..5f06db2b3c 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Editors; +using Umbraco.Cms.Core.Models.Validation; using Umbraco.Cms.Core.PropertyEditors.Validators; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; @@ -105,10 +106,10 @@ public class DataValueEditor : IDataValueEditor public List Validators { get; private set; } = new(); /// - public IEnumerable Validate(object? value, bool required, string? format) + public IEnumerable Validate(object? value, bool required, string? format, PropertyValidationContext validationContext) { List? results = null; - var r = Validators.SelectMany(v => v.Validate(value, ValueType, ConfigurationObject)).ToList(); + var r = Validators.SelectMany(v => v.Validate(value, ValueType, ConfigurationObject, validationContext)).ToList(); if (r.Any()) { results = r; diff --git a/src/Umbraco.Core/PropertyEditors/IValueValidator.cs b/src/Umbraco.Core/PropertyEditors/IValueValidator.cs index 7d26f8a96c..338c2384cb 100644 --- a/src/Umbraco.Core/PropertyEditors/IValueValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/IValueValidator.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using Umbraco.Cms.Core.Models.Validation; namespace Umbraco.Cms.Core.PropertyEditors; @@ -13,6 +14,7 @@ public interface IValueValidator /// The value to validate. /// The value type. /// A datatype configuration. + /// The context in which the value is being validated. /// Validation results. /// /// @@ -20,5 +22,5 @@ public interface IValueValidator /// editor. /// /// - IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration); + IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext); } diff --git a/src/Umbraco.Core/PropertyEditors/Validation/JsonPathExpression.cs b/src/Umbraco.Core/PropertyEditors/Validation/JsonPathExpression.cs new file mode 100644 index 0000000000..80582d50de --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/Validation/JsonPathExpression.cs @@ -0,0 +1,9 @@ +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.PropertyEditors.Validation; + +public static class JsonPathExpression +{ + public static string MissingPropertyValue(string propertyAlias, string? culture, string? segment) + => $"?(@.alias == '{propertyAlias}' && @.culture == {(culture.IsNullOrWhiteSpace() ? "null" : $"'{culture}'")} && @.segment == {(segment.IsNullOrWhiteSpace() ? "null" : $"'{segment}'")})"; +} diff --git a/src/Umbraco.Core/PropertyEditors/Validators/DateTimeValidator.cs b/src/Umbraco.Core/PropertyEditors/Validators/DateTimeValidator.cs index 530935d276..1c8fcedab2 100644 --- a/src/Umbraco.Core/PropertyEditors/Validators/DateTimeValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/Validators/DateTimeValidator.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using Umbraco.Cms.Core.Models.Validation; using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors.Validators; @@ -8,7 +9,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.Validators; /// public class DateTimeValidator : IValueValidator { - public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext) { // don't validate if empty if (value == null || value.ToString().IsNullOrWhiteSpace()) diff --git a/src/Umbraco.Core/PropertyEditors/Validators/DecimalValidator.cs b/src/Umbraco.Core/PropertyEditors/Validators/DecimalValidator.cs index 3dc3774d2f..ea04699aed 100644 --- a/src/Umbraco.Core/PropertyEditors/Validators/DecimalValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/Validators/DecimalValidator.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using Umbraco.Cms.Core.Models.Validation; using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors.Validators; @@ -9,7 +10,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.Validators; public sealed class DecimalValidator : IValueValidator { /// - public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext) { if (value == null || value.ToString() == string.Empty) { diff --git a/src/Umbraco.Core/PropertyEditors/Validators/EmailValidator.cs b/src/Umbraco.Core/PropertyEditors/Validators/EmailValidator.cs index 69b7dafff9..592b2dc2c7 100644 --- a/src/Umbraco.Core/PropertyEditors/Validators/EmailValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/Validators/EmailValidator.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using Umbraco.Cms.Core.Models.Validation; namespace Umbraco.Cms.Core.PropertyEditors.Validators; @@ -8,7 +9,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.Validators; public sealed class EmailValidator : IValueValidator { /// - public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext) { var asString = value == null ? string.Empty : value.ToString(); diff --git a/src/Umbraco.Core/PropertyEditors/Validators/IntegerValidator.cs b/src/Umbraco.Core/PropertyEditors/Validators/IntegerValidator.cs index 4e344dffff..5027bd69ef 100644 --- a/src/Umbraco.Core/PropertyEditors/Validators/IntegerValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/Validators/IntegerValidator.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using Umbraco.Cms.Core.Models.Validation; using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors.Validators; @@ -9,7 +10,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.Validators; public sealed class IntegerValidator : IValueValidator { /// - public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext) { if (value != null && value.ToString() != string.Empty) { diff --git a/src/Umbraco.Core/PropertyEditors/Validators/RegexValidator.cs b/src/Umbraco.Core/PropertyEditors/Validators/RegexValidator.cs index 4e985937cc..f1fe0a199e 100644 --- a/src/Umbraco.Core/PropertyEditors/Validators/RegexValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/Validators/RegexValidator.cs @@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations; using System.Text.RegularExpressions; +using Umbraco.Cms.Core.Models.Validation; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.PropertyEditors.Validators; @@ -49,7 +50,7 @@ public sealed class RegexValidator : IValueFormatValidator, IValueValidator => _regex = regex; /// - public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext) { if (_regex == null) { diff --git a/src/Umbraco.Core/PropertyEditors/Validators/RequiredValidator.cs b/src/Umbraco.Core/PropertyEditors/Validators/RequiredValidator.cs index 92e3678e7d..45389caec4 100644 --- a/src/Umbraco.Core/PropertyEditors/Validators/RequiredValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/Validators/RequiredValidator.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using Umbraco.Cms.Core.Models.Validation; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; @@ -20,7 +21,7 @@ public class RequiredValidator : IValueRequiredValidator, IValueValidator } /// - public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) => + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext) => ValidateRequired(value, valueType); /// diff --git a/src/Umbraco.Core/Services/ContentPublishingService.cs b/src/Umbraco.Core/Services/ContentPublishingService.cs index 61592ee021..24c73271ee 100644 --- a/src/Umbraco.Core/Services/ContentPublishingService.cs +++ b/src/Umbraco.Core/Services/ContentPublishingService.cs @@ -77,17 +77,13 @@ internal sealed class ContentPublishingService : IContentPublishingService } ContentValidationResult validationResult = await ValidateCurrentContentAsync(content, cultures); - - var errors = validationResult.ValidationErrors.Where(err => - cultures.Contains(err.Culture ?? "*", StringComparer.InvariantCultureIgnoreCase)); - if (errors.Any()) + if (validationResult.ValidationErrors.Any()) { scope.Complete(); return Attempt.FailWithStatus(ContentPublishingOperationStatus.ContentInvalid, new ContentPublishingResult { Content = content, - InvalidPropertyAliases = errors.Select(property => property.Alias).ToArray() - ?? Enumerable.Empty() + InvalidPropertyAliases = validationResult.ValidationErrors.Select(property => property.Alias).ToArray() }); } @@ -131,11 +127,12 @@ internal sealed class ContentPublishingService : IContentPublishingService var model = new ContentUpdateModel() { InvariantName = content.Name, - InvariantProperties = cultures.Contains("*") ? content.Properties.Where(x=>x.PropertyType.VariesByCulture() is false).Select(x=> new PropertyValueModel() + // NOTE KJA: this needs redoing; we need to make an informed decision whether to include invariant properties, depending on if editing invariant properties is allowed on all variants, or if the default language is included in cultures + InvariantProperties = content.Properties.Where(x => x.PropertyType.VariesByCulture() is false).Select(x => new PropertyValueModel() { Alias = x.Alias, Value = x.GetValue() - }) : Array.Empty(), + }), Variants = cultures.Select(culture => new VariantModel() { Name = content.GetPublishName(culture) ?? string.Empty, @@ -149,7 +146,7 @@ internal sealed class ContentPublishingService : IContentPublishingService }) }; IContentType? contentType = _contentTypeService.Get(content.ContentType.Key)!; - ContentValidationResult validationResult = await _contentValidationService.ValidatePropertiesAsync(model, contentType); + ContentValidationResult validationResult = await _contentValidationService.ValidatePropertiesAsync(model, contentType, cultures); return validationResult; } diff --git a/src/Umbraco.Core/Services/ContentValidationServiceBase.cs b/src/Umbraco.Core/Services/ContentValidationServiceBase.cs index 63add24731..03d2590f65 100644 --- a/src/Umbraco.Core/Services/ContentValidationServiceBase.cs +++ b/src/Umbraco.Core/Services/ContentValidationServiceBase.cs @@ -2,6 +2,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.ContentEditing.Validation; +using Umbraco.Cms.Core.Models.Validation; using Umbraco.Cms.Core.PropertyEditors.Validation; using Umbraco.Extensions; @@ -34,9 +35,28 @@ internal abstract class ContentValidationServiceBase .ToArray(); IPropertyType[] variantPropertyTypes = contentTypePropertyTypes.Except(invariantPropertyTypes).ToArray(); + var cultures = culturesToValidate?.WhereNotNull().Except(["*"]).ToArray(); + if (cultures?.Any() is not true) + { + cultures = await GetCultureCodes(); + } + + // we don't have any managed segments, so we have to make do with the ones passed in the model + var segments = contentEditingModelBase.Variants + .Where(variant => variant.Culture is null || cultures.Contains(variant.Culture)) + .DistinctBy(variant => variant.Segment).Select(variant => variant.Segment) + .WhereNotNull() + .ToArray(); + foreach (IPropertyType propertyType in invariantPropertyTypes) { - validationErrors.AddRange(ValidateProperty(contentEditingModelBase, propertyType, null, null)); + var validationContext = new PropertyValidationContext + { + Culture = null, Segment = null, CulturesBeingValidated = cultures, SegmentsBeingValidated = segments + }; + + PropertyValueModel? propertyValueModel = contentEditingModelBase.InvariantProperties.FirstOrDefault(propertyValue => propertyValue.Alias == propertyType.Alias); + validationErrors.AddRange(ValidateProperty(propertyType, propertyValueModel, validationContext)); } if (variantPropertyTypes.Any() is false) @@ -44,17 +64,23 @@ internal abstract class ContentValidationServiceBase return new ContentValidationResult { ValidationErrors = validationErrors }; } - var cultures = culturesToValidate?.ToArray() ?? await GetCultureCodes(); - // we don't have any managed segments, so we have to make do with the ones passed in the model - var segments = contentEditingModelBase.Variants.DistinctBy(variant => variant.Segment).Select(variant => variant.Segment).ToArray(); - foreach (IPropertyType propertyType in variantPropertyTypes) { foreach (var culture in cultures) { - foreach (var segment in segments) + foreach (var segment in segments.DefaultIfEmpty(null)) { - validationErrors.AddRange(ValidateProperty(contentEditingModelBase, propertyType, culture, segment)); + var validationContext = new PropertyValidationContext + { + Culture = culture, Segment = segment, CulturesBeingValidated = cultures, SegmentsBeingValidated = segments + }; + + PropertyValueModel? propertyValueModel = contentEditingModelBase + .Variants + .FirstOrDefault(variant => string.Equals(variant.Culture, culture, StringComparison.InvariantCultureIgnoreCase) && string.Equals(segment, variant.Segment, StringComparison.InvariantCultureIgnoreCase))? + .Properties + .FirstOrDefault(propertyValue => propertyValue.Alias == propertyType.Alias); + validationErrors.AddRange(ValidateProperty(propertyType, propertyValueModel, validationContext)); } } } @@ -75,19 +101,10 @@ internal abstract class ContentValidationServiceBase private async Task GetCultureCodes() => (await _languageService.GetAllAsync()).Select(language => language.IsoCode).ToArray(); - private IEnumerable ValidateProperty(ContentEditingModelBase contentEditingModelBase, IPropertyType propertyType, string? culture, string? segment) + private IEnumerable ValidateProperty(IPropertyType propertyType, PropertyValueModel? propertyValueModel, PropertyValidationContext validationContext) { - IEnumerable? properties = culture is null && segment is null - ? contentEditingModelBase.InvariantProperties - : contentEditingModelBase - .Variants - .FirstOrDefault(variant => string.Equals(variant.Culture, culture, StringComparison.InvariantCultureIgnoreCase) && string.Equals(segment, variant.Segment, StringComparison.InvariantCultureIgnoreCase))? - .Properties; - - PropertyValueModel? propertyValueModel = properties?.FirstOrDefault(p => p.Alias == propertyType.Alias); - ValidationResult[] validationResults = _propertyValidationService - .ValidatePropertyValue(propertyType, propertyValueModel?.Value) + .ValidatePropertyValue(propertyType, propertyValueModel?.Value, validationContext) .ToArray(); if (validationResults.Any() is false) @@ -96,7 +113,7 @@ internal abstract class ContentValidationServiceBase } PropertyValidationError[] validationErrors = validationResults - .SelectMany(validationResult => ExtractPropertyValidationResultJsonPath(validationResult, propertyType.Alias, culture, segment)) + .SelectMany(validationResult => ExtractPropertyValidationResultJsonPath(validationResult, propertyType.Alias, validationContext.Culture, validationContext.Segment)) .ToArray(); if (validationErrors.Any() is false) { @@ -107,8 +124,8 @@ internal abstract class ContentValidationServiceBase JsonPath = string.Empty, ErrorMessages = validationResults.Select(v => v.ErrorMessage).WhereNotNull().ToArray(), Alias = propertyType.Alias, - Culture = culture, - Segment = segment + Culture = validationContext.Culture, + Segment = validationContext.Segment } }; } diff --git a/src/Umbraco.Core/Services/IPropertyValidationService.cs b/src/Umbraco.Core/Services/IPropertyValidationService.cs index e854d0f7f5..04ef31fb6a 100644 --- a/src/Umbraco.Core/Services/IPropertyValidationService.cs +++ b/src/Umbraco.Core/Services/IPropertyValidationService.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Validation; using Umbraco.Cms.Core.PropertyEditors; namespace Umbraco.Cms.Core.Services; @@ -14,6 +15,9 @@ public interface IPropertyValidationService /// /// Gets a value indicating whether the property has valid values. /// + bool IsPropertyValid(IProperty property, PropertyValidationContext validationContext); + + [Obsolete("Please use the overload that accepts a PropertyValidationContext. Will be removed in V16.")] bool IsPropertyValid(IProperty property, string culture = "*", string segment = "*"); /// @@ -26,11 +30,28 @@ public interface IPropertyValidationService bool isRequired, string? validationRegExp, string? isRequiredMessage, - string? validationRegExpMessage); + string? validationRegExpMessage, + PropertyValidationContext validationContext); /// /// Validates a property value. /// + IEnumerable ValidatePropertyValue( + IPropertyType propertyType, + object? postedValue, + PropertyValidationContext validationContext); + + [Obsolete("Please use the overload that accepts a PropertyValidationContext. Will be removed in V16.")] + IEnumerable ValidatePropertyValue( + IDataEditor editor, + IDataType dataType, + object? postedValue, + bool isRequired, + string? validationRegExp, + string? isRequiredMessage, + string? validationRegExpMessage); + + [Obsolete("Please use the overload that accepts a PropertyValidationContext. Will be removed in V16.")] IEnumerable ValidatePropertyValue( IPropertyType propertyType, object? postedValue); diff --git a/src/Umbraco.Core/Services/PropertyValidationService.cs b/src/Umbraco.Core/Services/PropertyValidationService.cs index 0f12e5cbdd..e56b68e86e 100644 --- a/src/Umbraco.Core/Services/PropertyValidationService.cs +++ b/src/Umbraco.Core/Services/PropertyValidationService.cs @@ -4,6 +4,7 @@ using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Dictionary; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Validation; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Extensions; @@ -44,29 +45,36 @@ public class PropertyValidationService : IPropertyValidationService /// public IEnumerable ValidatePropertyValue( IPropertyType propertyType, - object? postedValue) + object? postedValue, + PropertyValidationContext validationContext) { if (propertyType is null) { throw new ArgumentNullException(nameof(propertyType)); } - IDataType? dataType = _dataTypeService.GetDataType(propertyType.DataTypeId); + IDataType? dataType = GetDataType(propertyType); if (dataType == null) { throw new InvalidOperationException("No data type found by id " + propertyType.DataTypeId); } - IDataEditor? editor = _propertyEditors[propertyType.PropertyEditorAlias]; - if (editor == null) + IDataEditor? dataEditor = GetDataEditor(propertyType); + if (dataEditor == null) { throw new InvalidOperationException("No property editor found by alias " + propertyType.PropertyEditorAlias); } - return ValidatePropertyValue(editor, dataType, postedValue, propertyType.Mandatory, propertyType.ValidationRegExp, propertyType.MandatoryMessage, propertyType.ValidationRegExpMessage); + return ValidatePropertyValue(dataEditor, dataType, postedValue, propertyType.Mandatory, propertyType.ValidationRegExp, propertyType.MandatoryMessage, propertyType.ValidationRegExpMessage, validationContext); } + [Obsolete("Please use the overload that accepts a PropertyValidationContext. Will be removed in V16.")] + public IEnumerable ValidatePropertyValue( + IPropertyType propertyType, + object? postedValue) + => ValidatePropertyValue(propertyType, postedValue, PropertyValidationContext.Empty()); + /// public IEnumerable ValidatePropertyValue( IDataEditor editor, @@ -75,7 +83,8 @@ public class PropertyValidationService : IPropertyValidationService bool isRequired, string? validationRegExp, string? isRequiredMessage, - string? validationRegExpMessage) + string? validationRegExpMessage, + PropertyValidationContext validationContext) { // Retrieve default messages used for required and regex validatation. We'll replace these // if set with custom ones if they've been provided for a given property. @@ -83,7 +92,7 @@ public class PropertyValidationService : IPropertyValidationService var formatDefaultMessages = new[] { Constants.Validation.ErrorMessages.Properties.PatternMismatch }; IDataValueEditor valueEditor = _valueEditorCache.GetValueEditor(editor, dataType); - foreach (ValidationResult validationResult in valueEditor.Validate(postedValue, isRequired, validationRegExp)) + foreach (ValidationResult validationResult in valueEditor.Validate(postedValue, isRequired, validationRegExp, validationContext)) { // If we've got custom error messages, we'll replace the default ones that will have been applied in the call to Validate(). if (isRequired && !string.IsNullOrWhiteSpace(isRequiredMessage) && @@ -102,6 +111,17 @@ public class PropertyValidationService : IPropertyValidationService } } + [Obsolete("Please use the overload that accepts a PropertyValidationContext. Will be removed in V16.")] + public IEnumerable ValidatePropertyValue( + IDataEditor editor, + IDataType dataType, + object? postedValue, + bool isRequired, + string? validationRegExp, + string? isRequiredMessage, + string? validationRegExpMessage) + => ValidatePropertyValue(editor, dataType, postedValue, isRequired, validationRegExp, isRequiredMessage, validationRegExpMessage, PropertyValidationContext.Empty()); + /// public bool IsPropertyDataValid(IContent content, out IProperty[] invalidProperties, CultureImpact? impact) { @@ -118,19 +138,30 @@ public class PropertyValidationService : IPropertyValidationService // impacts invariant = validate invariant property, invariant culture if (impact.ImpactsOnlyInvariantCulture) { - return !(propertyTypeVaries || IsPropertyValid(x, null)); + return !(propertyTypeVaries || IsPropertyValid(x, PropertyValidationContext.Empty())); } // impacts all = validate property, all cultures (incl. invariant) if (impact.ImpactsAllCultures) { - return !IsPropertyValid(x); + return !IsPropertyValid(x, PropertyValidationContext.CultureAndSegment("*", null)); } // impacts explicit culture = validate variant property, explicit culture if (propertyTypeVaries) { - return !IsPropertyValid(x, impact.Culture); + return !IsPropertyValid(x, PropertyValidationContext.CultureAndSegment(impact.Culture, null)); + } + + if (impact.ImpactsExplicitCulture && GetDataEditor(x.PropertyType)?.CanMergePartialPropertyValues(x.PropertyType) is true) + { + return !IsPropertyValid(x, new PropertyValidationContext + { + Culture = null, + Segment = null, + CulturesBeingValidated = [impact.Culture!], + SegmentsBeingValidated = [] + }); } // and, for explicit culture, we may also have to validate invariant property, invariant culture @@ -138,19 +169,31 @@ public class PropertyValidationService : IPropertyValidationService // - it is impacted (default culture), or // - there is no published version of the content - maybe non-default culture, but no published version var alsoInvariant = impact.ImpactsAlsoInvariantProperties || !content.Published; - return alsoInvariant && !IsPropertyValid(x, null); + return alsoInvariant && !IsPropertyValid(x, PropertyValidationContext.Empty()); }).ToArray(); return invalidProperties.Length == 0; } + [Obsolete("Please use the overload that accepts a PropertyValidationContext. Will be removed in V16.")] + public bool IsPropertyValid(IProperty property, string culture = "*", string segment = "*") + => IsPropertyValid(property, PropertyValidationContext.CultureAndSegment(culture, segment)); + /// - public bool IsPropertyValid(IProperty property, string? culture = "*", string? segment = "*") + public bool IsPropertyValid(IProperty property, PropertyValidationContext validationContext) { // NOTE - the pvalue and vvalues logic in here is borrowed directly from the Property.Values setter so if you are wondering what that's all about, look there. // The underlying Property._pvalue and Property._vvalues are not exposed but we can re-create these values ourselves which is what it's doing. - culture = culture?.NullOrWhiteSpaceAsNull(); - segment = segment?.NullOrWhiteSpaceAsNull(); + validationContext = new PropertyValidationContext + { + Culture = validationContext.Culture?.NullOrWhiteSpaceAsNull(), + Segment = validationContext.Segment?.NullOrWhiteSpaceAsNull(), + CulturesBeingValidated = validationContext.CulturesBeingValidated, + SegmentsBeingValidated = validationContext.SegmentsBeingValidated + }; + + var culture = validationContext.Culture; + var segment = validationContext.Segment; IPropertyValue? pvalue = null; @@ -161,7 +204,7 @@ public class PropertyValidationService : IPropertyValidationService { // validate pvalue (which is the invariant value) pvalue = property.Values.FirstOrDefault(x => x.Culture == null && x.Segment == null); - if (!IsValidPropertyValue(property, pvalue?.EditedValue)) + if (!IsValidPropertyValue(property, pvalue?.EditedValue, validationContext)) { return false; } @@ -188,7 +231,7 @@ public class PropertyValidationService : IPropertyValidationService // if we don't have vvalues (property.Values is empty or only contains pvalue), validate null if (property.Values.Count == (pvalue == null ? 0 : 1)) { - return culture == "*" || IsValidPropertyValue(property, null); + return culture == "*" || IsValidPropertyValue(property, null, validationContext); } // else validate vvalues (but don't revalidate pvalue) @@ -202,10 +245,10 @@ public class PropertyValidationService : IPropertyValidationService // if we do not have any vvalues at this point, validate null (no variant values present) if (vvalues.Any() is false) { - return IsValidPropertyValue(property, null); + return IsValidPropertyValue(property, null, validationContext); } - return vvalues.All(x => IsValidPropertyValue(property, x.EditedValue)); + return vvalues.All(x => IsValidPropertyValue(property, x.EditedValue, validationContext)); } /// @@ -214,15 +257,15 @@ public class PropertyValidationService : IPropertyValidationService /// /// /// True is property value is valid, otherwise false - private bool IsValidPropertyValue(IProperty property, object? value) => - IsPropertyValueValid(property.PropertyType, value); + private bool IsValidPropertyValue(IProperty property, object? value, PropertyValidationContext validationContext) => + IsPropertyValueValid(property.PropertyType, value, validationContext); /// /// Determines whether a value is valid for this property type. /// - private bool IsPropertyValueValid(IPropertyType propertyType, object? value) + private bool IsPropertyValueValid(IPropertyType propertyType, object? value, PropertyValidationContext validationContext) { - IDataEditor? editor = _propertyEditors[propertyType.PropertyEditorAlias]; + IDataEditor? editor = GetDataEditor(propertyType); if (editor == null) { // nothing much we can do validation wise if the property editor has been removed. @@ -230,8 +273,15 @@ public class PropertyValidationService : IPropertyValidationService return true; } - var configuration = _dataTypeService.GetDataType(propertyType.DataTypeId)?.ConfigurationObject; + var configuration = GetDataType(propertyType)?.ConfigurationObject; IDataValueEditor valueEditor = editor.GetValueEditor(configuration); - return !valueEditor.Validate(value, propertyType.Mandatory, propertyType.ValidationRegExp).Any(); + + return !valueEditor.Validate(value, propertyType.Mandatory, propertyType.ValidationRegExp, validationContext).Any(); } + + private IDataType? GetDataType(IPropertyType propertyType) + => _dataTypeService.GetDataType(propertyType.DataTypeId); + + private IDataEditor? GetDataEditor(IPropertyType propertyType) + => _propertyEditors[propertyType.PropertyEditorAlias]; } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorMinMaxValidatorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorMinMaxValidatorBase.cs index f86b47b53d..f8f1dde8a1 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorMinMaxValidatorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorMinMaxValidatorBase.cs @@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations; using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.Validation; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; @@ -19,7 +20,7 @@ internal abstract class BlockEditorMinMaxValidatorBase : IValue protected ILocalizedTextService TextService { get; } - public abstract IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration); + public abstract IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext); protected IEnumerable ValidateNumberOfBlocks(BlockEditorData? blockEditorData, int? min, int? max) { diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidator.cs index 332c414628..9c1041c143 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidator.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidator.cs @@ -3,6 +3,7 @@ using Umbraco.Cms.Core.Cache.PropertyEditors; using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.Validation; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.PropertyEditors; @@ -20,11 +21,11 @@ public class BlockEditorValidator : BlockEditorValidatorBase _blockEditorValues = blockEditorValues; - protected override IEnumerable GetElementTypeValidation(object? value) + protected override IEnumerable GetElementTypeValidation(object? value, PropertyValidationContext validationContext) { BlockEditorData? blockEditorData = _blockEditorValues.DeserializeAndClean(value); return blockEditorData is not null - ? GetBlockEditorDataValidation(blockEditorData) + ? GetBlockEditorDataValidation(blockEditorData, validationContext) : Array.Empty(); } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs index 281b5151ed..243f130875 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs @@ -1,6 +1,8 @@ using Umbraco.Cms.Core.Cache.PropertyEditors; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.Validation; +using Umbraco.Cms.Core.PropertyEditors.Validation; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; @@ -16,16 +18,55 @@ public abstract class BlockEditorValidatorBase : ComplexEditorV : base(propertyValidationService) => _elementTypeCache = elementTypeCache; - protected IEnumerable GetBlockEditorDataValidation(BlockEditorData blockEditorData) + protected IEnumerable GetBlockEditorDataValidation(BlockEditorData blockEditorData, PropertyValidationContext validationContext) + { + var elementTypeValidation = new List(); + var isWildcardCulture = validationContext.Culture == "*"; + var validationContextCulture = isWildcardCulture ? null : validationContext.Culture.NullOrWhiteSpaceAsNull(); + elementTypeValidation.AddRange(GetBlockEditorDataValidation(blockEditorData, validationContextCulture, validationContext.Segment)); + + if (validationContextCulture is null) + { + IEnumerable validationContextCulturesBeingValidated = isWildcardCulture + ? blockEditorData.BlockValue.Expose.Select(e => e.Culture).WhereNotNull().Distinct() + : validationContext.CulturesBeingValidated; + foreach (var culture in validationContextCulturesBeingValidated) + { + foreach (var segment in validationContext.SegmentsBeingValidated.DefaultIfEmpty(null)) + { + elementTypeValidation.AddRange(GetBlockEditorDataValidation(blockEditorData, culture, segment)); + } + } + } + + return elementTypeValidation; + } + + private IEnumerable GetBlockEditorDataValidation(BlockEditorData blockEditorData, string? culture, string? segment) { // There is no guarantee that the client will post data for every property defined in the Element Type but we still // need to validate that data for each property especially for things like 'required' data to work. // Lookup all element types for all content/settings and then we can populate any empty properties. + if (blockEditorData.Layout is null) + { + yield break; + } + + Guid[] exposedContentKeys = blockEditorData.BlockValue.Expose + .Where(expose => culture is null || expose.Culture == culture) + .Select(expose => expose.ContentKey) + .Distinct() + .ToArray(); + Guid[] exposedSettingsKeys = blockEditorData.Layout + .Where(layout => layout.SettingsKey.HasValue && exposedContentKeys.Contains(layout.ContentKey)) + .Select(layout => layout.SettingsKey!.Value) + .ToArray(); + var itemDataGroups = new[] { - new { Path = nameof(BlockValue.ContentData).ToFirstLowerInvariant(), Items = blockEditorData.BlockValue.ContentData }, - new { Path = nameof(BlockValue.SettingsData).ToFirstLowerInvariant(), Items = blockEditorData.BlockValue.SettingsData } + new { Path = nameof(BlockValue.ContentData).ToFirstLowerInvariant(), Items = blockEditorData.BlockValue.ContentData.Where(cd => exposedContentKeys.Contains(cd.Key)).ToArray() }, + new { Path = nameof(BlockValue.SettingsData).ToFirstLowerInvariant(), Items = blockEditorData.BlockValue.SettingsData.Where(sd => exposedSettingsKeys.Contains(sd.Key)).ToArray() } }; var valuesJsonPathPart = nameof(BlockItemData.Values).ToFirstLowerInvariant(); @@ -34,7 +75,7 @@ public abstract class BlockEditorValidatorBase : ComplexEditorV { var allElementTypes = _elementTypeCache.GetMany(group.Items.Select(x => x.ContentTypeKey).ToArray()).ToDictionary(x => x.Key); - for (var i = 0; i < group.Items.Count; i++) + for (var i = 0; i < group.Items.Length; i++) { BlockItemData item = group.Items[i]; if (!allElementTypes.TryGetValue(item.ContentTypeKey, out IContentType? elementType)) @@ -42,9 +83,6 @@ public abstract class BlockEditorValidatorBase : ComplexEditorV throw new InvalidOperationException($"No element type found with key {item.ContentTypeKey}"); } - // NOTE: for now this only validates the property data actually sent by the client, not all element properties. - // we need to ensure that all properties for all languages have a matching "item" entry here, to handle validation of - // required properties (see comment in the top of this method). a separate task has been created, get in touch with KJA. var elementValidation = new ElementTypeValidationModel(item.ContentTypeAlias, item.Key); for (var j = 0; j < item.Values.Count; j++) { @@ -55,10 +93,45 @@ public abstract class BlockEditorValidatorBase : ComplexEditorV throw new ArgumentException("One or more block properties did not have a resolved property type. Block editor values must be resolved before attempting to validate them.", nameof(blockEditorData)); } + if (propertyType.VariesByCulture() != (culture is not null) || blockPropertyValue.Culture.InvariantEquals(culture) is false) + { + continue; + } + + if (segment != "*") + { + if (propertyType.VariesBySegment() != (segment is not null) || blockPropertyValue.Segment.InvariantEquals(segment) is false) + { + continue; + } + } + elementValidation.AddPropertyTypeValidation( new PropertyTypeValidationModel(propertyType, blockPropertyValue.Value, $"{group.Path}[{i}].{valuesJsonPathPart}[{j}].value")); } + var handledPropertyTypeAliases = elementValidation.PropertyTypeValidation.Select(v => v.PropertyType.Alias).ToArray(); + foreach (IPropertyType propertyType in elementType.CompositionPropertyTypes) + { + if (handledPropertyTypeAliases.Contains(propertyType.Alias)) + { + continue; + } + + if (propertyType.VariesByCulture() != (culture is not null)) + { + continue; + } + + if (segment == "*" || propertyType.VariesBySegment() != (segment is not null)) + { + continue; + } + + elementValidation.AddPropertyTypeValidation( + new PropertyTypeValidationModel(propertyType, null, $"{group.Path}[{i}].{valuesJsonPathPart}[{JsonPathExpression.MissingPropertyValue(propertyType.Alias, culture, segment)}].value")); + } + yield return elementValidation; } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs index b0e8ac46fc..4e410841a1 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs @@ -7,6 +7,7 @@ using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Cache.PropertyEditors; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.Validation; using Umbraco.Cms.Core.PropertyEditors.ValueConverters; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; @@ -69,7 +70,7 @@ public abstract class BlockGridPropertyEditorBase : DataEditor : base(textService) => _blockEditorValues = blockEditorValues; - public override IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + public override IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext) { if (dataTypeConfiguration is not BlockGridConfiguration blockConfig) { diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs index 2c194ba2c2..749937bd4d 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs @@ -5,6 +5,7 @@ using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Cache.PropertyEditors; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.Validation; using Umbraco.Cms.Core.PropertyEditors.ValueConverters; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; @@ -80,7 +81,7 @@ public abstract class BlockListPropertyEditorBase : DataEditor : base(textService) => _blockEditorValues = blockEditorValues; - public override IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + public override IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext) { var blockConfig = (BlockListConfiguration?)dataTypeConfiguration; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs index 6a3f73e9bb..0ce5f4bfdf 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs @@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.Text.RegularExpressions; using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Models.Validation; using Umbraco.Cms.Core.Serialization; using Umbraco.Extensions; @@ -25,7 +26,7 @@ internal class ColorPickerConfigurationEditor : ConfigurationEditor _configurationEditorJsonSerializer = configurationEditorJsonSerializer; - public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext) { var stringValue = value?.ToString(); if (stringValue.IsNullOrWhiteSpace()) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ComplexEditorValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/ComplexEditorValidator.cs index 9717eac14d..dea6c098fc 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ComplexEditorValidator.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ComplexEditorValidator.cs @@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Validation; using Umbraco.Cms.Core.PropertyEditors.Validation; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; @@ -19,18 +20,11 @@ public abstract class ComplexEditorValidator : IValueValidator public ComplexEditorValidator(IPropertyValidationService propertyValidationService) => _propertyValidationService = propertyValidationService; - /// - /// Return a single for all sub nested validation results in the complex - /// editor - /// - /// - /// - /// - /// - public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + /// + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext) { - var elementTypeValues = GetElementTypeValidation(value).ToList(); - var rowResults = GetNestedValidationResults(elementTypeValues).ToList(); + var elementTypeValues = GetElementTypeValidation(value, validationContext).ToList(); + var rowResults = GetNestedValidationResults(elementTypeValues, validationContext).ToList(); if (rowResults.Count > 0) { @@ -46,13 +40,14 @@ public abstract class ComplexEditorValidator : IValueValidator return Enumerable.Empty(); } - protected abstract IEnumerable GetElementTypeValidation(object? value); + protected abstract IEnumerable GetElementTypeValidation(object? value, PropertyValidationContext validationContext); /// /// Return a nested validation result per row (Element Type) /// protected IEnumerable GetNestedValidationResults( - IEnumerable elements) + IEnumerable elements, + PropertyValidationContext validationContext) { foreach (ElementTypeValidationModel row in elements) { @@ -63,7 +58,7 @@ public abstract class ComplexEditorValidator : IValueValidator var propValidationResult = new NestedJsonPathValidationResults(prop.JsonPath); foreach (ValidationResult validationResult in _propertyValidationService.ValidatePropertyValue( - prop.PropertyType, prop.PostedValue)) + prop.PropertyType, prop.PostedValue, validationContext)) { // add the result to the property results propValidationResult.ValidationResults.Add(validationResult); diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorBlockValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorBlockValidator.cs index c3a2a57c13..74428dbd30 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorBlockValidator.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorBlockValidator.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Cache.PropertyEditors; using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.Validation; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; @@ -25,7 +26,7 @@ internal class RichTextEditorBlockValidator: BlockEditorValidatorBase GetElementTypeValidation(object? value) + protected override IEnumerable GetElementTypeValidation(object? value, PropertyValidationContext validationContext) { RichTextPropertyEditorHelper.TryParseRichTextEditorValue(value, _jsonSerializer, _logger, out RichTextEditorValue? richTextEditorValue); if (richTextEditorValue?.Blocks is null) @@ -35,7 +36,7 @@ internal class RichTextEditorBlockValidator: BlockEditorValidatorBase? blockEditorData = _blockEditorValues.ConvertAndClean(richTextEditorValue.Blocks); return blockEditorData is not null - ? GetBlockEditorDataValidation(blockEditorData) + ? GetBlockEditorDataValidation(blockEditorData, validationContext) : Array.Empty(); } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/TemporaryFileUploadValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/TemporaryFileUploadValidator.cs index d53fe22068..71aba644cd 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/TemporaryFileUploadValidator.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/TemporaryFileUploadValidator.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models.TemporaryFile; +using Umbraco.Cms.Core.Models.Validation; using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors; @@ -32,7 +33,7 @@ internal class TemporaryFileUploadValidator : IValueValidator _validateFileType = validateFileType; } - public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext) { Guid? temporaryFileKey = _parseTemporaryFileKey(value); if (temporaryFileKey.HasValue == false) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueListUniqueValueValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueListUniqueValueValidator.cs index b8fd53be8a..9edde833b1 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueListUniqueValueValidator.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueListUniqueValueValidator.cs @@ -2,6 +2,7 @@ // See LICENSE for more details. using System.ComponentModel.DataAnnotations; +using Umbraco.Cms.Core.Models.Validation; using Umbraco.Cms.Core.Serialization; using Umbraco.Extensions; @@ -17,7 +18,7 @@ public class ValueListUniqueValueValidator : IValueValidator public ValueListUniqueValueValidator(IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) => _configurationEditorJsonSerializer = configurationEditorJsonSerializer; - public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext) { if (value is null) { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Publishing.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Publishing.cs index 29131e4fa6..5c5d077ca3 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Publishing.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Publishing.cs @@ -1548,4 +1548,332 @@ public partial class BlockListElementLevelVariationTests validateBlocks?.Invoke(value); } } + + [Test] + public async Task Can_Publish_Valid_Properties() + { + var elementType = CreateElementTypeWithValidation(); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType); + + var content = CreateContent(contentType, elementType, [], false); + var blockListValue = BlockListPropertyValue( + elementType, + [ + ( + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "Valid invariant content value" }, + new() { Alias = "variantText", Value = "Valid content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "Valid content value in Danish", Culture = "da-DK" } + }, + new List + { + new() { Alias = "invariantText", Value = "Valid invariant settings value" }, + new() { Alias = "variantText", Value = "Valid value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "Valid value in Danish", Culture = "da-DK" }, + }, + null, + null + ) + ) + ] + ); + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + + var publishResult = ContentService.Publish(content, ["en-US", "da-DK"]); + Assert.IsTrue(publishResult.Success); + Assert.IsTrue(publishResult.Content.PublishedCultures.Contains("en-US")); + Assert.IsTrue(publishResult.Content.PublishedCultures.Contains("da-DK")); + } + + [Test] + public async Task Can_Publish_Valid_Properties_Specific_Culture_Only() + { + var elementType = CreateElementTypeWithValidation(); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType); + + var content = CreateContent(contentType, elementType, [], false); + var blockListValue = BlockListPropertyValue( + elementType, + [ + ( + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "Valid invariant content value" }, + new() { Alias = "variantText", Value = "Valid content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "Valid content value in Danish", Culture = "da-DK" } + }, + new List + { + new() { Alias = "invariantText", Value = "Valid invariant settings value" }, + new() { Alias = "variantText", Value = "Valid value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "Valid value in Danish", Culture = "da-DK" }, + }, + null, + null + ) + ) + ] + ); + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + + var publishResult = ContentService.Publish(content, ["en-US"]); + Assert.IsTrue(publishResult.Success); + Assert.IsTrue(publishResult.Content.PublishedCultures.Contains("en-US")); + Assert.IsFalse(publishResult.Content.PublishedCultures.Contains("da-DK")); + } + + [Test] + public async Task Can_Publish_Valid_Properties_With_Wildcard_Culture() + { + var elementType = CreateElementTypeWithValidation(); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType); + + var content = CreateContent(contentType, elementType, [], false); + var blockListValue = BlockListPropertyValue( + elementType, + [ + ( + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "Valid invariant content value" }, + new() { Alias = "variantText", Value = "Valid content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "Valid content value in Danish", Culture = "da-DK" } + }, + new List + { + new() { Alias = "invariantText", Value = "Valid invariant settings value" }, + new() { Alias = "variantText", Value = "Valid value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "Valid value in Danish", Culture = "da-DK" }, + }, + null, + null + ) + ) + ] + ); + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + + var publishResult = ContentService.Publish(content, ["*"]); + Assert.IsTrue(publishResult.Success); + Assert.IsTrue(publishResult.Content.PublishedCultures.Contains("en-US")); + Assert.IsTrue(publishResult.Content.PublishedCultures.Contains("da-DK")); + } + + [TestCase(true)] + [TestCase(false)] + public async Task Cannot_Publish_Invalid_Invariant_Properties(bool invalidSettingsValue) + { + var elementType = CreateElementTypeWithValidation(); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType); + + var content = CreateContent(contentType, elementType, [], false); + var blockListValue = BlockListPropertyValue( + elementType, + [ + ( + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = $"{(invalidSettingsValue ? "Valid" : "Invalid")} invariant content value" }, + new() { Alias = "variantText", Value = "Valid content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "Valid content value in Danish", Culture = "da-DK" } + }, + new List + { + new() { Alias = "invariantText", Value = $"{(invalidSettingsValue ? "Invalid" : "Valid")} invariant settings value" }, + new() { Alias = "variantText", Value = "Valid value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "Valid value in Danish", Culture = "da-DK" }, + }, + null, + null + ) + ) + ] + ); + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + + var publishResult = ContentService.Publish(content, ["en-US", "da-DK"]); + Assert.Multiple(() => + { + Assert.IsFalse(publishResult.Success); + Assert.IsNotNull(publishResult.InvalidProperties); + Assert.AreEqual(1, publishResult.InvalidProperties.Count()); + Assert.AreEqual("blocks", publishResult.InvalidProperties.First().Alias); + }); + } + + [Test] + public async Task Cannot_Publish_Missing_Invariant_Properties() + { + var elementType = CreateElementTypeWithValidation(); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType); + + var content = CreateContent(contentType, elementType, [], false); + var blockListValue = BlockListPropertyValue( + elementType, + [ + ( + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List + { + new() { Alias = "variantText", Value = "Valid content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "Valid content value in Danish", Culture = "da-DK" } + }, + new List + { + new() { Alias = "variantText", Value = "Valid value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "Valid value in Danish", Culture = "da-DK" }, + }, + null, + null + ) + ) + ] + ); + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + + var publishResult = ContentService.Publish(content, ["en-US", "da-DK"]); + Assert.Multiple(() => + { + Assert.IsFalse(publishResult.Success); + Assert.IsNotNull(publishResult.InvalidProperties); + Assert.AreEqual(1, publishResult.InvalidProperties.Count()); + Assert.AreEqual("blocks", publishResult.InvalidProperties.First().Alias); + }); + } + + [TestCase(true)] + [TestCase(false)] + public async Task Cannot_Publish_Invalid_Variant_Properties(bool invalidSettingsValue) + { + var elementType = CreateElementTypeWithValidation(); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType); + + var content = CreateContent(contentType, elementType, [], false); + var blockListValue = BlockListPropertyValue( + elementType, + [ + ( + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "Valid invariant content value" }, + new() { Alias = "variantText", Value = "Valid content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = $"{(invalidSettingsValue ? "Valid" : "Invalid")} content value in Danish", Culture = "da-DK" } + }, + new List + { + new() { Alias = "invariantText", Value = "Valid invariant settings value" }, + new() { Alias = "variantText", Value = "Valid settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = $"{(invalidSettingsValue ? "Invalid" : "Valid")} settings value in Danish", Culture = "da-DK" }, + }, + null, + null + ) + ) + ] + ); + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + + var publishResult = ContentService.Publish(content, ["en-US"]); + Assert.IsTrue(publishResult.Success); + + publishResult = ContentService.Publish(content, ["da-DK"]); + Assert.Multiple(() => + { + Assert.IsFalse(publishResult.Success); + Assert.IsNotNull(publishResult.InvalidProperties); + Assert.AreEqual(1, publishResult.InvalidProperties.Count()); + Assert.AreEqual("blocks", publishResult.InvalidProperties.First().Alias); + }); + } + + [Test] + public async Task Cannot_Publish_Missing_Variant_Properties() + { + var elementType = CreateElementTypeWithValidation(); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType); + + var content = CreateContent(contentType, elementType, [], false); + var blockListValue = BlockListPropertyValue( + elementType, + [ + ( + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "Valid invariant content value" }, + new() { Alias = "variantText", Value = "Valid content value in English", Culture = "en-US" }, + }, + new List + { + new() { Alias = "invariantText", Value = "Valid invariant settings value" }, + new() { Alias = "variantText", Value = "Valid settings value in English", Culture = "en-US" }, + }, + null, + null + ) + ) + ] + ); + + // make sure all blocks are exposed + blockListValue.Expose = + [ + new() { ContentKey = blockListValue.ContentData[0].Key, Culture = "en-US" }, + new() { ContentKey = blockListValue.ContentData[0].Key, Culture = "da-DK" }, + ]; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + + var publishResult = ContentService.Publish(content, ["en-US"]); + Assert.IsTrue(publishResult.Success); + + publishResult = ContentService.Publish(content, ["da-DK"]); + Assert.Multiple(() => + { + Assert.IsFalse(publishResult.Success); + Assert.IsNotNull(publishResult.InvalidProperties); + Assert.AreEqual(1, publishResult.InvalidProperties.Count()); + Assert.AreEqual("blocks", publishResult.InvalidProperties.First().Alias); + }); + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Validation.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Validation.cs new file mode 100644 index 0000000000..e2bd4b3ffe --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Validation.cs @@ -0,0 +1,515 @@ +using NUnit.Framework; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.PropertyEditors; + +public partial class BlockListElementLevelVariationTests +{ + private IContentValidationService ContentValidationService => GetRequiredService(); + + [Test] + public async Task Can_Validate_Invalid_Properties() + { + var elementType = CreateElementTypeWithValidation(); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType); + var blockListValue = BlockListPropertyValue( + elementType, + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "Invalid invariant content value" }, + new() { Alias = "variantText", Value = "Valid content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "Invalid content value in Danish", Culture = "da-DK" }, + }, + new List + { + new() { Alias = "invariantText", Value = "Valid invariant settings value" }, + new() { Alias = "variantText", Value = "Invalid settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "Valid settings value in Danish", Culture = "da-DK" }, + }, + null, + null)); + + var result = await ContentValidationService.ValidatePropertiesAsync( + new ContentCreateModel + { + ContentTypeKey = contentType.Key, + Variants = + [ + new VariantModel { Name = "Name en-US", Properties = [], Culture = "en-US", Segment = null }, + new VariantModel { Name = "Name da-DK", Properties = [], Culture = "da-DK", Segment = null } + ], + InvariantProperties = + [ + new PropertyValueModel { Alias = "blocks", Value = JsonSerializer.Serialize(blockListValue) } + ] + }, + contentType); + + var errors = result.ValidationErrors.ToArray(); + Assert.Multiple(() => + { + Assert.AreEqual(3, errors.Length); + Assert.IsTrue(errors.All(error => error.Alias == "blocks" && error.Culture == null && error.Segment == null)); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[0].value")); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[2].value")); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".settingsData[0].values[1].value")); + }); + } + + [Test] + public async Task Can_Validate_Invalid_Properties_Nested_Blocks() + { + var (rootElementType, nestedElementType) = await CreateElementTypeWithValidationAndNestedBlocksAsync(); + var rootBlockListDataType = await CreateBlockListDataType(rootElementType); + var contentType = CreateContentType(ContentVariation.Culture, rootBlockListDataType); + + var blockListValue = BlockListPropertyValue( + rootElementType, + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List + { + new() + { + Alias = "nestedBlocks", + Value = BlockListPropertyValue( + nestedElementType, + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "Invalid nested invariant content value" }, + new() { Alias = "variantText", Value = "Valid nested content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "Invalid nested content value in Danish", Culture = "da-DK" }, + }, + new List + { + new() { Alias = "invariantText", Value = "Valid nested invariant settings value" }, + new() { Alias = "variantText", Value = "Invalid nested settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "Valid nested settings value in Danish", Culture = "da-DK" }, + }, + null, + null)) + }, + new() { Alias = "invariantText", Value = "Invalid invariant content value" }, + new() { Alias = "variantText", Value = "Valid content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "Invalid content value in Danish", Culture = "da-DK" }, + }, + new List + { + new() + { + Alias = "nestedBlocks", + Value = BlockListPropertyValue( + nestedElementType, + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "Valid nested invariant content value" }, + new() { Alias = "variantText", Value = "Invalid nested content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "Valid nested content value in Danish", Culture = "da-DK" }, + }, + new List + { + new() { Alias = "invariantText", Value = "Invalid nested invariant settings value" }, + new() { Alias = "variantText", Value = "Valid nested settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "Invalid nested settings value in Danish", Culture = "da-DK" }, + }, + null, + null)) + }, + new() { Alias = "invariantText", Value = "Valid invariant content value" }, + new() { Alias = "variantText", Value = "Invalid content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "Valid content value in Danish", Culture = "da-DK" }, + }, + null, + null)); + + var result = await ContentValidationService.ValidatePropertiesAsync( + new ContentCreateModel + { + ContentTypeKey = contentType.Key, + Variants = + [ + new VariantModel { Name = "Name en-US", Properties = [], Culture = "en-US", Segment = null }, + new VariantModel { Name = "Name da-DK", Properties = [], Culture = "da-DK", Segment = null } + ], + InvariantProperties = + [ + new PropertyValueModel { Alias = "blocks", Value = JsonSerializer.Serialize(blockListValue) } + ] + }, + contentType); + + var errors = result.ValidationErrors.ToArray(); + Assert.Multiple(() => + { + Assert.AreEqual(9, errors.Length); + Assert.IsTrue(errors.All(error => error.Alias == "blocks" && error.Culture == null && error.Segment == null)); + + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[0].value.contentData[0].values[0].value")); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[0].value.contentData[0].values[2].value")); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[0].value.settingsData[0].values[1].value")); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[1].value")); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[3].value")); + + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".settingsData[0].values[0].value.contentData[0].values[1].value")); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".settingsData[0].values[0].value.settingsData[0].values[0].value")); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".settingsData[0].values[0].value.settingsData[0].values[2].value")); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".settingsData[0].values[2].value")); + }); + } + + [Test] + public async Task Can_Validate_Invalid_Properties_Specific_Culture_Only() + { + var elementType = CreateElementTypeWithValidation(); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType); + var blockListValue = BlockListPropertyValue( + elementType, + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "Invalid invariant content value" }, + new() { Alias = "variantText", Value = "Valid content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "Invalid content value in Danish", Culture = "da-DK" }, + }, + new List + { + new() { Alias = "invariantText", Value = "Valid invariant settings value" }, + new() { Alias = "variantText", Value = "Invalid settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "Valid settings value in Danish", Culture = "da-DK" }, + }, + null, + null)); + + var result = await ContentValidationService.ValidatePropertiesAsync( + new ContentCreateModel + { + ContentTypeKey = contentType.Key, + Variants = + [ + new VariantModel { Name = "Name en-US", Properties = [], Culture = "en-US", Segment = null }, + new VariantModel { Name = "Name da-DK", Properties = [], Culture = "da-DK", Segment = null } + ], + InvariantProperties = + [ + new PropertyValueModel { Alias = "blocks", Value = JsonSerializer.Serialize(blockListValue) } + ] + }, + contentType, + new[] { "en-US" }); + + var errors = result.ValidationErrors.ToArray(); + Assert.Multiple(() => + { + Assert.AreEqual(2, errors.Length); + Assert.IsTrue(errors.All(error => error.Alias == "blocks" && error.Culture == null && error.Segment == null)); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[0].value")); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".settingsData[0].values[1].value")); + }); + } + + [Test] + public async Task Can_Validate_Invalid_Properties_With_Wildcard_Culture() + { + var elementType = CreateElementTypeWithValidation(); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType); + var blockListValue = BlockListPropertyValue( + elementType, + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "Invalid invariant content value" }, + new() { Alias = "variantText", Value = "Valid content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "Invalid content value in Danish", Culture = "da-DK" }, + }, + new List + { + new() { Alias = "invariantText", Value = "Valid invariant settings value" }, + new() { Alias = "variantText", Value = "Invalid settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "Valid settings value in Danish", Culture = "da-DK" }, + }, + null, + null)); + + var result = await ContentValidationService.ValidatePropertiesAsync( + new ContentCreateModel + { + ContentTypeKey = contentType.Key, + Variants = + [ + new VariantModel { Name = "Name en-US", Properties = [], Culture = "en-US", Segment = null }, + new VariantModel { Name = "Name da-DK", Properties = [], Culture = "da-DK", Segment = null } + ], + InvariantProperties = + [ + new PropertyValueModel { Alias = "blocks", Value = JsonSerializer.Serialize(blockListValue) } + ] + }, + contentType, + ["*"]); + + var errors = result.ValidationErrors.ToArray(); + Assert.Multiple(() => + { + Assert.AreEqual(3, errors.Length); + Assert.IsTrue(errors.All(error => error.Alias == "blocks" && error.Culture == null && error.Segment == null)); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[0].value")); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[2].value")); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".settingsData[0].values[1].value")); + }); + } + + [Test] + public async Task Can_Validate_Missing_Properties() + { + var elementType = CreateElementTypeWithValidation(); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType); + var blockListValue = BlockListPropertyValue( + elementType, + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List + { + // missing the mandatory "invariantText" (invariant) and "variantText" (in Danish) + new() { Alias = "variantText", Value = "Valid content value in English", Culture = "en-US" }, + }, + new List + { + // missing the mandatory "variantText" (in English) + new() { Alias = "invariantText", Value = "Valid invariant settings value" }, + new() { Alias = "variantText", Value = "Valid settings value in Danish", Culture = "da-DK" }, + }, + null, + null)); + + // make sure all blocks are exposed + blockListValue.Expose = + [ + new() { ContentKey = blockListValue.ContentData[0].Key, Culture = "en-US" }, + new() { ContentKey = blockListValue.ContentData[0].Key, Culture = "da-DK" }, + ]; + + var result = await ContentValidationService.ValidatePropertiesAsync( + new ContentCreateModel + { + ContentTypeKey = contentType.Key, + Variants = + [ + new VariantModel { Name = "Name en-US", Properties = [], Culture = "en-US", Segment = null }, + new VariantModel { Name = "Name da-DK", Properties = [], Culture = "da-DK", Segment = null } + ], + InvariantProperties = + [ + new PropertyValueModel { Alias = "blocks", Value = JsonSerializer.Serialize(blockListValue) } + ] + }, + contentType); + + var errors = result.ValidationErrors.ToArray(); + Assert.Multiple(() => + { + Assert.AreEqual(3, errors.Length); + Assert.IsTrue(errors.All(error => error.Alias == "blocks" && error.Culture == null && error.Segment == null)); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[?(@.alias == 'invariantText' && @.culture == null && @.segment == null)].value")); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[?(@.alias == 'variantText' && @.culture == 'da-DK' && @.segment == null)].value")); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".settingsData[0].values[?(@.alias == 'variantText' && @.culture == 'en-US' && @.segment == null)].value")); + }); + } + + [Test] + public async Task Can_Validate_Missing_Properties_Nested_Blocks_Specific_Culture_Only() + { + var (rootElementType, nestedElementType) = await CreateElementTypeWithValidationAndNestedBlocksAsync(); + var rootBlockListDataType = await CreateBlockListDataType(rootElementType); + var contentType = CreateContentType(ContentVariation.Culture, rootBlockListDataType); + + var nestedContentBlocks = BlockListPropertyValue( + nestedElementType, + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List + { + // missing the mandatory "invariantText" (invariant) and "variantText" (in Danish) + new() { Alias = "variantText", Value = "Valid nested content value in English", Culture = "en-US" }, + }, + new List + { + // missing the mandatory "variantText" (in English) + new() { Alias = "invariantText", Value = "Valid nested invariant settings value" }, + new() { Alias = "variantText", Value = "Valid nested settings value in Danish", Culture = "da-DK" }, + }, + null, + null)); + + var nestedSettingsBlocks = BlockListPropertyValue( + nestedElementType, + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List + { + // missing the mandatory "variantText" (in English) + new() { Alias = "invariantText", Value = "Valid nested invariant content value" }, + new() { Alias = "variantText", Value = "Valid nested content value in Danish", Culture = "da-DK" }, + }, + new List + { + // missing the mandatory "invariantText" (invariant) and "variantText" (in Danish) + new() { Alias = "variantText", Value = "Valid nested settings value in English", Culture = "en-US" }, + }, + null, + null)); + + // make sure all nested blocks are exposed + nestedContentBlocks.Expose = + [ + new() { ContentKey = nestedContentBlocks.ContentData[0].Key, Culture = "en-US" }, + new() { ContentKey = nestedContentBlocks.ContentData[0].Key, Culture = "da-DK" }, + ]; + nestedSettingsBlocks.Expose = + [ + new() { ContentKey = nestedSettingsBlocks.ContentData[0].Key, Culture = "en-US" }, + new() { ContentKey = nestedSettingsBlocks.ContentData[0].Key, Culture = "da-DK" }, + ]; + + var blockListValue = BlockListPropertyValue( + rootElementType, + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List + { + new() + { + Alias = "nestedBlocks", + Value = nestedContentBlocks + }, + // missing the mandatory "variantText" (in both English and Danish) + new() { Alias = "invariantText", Value = "Valid root invariant content value" } + }, + new List + { + new() + { + Alias = "nestedBlocks", + Value = nestedSettingsBlocks + }, + // missing the mandatory "invariantText" + new() { Alias = "variantText", Value = "Valid root settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "Valid root settings value in Danish", Culture = "da-DK" } + }, + null, + null)); + + // make sure all root blocks are exposed + blockListValue.Expose = + [ + new() { ContentKey = blockListValue.ContentData[0].Key, Culture = "en-US" }, + new() { ContentKey = blockListValue.ContentData[0].Key, Culture = "da-DK" }, + ]; + + var result = await ContentValidationService.ValidatePropertiesAsync( + new ContentCreateModel + { + ContentTypeKey = contentType.Key, + Variants = + [ + new VariantModel { Name = "Name en-US", Properties = [], Culture = "en-US", Segment = null }, + new VariantModel { Name = "Name da-DK", Properties = [], Culture = "da-DK", Segment = null } + ], + InvariantProperties = + [ + new PropertyValueModel { Alias = "blocks", Value = JsonSerializer.Serialize(blockListValue) } + ] + }, + contentType, + new[] { "da-DK" }); + + var errors = result.ValidationErrors.ToArray(); + Assert.Multiple(() => + { + Assert.AreEqual(6, errors.Length); + Assert.IsTrue(errors.All(error => error.Alias == "blocks" && error.Culture == null && error.Segment == null)); + + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[0].value.contentData[0].values[?(@.alias == 'invariantText' && @.culture == null && @.segment == null)].value")); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[0].value.contentData[0].values[?(@.alias == 'variantText' && @.culture == 'da-DK' && @.segment == null)].value")); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[?(@.alias == 'variantText' && @.culture == 'da-DK' && @.segment == null)].value")); + + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".settingsData[0].values[0].value.settingsData[0].values[?(@.alias == 'invariantText' && @.culture == null && @.segment == null)].value")); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".settingsData[0].values[0].value.settingsData[0].values[?(@.alias == 'variantText' && @.culture == 'da-DK' && @.segment == null)].value")); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".settingsData[0].values[?(@.alias == 'invariantText' && @.culture == null && @.segment == null)].value")); + }); + } + + [Test] + public async Task Does_Not_Validate_Unexposed_Blocks() + { + var elementType = CreateElementTypeWithValidation(); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType); + var blockListValue = BlockListPropertyValue( + elementType, + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "Valid invariant content value" }, + new() { Alias = "variantText", Value = "Valid content value in English", Culture = "en-US" }, + }, + new List + { + new() { Alias = "invariantText", Value = "Valid invariant settings value" }, + new() { Alias = "variantText", Value = "Valid settings value in English", Culture = "en-US" }, + }, + null, + null)); + + // only expose the block in English + blockListValue.Expose = + [ + new() { ContentKey = blockListValue.ContentData[0].Key, Culture = "en-US" }, + ]; + + var result = await ContentValidationService.ValidatePropertiesAsync( + new ContentCreateModel + { + ContentTypeKey = contentType.Key, + Variants = + [ + new VariantModel { Name = "Name en-US", Properties = [], Culture = "en-US", Segment = null }, + new VariantModel { Name = "Name da-DK", Properties = [], Culture = "da-DK", Segment = null } + ], + InvariantProperties = + [ + new PropertyValueModel { Alias = "blocks", Value = JsonSerializer.Serialize(blockListValue) } + ] + }, + contentType, + ["da-DK"]); + + Assert.IsEmpty(result.ValidationErrors); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.cs index 8a798a603c..53af4af967 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.cs @@ -146,6 +146,63 @@ public partial class BlockListElementLevelVariationTests : BlockEditorElementVar return GetPublishedContent(content.Key); } + private IContentType CreateElementTypeWithValidation() + { + var elementType = CreateElementType(ContentVariation.Culture); + foreach (var propertyType in elementType.PropertyTypes) + { + propertyType.Mandatory = true; + propertyType.ValidationRegExp = "^Valid.*$"; + } + + ContentTypeService.Save(elementType); + return elementType; + } + + private async Task<(IContentType RootElementType, IContentType NestedElementType)> CreateElementTypeWithValidationAndNestedBlocksAsync() + { + var nestedElementType = CreateElementTypeWithValidation(); + var nestedBlockListDataType = await CreateBlockListDataType(nestedElementType); + + var rootElementType = new ContentTypeBuilder() + .WithAlias("myRootElementType") + .WithName("My Root Element Type") + .WithIsElement(true) + .WithContentVariation(ContentVariation.Culture) + .AddPropertyType() + .WithAlias("invariantText") + .WithName("Invariant text") + .WithMandatory(true) + .WithValidationRegExp("^Valid.*$") + .WithDataTypeId(Constants.DataTypes.Textbox) + .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextBox) + .WithValueStorageType(ValueStorageType.Nvarchar) + .WithVariations(ContentVariation.Nothing) + .Done() + .AddPropertyType() + .WithAlias("variantText") + .WithName("Variant text") + .WithMandatory(true) + .WithValidationRegExp("^Valid.*$") + .WithDataTypeId(Constants.DataTypes.Textbox) + .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextBox) + .WithValueStorageType(ValueStorageType.Nvarchar) + .WithVariations(ContentVariation.Culture) + .Done() + .AddPropertyType() + .WithAlias("nestedBlocks") + .WithName("Nested blocks") + .WithDataTypeId(nestedBlockListDataType.Id) + .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.BlockList) + .WithValueStorageType(ValueStorageType.Ntext) + .WithVariations(ContentVariation.Nothing) + .Done() + .Build(); + ContentTypeService.Save(rootElementType); + + return (rootElementType, nestedElementType); + } + private class BlockProperty { public BlockProperty(IList blockContentValues, IList blockSettingsValues, string? culture, string? segment) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentValidationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentValidationServiceTests.cs index 00e8284135..5640afeae2 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentValidationServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentValidationServiceTests.cs @@ -102,6 +102,10 @@ public class ContentValidationServiceTests : UmbracoIntegrationTestWithContent { "alias": "text", "value": "Valid nested setting text)" } ] } + ], + "expose": [ + { "contentKey": "f36cebfa-d03b-4451-9e60-4bf32c5b1e2f", "culture": null, "segment": null }, + { "contentKey": "b8173e4a-0618-475c-8277-c3c6af68bee6", "culture": null, "segment": null } ] } } @@ -130,6 +134,10 @@ public class ContentValidationServiceTests : UmbracoIntegrationTestWithContent { "alias": "text", "value": "Invalid root setting text (ref #3)" } ] } + ], + "expose": [ + { "contentKey": "9addc377-c02c-4db0-88c2-73b933704f7b", "culture": null, "segment": null }, + { "contentKey": "3af93b5b-5e40-4c64-b142-2564309fc4c7", "culture": null, "segment": null } ] } """ diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index 705eb29f87..8f9910e07d 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj +++ b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -167,6 +167,9 @@ BlockListElementLevelVariationTests.cs + + BlockListElementLevelVariationTests.cs + DocumentNavigationServiceTests.cs diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ColorListValidatorTest.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ColorListValidatorTest.cs index c6e8fd5cf3..5ed3a5de3b 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ColorListValidatorTest.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ColorListValidatorTest.cs @@ -3,6 +3,7 @@ using System.Text.Json.Nodes; using NUnit.Framework; +using Umbraco.Cms.Core.Models.Validation; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Infrastructure.Serialization; @@ -23,7 +24,8 @@ public class ColorListValidatorTest validator.Validate( "hello", null, - null); + null, + PropertyValidationContext.Empty()); Assert.AreEqual(1, result.Count()); } @@ -35,7 +37,8 @@ public class ColorListValidatorTest validator.Validate( new JsonArray("hello", "world"), null, - null); + null, + PropertyValidationContext.Empty()); Assert.AreEqual(1, result.Count()); } @@ -51,7 +54,8 @@ public class ColorListValidatorTest JsonNode.Parse("""{"value": "ABC", "label": "Three"}"""), JsonNode.Parse("""{"value": "1234567", "label": "Four"}""")), null, - null); + null, + PropertyValidationContext.Empty()); Assert.AreEqual(2, result.Count()); } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/EnsureUniqueValuesValidatorTest.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/EnsureUniqueValuesValidatorTest.cs index 8192afda99..efb61bb0f2 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/EnsureUniqueValuesValidatorTest.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/EnsureUniqueValuesValidatorTest.cs @@ -3,6 +3,7 @@ using System.Text.Json.Nodes; using NUnit.Framework; +using Umbraco.Cms.Core.Models.Validation; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Infrastructure.Serialization; @@ -22,7 +23,8 @@ public class EnsureUniqueValuesValidatorTest var result = validator.Validate( "hello", null, - null); + null, + PropertyValidationContext.Empty()); Assert.AreEqual(1, result.Count()); } @@ -34,7 +36,8 @@ public class EnsureUniqueValuesValidatorTest validator.Validate( new JsonArray("hello", "world"), null, - null); + null, + PropertyValidationContext.Empty()); Assert.AreEqual(0, result.Count()); } @@ -46,7 +49,8 @@ public class EnsureUniqueValuesValidatorTest validator.Validate( new JsonArray("one", "two", "three"), null, - null); + null, + PropertyValidationContext.Empty()); Assert.AreEqual(0, result.Count()); } @@ -58,7 +62,8 @@ public class EnsureUniqueValuesValidatorTest validator.Validate( new JsonArray("one", "one"), null, - null); + null, + PropertyValidationContext.Empty()); Assert.AreEqual(1, result.Count()); } @@ -70,7 +75,8 @@ public class EnsureUniqueValuesValidatorTest validator.Validate( new JsonArray("one", "two", "three", "one", "two"), null, - null); + null, + PropertyValidationContext.Empty()); Assert.AreEqual(2, result.Count()); } @@ -82,7 +88,8 @@ public class EnsureUniqueValuesValidatorTest validator.Validate( null, null, - null); + null, + PropertyValidationContext.Empty()); Assert.AreEqual(0, result.Count()); } @@ -95,7 +102,8 @@ public class EnsureUniqueValuesValidatorTest validator.Validate( value, null, - null); + null, + PropertyValidationContext.Empty()); Assert.AreEqual(0, result.Count()); } @@ -108,7 +116,8 @@ public class EnsureUniqueValuesValidatorTest validator.Validate( value, null, - null); + null, + PropertyValidationContext.Empty()); Assert.AreEqual(0, result.Count()); } @@ -121,7 +130,8 @@ public class EnsureUniqueValuesValidatorTest validator.Validate( value, null, - null); + null, + PropertyValidationContext.Empty()); Assert.AreEqual(0, result.Count()); } }