35
35
import org .springframework .context .MessageSourceResolvable ;
36
36
import org .springframework .lang .Nullable ;
37
37
import org .springframework .util .ClassUtils ;
38
- import org .springframework .validation .FieldError ;
38
+ import org .springframework .validation .ObjectError ;
39
39
import org .springframework .validation .method .MethodValidationResult ;
40
40
import org .springframework .validation .method .ParameterErrors ;
41
41
import org .springframework .validation .method .ParameterValidationResult ;
@@ -54,6 +54,12 @@ public class MethodValidationAdapterTests {
54
54
55
55
private static final Person cayetana6789 = new Person ("Cayetana6789" , List .of (" " ));
56
56
57
+ private static final Person baure4567 = new Person ("Baure4567" , List .of ());
58
+
59
+ private static final Course programming203 = new Course ("Effective Unit Testing" , Set .of (faustino1234 ), baure4567 );
60
+
61
+ private static final Course programming142 = new Course ("Debugging Fundamentals" , Set .of (baure4567 ), cayetana6789 );
62
+
57
63
58
64
private final MethodValidationAdapter validationAdapter = new MethodValidationAdapter ();
59
65
@@ -85,6 +91,7 @@ void validateArguments() {
85
91
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
86
92
codes [student.name,name]; arguments []; default message [name],10,1]; \
87
93
default message [size must be between 1 and 10]""" ));
94
+ assertThat (ex .getBeanResults ().get (0 ).getContainer ()).isNull ();
88
95
89
96
assertBeanResult (ex .getBeanResults ().get (1 ), 1 , "guardian" , cayetana6789 , List .of ("""
90
97
Field error in object 'guardian' on field 'name': rejected value [Cayetana6789]; \
@@ -98,6 +105,7 @@ void validateArguments() {
98
105
[org.springframework.context.support.DefaultMessageSourceResolvable: codes \
99
106
[guardian.hobbies[0],hobbies[0]]; arguments []; default message [hobbies[0]]]; \
100
107
default message [must not be blank]""" ));
108
+ assertThat (ex .getBeanResults ().get (1 ).getContainer ()).isNull ();
101
109
102
110
assertValueResult (ex .getValueResults ().get (0 ), 2 , 3 , List .of ("""
103
111
org.springframework.context.support.DefaultMessageSourceResolvable: \
@@ -125,6 +133,7 @@ void validateArgumentWithCustomObjectName() {
125
133
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
126
134
codes [studentToAdd.name,name]; arguments []; default message [name],10,1]; \
127
135
default message [size must be between 1 and 10]""" ));
136
+ assertThat (ex .getBeanResults ().get (0 ).getContainer ()).isNull ();
128
137
});
129
138
}
130
139
@@ -159,6 +168,7 @@ void validateReturnValueBean() {
159
168
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
160
169
codes [person.name,name]; arguments []; default message [name],10,1]; \
161
170
default message [size must be between 1 and 10]""" ));
171
+ assertThat (ex .getBeanResults ().get (0 ).getContainer ()).isNull ();
162
172
});
163
173
}
164
174
@@ -167,7 +177,8 @@ void validateListArgument() {
167
177
MyService target = new MyService ();
168
178
Method method = getMethod (target , "addPeople" );
169
179
170
- testArgs (target , method , new Object [] {List .of (faustino1234 , cayetana6789 )}, ex -> {
180
+ List <Person > arg = List .of (faustino1234 , cayetana6789 );
181
+ testArgs (target , method , new Object [] {arg }, ex -> {
171
182
172
183
assertThat (ex .getAllValidationResults ()).hasSize (2 );
173
184
@@ -181,6 +192,7 @@ void validateListArgument() {
181
192
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
182
193
codes [people.name,name]; arguments []; default message [name],10,1]; \
183
194
default message [size must be between 1 and 10]""" ));
195
+ assertThat (results .get (0 ).getContainer ()).isEqualTo (arg );
184
196
185
197
assertBeanResult (results .get (1 ), paramIndex , objectName , cayetana6789 , List .of ("""
186
198
Field error in object 'people' on field 'name': rejected value [Cayetana6789]; \
@@ -194,6 +206,7 @@ void validateListArgument() {
194
206
[org.springframework.context.support.DefaultMessageSourceResolvable: codes \
195
207
[people.hobbies[0],hobbies[0]]; arguments []; default message [hobbies[0]]]; \
196
208
default message [must not be blank]""" ));
209
+ assertThat (results .get (0 ).getContainer ()).isEqualTo (arg );
197
210
});
198
211
}
199
212
@@ -202,7 +215,8 @@ void validateSetArgument() {
202
215
MyService target = new MyService ();
203
216
Method method = getMethod (target , "addPeople" );
204
217
205
- testArgs (target , method , new Object [] {Set .of (faustino1234 , cayetana6789 )}, ex -> {
218
+ Set <Person > arg = Set .of (faustino1234 , cayetana6789 );
219
+ testArgs (target , method , new Object [] {arg }, ex -> {
206
220
207
221
assertThat (ex .getAllValidationResults ()).hasSize (2 );
208
222
@@ -211,28 +225,194 @@ void validateSetArgument() {
211
225
List <ParameterErrors > results = ex .getBeanResults ();
212
226
213
227
assertThat (results ).satisfiesExactlyInAnyOrder (
214
- result -> assertBeanResult (result , paramIndex , objectName , faustino1234 , List .of ("""
215
- Field error in object 'people' on field 'name': rejected value [Faustino1234]; \
216
- codes [Size.people.name,Size.name,Size.java.lang.String,Size]; \
217
- arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
218
- codes [people.name,name]; arguments []; default message [name],10,1]; \
219
- default message [size must be between 1 and 10]""" )),
220
- result -> assertBeanResult (result , paramIndex , objectName , cayetana6789 , List .of ("""
221
- Field error in object 'people' on field 'name': rejected value [Cayetana6789]; \
222
- codes [Size.people.name,Size.name,Size.java.lang.String,Size]; \
223
- arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
224
- codes [people.name,name]; arguments []; default message [name],10,1]; \
225
- default message [size must be between 1 and 10]""" , """
226
- Field error in object 'people' on field 'hobbies[0]': rejected value [ ]; \
227
- codes [NotBlank.people.hobbies[0],NotBlank.people.hobbies,NotBlank.hobbies[0],\
228
- NotBlank.hobbies,NotBlank.java.lang.String,NotBlank]; arguments \
229
- [org.springframework.context.support.DefaultMessageSourceResolvable: codes \
230
- [people.hobbies[0],hobbies[0]]; arguments []; default message [hobbies[0]]]; \
231
- default message [must not be blank]""" ))
228
+ result -> {
229
+ assertBeanResult (result , paramIndex , objectName , faustino1234 , List .of ("""
230
+ Field error in object 'people' on field 'name': rejected value [Faustino1234]; \
231
+ codes [Size.people.name,Size.name,Size.java.lang.String,Size]; \
232
+ arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
233
+ codes [people.name,name]; arguments []; default message [name],10,1]; \
234
+ default message [size must be between 1 and 10]""" ));
235
+ assertThat (result .getContainer ()).isEqualTo (arg );
236
+ },
237
+ result -> {
238
+ assertBeanResult (result , paramIndex , objectName , cayetana6789 , List .of ("""
239
+ Field error in object 'people' on field 'name': rejected value [Cayetana6789]; \
240
+ codes [Size.people.name,Size.name,Size.java.lang.String,Size]; \
241
+ arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
242
+ codes [people.name,name]; arguments []; default message [name],10,1]; \
243
+ default message [size must be between 1 and 10]""" , """
244
+ Field error in object 'people' on field 'hobbies[0]': rejected value [ ]; \
245
+ codes [NotBlank.people.hobbies[0],NotBlank.people.hobbies,NotBlank.hobbies[0],\
246
+ NotBlank.hobbies,NotBlank.java.lang.String,NotBlank]; arguments \
247
+ [org.springframework.context.support.DefaultMessageSourceResolvable: codes \
248
+ [people.hobbies[0],hobbies[0]]; arguments []; default message [hobbies[0]]]; \
249
+ default message [must not be blank]""" ));
250
+ assertThat (result .getContainer ()).isEqualTo (arg );
251
+ }
232
252
);
233
253
});
234
254
}
235
255
256
+ // Problem 1 - Cascaded validation produces JSR error on some paths
257
+ @ Test
258
+ void problemOne_validateNestedArgument () {
259
+ MyService target = new MyService ();
260
+ Method method = getMethod (target , "registerCourse" );
261
+
262
+ // Fails attempting to validate as path professor.name is not valid for type Person
263
+ testArgs (target , method , new Object [] {programming142 }, ex -> {
264
+
265
+ assertThat (ex .getAllValidationResults ()).hasSize (1 );
266
+
267
+ assertBeanResult (ex .getBeanResults ().get (0 ), 0 , "course" , faustino1234 , List .of ("""
268
+ Field error in object 'course' on field 'professor.name': rejected value [Faustino1234]; \
269
+ codes [Size.course.professor.name,Size.professor.name,Size.name,Size]; \
270
+ arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
271
+ codes [course.professor.name,professor.name]; arguments []; default message [professor.name],10,1]; \
272
+ default message [size must be between 1 and 10]""" ));
273
+ assertThat (ex .getBeanResults ().get (0 ).getContainer ()).isNull ();
274
+ });
275
+ }
276
+
277
+ @ Test
278
+ void problemOne_validateNestedReturnValueBean () {
279
+ MyService target = new MyService ();
280
+
281
+ // Fails attempting to validate as path professor.name is not valid for type Person
282
+ testReturnValue (target , getMethod (target , "getCourse" ), programming142 , ex -> {
283
+
284
+ assertThat (ex .getAllValidationResults ()).hasSize (1 );
285
+
286
+ assertBeanResult (ex .getBeanResults ().get (0 ), -1 , "course" , faustino1234 , List .of ("""
287
+ Field error in object 'course' on field 'professor.name': rejected value [Faustino1234]; \
288
+ codes [Size.course.professor.name,Size.professor.name,Size.name,Size]; \
289
+ arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
290
+ codes [course.professor.name,professor.name]; arguments []; default message [professor.name],10,1]; \
291
+ default message [size must be between 1 and 10]""" ));
292
+ assertThat (ex .getBeanResults ().get (0 ).getContainer ()).isNull ();
293
+ });
294
+ }
295
+
296
+
297
+ // Problem 2 - Container argument incorrect when validating non-field object error
298
+ @ Test
299
+ void problemTwo_validateListArgument () {
300
+ MyService target = new MyService ();
301
+ Method method = getMethod (target , "setHobbiesList" );
302
+
303
+ List <String > arg = List .of (" " , "Developing" , "" );
304
+ testArgs (target , method , new Object [] {arg }, ex -> {
305
+
306
+ assertThat (ex .getAllValidationResults ()).hasSize (2 );
307
+
308
+ int paramIndex = 0 ;
309
+ String objectName = "hobbies" ;
310
+ List <ParameterErrors > results = ex .getBeanResults ();
311
+
312
+ assertThat (results ).satisfiesExactlyInAnyOrder (
313
+ result -> {
314
+ // Fails argument assertion as the argument is set as the service class
315
+ assertBeanResult (result , paramIndex , objectName , " " , List .of ("""
316
+ Error in object 'hobbies': \
317
+ codes [NotBlank.hobbies,NotBlank]; \
318
+ arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
319
+ codes [hobbies]; arguments []; default message []]; \
320
+ default message [must not be blank]""" ));
321
+ assertThat (result .getContainer ()).isEqualTo (arg );
322
+ },
323
+ result -> {
324
+ // Fails argument assertion as the argument is set as the service class
325
+ assertBeanResult (result , paramIndex , objectName , "" , List .of ("""
326
+ Error in object 'hobbies': \
327
+ codes [NotBlank.hobbies,NotBlank]; \
328
+ arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
329
+ codes [hobbies]; arguments []; default message []]; \
330
+ default message [must not be blank]""" ));
331
+ assertThat (result .getContainer ()).isEqualTo (arg );
332
+ }
333
+ );
334
+ });
335
+ }
336
+
337
+ @ Test
338
+ void problemTwo_validateSetArgument () {
339
+ MyService target = new MyService ();
340
+ Method method = getMethod (target , "setHobbiesSet" );
341
+
342
+ Set <String > arg = Set .of (" " , "Developing" , "" );
343
+ testArgs (target , method , new Object [] {arg }, ex -> {
344
+
345
+ // Fails result count as all violations are mapped to the service class
346
+ assertThat (ex .getAllValidationResults ()).hasSize (2 );
347
+
348
+ int paramIndex = 0 ;
349
+ String objectName = "hobbies" ;
350
+ List <ParameterErrors > results = ex .getBeanResults ();
351
+
352
+ assertThat (results ).satisfiesExactlyInAnyOrder (
353
+ result -> {
354
+ assertBeanResult (result , paramIndex , objectName , " " , List .of ("""
355
+ Error in object 'hobbies': \
356
+ codes [NotBlank.hobbies,NotBlank]; \
357
+ arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
358
+ codes [hobbies]; arguments []; default message []]; \
359
+ default message [must not be blank]""" ));
360
+ assertThat (result .getContainer ()).isEqualTo (arg );
361
+ },
362
+ result -> {
363
+ assertBeanResult (result , paramIndex , objectName , "" , List .of ("""
364
+ Error in object 'hobbies': \
365
+ codes [NotBlank.hobbies,NotBlank]; \
366
+ arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
367
+ codes [people.name,name]; arguments []; default message []]; \
368
+ default message [must not be blank]""" ));
369
+ assertThat (result .getContainer ()).isEqualTo (arg );
370
+ }
371
+ );
372
+ });
373
+ }
374
+
375
+
376
+ // Problem 3 - Bean result container incorrectly set on nested validation
377
+ @ Test
378
+ void problemThree_validateNestedArgument () {
379
+ MyService target = new MyService ();
380
+ Method method = getMethod (target , "registerCourse" );
381
+
382
+ testArgs (target , method , new Object [] {programming203 }, ex -> {
383
+
384
+ assertThat (ex .getAllValidationResults ()).hasSize (1 );
385
+
386
+ assertBeanResult (ex .getBeanResults ().get (0 ), 0 , "course" , faustino1234 , List .of ("""
387
+ Field error in object 'course' on field 'students[].name': rejected value [Faustino1234]; \
388
+ codes [Size.course.students[].name,Size.course.students.name,Size.students[].name,Size.students.name,Size.name,Size]; \
389
+ arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
390
+ codes [course.students[].name,students[].name]; arguments []; default message [students[].name],10,1]; \
391
+ default message [size must be between 1 and 10]""" ));
392
+ // Fails as the container type is set due because leaf bean != arg
393
+ assertThat (ex .getBeanResults ().get (0 ).getContainer ()).isNull ();
394
+ });
395
+ }
396
+
397
+ @ Test
398
+ void problemThree_validateNestedReturnValueBean () {
399
+ MyService target = new MyService ();
400
+
401
+ testReturnValue (target , getMethod (target , "getCourse" ), programming203 , ex -> {
402
+
403
+ assertThat (ex .getAllValidationResults ()).hasSize (1 );
404
+
405
+ assertBeanResult (ex .getBeanResults ().get (0 ), -1 , "course" , faustino1234 , List .of ("""
406
+ Field error in object 'course' on field 'students[].name': rejected value [Faustino1234]; \
407
+ codes [Size.course.students[].name,Size.course.students.name,Size.students[].name,Size.students.name,Size.name,Size]; \
408
+ arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
409
+ codes [course.students[].name,students[].name]; arguments []; default message [students[].name],10,1]; \
410
+ default message [size must be between 1 and 10]""" ));
411
+ // Fails as the container type is set due because leaf bean != arg
412
+ assertThat (ex .getBeanResults ().get (0 ).getContainer ()).isNull ();
413
+ });
414
+ }
415
+
236
416
private void testArgs (Object target , Method method , Object [] args , Consumer <MethodValidationResult > consumer ) {
237
417
consumer .accept (this .validationAdapter .validateArguments (target , method , null , args , new Class <?>[0 ]));
238
418
}
@@ -243,15 +423,15 @@ private void testReturnValue(Object target, Method method, @Nullable Object valu
243
423
244
424
private static void assertBeanResult (
245
425
ParameterErrors errors , int parameterIndex , String objectName , @ Nullable Object argument ,
246
- List <String > fieldErrors ) {
426
+ List <String > objectErrors ) {
247
427
248
428
assertThat (errors .getMethodParameter ().getParameterIndex ()).isEqualTo (parameterIndex );
249
429
assertThat (errors .getObjectName ()).isEqualTo (objectName );
250
430
assertThat (errors .getArgument ()).isSameAs (argument );
251
431
252
- assertThat (errors .getFieldErrors ())
253
- .extracting (FieldError ::toString )
254
- .containsExactlyInAnyOrderElementsOf (fieldErrors );
432
+ assertThat (errors .getAllErrors ())
433
+ .extracting (ObjectError ::toString )
434
+ .containsExactlyInAnyOrderElementsOf (objectErrors );
255
435
}
256
436
257
437
private static void assertValueResult (
@@ -288,8 +468,25 @@ public Person getPerson() {
288
468
public void addPeople (@ Valid Collection <Person > people ) {
289
469
}
290
470
471
+ public void registerCourse (@ Valid Course course ) {
472
+ }
473
+
474
+ @ Valid
475
+ public Course getCourse () {
476
+ throw new UnsupportedOperationException ();
477
+ }
478
+
479
+ public void setHobbiesSet (@ Valid Set <@ NotBlank String > hobbies ) {
480
+ }
481
+
482
+ public void setHobbiesList (@ Valid List <@ NotBlank String > hobbies ) {
483
+ }
484
+
291
485
}
292
486
487
+ @ SuppressWarnings ("unused" )
488
+ private record Course (@ NotBlank String title , @ Valid Set <Person > students , @ Valid Person professor ) {
489
+ }
293
490
294
491
@ SuppressWarnings ("unused" )
295
492
private record Person (@ Size (min = 1 , max = 10 ) String name , List <@ NotBlank String > hobbies ) {
0 commit comments