Adapt ConstraintViolation's from method validation

See gh-29825
This commit is contained in:
Rossen Stoyanchev
2023-06-06 15:10:21 +01:00
committed by rstoyanchev
parent 38abee00e2
commit 425d5a94cb
8 changed files with 1237 additions and 169 deletions

View File

@@ -0,0 +1,236 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.validation.beanvalidation;
import java.lang.reflect.Method;
import java.util.List;
import java.util.function.Consumer;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Size;
import org.junit.jupiter.api.Test;
import org.springframework.context.MessageSourceResolvable;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.validation.FieldError;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Unit tests for {@link MethodValidationAdapter}.
* @author Rossen Stoyanchev
*/
public class MethodValidationAdapterTests {
private static final Person faustino1234 = new Person("Faustino1234");
private static final Person cayetana6789 = new Person("Cayetana6789");
@Test
void validateArguments() {
MyService target = new MyService();
Method method = getMethod(target, "addStudent");
validateArguments(target, method, new Object[] {faustino1234, cayetana6789, 3}, ex -> {
assertThat(ex.getConstraintViolations()).hasSize(3);
assertThat(ex.getAllValidationResults()).hasSize(3);
assertBeanResult(ex.getBeanResults().get(0), 0, "student", faustino1234, List.of(
"""
Field error in object 'student' on field 'name': rejected value [Faustino1234]; \
codes [Size.student.name,Size.name,Size.java.lang.String,Size]; \
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
codes [student.name,name]; arguments []; default message [name],10,1]; \
default message [size must be between 1 and 10]"""));
assertBeanResult(ex.getBeanResults().get(1), 1, "guardian", cayetana6789, List.of(
"""
Field error in object 'guardian' on field 'name': rejected value [Cayetana6789]; \
codes [Size.guardian.name,Size.name,Size.java.lang.String,Size]; \
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
codes [guardian.name,name]; arguments []; default message [name],10,1]; \
default message [size must be between 1 and 10]"""));
assertValueResult(ex.getValueResults().get(0), 2, 3, List.of(
"""
org.springframework.context.support.DefaultMessageSourceResolvable: \
codes [Max.myService#addStudent.degrees,Max.degrees,Max.int,Max]; \
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
codes [myService#addStudent.degrees,degrees]; arguments []; default message [degrees],2]; \
default message [must be less than or equal to 2]"""
));
});
}
@Test
void validateReturnValue() {
MyService target = new MyService();
validateReturnValue(target, getMethod(target, "getIntValue"), 4, ex -> {
assertThat(ex.getConstraintViolations()).hasSize(1);
assertThat(ex.getAllValidationResults()).hasSize(1);
assertValueResult(ex.getValueResults().get(0), -1, 4, List.of(
"""
org.springframework.context.support.DefaultMessageSourceResolvable: \
codes [Min.myService#getIntValue,Min,Min.int]; \
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
codes [myService#getIntValue]; arguments []; default message [],5]; \
default message [must be greater than or equal to 5]"""
));
});
}
@Test
void validateReturnValueBean() {
MyService target = new MyService();
validateReturnValue(target, getMethod(target, "getPerson"), faustino1234, ex -> {
assertThat(ex.getConstraintViolations()).hasSize(1);
assertThat(ex.getAllValidationResults()).hasSize(1);
assertBeanResult(ex.getBeanResults().get(0), -1, "person", faustino1234, List.of(
"""
Field error in object 'person' on field 'name': rejected value [Faustino1234]; \
codes [Size.person.name,Size.name,Size.java.lang.String,Size]; \
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
codes [person.name,name]; arguments []; default message [name],10,1]; \
default message [size must be between 1 and 10]"""));
});
}
@Test
void validateListArgument() {
MyService target = new MyService();
Method method = getMethod(target, "addPeople");
validateArguments(target, method, new Object[] {List.of(faustino1234, cayetana6789)}, ex -> {
assertThat(ex.getConstraintViolations()).hasSize(2);
assertThat(ex.getAllValidationResults()).hasSize(2);
int paramIndex = 0;
String objectName = "people";
List<ParameterErrors> results = ex.getBeanResults();
assertBeanResult(results.get(0), paramIndex, objectName, faustino1234, List.of(
"""
Field error in object 'people' on field 'name': rejected value [Faustino1234]; \
codes [Size.people.name,Size.name,Size.java.lang.String,Size]; \
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
codes [people.name,name]; arguments []; default message [name],10,1]; \
default message [size must be between 1 and 10]"""));
assertBeanResult(results.get(1), paramIndex, objectName, cayetana6789, List.of(
"""
Field error in object 'people' on field 'name': rejected value [Cayetana6789]; \
codes [Size.people.name,Size.name,Size.java.lang.String,Size]; \
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
codes [people.name,name]; arguments []; default message [name],10,1]; \
default message [size must be between 1 and 10]"""));
});
}
private void validateArguments(
Object target, Method method, Object[] arguments, Consumer<MethodValidationException> assertions) {
MethodValidationAdapter adapter = new MethodValidationAdapter();
assertThatExceptionOfType(MethodValidationException.class)
.isThrownBy(() -> adapter.validateMethodArguments(target, method, arguments, new Class<?>[0]))
.satisfies(assertions);
}
private void validateReturnValue(
Object target, Method method, @Nullable Object returnValue, Consumer<MethodValidationException> assertions) {
MethodValidationAdapter adapter = new MethodValidationAdapter();
assertThatExceptionOfType(MethodValidationException.class)
.isThrownBy(() -> adapter.validateMethodReturnValue(target, method, returnValue, new Class<?>[0]))
.satisfies(assertions);
}
private static void assertBeanResult(
ParameterErrors errors, int parameterIndex, String objectName, Object argument,
List<String> fieldErrors) {
assertThat(errors.getMethodParameter().getParameterIndex()).isEqualTo(parameterIndex);
assertThat(errors.getObjectName()).isEqualTo(objectName);
assertThat(errors.getArgument()).isSameAs(argument);
assertThat(errors.getFieldErrors())
.extracting(FieldError::toString)
.containsExactlyInAnyOrderElementsOf(fieldErrors);
}
private static void assertValueResult(
ParameterValidationResult result, int parameterIndex, Object argument, List<String> errors) {
assertThat(result.getMethodParameter().getParameterIndex()).isEqualTo(parameterIndex);
assertThat(result.getArgument()).isEqualTo(argument);
assertThat(result.getResolvableErrors())
.extracting(MessageSourceResolvable::toString)
.containsExactlyInAnyOrderElementsOf(errors);
}
private static Method getMethod(Object target, String methodName) {
return ClassUtils.getMethod(target.getClass(), methodName, (Class<?>[]) null);
}
@SuppressWarnings("unused")
private static class MyService {
public void addStudent(@Valid Person student, @Valid Person guardian, @Max(2) int degrees) {
}
@Min(5)
public int getIntValue() {
throw new UnsupportedOperationException();
}
@Valid
public Person getPerson() {
throw new UnsupportedOperationException();
}
public void addPeople(@Valid List<Person> people) {
}
}
@SuppressWarnings("unused")
private record Person(@Size(min = 1, max = 10) String name) {
@Override
public String name() {
return this.name;
}
}
}