Skip to content

Commit c0035aa

Browse files
committed
Example test cases for spring-projectsgh-31746
1 parent cd62dfe commit c0035aa

File tree

2 files changed

+232
-25
lines changed

2 files changed

+232
-25
lines changed

spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java

+10
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,7 @@ public BeanResultBuilder(MethodParameter param, @Nullable Object arg, Path.Node
452452
this.parameter = param;
453453
this.bean = leafBean;
454454
this.container = (arg != null && !arg.equals(leafBean) ? arg : null);
455+
// this.container = (isContainerElement(node) ? arg : null);
455456
this.containerIndex = node.getIndex();
456457
this.containerKey = node.getKey();
457458
this.errors = createBindingResult(param, leafBean);
@@ -467,6 +468,15 @@ public ParameterErrors build() {
467468
this.parameter, this.bean, this.errors, this.container,
468469
this.containerIndex, this.containerKey);
469470
}
471+
472+
private static boolean isContainerElement(Path.Node node) {
473+
return switch (node.getKind()) {
474+
case BEAN -> node.as(Path.BeanNode.class).getContainerClass() != null;
475+
case PROPERTY -> node.as(Path.PropertyNode.class).getContainerClass() != null;
476+
case CONTAINER_ELEMENT -> true;
477+
default -> false;
478+
};
479+
}
470480
}
471481

472482

spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationAdapterTests.java

+222-25
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
import org.springframework.context.MessageSourceResolvable;
3636
import org.springframework.lang.Nullable;
3737
import org.springframework.util.ClassUtils;
38-
import org.springframework.validation.FieldError;
38+
import org.springframework.validation.ObjectError;
3939
import org.springframework.validation.method.MethodValidationResult;
4040
import org.springframework.validation.method.ParameterErrors;
4141
import org.springframework.validation.method.ParameterValidationResult;
@@ -54,6 +54,12 @@ public class MethodValidationAdapterTests {
5454

5555
private static final Person cayetana6789 = new Person("Cayetana6789", List.of(" "));
5656

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+
5763

5864
private final MethodValidationAdapter validationAdapter = new MethodValidationAdapter();
5965

@@ -85,6 +91,7 @@ void validateArguments() {
8591
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
8692
codes [student.name,name]; arguments []; default message [name],10,1]; \
8793
default message [size must be between 1 and 10]"""));
94+
assertThat(ex.getBeanResults().get(0).getContainer()).isNull();
8895

8996
assertBeanResult(ex.getBeanResults().get(1), 1, "guardian", cayetana6789, List.of("""
9097
Field error in object 'guardian' on field 'name': rejected value [Cayetana6789]; \
@@ -98,6 +105,7 @@ void validateArguments() {
98105
[org.springframework.context.support.DefaultMessageSourceResolvable: codes \
99106
[guardian.hobbies[0],hobbies[0]]; arguments []; default message [hobbies[0]]]; \
100107
default message [must not be blank]"""));
108+
assertThat(ex.getBeanResults().get(1).getContainer()).isNull();
101109

102110
assertValueResult(ex.getValueResults().get(0), 2, 3, List.of("""
103111
org.springframework.context.support.DefaultMessageSourceResolvable: \
@@ -125,6 +133,7 @@ void validateArgumentWithCustomObjectName() {
125133
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
126134
codes [studentToAdd.name,name]; arguments []; default message [name],10,1]; \
127135
default message [size must be between 1 and 10]"""));
136+
assertThat(ex.getBeanResults().get(0).getContainer()).isNull();
128137
});
129138
}
130139

@@ -159,6 +168,7 @@ void validateReturnValueBean() {
159168
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
160169
codes [person.name,name]; arguments []; default message [name],10,1]; \
161170
default message [size must be between 1 and 10]"""));
171+
assertThat(ex.getBeanResults().get(0).getContainer()).isNull();
162172
});
163173
}
164174

@@ -167,7 +177,8 @@ void validateListArgument() {
167177
MyService target = new MyService();
168178
Method method = getMethod(target, "addPeople");
169179

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 -> {
171182

172183
assertThat(ex.getAllValidationResults()).hasSize(2);
173184

@@ -181,6 +192,7 @@ void validateListArgument() {
181192
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
182193
codes [people.name,name]; arguments []; default message [name],10,1]; \
183194
default message [size must be between 1 and 10]"""));
195+
assertThat(results.get(0).getContainer()).isEqualTo(arg);
184196

185197
assertBeanResult(results.get(1), paramIndex, objectName, cayetana6789, List.of("""
186198
Field error in object 'people' on field 'name': rejected value [Cayetana6789]; \
@@ -194,6 +206,7 @@ void validateListArgument() {
194206
[org.springframework.context.support.DefaultMessageSourceResolvable: codes \
195207
[people.hobbies[0],hobbies[0]]; arguments []; default message [hobbies[0]]]; \
196208
default message [must not be blank]"""));
209+
assertThat(results.get(0).getContainer()).isEqualTo(arg);
197210
});
198211
}
199212

@@ -202,7 +215,8 @@ void validateSetArgument() {
202215
MyService target = new MyService();
203216
Method method = getMethod(target, "addPeople");
204217

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 -> {
206220

207221
assertThat(ex.getAllValidationResults()).hasSize(2);
208222

@@ -211,28 +225,194 @@ void validateSetArgument() {
211225
List<ParameterErrors> results = ex.getBeanResults();
212226

213227
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+
}
232252
);
233253
});
234254
}
235255

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+
236416
private void testArgs(Object target, Method method, Object[] args, Consumer<MethodValidationResult> consumer) {
237417
consumer.accept(this.validationAdapter.validateArguments(target, method, null, args, new Class<?>[0]));
238418
}
@@ -243,15 +423,15 @@ private void testReturnValue(Object target, Method method, @Nullable Object valu
243423

244424
private static void assertBeanResult(
245425
ParameterErrors errors, int parameterIndex, String objectName, @Nullable Object argument,
246-
List<String> fieldErrors) {
426+
List<String> objectErrors) {
247427

248428
assertThat(errors.getMethodParameter().getParameterIndex()).isEqualTo(parameterIndex);
249429
assertThat(errors.getObjectName()).isEqualTo(objectName);
250430
assertThat(errors.getArgument()).isSameAs(argument);
251431

252-
assertThat(errors.getFieldErrors())
253-
.extracting(FieldError::toString)
254-
.containsExactlyInAnyOrderElementsOf(fieldErrors);
432+
assertThat(errors.getAllErrors())
433+
.extracting(ObjectError::toString)
434+
.containsExactlyInAnyOrderElementsOf(objectErrors);
255435
}
256436

257437
private static void assertValueResult(
@@ -288,8 +468,25 @@ public Person getPerson() {
288468
public void addPeople(@Valid Collection<Person> people) {
289469
}
290470

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+
291485
}
292486

487+
@SuppressWarnings("unused")
488+
private record Course(@NotBlank String title, @Valid Set<Person> students, @Valid Person professor) {
489+
}
293490

294491
@SuppressWarnings("unused")
295492
private record Person(@Size(min = 1, max = 10) String name, List<@NotBlank String> hobbies) {

0 commit comments

Comments
 (0)