Skip to content

Commit 68c8fdd

Browse files
Copilotcaptainsafia
andcommitted
Update RuntimeValidatableTypeInfoResolver tests to verify validation errors
Co-authored-by: captainsafia <1857993+captainsafia@users.noreply.github.com>
1 parent 1015335 commit 68c8fdd

File tree

1 file changed

+263
-1
lines changed

1 file changed

+263
-1
lines changed

src/Http/Http.Abstractions/test/Validation/RuntimeValidatableTypeInfoResolverTests.cs

Lines changed: 263 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,33 @@
33
// Licensed to the .NET Foundation under one or more agreements.
44
// The .NET Foundation licenses this file to you under the MIT license.
55

6+
using System.Collections.Generic;
67
using System.ComponentModel.DataAnnotations;
8+
using System.Text.Json.Serialization;
9+
using System.Threading.Tasks;
710

811
namespace Microsoft.AspNetCore.Http.Validation.Tests;
912

1013
public class RuntimeValidatableTypeInfoResolverTests
1114
{
1215
private readonly RuntimeValidatableTypeInfoResolver _resolver = new();
16+
private readonly ValidationOptions _validationOptions;
17+
18+
public RuntimeValidatableTypeInfoResolverTests()
19+
{
20+
_validationOptions = new ValidationOptions();
21+
22+
// Register our resolver in the validation options
23+
_validationOptions.Resolvers.Add(_resolver);
24+
}
1325

1426
[Fact]
1527
public void TryGetValidatableParameterInfo_AlwaysReturnsFalse()
1628
{
1729
var parameterInfo = typeof(RuntimeValidatableTypeInfoResolverTests).GetMethod(nameof(TestMethod))!.GetParameters()[0];
30+
var resolver = new RuntimeValidatableTypeInfoResolver();
1831

19-
var result = _resolver.TryGetValidatableParameterInfo(parameterInfo, out var validatableInfo);
32+
var result = resolver.TryGetValidatableParameterInfo(parameterInfo, out var validatableInfo);
2033

2134
Assert.False(result);
2235
Assert.Null(validatableInfo);
@@ -88,6 +101,195 @@ public void TryGetValidatableTypeInfo_WithCyclicReference_HandlesGracefully()
88101
// Should not throw StackOverflowException due to cycle detection
89102
}
90103

104+
[Fact]
105+
public async Task ValidateAsync_WithRequiredAndRangeValidation_ReturnsErrors()
106+
{
107+
// Arrange
108+
var person = new PersonWithValidation
109+
{
110+
Name = "", // Invalid - required
111+
Age = 150 // Invalid - range is 0-120
112+
};
113+
114+
var validationResult = await ValidateInstanceAsync(person);
115+
116+
// Assert
117+
Assert.Equal(2, validationResult.Count);
118+
Assert.Contains(validationResult, e => e.Key == "Name");
119+
Assert.Contains(validationResult, e => e.Key == "Age");
120+
Assert.Contains("required", validationResult["Name"][0]);
121+
Assert.Contains("between", validationResult["Age"][0]);
122+
Assert.Contains("0", validationResult["Age"][0]);
123+
Assert.Contains("120", validationResult["Age"][0]);
124+
}
125+
126+
[Fact]
127+
public async Task ValidateAsync_WithDisplayAttribute_UsesDisplayNameInError()
128+
{
129+
// Arrange
130+
var personWithDisplayName = new PersonWithDisplayName
131+
{
132+
FirstName = "" // Invalid - required
133+
};
134+
135+
var validationResult = await ValidateInstanceAsync(personWithDisplayName);
136+
137+
// Assert
138+
Assert.Single(validationResult);
139+
// Check that the error message contains "First Name" (the display name) rather than "FirstName"
140+
Assert.Contains("First Name", validationResult["FirstName"][0]);
141+
}
142+
143+
[Fact]
144+
public async Task ValidateAsync_WithNestedValidation_ValidatesNestedProperties()
145+
{
146+
// Arrange
147+
var personWithNested = new PersonWithNestedValidation
148+
{
149+
Name = "Valid Name",
150+
Address = new AddressWithValidation
151+
{
152+
Street = "", // Invalid - required
153+
City = "" // Invalid - required
154+
}
155+
};
156+
157+
var validationResult = await ValidateInstanceAsync(personWithNested);
158+
159+
// Assert
160+
Assert.Equal(2, validationResult.Count);
161+
foreach (var entry in validationResult)
162+
{
163+
if (entry.Key == "Address.Street")
164+
{
165+
Assert.Contains("Street field is required", entry.Value[0]);
166+
}
167+
else if (entry.Key == "Address.City")
168+
{
169+
Assert.Contains("City field is required", entry.Value[0]);
170+
}
171+
else
172+
{
173+
Assert.Fail($"Unexpected validation error key: {entry.Key}");
174+
}
175+
}
176+
}
177+
178+
[Fact]
179+
public async Task ValidateAsync_WithListOfValidatableTypes_ValidatesEachItem()
180+
{
181+
// Arrange
182+
var personWithList = new PersonWithList
183+
{
184+
Name = "Valid Name",
185+
Addresses = new List<AddressWithValidation>
186+
{
187+
new AddressWithValidation { Street = "Valid Street", City = "Valid City" }, // Valid
188+
new AddressWithValidation { Street = "", City = "" }, // Invalid
189+
new AddressWithValidation { Street = "Another Valid Street", City = "" } // Invalid City only
190+
}
191+
};
192+
193+
var validationResult = await ValidateInstanceAsync(personWithList);
194+
195+
// Assert
196+
Assert.Equal(3, validationResult.Count);
197+
foreach (var entry in validationResult)
198+
{
199+
if (entry.Key == "Addresses[1].Street")
200+
{
201+
Assert.Contains("Street field is required", entry.Value[0]);
202+
}
203+
else if (entry.Key == "Addresses[1].City")
204+
{
205+
Assert.Contains("City field is required", entry.Value[0]);
206+
}
207+
else if (entry.Key == "Addresses[2].City")
208+
{
209+
Assert.Contains("City field is required", entry.Value[0]);
210+
}
211+
else
212+
{
213+
Assert.Fail($"Unexpected validation error key: {entry.Key}");
214+
}
215+
}
216+
}
217+
218+
[Fact]
219+
public async Task ValidateAsync_WithParsableType_ValidatesCorrectly()
220+
{
221+
// Arrange
222+
var personWithParsable = new PersonWithParsableProperty
223+
{
224+
Name = "Valid Name",
225+
Email = "invalid-email" // Invalid - not an email
226+
};
227+
228+
var validationResult = await ValidateInstanceAsync(personWithParsable);
229+
230+
// Assert
231+
Assert.Single(validationResult);
232+
Assert.Contains(validationResult, e => e.Key == "Email");
233+
Assert.Contains("not a valid e-mail address", validationResult["Email"][0]);
234+
}
235+
236+
[Fact]
237+
public async Task ValidateAsync_WithPolymorphicType_ValidatesDerivedTypes()
238+
{
239+
// Arrange
240+
var person = new PersonWithPolymorphicProperty
241+
{
242+
Name = "Valid Name",
243+
Contact = new BusinessContact // Invalid business contact with missing company name
244+
{
245+
Email = "business@example.com",
246+
Phone = "555-1234",
247+
CompanyName = "" // Invalid - required
248+
}
249+
};
250+
251+
var validationResult = await ValidateInstanceAsync(person);
252+
253+
// Assert
254+
Assert.Single(validationResult);
255+
Assert.Contains(validationResult, e => e.Key == "Contact.CompanyName");
256+
Assert.Contains("required", validationResult["Contact.CompanyName"][0]);
257+
}
258+
259+
[Fact]
260+
public async Task ValidateAsync_WithValidInput_HasNoErrors()
261+
{
262+
// Arrange
263+
var person = new PersonWithValidation
264+
{
265+
Name = "Valid Name",
266+
Age = 30
267+
};
268+
269+
var validationResult = await ValidateInstanceAsync(person);
270+
271+
// Assert
272+
Assert.Empty(validationResult);
273+
}
274+
275+
private async Task<Dictionary<string, string[]>> ValidateInstanceAsync<T>(T instance)
276+
{
277+
if (!_validationOptions.TryGetValidatableTypeInfo(typeof(T), out var validatableInfo))
278+
{
279+
return new Dictionary<string, string[]>();
280+
}
281+
282+
var validateContext = new ValidateContext
283+
{
284+
ValidationContext = new System.ComponentModel.DataAnnotations.ValidationContext(instance!),
285+
ValidationOptions = _validationOptions
286+
};
287+
288+
await validatableInfo.ValidateAsync(instance, validateContext, CancellationToken.None);
289+
290+
return validateContext.ValidationErrors ?? new Dictionary<string, string[]>();
291+
}
292+
91293
private void TestMethod(string parameter) { }
92294

93295
private class PersonWithValidation
@@ -99,6 +301,13 @@ private class PersonWithValidation
99301
public int Age { get; set; }
100302
}
101303

304+
private class PersonWithDisplayName
305+
{
306+
[Required]
307+
[Display(Name = "First Name")]
308+
public string FirstName { get; set; } = "";
309+
}
310+
102311
private class PersonWithoutValidation
103312
{
104313
public string Name { get; set; } = "";
@@ -113,6 +322,23 @@ private class PersonWithNestedValidation
113322
public AddressWithValidation Address { get; set; } = new();
114323
}
115324

325+
private class PersonWithList
326+
{
327+
[Required]
328+
public string Name { get; set; } = "";
329+
330+
public List<AddressWithValidation> Addresses { get; set; } = new();
331+
}
332+
333+
private class PersonWithParsableProperty
334+
{
335+
[Required]
336+
public string Name { get; set; } = "";
337+
338+
[EmailAddress]
339+
public string Email { get; set; } = "";
340+
}
341+
116342
private class AddressWithValidation
117343
{
118344
[Required]
@@ -129,4 +355,40 @@ private class PersonWithCyclicReference
129355

130356
public PersonWithCyclicReference? Friend { get; set; }
131357
}
358+
359+
private class PersonWithPolymorphicProperty
360+
{
361+
[Required]
362+
public string Name { get; set; } = "";
363+
364+
public Contact Contact { get; set; } = null!;
365+
}
366+
367+
[JsonDerivedType(typeof(PersonalContact), typeDiscriminator: "personal")]
368+
[JsonDerivedType(typeof(BusinessContact), typeDiscriminator: "business")]
369+
370+
private abstract class Contact
371+
{
372+
[Required]
373+
[EmailAddress]
374+
public string Email { get; set; } = "";
375+
376+
[Phone]
377+
public string Phone { get; set; } = "";
378+
}
379+
380+
private class PersonalContact : Contact
381+
{
382+
[Required]
383+
public string FirstName { get; set; } = "";
384+
385+
[Required]
386+
public string LastName { get; set; } = "";
387+
}
388+
389+
private class BusinessContact : Contact
390+
{
391+
[Required]
392+
public string CompanyName { get; set; } = "";
393+
}
132394
}

0 commit comments

Comments
 (0)