3
3
// Licensed to the .NET Foundation under one or more agreements.
4
4
// The .NET Foundation licenses this file to you under the MIT license.
5
5
6
+ using System . Collections . Generic ;
6
7
using System . ComponentModel . DataAnnotations ;
8
+ using System . Text . Json . Serialization ;
9
+ using System . Threading . Tasks ;
7
10
8
11
namespace Microsoft . AspNetCore . Http . Validation . Tests ;
9
12
10
13
public class RuntimeValidatableTypeInfoResolverTests
11
14
{
12
15
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
+ }
13
25
14
26
[ Fact ]
15
27
public void TryGetValidatableParameterInfo_AlwaysReturnsFalse ( )
16
28
{
17
29
var parameterInfo = typeof ( RuntimeValidatableTypeInfoResolverTests ) . GetMethod ( nameof ( TestMethod ) ) ! . GetParameters ( ) [ 0 ] ;
30
+ var resolver = new RuntimeValidatableTypeInfoResolver ( ) ;
18
31
19
- var result = _resolver . TryGetValidatableParameterInfo ( parameterInfo , out var validatableInfo ) ;
32
+ var result = resolver . TryGetValidatableParameterInfo ( parameterInfo , out var validatableInfo ) ;
20
33
21
34
Assert . False ( result ) ;
22
35
Assert . Null ( validatableInfo ) ;
@@ -88,6 +101,195 @@ public void TryGetValidatableTypeInfo_WithCyclicReference_HandlesGracefully()
88
101
// Should not throw StackOverflowException due to cycle detection
89
102
}
90
103
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
+
91
293
private void TestMethod ( string parameter ) { }
92
294
93
295
private class PersonWithValidation
@@ -99,6 +301,13 @@ private class PersonWithValidation
99
301
public int Age { get ; set ; }
100
302
}
101
303
304
+ private class PersonWithDisplayName
305
+ {
306
+ [ Required ]
307
+ [ Display ( Name = "First Name" ) ]
308
+ public string FirstName { get ; set ; } = "" ;
309
+ }
310
+
102
311
private class PersonWithoutValidation
103
312
{
104
313
public string Name { get ; set ; } = "" ;
@@ -113,6 +322,23 @@ private class PersonWithNestedValidation
113
322
public AddressWithValidation Address { get ; set ; } = new ( ) ;
114
323
}
115
324
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
+
116
342
private class AddressWithValidation
117
343
{
118
344
[ Required ]
@@ -129,4 +355,40 @@ private class PersonWithCyclicReference
129
355
130
356
public PersonWithCyclicReference ? Friend { get ; set ; }
131
357
}
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
+ }
132
394
}
0 commit comments