diff --git a/src/Umbraco.Core/PropertyEditors/Validators/DictionaryConfigurationValidatorBase.cs b/src/Umbraco.Core/PropertyEditors/Validators/DictionaryConfigurationValidatorBase.cs index 0ed0af57e2..ce9dc4dff0 100644 --- a/src/Umbraco.Core/PropertyEditors/Validators/DictionaryConfigurationValidatorBase.cs +++ b/src/Umbraco.Core/PropertyEditors/Validators/DictionaryConfigurationValidatorBase.cs @@ -21,7 +21,7 @@ public abstract class DictionaryConfigurationValidatorBase return false; } - if (configuration.TryGetValue(key, out object? obj) && obj is TValue castValue) + if (configuration.TryGetValue(key, out object? obj) && TryCastValue(obj, out TValue? castValue)) { value = castValue; return true; @@ -30,4 +30,24 @@ public abstract class DictionaryConfigurationValidatorBase value = default; return false; } + + private static bool TryCastValue(object? value, [NotNullWhen(true)] out TValue? castValue) + { + if (value is TValue valueAsType) + { + castValue = valueAsType; + return true; + } + + // Special case for floating point numbers - when deserialized these will be integers if whole numbers rather + // than double. + if (typeof(TValue) == typeof(double) && value is int valueAsInt) + { + castValue = (TValue)(object)Convert.ToDouble(valueAsInt); + return true; + } + + castValue = default; + return false; + } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DecimalValueEditorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DecimalValueEditorTests.cs index 95d92c511b..c50c1b1301 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DecimalValueEditorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DecimalValueEditorTests.cs @@ -132,6 +132,25 @@ public class DecimalValueEditorTests } } + [TestCase(1.8, true)] + [TestCase(2.2, false)] + public void Validates_Is_Less_Than_Or_Equal_To_Configured_Max_With_Configured_Whole_Numbers(object value, bool expectedSuccess) + { + var editor = CreateValueEditor(min: 1, max: 2); + 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(validationResult.ErrorMessage, "validation_outOfRangeMaximum"); + } + } + [TestCase(0.2, 1.4, false)] [TestCase(0.2, 1.5, true)] [TestCase(0.0, 1.4, true)] // A step of zero would trigger a divide by zero error in evaluating. So we always pass validation for zero, as effectively any step value is valid. @@ -165,7 +184,7 @@ public class DecimalValueEditorTests return CreateValueEditor().ToEditor(property.Object); } - private static DecimalPropertyEditor.DecimalPropertyValueEditor CreateValueEditor(double step = 0.2) + private static DecimalPropertyEditor.DecimalPropertyValueEditor CreateValueEditor(double min = 1.1, double max = 1.9, double step = 0.2) { var localizedTextServiceMock = new Mock(); localizedTextServiceMock.Setup(x => x.Localize( @@ -174,6 +193,37 @@ public class DecimalValueEditorTests It.IsAny(), It.IsAny>())) .Returns((string key, string alias, CultureInfo culture, IDictionary args) => $"{key}_{alias}"); + + // When configuration is populated from the deserialized JSON, whole number values are deserialized as integers. + // So we want to replicate that in our tests. + var configuration = new Dictionary(); + if (min % 1 == 0) + { + configuration.Add("min", (int)min); + } + else + { + configuration.Add("min", min); + } + + if (max % 1 == 0) + { + configuration.Add("max", (int)max); + } + else + { + configuration.Add("max", max); + } + + if (step % 1 == 0) + { + configuration.Add("step", (int)step); + } + else + { + configuration.Add("step", step); + } + return new DecimalPropertyEditor.DecimalPropertyValueEditor( Mock.Of(), Mock.Of(), @@ -181,12 +231,7 @@ public class DecimalValueEditorTests new DataEditorAttribute("alias"), localizedTextServiceMock.Object) { - ConfigurationObject = new Dictionary - { - { "min", 1.1 }, - { "max", 1.9 }, - { "step", step } - } + ConfigurationObject = configuration }; } }