From 45ea6a3cfc13f2db3ab088823e033acabbf45cb4 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Thu, 6 Mar 2025 10:48:48 +0100 Subject: [PATCH] Server side validation for property editors (multiple text strings) (#18581) * Server-side validation for multiple text strings property editor. * Added unit tests for block list min/max server validation. * Add danish translations * Add test showing issue * Fix issue --------- Co-authored-by: mole --- .../EmbeddedResources/Lang/da.xml | 3 + .../EmbeddedResources/Lang/en.xml | 3 + .../EmbeddedResources/Lang/en_us.xml | 3 + .../BlockEditorMinMaxValidatorBase.cs | 20 ++- .../BlockListPropertyEditor.cs | 15 +- .../BlockListPropertyEditorBase.cs | 25 ++- .../MultipleTextStringPropertyEditor.cs | 98 +++++++++-- ...BlockListEditorPropertyValueEditorTests.cs | 165 ++++++++++++++++++ ...=> ColorPickerPropertyValueEditorTests.cs} | 2 +- ....cs => DecimalPropertyValueEditorTests.cs} | 2 +- ....cs => IntegerPropertyValueEditorTests.cs} | 2 +- ...ipleTextStringPropertyValueEditorTests.cs} | 92 +++++++++- ...s.cs => SliderPropertyValueEditorTests.cs} | 2 +- 13 files changed, 392 insertions(+), 40 deletions(-) create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListEditorPropertyValueEditorTests.cs rename tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/{ColorPickerValueEditorTests.cs => ColorPickerPropertyValueEditorTests.cs} (97%) rename tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/{DecimalValueEditorTests.cs => DecimalPropertyValueEditorTests.cs} (99%) rename tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/{IntegerValueEditorTests.cs => IntegerPropertyValueEditorTests.cs} (99%) rename tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/{MultipleTextStringValueEditorTests.cs => MultipleTextStringPropertyValueEditorTests.cs} (55%) rename tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/{SliderValueEditorTests.cs => SliderPropertyValueEditorTests.cs} (99%) diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml index 0cb7b9e781..090958bbd1 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml @@ -120,6 +120,9 @@ Mange hilsner fra Umbraco robotten Valgt indhold kommer fra en ugyldig mappe. Værdien %0% er mindre end det tilladte minimum af %1%. Værdien %0% er større end det tilladte maksimum af %1%. + Den ene enhed givet er mindre end det tilladte minimum af %1%. + De %0% enheder givet er mindre end det tilladte minimum af %1%. + De %0% enheder givet er større end det tilladte minimum af %1%. Værdien %0% passer ikke med den konfigureret trin værdi af %1% og mindste værdi af %2%. Værdien %0% forventes ikke at indeholde et spænd. Værdien %0% forventes at have en værdi der er større end fra værdien. diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml index 651341cff0..bb6d39d3fc 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml @@ -389,6 +389,9 @@ Username '%0%' is already taken The value %0% is less than the allowed minimum value of %1% The value %0% is greater than the allowed maximum value of %1% + The 1 item provided is less than the allowed minimum of %1% + The %0% items provided are less than the allowed minimum of %1% + The %0% items provided are greater than the allowed maximum of %1% The value %0% does not correspond with the configured step value of %1% and minimum value of %2% The value %0% is not expected to contain a range The value %0% is not expected to have a to value less than the from value diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index 1b82f44600..bb2ba79a3b 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -387,6 +387,9 @@ Username '%0%' is already taken The value %0% is less than the allowed minimum value of %1% The value %0% is greater than the allowed maximum value of %1% + The 1 item provided is less than the allowed minimum of %1% + The %0% items provided are less than the allowed minimum of %1% + The %0% items provided are greater than the allowed maximum of %1% The value %0% does not correspond with the configured step value of %1% and minimum value of %2% The value %0% is not expected to contain a range The value %0% is not expected to have a to value less than the from value diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorMinMaxValidatorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorMinMaxValidatorBase.cs index f8f1dde8a1..f970ae8f9a 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorMinMaxValidatorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorMinMaxValidatorBase.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System.ComponentModel.DataAnnotations; @@ -16,12 +16,22 @@ internal abstract class BlockEditorMinMaxValidatorBase : IValue where TValue : BlockValue, new() where TLayout : class, IBlockLayoutItem, new() { + /// + /// Initializes a new instance of the class. + /// protected BlockEditorMinMaxValidatorBase(ILocalizedTextService textService) => TextService = textService; + /// + /// Gets the + /// protected ILocalizedTextService TextService { get; } + /// public abstract IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext); + /// + /// Validates the number of blocks are within the configured minimum and maximum values. + /// protected IEnumerable ValidateNumberOfBlocks(BlockEditorData? blockEditorData, int? min, int? max) { var numberOfBlocks = blockEditorData?.Layout?.Count() ?? 0; @@ -35,8 +45,8 @@ internal abstract class BlockEditorMinMaxValidatorBase : IValue TextService.Localize( "validation", "entriesShort", - new[] { min.ToString(), (min - numberOfBlocks).ToString(), }), - new[] { "minCount" }); + [min.ToString(), (min - numberOfBlocks).ToString(),]), + ["value"]); } } @@ -46,8 +56,8 @@ internal abstract class BlockEditorMinMaxValidatorBase : IValue TextService.Localize( "validation", "entriesExceed", - new[] { max.ToString(), (numberOfBlocks - max).ToString(), }), - new[] { "maxCount" }); + [max.ToString(), (numberOfBlocks - max).ToString(),]), + ["value"]); } } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs index eee452892f..816fad7098 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs @@ -9,7 +9,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors; /// -/// Represents a block list property editor. +/// Represents a block list property editor. /// [DataEditor( Constants.PropertyEditors.Aliases.BlockList, @@ -19,6 +19,9 @@ public class BlockListPropertyEditor : BlockListPropertyEditorBase { private readonly IIOHelper _ioHelper; + /// + /// Initializes a new instance of the class. + /// public BlockListPropertyEditor( IDataValueEditorFactory dataValueEditorFactory, IIOHelper ioHelper, @@ -27,6 +30,9 @@ public class BlockListPropertyEditor : BlockListPropertyEditorBase : base(dataValueEditorFactory, blockValuePropertyIndexValueFactory, jsonSerializer) => _ioHelper = ioHelper; + /// + /// Initializes a new instance of the class. + /// [Obsolete("Use constructor that doesn't take PropertyEditorCollection, scheduled for removal in V15")] public BlockListPropertyEditor( IDataValueEditorFactory dataValueEditorFactory, @@ -38,6 +44,7 @@ public class BlockListPropertyEditor : BlockListPropertyEditorBase { } + /// public override bool SupportsConfigurableElements => true; /// @@ -50,6 +57,7 @@ public class BlockListPropertyEditor : BlockListPropertyEditorBase return valueEditor.MergePartialPropertyValueForCulture(sourceValue, targetValue, culture); } + /// public override object? MergeVariantInvariantPropertyValue( object? sourceValue, object? targetValue, @@ -60,10 +68,7 @@ public class BlockListPropertyEditor : BlockListPropertyEditorBase return valueEditor.MergeVariantInvariantPropertyValue(sourceValue, targetValue, canUpdateInvariantData, allowedCultures); } - #region Pre Value Editor - + /// protected override IConfigurationEditor CreateConfigurationEditor() => new BlockListConfigurationEditor(_ioHelper); - - #endregion } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs index 25d24db6d0..feb44a784c 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs @@ -24,12 +24,18 @@ public abstract class BlockListPropertyEditorBase : DataEditor private readonly IBlockValuePropertyIndexValueFactory _blockValuePropertyIndexValueFactory; private readonly IJsonSerializer _jsonSerializer; + /// + /// Initializes a new instance of the class. + /// [Obsolete("Use non-obsoleted ctor. This will be removed in Umbraco 15.")] protected BlockListPropertyEditorBase(IDataValueEditorFactory dataValueEditorFactory, IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory) : this(dataValueEditorFactory,blockValuePropertyIndexValueFactory, StaticServiceProvider.Instance.GetRequiredService()) { } + /// + /// Initializes a new instance of the class. + /// protected BlockListPropertyEditorBase(IDataValueEditorFactory dataValueEditorFactory, IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory, IJsonSerializer jsonSerializer) : base(dataValueEditorFactory) { @@ -38,21 +44,27 @@ public abstract class BlockListPropertyEditorBase : DataEditor SupportsReadOnly = true; } + /// public override IPropertyIndexValueFactory PropertyIndexValueFactory => _blockValuePropertyIndexValueFactory; - #region Value Editor - /// - /// Instantiates a new for use with the block list editor property value editor. + /// Instantiates a new for use with the block list editor property value editor. /// /// A new instance of . protected virtual BlockEditorDataConverter CreateBlockEditorDataConverter() => new BlockListEditorDataConverter(_jsonSerializer); + /// protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create(Attribute!, CreateBlockEditorDataConverter()); + /// + /// Defines the value editor for the block list property editors. + /// internal class BlockListEditorPropertyValueEditor : BlockEditorPropertyValueEditor { + /// + /// Initializes a new instance of the class. + /// public BlockListEditorPropertyValueEditor( DataEditorAttribute attribute, BlockEditorDataConverter blockEditorDataConverter, @@ -78,8 +90,12 @@ public abstract class BlockListPropertyEditorBase : DataEditor /// public override IValueRequiredValidator RequiredValidator => new BlockListValueRequiredValidator(JsonSerializer); + /// protected override BlockListValue CreateWithLayout(IEnumerable layout) => new(layout); + /// + /// Validates the min/max configuration for block list property editors. + /// private class MinMaxValidator : BlockEditorMinMaxValidatorBase { private readonly BlockEditorValues _blockEditorValues; @@ -104,12 +120,11 @@ public abstract class BlockListPropertyEditorBase : DataEditor } } + /// public override IEnumerable ConfiguredElementTypeKeys() { var configuration = ConfigurationObject as BlockListConfiguration; return configuration?.Blocks.SelectMany(ConfiguredElementTypeKeys) ?? Enumerable.Empty(); } } - - #endregion } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/MultipleTextStringPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/MultipleTextStringPropertyEditor.cs index 7b86cb1bb4..9fe8dbe4ec 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/MultipleTextStringPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/MultipleTextStringPropertyEditor.cs @@ -6,14 +6,17 @@ using Umbraco.Cms.Core.Exceptions; 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; using Umbraco.Cms.Core.Strings; +using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors; /// -/// Represents a multiple text string property editor. +/// Represents a multiple text string property editor. /// [DataEditor( Constants.PropertyEditors.Aliases.MultipleTextstring, @@ -24,7 +27,7 @@ public class MultipleTextStringPropertyEditor : DataEditor private readonly IIOHelper _ioHelper; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public MultipleTextStringPropertyEditor(IIOHelper ioHelper, IDataValueEditorFactory dataValueEditorFactory) : base(dataValueEditorFactory) @@ -42,37 +45,42 @@ public class MultipleTextStringPropertyEditor : DataEditor new MultipleTextStringConfigurationEditor(_ioHelper); /// - /// Custom value editor so we can format the value for the editor and the database + /// Defines the value editor for the multiple text string property editor. /// internal class MultipleTextStringPropertyValueEditor : DataValueEditor { - private static readonly string NewLine = "\n"; - private static readonly string[] NewLineDelimiters = { "\r\n", "\r", "\n" }; + private static readonly string _newLine = "\n"; + private static readonly string[] _newLineDelimiters = { "\r\n", "\r", "\n" }; + /// + /// Initializes a new instance of the class. + /// public MultipleTextStringPropertyValueEditor( IShortStringHelper shortStringHelper, IJsonSerializer jsonSerializer, IIOHelper ioHelper, - DataEditorAttribute attribute) + DataEditorAttribute attribute, + ILocalizedTextService localizedTextService) : base(shortStringHelper, jsonSerializer, ioHelper, attribute) { + Validators.AddRange(new MinMaxValidator(localizedTextService)); } /// - /// A custom FormatValidator is used as for multiple text strings, each string should individually be checked - /// against the configured regular expression, rather than the JSON representing all the strings as a whole. + /// A custom is used as for multiple text strings, each string should individually + /// be checked against the configured regular expression, rather than the JSON representing all the strings as a whole. /// public override IValueFormatValidator FormatValidator => new MultipleTextStringFormatValidator(); /// - /// The value passed in from the editor will be an array of simple objects so we'll need to parse them to get the - /// string + /// The value passed in from the editor will be an array of simple objects so we'll need to parse them to get the + /// string. /// /// /// /// /// - /// We will also check the pre-values here, if there are more items than what is allowed we'll just trim the end + /// We will also check the pre-values here, if there are more items than what is allowed we'll just trim the end. /// public override object? FromEditor(ContentPropertyData editorValue, object? currentValue) { @@ -93,30 +101,35 @@ public class MultipleTextStringPropertyEditor : DataEditor // only allow the max if over 0 if (max > 0) { - return string.Join(NewLine, value.Take(max)); + return string.Join(_newLine, value.Take(max)); } - return string.Join(NewLine, value); + return string.Join(_newLine, value); } + /// public override object ToEditor(IProperty property, string? culture = null, string? segment = null) { var value = property.GetValue(culture, segment); // The legacy property editor saved this data as new line delimited! strange but we have to maintain that. return value is string stringValue - ? stringValue.Split(NewLineDelimiters, StringSplitOptions.None) + ? stringValue.Split(_newLineDelimiters, StringSplitOptions.None) : Array.Empty(); } } + /// + /// A custom to check each string against the configured format. + /// internal class MultipleTextStringFormatValidator : IValueFormatValidator { + /// public IEnumerable ValidateFormat(object? value, string valueType, string format) { if (value is not IEnumerable textStrings) { - return Enumerable.Empty(); + return []; } var textStringValidator = new RegexValidator(); @@ -129,7 +142,60 @@ public class MultipleTextStringPropertyEditor : DataEditor } } - return Enumerable.Empty(); + return []; + } + } + + /// + /// Validates the min/max configuration for the multiple text strings property editor. + /// + internal class MinMaxValidator : IValueValidator + { + private readonly ILocalizedTextService _localizedTextService; + + /// + /// Initializes a new instance of the class. + /// + public MinMaxValidator(ILocalizedTextService localizedTextService) => _localizedTextService = localizedTextService; + + /// + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext) + { + if (dataTypeConfiguration is not MultipleTextStringConfiguration multipleTextStringConfiguration) + { + yield break; + } + + // If we have a null value, treat as an empty collection for minimum number validation. + if (value is not IEnumerable stringValues) + { + stringValues = []; + } + + var stringCount = stringValues.Count(); + + if (stringCount < multipleTextStringConfiguration.Min) + { + if (stringCount == 1) + { + yield return new ValidationResult( + _localizedTextService.Localize("validation", "outOfRangeSingleItemMinimum", [multipleTextStringConfiguration.Min.ToString()]), + ["value"]); + } + else + { + yield return new ValidationResult( + _localizedTextService.Localize("validation", "outOfRangeMultipleItemsMinimum", [stringCount.ToString(), multipleTextStringConfiguration.Min.ToString()]), + ["value"]); + } + } + + if (multipleTextStringConfiguration.Max > 0 && stringCount > multipleTextStringConfiguration.Max) + { + yield return new ValidationResult( + _localizedTextService.Localize("validation", "outOfRangeMultipleItemsMaximum", [stringCount.ToString(), multipleTextStringConfiguration.Max.ToString()]), + ["value"]); + } } } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListEditorPropertyValueEditorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListEditorPropertyValueEditorTests.cs new file mode 100644 index 0000000000..9edc858532 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListEditorPropertyValueEditorTests.cs @@ -0,0 +1,165 @@ +using System.Globalization; +using System.Text.Json.Nodes; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Cache.PropertyEditors; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.Validation; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.PropertyEditors.ValueConverters; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Strings; +using Umbraco.Cms.Infrastructure.Serialization; +using static Umbraco.Cms.Core.PropertyEditors.BlockListPropertyEditorBase; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; + +[TestFixture] +public class BlockListEditorPropertyValueEditorTests +{ + [Test] + public void Validates_Null_As_Below_Configured_Min() + { + var editor = CreateValueEditor(); + var result = editor.Validate(null, false, null, PropertyValidationContext.Empty()); + Assert.AreEqual(1, result.Count()); + + var validationResult = result.First(); + Assert.AreEqual($"validation_entriesShort", validationResult.ErrorMessage); + } + + [TestCase(0, false)] + [TestCase(1, false)] + [TestCase(2, true)] + [TestCase(3, true)] + public void Validates_Number_Of_Items_Is_Greater_Than_Or_Equal_To_Configured_Min(int numberOfBlocks, bool expectedSuccess) + { + var value = CreateBlocksJson(numberOfBlocks); + var editor = CreateValueEditor(); + var result = editor.Validate(value, false, null, PropertyValidationContext.Empty()); + if (expectedSuccess) + { + Assert.IsEmpty(result); + } + else + { + Assert.AreEqual(1, result.Count()); + + var validationResult = result.First(); + Assert.AreEqual("validation_entriesShort", validationResult.ErrorMessage); + } + } + + [TestCase(3, true)] + [TestCase(4, true)] + [TestCase(5, false)] + public void Validates_Number_Of_Items_Is_Less_Than_Or_Equal_To_Configured_Max(int numberOfBlocks, bool expectedSuccess) + { + var value = CreateBlocksJson(numberOfBlocks); + var editor = CreateValueEditor(); + var result = editor.Validate(value, false, null, PropertyValidationContext.Empty()); + if (expectedSuccess) + { + Assert.IsEmpty(result); + } + else + { + Assert.AreEqual(1, result.Count()); + + var validationResult = result.First(); + Assert.AreEqual("validation_entriesExceed", validationResult.ErrorMessage); + } + } + + private static JsonObject CreateBlocksJson(int numberOfBlocks) + { + var layoutItems = new JsonArray(); + var contentData = new JsonArray(); + for (int i = 0; i < numberOfBlocks; i++) + { + layoutItems.Add(CreateLayoutBlockJson()); + contentData.Add(CreateContentDataBlockJson()); + } + + return new JsonObject + { + { + "layout", new JsonObject + { + { "Umbraco.BlockList", layoutItems }, + } + }, + { "contentData", contentData }, + }; + } + + private static JsonObject CreateLayoutBlockJson() => + new() + { + { "$type", "BlockListLayoutItem" }, + { "contentKey", Guid.NewGuid() }, + }; + + private static JsonObject CreateContentDataBlockJson() => + new() + { + { "contentTypeKey", Guid.Parse("01935a73-c86b-4521-9dcb-ad7cea402215") }, + { "key", Guid.NewGuid() }, + { + "values", + new JsonArray + { + new JsonObject + { + { "editorAlias", "Umbraco.TextBox" }, + { "alias", "message" }, + { "value", "Hello" }, + }, + } + } + }; + + private static BlockListEditorPropertyValueEditor CreateValueEditor() + { + var localizedTextServiceMock = new Mock(); + localizedTextServiceMock.Setup(x => x.Localize( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>())) + .Returns((string key, string alias, CultureInfo culture, IDictionary args) => $"{key}_{alias}"); + + var jsonSerializer = new SystemTextJsonSerializer(); + var languageService = Mock.Of(); + + return new BlockListEditorPropertyValueEditor( + new DataEditorAttribute("alias"), + new BlockListEditorDataConverter(jsonSerializer), + new(new DataEditorCollection(() => [])), + new DataValueReferenceFactoryCollection(Enumerable.Empty), + Mock.Of(), + Mock.Of(), + localizedTextServiceMock.Object, + new NullLogger(), + Mock.Of(), + jsonSerializer, + Mock.Of(), + new BlockEditorVarianceHandler(languageService, Mock.Of()), + languageService, + Mock.Of()) + { + ConfigurationObject = new BlockListConfiguration + { + ValidationLimit = new BlockListConfiguration.NumberRange + { + Min = 2, + Max = 4 + }, + }, + }; + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ColorPickerValueEditorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ColorPickerPropertyValueEditorTests.cs similarity index 97% rename from tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ColorPickerValueEditorTests.cs rename to tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ColorPickerPropertyValueEditorTests.cs index b008d07a61..17b49f6b13 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ColorPickerValueEditorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ColorPickerPropertyValueEditorTests.cs @@ -13,7 +13,7 @@ using Umbraco.Cms.Core.Strings; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; [TestFixture] -public class ColorPickerValueEditorTests +public class ColorPickerPropertyValueEditorTests { [TestCase("#ffffff", true)] [TestCase("#f0f0f0", false)] diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DecimalValueEditorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DecimalPropertyValueEditorTests.cs similarity index 99% rename from tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DecimalValueEditorTests.cs rename to tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DecimalPropertyValueEditorTests.cs index efe459a2ed..92b4f8579a 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DecimalValueEditorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DecimalPropertyValueEditorTests.cs @@ -14,7 +14,7 @@ using Umbraco.Cms.Core.Strings; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; [TestFixture] -public class DecimalValueEditorTests +public class DecimalPropertyValueEditorTests { // annoyingly we can't use decimals etc. in attributes, so we can't turn these into test cases :( private Dictionary _valuesAndExpectedResults = new(); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/IntegerValueEditorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/IntegerPropertyValueEditorTests.cs similarity index 99% rename from tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/IntegerValueEditorTests.cs rename to tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/IntegerPropertyValueEditorTests.cs index 1617d0edb6..a0c0a38ab3 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/IntegerValueEditorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/IntegerPropertyValueEditorTests.cs @@ -14,7 +14,7 @@ using Umbraco.Cms.Core.Strings; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; [TestFixture] -public class IntegerValueEditorTests +public class IntegerPropertyValueEditorTests { // annoyingly we can't use decimals etc. in attributes, so we can't turn these into test cases :( private Dictionary _valuesAndExpectedResults = new(); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MultipleTextStringValueEditorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MultipleTextStringPropertyValueEditorTests.cs similarity index 55% rename from tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MultipleTextStringValueEditorTests.cs rename to tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MultipleTextStringPropertyValueEditorTests.cs index 94452a2ec4..c3a814075a 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MultipleTextStringValueEditorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MultipleTextStringPropertyValueEditorTests.cs @@ -1,9 +1,12 @@ -using Moq; +using System.Globalization; +using System.Text.Json.Nodes; +using Moq; using NUnit.Framework; using Umbraco.Cms.Core; 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; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; @@ -12,7 +15,7 @@ using Umbraco.Cms.Core.Strings; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; [TestFixture] -public class MultipleTextStringValueEditorTests +public class MultipleTextStringPropertyValueEditorTests { [Test] public void Can_Handle_Invalid_Values_From_Editor() @@ -114,6 +117,71 @@ public class MultipleTextStringValueEditorTests Assert.IsEmpty(result); } + [Test] + public void Validates_Null_As_Below_Configured_Min() + { + var editor = CreateValueEditor(); + var result = editor.Validate(null, false, null, PropertyValidationContext.Empty()); + Assert.AreEqual(1, result.Count()); + + var validationResult = result.First(); + Assert.AreEqual($"validation_outOfRangeMultipleItemsMinimum", validationResult.ErrorMessage); + } + + [TestCase(0, false, "outOfRangeMultipleItemsMinimum")] + [TestCase(1, false, "outOfRangeSingleItemMinimum")] + [TestCase(2, true, "")] + [TestCase(3, true, "")] + public void Validates_Number_Of_Items_Is_Greater_Than_Or_Equal_To_Configured_Min(int numberOfStrings, bool expectedSuccess, string expectedValidationMessageKey) + { + var value = Enumerable.Range(1, numberOfStrings).Select(x => x.ToString()); + var editor = CreateValueEditor(); + var result = editor.Validate(value, false, null, PropertyValidationContext.Empty()); + if (expectedSuccess) + { + Assert.IsEmpty(result); + } + else + { + Assert.AreEqual(1, result.Count()); + + var validationResult = result.First(); + Assert.AreEqual($"validation_{expectedValidationMessageKey}", validationResult.ErrorMessage); + } + } + + [TestCase(3, true)] + [TestCase(4, true)] + [TestCase(5, false)] + public void Validates_Number_Of_Items_Is_Less_Than_Or_Equal_To_Configured_Max(int numberOfStrings, bool expectedSuccess) + { + var value = Enumerable.Range(1, numberOfStrings).Select(x => x.ToString()); + var editor = CreateValueEditor(); + var result = editor.Validate(value, false, null, PropertyValidationContext.Empty()); + if (expectedSuccess) + { + Assert.IsEmpty(result); + } + else + { + Assert.AreEqual(1, result.Count()); + + var validationResult = result.First(); + Assert.AreEqual("validation_outOfRangeMultipleItemsMaximum", validationResult.ErrorMessage); + } + } + + [Test] + public void Max_Item_Validation_Respects_0_As_Unlimited() + { + var value = Enumerable.Range(1, 100).Select(x => x.ToString()); + var editor = CreateValueEditor(); + editor.ConfigurationObject = new MultipleTextStringConfiguration(); + + var result = editor.Validate(value, false, null, PropertyValidationContext.Empty()); + Assert.IsEmpty(result); + } + private static object? FromEditor(object? value, int max = 0) => CreateValueEditor().FromEditor(new ContentPropertyData(value, new MultipleTextStringConfiguration { Max = max }), null); @@ -129,11 +197,25 @@ public class MultipleTextStringValueEditorTests private static MultipleTextStringPropertyEditor.MultipleTextStringPropertyValueEditor CreateValueEditor() { - var valueEditor = new MultipleTextStringPropertyEditor.MultipleTextStringPropertyValueEditor( + var localizedTextServiceMock = new Mock(); + localizedTextServiceMock.Setup(x => x.Localize( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>())) + .Returns((string key, string alias, CultureInfo culture, IDictionary args) => $"{key}_{alias}"); + return new MultipleTextStringPropertyEditor.MultipleTextStringPropertyValueEditor( Mock.Of(), Mock.Of(), Mock.Of(), - new DataEditorAttribute("alias")); - return valueEditor; + new DataEditorAttribute("alias"), + localizedTextServiceMock.Object) + { + ConfigurationObject = new MultipleTextStringConfiguration + { + Min = 2, + Max = 4 + }, + }; } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/SliderValueEditorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/SliderPropertyValueEditorTests.cs similarity index 99% rename from tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/SliderValueEditorTests.cs rename to tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/SliderPropertyValueEditorTests.cs index 3b3a473f38..744f2b59c0 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/SliderValueEditorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/SliderPropertyValueEditorTests.cs @@ -15,7 +15,7 @@ using Umbraco.Cms.Infrastructure.Serialization; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; [TestFixture] -public class SliderValueEditorTests +public class SliderPropertyValueEditorTests { #pragma warning disable IDE1006 // Naming Styles public static object[] InvalidCaseData = new object[]