Skip to content

Commit 51223f4

Browse files
Fix issue with server validation from dictionary configuration where floating point values can be be accessed as doubles or ints (#18508)
* Fix issue with server validation from dictionary configuration where floating point values can be be accessed as doubles or ints. * Fixed typo in comment. --------- Co-authored-by: Mole <[email protected]>
1 parent d164892 commit 51223f4

File tree

2 files changed

+73
-8
lines changed

2 files changed

+73
-8
lines changed

src/Umbraco.Core/PropertyEditors/Validators/DictionaryConfigurationValidatorBase.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ protected static bool TryGetConfiguredValue<TValue>(object? dataTypeConfiguratio
2121
return false;
2222
}
2323

24-
if (configuration.TryGetValue(key, out object? obj) && obj is TValue castValue)
24+
if (configuration.TryGetValue(key, out object? obj) && TryCastValue(obj, out TValue? castValue))
2525
{
2626
value = castValue;
2727
return true;
@@ -30,4 +30,24 @@ protected static bool TryGetConfiguredValue<TValue>(object? dataTypeConfiguratio
3030
value = default;
3131
return false;
3232
}
33+
34+
private static bool TryCastValue<TValue>(object? value, [NotNullWhen(true)] out TValue? castValue)
35+
{
36+
if (value is TValue valueAsType)
37+
{
38+
castValue = valueAsType;
39+
return true;
40+
}
41+
42+
// Special case for floating point numbers - when deserialized these will be integers if whole numbers rather
43+
// than double.
44+
if (typeof(TValue) == typeof(double) && value is int valueAsInt)
45+
{
46+
castValue = (TValue)(object)Convert.ToDouble(valueAsInt);
47+
return true;
48+
}
49+
50+
castValue = default;
51+
return false;
52+
}
3353
}

tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DecimalValueEditorTests.cs

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,25 @@ public void Validates_Is_Less_Than_Or_Equal_To_Configured_Max(object value, bool
132132
}
133133
}
134134

135+
[TestCase(1.8, true)]
136+
[TestCase(2.2, false)]
137+
public void Validates_Is_Less_Than_Or_Equal_To_Configured_Max_With_Configured_Whole_Numbers(object value, bool expectedSuccess)
138+
{
139+
var editor = CreateValueEditor(min: 1, max: 2);
140+
var result = editor.Validate(value, false, null, PropertyValidationContext.Empty());
141+
if (expectedSuccess)
142+
{
143+
Assert.IsEmpty(result);
144+
}
145+
else
146+
{
147+
Assert.AreEqual(1, result.Count());
148+
149+
var validationResult = result.First();
150+
Assert.AreEqual(validationResult.ErrorMessage, "validation_outOfRangeMaximum");
151+
}
152+
}
153+
135154
[TestCase(0.2, 1.4, false)]
136155
[TestCase(0.2, 1.5, true)]
137156
[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 void Validates_Matches_Configured_Step(double step, object value, bool ex
165184
return CreateValueEditor().ToEditor(property.Object);
166185
}
167186

168-
private static DecimalPropertyEditor.DecimalPropertyValueEditor CreateValueEditor(double step = 0.2)
187+
private static DecimalPropertyEditor.DecimalPropertyValueEditor CreateValueEditor(double min = 1.1, double max = 1.9, double step = 0.2)
169188
{
170189
var localizedTextServiceMock = new Mock<ILocalizedTextService>();
171190
localizedTextServiceMock.Setup(x => x.Localize(
@@ -174,19 +193,45 @@ private static DecimalPropertyEditor.DecimalPropertyValueEditor CreateValueEdito
174193
It.IsAny<CultureInfo>(),
175194
It.IsAny<IDictionary<string, string>>()))
176195
.Returns((string key, string alias, CultureInfo culture, IDictionary<string, string> args) => $"{key}_{alias}");
196+
197+
// When configuration is populated from the deserialized JSON, whole number values are deserialized as integers.
198+
// So we want to replicate that in our tests.
199+
var configuration = new Dictionary<string, object>();
200+
if (min % 1 == 0)
201+
{
202+
configuration.Add("min", (int)min);
203+
}
204+
else
205+
{
206+
configuration.Add("min", min);
207+
}
208+
209+
if (max % 1 == 0)
210+
{
211+
configuration.Add("max", (int)max);
212+
}
213+
else
214+
{
215+
configuration.Add("max", max);
216+
}
217+
218+
if (step % 1 == 0)
219+
{
220+
configuration.Add("step", (int)step);
221+
}
222+
else
223+
{
224+
configuration.Add("step", step);
225+
}
226+
177227
return new DecimalPropertyEditor.DecimalPropertyValueEditor(
178228
Mock.Of<IShortStringHelper>(),
179229
Mock.Of<IJsonSerializer>(),
180230
Mock.Of<IIOHelper>(),
181231
new DataEditorAttribute("alias"),
182232
localizedTextServiceMock.Object)
183233
{
184-
ConfigurationObject = new Dictionary<string, object>
185-
{
186-
{ "min", 1.1 },
187-
{ "max", 1.9 },
188-
{ "step", step }
189-
}
234+
ConfigurationObject = configuration
190235
};
191236
}
192237
}

0 commit comments

Comments
 (0)