Skip to content

Commit 71e5c64

Browse files
committed
[add] expand test coverage for edge cases in binding, validation, and HTTP parsing
- Add Turkish culture test for ListToModelParser to verify ToLowerInvariant usage - Add de-DE culture test for StringToSpecifiedObjectParser decimal parsing - Add malformed If-Modified-Since header test for HeaderTimeExtensions - Add null RouteParameters test for Controller2Executor (NullRef vs InvalidOp) - Add custom error message tests for Min/Max/Range validation attributes - Add closed generic List<T> test for ValidationAttributesExecutor - Add TestModelWithIdProperty fixture for Turkish culture binding test
1 parent 0cbd9fa commit 71e5c64

21 files changed

Lines changed: 273 additions & 30 deletions

File tree

src/Simplify.Web.Tests/Controllers/V2/Execution/Controller2ExecutorTests.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Collections.Generic;
1+
using System;
2+
using System.Collections.Generic;
23
using System.Threading.Tasks;
34
using Moq;
45
using NUnit.Framework;
@@ -245,4 +246,22 @@ public async Task ExecuteAsync_AllParamsController_InvokedAndContentResponseRetu
245246

246247
_controllerFactory.Verify(x => x.CreateController(It.Is<IMatchedController>(c => c == mc)));
247248
}
249+
250+
[Test]
251+
public void ExecuteAsync_ParameterizedControllerWithNullRouteParameters_InvalidOperationExceptionNotNullReferenceException()
252+
{
253+
// Arrange
254+
255+
// RouteParameters is null for Global/Forbidden/NotFound V2 handlers (they never go
256+
// through route matching), so a parameterized Invoke used to throw NullReferenceException.
257+
var md = new Controller2Metadata(typeof(AllParamsController));
258+
var mc = new MatchedController(md);
259+
var controller = new AllParamsController();
260+
261+
_controllerFactory.Setup(x => x.CreateController(It.Is<IMatchedController>(c => c == mc)))
262+
.Returns(controller);
263+
264+
// Act & Assert
265+
Assert.ThrowsAsync<InvalidOperationException>(async () => await _executor.ExecuteAsync(mc));
266+
}
248267
}

src/Simplify.Web.Tests/Http/RequestTime/HeaderTimeExtensionsTests.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,21 @@ public void GetIfModifiedSinceTime_NoExists_Null()
4242
// Assert
4343
Assert.That(result, Is.Null);
4444
}
45+
46+
[Test]
47+
public void GetIfModifiedSinceTime_MalformedValue_NullInsteadOfException()
48+
{
49+
// Arrange
50+
51+
// Not exact RFC1123 format (e.g. a non-GMT-suffixed or obsolete date some
52+
// proxies/clients send) - this used to throw an unhandled FormatException.
53+
var headers = new Mock<IHeaderDictionary>();
54+
headers.SetupGet(x => x[It.Is<string>(p => p == "If-Modified-Since")]).Returns("not-a-valid-date");
55+
headers.Setup(x => x.ContainsKey(It.Is<string>(p => p == "If-Modified-Since"))).Returns(true);
56+
57+
// Act & Assert
58+
DateTime? result = null;
59+
Assert.DoesNotThrow(() => result = headers.Object.GetIfModifiedSinceTime());
60+
Assert.That(result, Is.Null);
61+
}
4562
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace Simplify.Web.Tests.Model.Binding.Parsers.ListToModelParserTestTypes;
2+
3+
public class TestModelWithIdProperty
4+
{
5+
public int Id { get; set; }
6+
}

src/Simplify.Web.Tests/Model/Binding/Parsers/ListToModelParserTests.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Globalization;
4+
using System.Threading;
35
using NUnit.Framework;
46
using Simplify.Web.Model.Binding;
57
using Simplify.Web.Model.Binding.Parsers;
@@ -125,6 +127,36 @@ public void Parse_WithExcludedProperty_Ignored()
125127
Assert.That(obj.Prop1, Is.Null);
126128
}
127129

130+
[Test]
131+
public void Parse_IdPropertyUnderTurkishCulture_Bound()
132+
{
133+
// Arrange
134+
135+
// Under tr-TR, "Id".ToLower() yields "ıd" (dotless i), which no longer matches the
136+
// lowercase request key "id" if ToLower() (culture-sensitive) is used instead of
137+
// ToLowerInvariant().
138+
var originalCulture = Thread.CurrentThread.CurrentCulture;
139+
Thread.CurrentThread.CurrentCulture = new CultureInfo("tr-TR");
140+
141+
try
142+
{
143+
var coll = new List<KeyValuePair<string, string[]>>
144+
{
145+
new("id", ["5"])
146+
};
147+
148+
// Act
149+
var obj = ListToModelParser.Parse<TestModelWithIdProperty>(coll);
150+
151+
// Assert
152+
Assert.That(obj.Id, Is.EqualTo(5));
153+
}
154+
finally
155+
{
156+
Thread.CurrentThread.CurrentCulture = originalCulture;
157+
}
158+
}
159+
128160
[Test]
129161
public void Parse_StringsArray_ModelNotSupportedExceptionThrown()
130162
{

src/Simplify.Web.Tests/Model/Binding/Parsers/StringToSpecifiedObjectParserTests.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System;
2+
using System.Globalization;
3+
using System.Threading;
24
using NUnit.Framework;
35
using Simplify.Web.Model.Binding;
46
using Simplify.Web.Model.Binding.Parsers;
@@ -151,6 +153,30 @@ public void ParseUndefined_NullableDecimalNull_Null() =>
151153
public void ParseUndefined_NullableDecimalBadValue_ExceptionThrown() =>
152154
Assert.Throws<ModelBindingException>(() => StringToSpecifiedObjectParser.ParseUndefined("test", typeof(decimal?)));
153155

156+
[Test]
157+
public void ParseDecimal_DeCultureThousandsSeparatorLookingValue_InvariantCultureParsed()
158+
{
159+
// Arrange
160+
161+
// Under de-DE, "." is a thousands separator, so a culture-sensitive parse would
162+
// silently misread "1.234" as 1234 instead of failing or reading it as invariant.
163+
var originalCulture = Thread.CurrentThread.CurrentCulture;
164+
Thread.CurrentThread.CurrentCulture = new CultureInfo("de-DE");
165+
166+
try
167+
{
168+
// Act
169+
var result = StringToSpecifiedObjectParser.ParseDecimal("1.234");
170+
171+
// Assert
172+
Assert.That(result, Is.EqualTo(1.234m));
173+
}
174+
finally
175+
{
176+
Thread.CurrentThread.CurrentCulture = originalCulture;
177+
}
178+
}
179+
154180
[Test]
155181
public void ParseUndefined_Long_Parsed() =>
156182
Assert.That(StringToSpecifiedObjectParser.ParseUndefined("15", typeof(long)), Is.EqualTo((long)15));

src/Simplify.Web.Tests/Model/Validation/Attributes/MaxAttributeTests/IntegerMaxAttributeTests.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,24 @@ public void Validate_NullValue_NoExceptions() =>
4343
public void Validate_DifferentTypes_ExceptionThrown() =>
4444
// Act & Assert
4545
Assert.Throws<ArgumentException>(() => TestAttributeForValidValue((long)15.2));
46+
47+
[Test]
48+
public void Validate_BelowMaxValueWithCustomMessage_NoExceptionThrown()
49+
{
50+
// Arrange
51+
var attribute = new MaxAttribute(MaxValue, "custom message", false);
52+
53+
// Act & Assert
54+
attribute.Validate(10, PropertyInfo, Resolver!);
55+
}
56+
57+
[Test]
58+
public void Validate_AboveMaxValueWithCustomMessage_CustomExceptionThrown()
59+
{
60+
// Arrange
61+
var attribute = new MaxAttribute(MaxValue, "custom message", false);
62+
63+
// Act & Assert
64+
TestAttribute(15, "custom message", attribute);
65+
}
4666
}

src/Simplify.Web.Tests/Model/Validation/Attributes/MinAttributeTests/IntegerMinAttributeTests.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,24 @@ public void Validate_NullValue_NoExceptions() =>
4444
public void Validate_DifferentTypes_ExceptionThrown() =>
4545
// Act & Assert
4646
Assert.Throws<ArgumentException>(() => TestAttributeForValidValue(12.5));
47+
48+
[Test]
49+
public void Validate_AboveMinValueWithCustomMessage_NoExceptionThrown()
50+
{
51+
// Arrange
52+
var attribute = new MinAttribute(MinValue, "custom message", false);
53+
54+
// Act & Assert
55+
attribute.Validate(15, PropertyInfo, Resolver!);
56+
}
57+
58+
[Test]
59+
public void Validate_BelowMinValueWithCustomMessage_CustomExceptionThrown()
60+
{
61+
// Arrange
62+
var attribute = new MinAttribute(MinValue, "custom message", false);
63+
64+
// Act & Assert
65+
TestAttribute(8, "custom message", attribute);
66+
}
4767
}

src/Simplify.Web.Tests/Model/Validation/Attributes/RangeAttributeTests/IntegerRangeAttributeTests.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,24 @@ public void Validate_NullValue_NoExceptions() =>
6060
public void Validate_DifferentTypes_ExceptionThrown() =>
6161
// Act & Assert
6262
Assert.Throws<ArgumentException>(() => TestAttributeForValidValue((long)7));
63+
64+
[Test]
65+
public void Validate_ValueInRangeWithCustomMessage_NoExceptionThrown()
66+
{
67+
// Arrange
68+
var attribute = new Web.Model.Validation.Attributes.RangeAttribute(MinValue, MaxValue, "custom message", false);
69+
70+
// Act & Assert
71+
attribute.Validate(10, PropertyInfo, Resolver!);
72+
}
73+
74+
[Test]
75+
public void Validate_OutOfRangeWithCustomMessage_CustomExceptionThrown()
76+
{
77+
// Arrange
78+
var attribute = new Web.Model.Validation.Attributes.RangeAttribute(MinValue, MaxValue, "custom message", false);
79+
80+
// Act & Assert
81+
TestAttribute(13, "custom message", attribute);
82+
}
6383
}

src/Simplify.Web.Tests/Model/Validation/ValidationAttributesExecutorTests.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,22 @@ public void Validate_Array_NoExceptions()
108108
// Act
109109
_validator.Validate(model, null!);
110110
}
111+
112+
[Test]
113+
public void Validate_ClosedGenericList_NoExceptions()
114+
{
115+
// Arrange
116+
117+
// Statically typed as List<TestModel> (not IList<TestModel>) so the generic type
118+
// definition resolved via reflection is List<>, not IList<> - this is the case that
119+
// used to slip past IsGenericList and crash reflecting over the compiler-generated
120+
// indexer property.
121+
var model = new List<TestModel>
122+
{
123+
new() { Prop1 = "test" }
124+
};
125+
126+
// Act
127+
_validator.Validate(model, null!);
128+
}
111129
}

src/Simplify.Web.Tests/Modules/Redirection/RedirectorTests.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,13 @@ public void Redirect_ProtocolRelativeUrl_ThrowsSecurityException() =>
241241
public void Redirect_BackslashSchemeSpoof_ThrowsSecurityException() =>
242242
Assert.Throws<SecurityException>(() => _redirector.Redirect("/\\evil.com"));
243243

244+
[Test]
245+
public void Redirect_TabControlCharSchemeSpoof_ThrowsSecurityException() =>
246+
// Browsers strip control characters like tab when resolving a Location header, so
247+
// "/\t/evil.com" would otherwise pass the leading-slash check and then be resolved
248+
// by the browser as the protocol-relative "//evil.com".
249+
Assert.Throws<SecurityException>(() => _redirector.Redirect("/\t/evil.com"));
250+
244251
[Test]
245252
public void Redirect_HostPrefixCollision_ThrowsSecurityException()
246253
{

0 commit comments

Comments
 (0)