4
4
// The .NET Foundation licenses this file to you under the MIT license.
5
5
6
6
using System . ComponentModel . DataAnnotations ;
7
+ using System . Linq ;
7
8
using System . Reflection ;
8
9
9
10
namespace Microsoft . Extensions . Validation . Tests ;
@@ -52,13 +53,61 @@ public void TryGetValidatableTypeInfo_WithEnum_ReturnsFalse()
52
53
}
53
54
54
55
[ Fact ]
55
- public void TryGetValidatableTypeInfo_WithSimplePocoWithValidationAttributes_ReturnsTrue ( )
56
+ public async Task TryGetValidatableTypeInfo_WithSimplePocoWithValidationAttributes_ReturnsTrue_AndValidatesCorrectly ( )
56
57
{
57
58
var result = _resolver . TryGetValidatableTypeInfo ( typeof ( SimplePocoWithValidation ) , out var validatableInfo ) ;
58
59
59
60
Assert . True ( result ) ;
60
61
Assert . NotNull ( validatableInfo ) ;
61
62
Assert . IsType < RuntimeValidatableTypeInfoResolver . RuntimeValidatableTypeInfo > ( validatableInfo ) ;
63
+
64
+ // Test validation with invalid data
65
+ var invalidPoco = new SimplePocoWithValidation
66
+ {
67
+ Name = "" , // Required but empty
68
+ Age = 150 // Out of range (0-100)
69
+ } ;
70
+
71
+ var validationOptions = new ValidationOptions ( ) ;
72
+ validationOptions . Resolvers . Add ( _resolver ) ;
73
+
74
+ var context = new ValidateContext
75
+ {
76
+ ValidationOptions = validationOptions ,
77
+ ValidationContext = new ValidationContext ( invalidPoco )
78
+ } ;
79
+
80
+ await validatableInfo . ValidateAsync ( invalidPoco , context , default ) ;
81
+
82
+ Assert . NotNull ( context . ValidationErrors ) ;
83
+ Assert . Collection ( context . ValidationErrors ,
84
+ kvp =>
85
+ {
86
+ Assert . Equal ( "Name" , kvp . Key ) ;
87
+ Assert . Equal ( "The Name field is required." , kvp . Value . First ( ) ) ;
88
+ } ,
89
+ kvp =>
90
+ {
91
+ Assert . Equal ( "Age" , kvp . Key ) ;
92
+ Assert . Equal ( "The field Age must be between 0 and 100." , kvp . Value . First ( ) ) ;
93
+ } ) ;
94
+
95
+ // Test validation with valid data
96
+ var validPoco = new SimplePocoWithValidation
97
+ {
98
+ Name = "John Doe" ,
99
+ Age = 25
100
+ } ;
101
+
102
+ var validContext = new ValidateContext
103
+ {
104
+ ValidationOptions = validationOptions ,
105
+ ValidationContext = new ValidationContext ( validPoco )
106
+ } ;
107
+
108
+ await validatableInfo . ValidateAsync ( validPoco , validContext , default ) ;
109
+
110
+ Assert . Null ( validContext . ValidationErrors ) ; // No validation errors for valid data
62
111
}
63
112
64
113
[ Fact ]
@@ -71,30 +120,217 @@ public void TryGetValidatableTypeInfo_WithSimplePocoWithoutValidation_ReturnsFal
71
120
}
72
121
73
122
[ Fact ]
74
- public void TryGetValidatableTypeInfo_WithNestedComplexType_ReturnsTrue ( )
123
+ public async Task TryGetValidatableTypeInfo_WithNestedComplexType_ReturnsTrue_AndValidatesCorrectly ( )
75
124
{
76
125
var result = _resolver . TryGetValidatableTypeInfo ( typeof ( PocoWithNestedType ) , out var validatableInfo ) ;
77
126
78
127
Assert . True ( result ) ;
79
128
Assert . NotNull ( validatableInfo ) ;
129
+
130
+ // Test validation with invalid nested data
131
+ var invalidPoco = new PocoWithNestedType
132
+ {
133
+ Name = "" , // Required but empty
134
+ NestedPoco = new SimplePocoWithValidation
135
+ {
136
+ Name = "" , // Required but empty in nested object
137
+ Age = - 5 // Out of range (0-100) in nested object
138
+ }
139
+ } ;
140
+
141
+ var validationOptions = new ValidationOptions ( ) ;
142
+ validationOptions . Resolvers . Add ( _resolver ) ;
143
+
144
+ var context = new ValidateContext
145
+ {
146
+ ValidationOptions = validationOptions ,
147
+ ValidationContext = new ValidationContext ( invalidPoco )
148
+ } ;
149
+
150
+ await validatableInfo . ValidateAsync ( invalidPoco , context , default ) ;
151
+
152
+ Assert . NotNull ( context . ValidationErrors ) ;
153
+ Assert . Collection ( context . ValidationErrors ,
154
+ kvp =>
155
+ {
156
+ Assert . Equal ( "Name" , kvp . Key ) ;
157
+ Assert . Equal ( "The Name field is required." , kvp . Value . First ( ) ) ;
158
+ } ,
159
+ kvp =>
160
+ {
161
+ Assert . Equal ( "NestedPoco.Name" , kvp . Key ) ;
162
+ Assert . Equal ( "The Name field is required." , kvp . Value . First ( ) ) ;
163
+ } ,
164
+ kvp =>
165
+ {
166
+ Assert . Equal ( "NestedPoco.Age" , kvp . Key ) ;
167
+ Assert . Equal ( "The field Age must be between 0 and 100." , kvp . Value . First ( ) ) ;
168
+ } ) ;
169
+
170
+ // Test validation with valid nested data
171
+ var validPoco = new PocoWithNestedType
172
+ {
173
+ Name = "John Doe" ,
174
+ NestedPoco = new SimplePocoWithValidation
175
+ {
176
+ Name = "Jane Smith" ,
177
+ Age = 30
178
+ }
179
+ } ;
180
+
181
+ var validContext = new ValidateContext
182
+ {
183
+ ValidationOptions = validationOptions ,
184
+ ValidationContext = new ValidationContext ( validPoco )
185
+ } ;
186
+
187
+ await validatableInfo . ValidateAsync ( validPoco , validContext , default ) ;
188
+
189
+ Assert . Null ( validContext . ValidationErrors ) ; // No validation errors for valid data
80
190
}
81
191
82
192
[ Fact ]
83
- public void TryGetValidatableTypeInfo_WithCyclicReference_DoesNotCauseStackOverflow ( )
193
+ public async Task TryGetValidatableTypeInfo_WithCyclicReference_DoesNotCauseStackOverflow_AndValidatesCorrectly ( )
84
194
{
85
195
var result = _resolver . TryGetValidatableTypeInfo ( typeof ( CyclicTypeA ) , out var validatableInfo ) ;
86
196
87
197
Assert . True ( result ) ;
88
198
Assert . NotNull ( validatableInfo ) ;
199
+
200
+ // Test validation with invalid data in cyclic structure
201
+ var cyclicA = new CyclicTypeA
202
+ {
203
+ Name = "" , // Required but empty
204
+ TypeB = new CyclicTypeB
205
+ {
206
+ Value = "" , // Required but empty
207
+ TypeA = new CyclicTypeA
208
+ {
209
+ Name = "Valid Name" , // This one is valid
210
+ TypeB = null // No further nesting
211
+ }
212
+ }
213
+ } ;
214
+
215
+ var validationOptions = new ValidationOptions ( ) ;
216
+ validationOptions . Resolvers . Add ( _resolver ) ;
217
+
218
+ var context = new ValidateContext
219
+ {
220
+ ValidationOptions = validationOptions ,
221
+ ValidationContext = new ValidationContext ( cyclicA )
222
+ } ;
223
+
224
+ await validatableInfo . ValidateAsync ( cyclicA , context , default ) ;
225
+
226
+ Assert . NotNull ( context . ValidationErrors ) ;
227
+ Assert . Collection ( context . ValidationErrors ,
228
+ kvp =>
229
+ {
230
+ Assert . Equal ( "Name" , kvp . Key ) ;
231
+ Assert . Equal ( "The Name field is required." , kvp . Value . First ( ) ) ;
232
+ } ,
233
+ kvp =>
234
+ {
235
+ Assert . Equal ( "TypeB.Value" , kvp . Key ) ;
236
+ Assert . Equal ( "The Value field is required." , kvp . Value . First ( ) ) ;
237
+ } ) ;
238
+
239
+ // Test validation with valid cyclic data
240
+ var validCyclicA = new CyclicTypeA
241
+ {
242
+ Name = "Valid A" ,
243
+ TypeB = new CyclicTypeB
244
+ {
245
+ Value = "Valid B" ,
246
+ TypeA = new CyclicTypeA
247
+ {
248
+ Name = "Valid Nested A" ,
249
+ TypeB = null // Stop the cycle
250
+ }
251
+ }
252
+ } ;
253
+
254
+ var validContext = new ValidateContext
255
+ {
256
+ ValidationOptions = validationOptions ,
257
+ ValidationContext = new ValidationContext ( validCyclicA )
258
+ } ;
259
+
260
+ await validatableInfo . ValidateAsync ( validCyclicA , validContext , default ) ;
261
+
262
+ Assert . Null ( validContext . ValidationErrors ) ; // No validation errors for valid data
89
263
}
90
264
91
265
[ Fact ]
92
- public void TryGetValidatableTypeInfo_WithCollectionOfComplexTypes_ReturnsTrue ( )
266
+ public async Task TryGetValidatableTypeInfo_WithCollectionOfComplexTypes_ReturnsTrue_AndValidatesCorrectly ( )
93
267
{
94
268
var result = _resolver . TryGetValidatableTypeInfo ( typeof ( PocoWithCollection ) , out var validatableInfo ) ;
95
269
96
270
Assert . True ( result ) ;
97
271
Assert . NotNull ( validatableInfo ) ;
272
+
273
+ // Test validation with invalid data in collection
274
+ var invalidPoco = new PocoWithCollection
275
+ {
276
+ Name = "" , // Required but empty
277
+ Items = new List < SimplePocoWithValidation >
278
+ {
279
+ new SimplePocoWithValidation { Name = "Valid Item" , Age = 25 } , // Valid item
280
+ new SimplePocoWithValidation { Name = "" , Age = 150 } , // Invalid: empty name and out of range age
281
+ new SimplePocoWithValidation { Name = "Another Valid" , Age = 30 } // Valid item
282
+ }
283
+ } ;
284
+
285
+ var validationOptions = new ValidationOptions ( ) ;
286
+ validationOptions . Resolvers . Add ( _resolver ) ;
287
+
288
+ var context = new ValidateContext
289
+ {
290
+ ValidationOptions = validationOptions ,
291
+ ValidationContext = new ValidationContext ( invalidPoco )
292
+ } ;
293
+
294
+ await validatableInfo . ValidateAsync ( invalidPoco , context , default ) ;
295
+
296
+ Assert . NotNull ( context . ValidationErrors ) ;
297
+ Assert . Collection ( context . ValidationErrors ,
298
+ kvp =>
299
+ {
300
+ Assert . Equal ( "Name" , kvp . Key ) ;
301
+ Assert . Equal ( "The Name field is required." , kvp . Value . First ( ) ) ;
302
+ } ,
303
+ kvp =>
304
+ {
305
+ Assert . Equal ( "Items[1].Name" , kvp . Key ) ;
306
+ Assert . Equal ( "The Name field is required." , kvp . Value . First ( ) ) ;
307
+ } ,
308
+ kvp =>
309
+ {
310
+ Assert . Equal ( "Items[1].Age" , kvp . Key ) ;
311
+ Assert . Equal ( "The field Age must be between 0 and 100." , kvp . Value . First ( ) ) ;
312
+ } ) ;
313
+
314
+ // Test validation with valid collection data
315
+ var validPoco = new PocoWithCollection
316
+ {
317
+ Name = "Collection Owner" ,
318
+ Items = new List < SimplePocoWithValidation >
319
+ {
320
+ new SimplePocoWithValidation { Name = "Item 1" , Age = 25 } ,
321
+ new SimplePocoWithValidation { Name = "Item 2" , Age = 30 }
322
+ }
323
+ } ;
324
+
325
+ var validContext = new ValidateContext
326
+ {
327
+ ValidationOptions = validationOptions ,
328
+ ValidationContext = new ValidationContext ( validPoco )
329
+ } ;
330
+
331
+ await validatableInfo . ValidateAsync ( validPoco , validContext , default ) ;
332
+
333
+ Assert . Null ( validContext . ValidationErrors ) ; // No validation errors for valid data
98
334
}
99
335
100
336
[ Fact ]
@@ -112,16 +348,53 @@ public void TryGetValidatableTypeInfo_UsesCaching()
112
348
}
113
349
114
350
[ Fact ]
115
- public void TryGetValidatableTypeInfo_WithReadOnlyProperty_IgnoresReadOnlyProperty ( )
351
+ public async Task TryGetValidatableTypeInfo_WithReadOnlyProperty_IgnoresReadOnlyProperty_AndValidatesCorrectly ( )
116
352
{
117
353
var result = _resolver . TryGetValidatableTypeInfo ( typeof ( PocoWithReadOnlyProperty ) , out var validatableInfo ) ;
118
354
119
355
Assert . True ( result ) ;
120
356
Assert . NotNull ( validatableInfo ) ;
121
357
122
358
var typeInfo = Assert . IsType < RuntimeValidatableTypeInfoResolver . RuntimeValidatableTypeInfo > ( validatableInfo ) ;
123
- // Should only include the writable property with validation
124
- // We can't directly access Members since it's internal, but we can validate through validation behavior
359
+
360
+ // Test validation with invalid writable property (read-only property should be ignored)
361
+ var invalidPoco = new PocoWithReadOnlyProperty
362
+ {
363
+ Name = "" // Required but empty (ReadOnlyValue should be ignored)
364
+ } ;
365
+
366
+ var validationOptions = new ValidationOptions ( ) ;
367
+ validationOptions . Resolvers . Add ( _resolver ) ;
368
+
369
+ var context = new ValidateContext
370
+ {
371
+ ValidationOptions = validationOptions ,
372
+ ValidationContext = new ValidationContext ( invalidPoco )
373
+ } ;
374
+
375
+ await validatableInfo . ValidateAsync ( invalidPoco , context , default ) ;
376
+
377
+ Assert . NotNull ( context . ValidationErrors ) ;
378
+ var error = Assert . Single ( context . ValidationErrors ) ;
379
+ Assert . Equal ( "Name" , error . Key ) ;
380
+ Assert . Equal ( "The Name field is required." , error . Value . First ( ) ) ;
381
+ // ReadOnlyValue should not generate validation errors even though it has [Required]
382
+
383
+ // Test validation with valid writable property
384
+ var validPoco = new PocoWithReadOnlyProperty
385
+ {
386
+ Name = "Valid Name" // ReadOnlyValue is always "ReadOnly" and should be ignored
387
+ } ;
388
+
389
+ var validContext = new ValidateContext
390
+ {
391
+ ValidationOptions = validationOptions ,
392
+ ValidationContext = new ValidationContext ( validPoco )
393
+ } ;
394
+
395
+ await validatableInfo . ValidateAsync ( validPoco , validContext , default ) ;
396
+
397
+ Assert . Null ( validContext . ValidationErrors ) ; // No validation errors for valid data
125
398
}
126
399
127
400
// Helper method for parameter test
0 commit comments