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 <nikolajlauridsen@protonmail.ch>
This commit is contained in:
Andy Butland
2025-03-06 10:48:48 +01:00
committed by GitHub
parent 6d0dd82781
commit 45ea6a3cfc
13 changed files with 392 additions and 40 deletions

View File

@@ -120,6 +120,9 @@ Mange hilsner fra Umbraco robotten
<key alias="invalidStartNode">Valgt indhold kommer fra en ugyldig mappe.</key>
<key alias="outOfRangeMinimum">Værdien %0% er mindre end det tilladte minimum af %1%.</key>
<key alias="outOfRangeMaximum">Værdien %0% er større end det tilladte maksimum af %1%.</key>
<key alias="outOfRangeSingleItemMinimum">Den ene enhed givet er mindre end det tilladte minimum af %1%.</key>
<key alias="outOfRangeMultipleItemsMinimum">De %0% enheder givet er mindre end det tilladte minimum af %1%.</key>
<key alias="outOfRangeMultipleItemsMaximum">De %0% enheder givet er større end det tilladte minimum af %1%.</key>
<key alias="invalidStep">Værdien %0% passer ikke med den konfigureret trin værdi af %1% og mindste værdi af %2%.</key>
<key alias="unexpectedRange">Værdien %0% forventes ikke at indeholde et spænd.</key>
<key alias="invalidRange">Værdien %0% forventes at have en værdi der er større end fra værdien.</key>

View File

@@ -389,6 +389,9 @@
<key alias="duplicateUsername">Username '%0%' is already taken</key>
<key alias="outOfRangeMinimum">The value %0% is less than the allowed minimum value of %1%</key>
<key alias="outOfRangeMaximum">The value %0% is greater than the allowed maximum value of %1%</key>
<key alias="outOfRangeSingleItemMinimum">The 1 item provided is less than the allowed minimum of %1%</key>
<key alias="outOfRangeMultipleItemsMinimum">The %0% items provided are less than the allowed minimum of %1%</key>
<key alias="outOfRangeMultipleItemsMaximum">The %0% items provided are greater than the allowed maximum of %1%</key>
<key alias="invalidStep">The value %0% does not correspond with the configured step value of %1% and minimum value of %2%</key>
<key alias="unexpectedRange">The value %0% is not expected to contain a range</key>
<key alias="invalidRange">The value %0% is not expected to have a to value less than the from value</key>

View File

@@ -387,6 +387,9 @@
<key alias="duplicateUsername">Username '%0%' is already taken</key>
<key alias="outOfRangeMinimum">The value %0% is less than the allowed minimum value of %1%</key>
<key alias="outOfRangeMaximum">The value %0% is greater than the allowed maximum value of %1%</key>
<key alias="outOfRangeSingleItemMinimum">The 1 item provided is less than the allowed minimum of %1%</key>
<key alias="outOfRangeMultipleItemsMinimum">The %0% items provided are less than the allowed minimum of %1%</key>
<key alias="outOfRangeMultipleItemsMaximum">The %0% items provided are greater than the allowed maximum of %1%</key>
<key alias="invalidStep">The value %0% does not correspond with the configured step value of %1% and minimum value of %2%</key>
<key alias="unexpectedRange">The value %0% is not expected to contain a range</key>
<key alias="invalidRange">The value %0% is not expected to have a to value less than the from value</key>

View File

@@ -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<TValue, TLayout> : IValue
where TValue : BlockValue<TLayout>, new()
where TLayout : class, IBlockLayoutItem, new()
{
/// <summary>
/// Initializes a new instance of the <see cref="BlockEditorMinMaxValidatorBase{TValue, TLayout}"/> class.
/// </summary>
protected BlockEditorMinMaxValidatorBase(ILocalizedTextService textService) => TextService = textService;
/// <summary>
/// Gets the <see cref="ILocalizedTextService"/>
/// </summary>
protected ILocalizedTextService TextService { get; }
/// <inheritdoc/>
public abstract IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext);
/// <summary>
/// Validates the number of blocks are within the configured minimum and maximum values.
/// </summary>
protected IEnumerable<ValidationResult> ValidateNumberOfBlocks(BlockEditorData<TValue, TLayout>? blockEditorData, int? min, int? max)
{
var numberOfBlocks = blockEditorData?.Layout?.Count() ?? 0;
@@ -35,8 +45,8 @@ internal abstract class BlockEditorMinMaxValidatorBase<TValue, TLayout> : 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<TValue, TLayout> : IValue
TextService.Localize(
"validation",
"entriesExceed",
new[] { max.ToString(), (numberOfBlocks - max).ToString(), }),
new[] { "maxCount" });
[max.ToString(), (numberOfBlocks - max).ToString(),]),
["value"]);
}
}
}

View File

@@ -9,7 +9,7 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors;
/// <summary>
/// Represents a block list property editor.
/// Represents a block list property editor.
/// </summary>
[DataEditor(
Constants.PropertyEditors.Aliases.BlockList,
@@ -19,6 +19,9 @@ public class BlockListPropertyEditor : BlockListPropertyEditorBase
{
private readonly IIOHelper _ioHelper;
/// <summary>
/// Initializes a new instance of the <see cref="BlockListPropertyEditor"/> class.
/// </summary>
public BlockListPropertyEditor(
IDataValueEditorFactory dataValueEditorFactory,
IIOHelper ioHelper,
@@ -27,6 +30,9 @@ public class BlockListPropertyEditor : BlockListPropertyEditorBase
: base(dataValueEditorFactory, blockValuePropertyIndexValueFactory, jsonSerializer)
=> _ioHelper = ioHelper;
/// <summary>
/// Initializes a new instance of the <see cref="BlockListPropertyEditor"/> class.
/// </summary>
[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
{
}
/// <inheritdoc/>
public override bool SupportsConfigurableElements => true;
/// <inheritdoc />
@@ -50,6 +57,7 @@ public class BlockListPropertyEditor : BlockListPropertyEditorBase
return valueEditor.MergePartialPropertyValueForCulture(sourceValue, targetValue, culture);
}
/// <inheritdoc/>
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
/// <inheritdoc/>
protected override IConfigurationEditor CreateConfigurationEditor() =>
new BlockListConfigurationEditor(_ioHelper);
#endregion
}

View File

@@ -24,12 +24,18 @@ public abstract class BlockListPropertyEditorBase : DataEditor
private readonly IBlockValuePropertyIndexValueFactory _blockValuePropertyIndexValueFactory;
private readonly IJsonSerializer _jsonSerializer;
/// <summary>
/// Initializes a new instance of the <see cref="BlockListPropertyEditorBase"/> class.
/// </summary>
[Obsolete("Use non-obsoleted ctor. This will be removed in Umbraco 15.")]
protected BlockListPropertyEditorBase(IDataValueEditorFactory dataValueEditorFactory, IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory)
: this(dataValueEditorFactory,blockValuePropertyIndexValueFactory, StaticServiceProvider.Instance.GetRequiredService<IJsonSerializer>())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="BlockListPropertyEditorBase"/> class.
/// </summary>
protected BlockListPropertyEditorBase(IDataValueEditorFactory dataValueEditorFactory, IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory, IJsonSerializer jsonSerializer)
: base(dataValueEditorFactory)
{
@@ -38,21 +44,27 @@ public abstract class BlockListPropertyEditorBase : DataEditor
SupportsReadOnly = true;
}
/// <inheritdoc/>
public override IPropertyIndexValueFactory PropertyIndexValueFactory => _blockValuePropertyIndexValueFactory;
#region Value Editor
/// <summary>
/// Instantiates a new <see cref="BlockEditorDataConverter"/> for use with the block list editor property value editor.
/// Instantiates a new <see cref="BlockEditorDataConverter{BlockListValue, BlockListLayoutItem}"/> for use with the block list editor property value editor.
/// </summary>
/// <returns>A new instance of <see cref="BlockListEditorDataConverter"/>.</returns>
protected virtual BlockEditorDataConverter<BlockListValue, BlockListLayoutItem> CreateBlockEditorDataConverter() => new BlockListEditorDataConverter(_jsonSerializer);
/// <inheritdoc/>
protected override IDataValueEditor CreateValueEditor() =>
DataValueEditorFactory.Create<BlockListEditorPropertyValueEditor>(Attribute!, CreateBlockEditorDataConverter());
/// <summary>
/// Defines the value editor for the block list property editors.
/// </summary>
internal class BlockListEditorPropertyValueEditor : BlockEditorPropertyValueEditor<BlockListValue, BlockListLayoutItem>
{
/// <summary>
/// Initializes a new instance of the <see cref="BlockListEditorPropertyValueEditor"/> class.
/// </summary>
public BlockListEditorPropertyValueEditor(
DataEditorAttribute attribute,
BlockEditorDataConverter<BlockListValue, BlockListLayoutItem> blockEditorDataConverter,
@@ -78,8 +90,12 @@ public abstract class BlockListPropertyEditorBase : DataEditor
/// <inheritdoc />
public override IValueRequiredValidator RequiredValidator => new BlockListValueRequiredValidator(JsonSerializer);
/// <inheritdoc/>
protected override BlockListValue CreateWithLayout(IEnumerable<BlockListLayoutItem> layout) => new(layout);
/// <summary>
/// Validates the min/max configuration for block list property editors.
/// </summary>
private class MinMaxValidator : BlockEditorMinMaxValidatorBase<BlockListValue, BlockListLayoutItem>
{
private readonly BlockEditorValues<BlockListValue, BlockListLayoutItem> _blockEditorValues;
@@ -104,12 +120,11 @@ public abstract class BlockListPropertyEditorBase : DataEditor
}
}
/// <inheritdoc/>
public override IEnumerable<Guid> ConfiguredElementTypeKeys()
{
var configuration = ConfigurationObject as BlockListConfiguration;
return configuration?.Blocks.SelectMany(ConfiguredElementTypeKeys) ?? Enumerable.Empty<Guid>();
}
}
#endregion
}

View File

@@ -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;
/// <summary>
/// Represents a multiple text string property editor.
/// Represents a multiple text string property editor.
/// </summary>
[DataEditor(
Constants.PropertyEditors.Aliases.MultipleTextstring,
@@ -24,7 +27,7 @@ public class MultipleTextStringPropertyEditor : DataEditor
private readonly IIOHelper _ioHelper;
/// <summary>
/// Initializes a new instance of the <see cref="MultipleTextStringPropertyEditor" /> class.
/// Initializes a new instance of the <see cref="MultipleTextStringPropertyEditor" /> class.
/// </summary>
public MultipleTextStringPropertyEditor(IIOHelper ioHelper, IDataValueEditorFactory dataValueEditorFactory)
: base(dataValueEditorFactory)
@@ -42,37 +45,42 @@ public class MultipleTextStringPropertyEditor : DataEditor
new MultipleTextStringConfigurationEditor(_ioHelper);
/// <summary>
/// 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.
/// </summary>
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" };
/// <summary>
/// Initializes a new instance of the <see cref="MultipleTextStringPropertyValueEditor"/> class.
/// </summary>
public MultipleTextStringPropertyValueEditor(
IShortStringHelper shortStringHelper,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper,
DataEditorAttribute attribute)
DataEditorAttribute attribute,
ILocalizedTextService localizedTextService)
: base(shortStringHelper, jsonSerializer, ioHelper, attribute)
{
Validators.AddRange(new MinMaxValidator(localizedTextService));
}
/// <summary>
/// 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 <see href="IValueFormatValidator" /> 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.
/// </summary>
public override IValueFormatValidator FormatValidator => new MultipleTextStringFormatValidator();
/// <summary>
/// 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.
/// </summary>
/// <param name="editorValue"></param>
/// <param name="currentValue"></param>
/// <returns></returns>
/// <remarks>
/// 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.
/// </remarks>
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);
}
/// <inheritdoc/>
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<string>();
}
}
/// <summary>
/// A custom <see href="IValueFormatValidator" /> to check each string against the configured format.
/// </summary>
internal class MultipleTextStringFormatValidator : IValueFormatValidator
{
/// <inheritdoc/>
public IEnumerable<ValidationResult> ValidateFormat(object? value, string valueType, string format)
{
if (value is not IEnumerable<string> textStrings)
{
return Enumerable.Empty<ValidationResult>();
return [];
}
var textStringValidator = new RegexValidator();
@@ -129,7 +142,60 @@ public class MultipleTextStringPropertyEditor : DataEditor
}
}
return Enumerable.Empty<ValidationResult>();
return [];
}
}
/// <summary>
/// Validates the min/max configuration for the multiple text strings property editor.
/// </summary>
internal class MinMaxValidator : IValueValidator
{
private readonly ILocalizedTextService _localizedTextService;
/// <summary>
/// Initializes a new instance of the <see cref="MinMaxValidator"/> class.
/// </summary>
public MinMaxValidator(ILocalizedTextService localizedTextService) => _localizedTextService = localizedTextService;
/// <inheritdoc/>
public IEnumerable<ValidationResult> 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<string> 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"]);
}
}
}
}

View File

@@ -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<ILocalizedTextService>();
localizedTextServiceMock.Setup(x => x.Localize(
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<CultureInfo>(),
It.IsAny<IDictionary<string, string>>()))
.Returns((string key, string alias, CultureInfo culture, IDictionary<string, string> args) => $"{key}_{alias}");
var jsonSerializer = new SystemTextJsonSerializer();
var languageService = Mock.Of<ILanguageService>();
return new BlockListEditorPropertyValueEditor(
new DataEditorAttribute("alias"),
new BlockListEditorDataConverter(jsonSerializer),
new(new DataEditorCollection(() => [])),
new DataValueReferenceFactoryCollection(Enumerable.Empty<IDataValueReferenceFactory>),
Mock.Of<IDataTypeConfigurationCache>(),
Mock.Of<IBlockEditorElementTypeCache>(),
localizedTextServiceMock.Object,
new NullLogger<BlockListEditorPropertyValueEditor>(),
Mock.Of<IShortStringHelper>(),
jsonSerializer,
Mock.Of<IPropertyValidationService>(),
new BlockEditorVarianceHandler(languageService, Mock.Of<IContentTypeService>()),
languageService,
Mock.Of<IIOHelper>())
{
ConfigurationObject = new BlockListConfiguration
{
ValidationLimit = new BlockListConfiguration.NumberRange
{
Min = 2,
Max = 4
},
},
};
}
}

View File

@@ -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)]

View File

@@ -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<object?,object?> _valuesAndExpectedResults = new();

View File

@@ -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<object?,object?> _valuesAndExpectedResults = new();

View File

@@ -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<ILocalizedTextService>();
localizedTextServiceMock.Setup(x => x.Localize(
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<CultureInfo>(),
It.IsAny<IDictionary<string, string>>()))
.Returns((string key, string alias, CultureInfo culture, IDictionary<string, string> args) => $"{key}_{alias}");
return new MultipleTextStringPropertyEditor.MultipleTextStringPropertyValueEditor(
Mock.Of<IShortStringHelper>(),
Mock.Of<IJsonSerializer>(),
Mock.Of<IIOHelper>(),
new DataEditorAttribute("alias"));
return valueEditor;
new DataEditorAttribute("alias"),
localizedTextServiceMock.Object)
{
ConfigurationObject = new MultipleTextStringConfiguration
{
Min = 2,
Max = 4
},
};
}
}

View File

@@ -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[]