Add method validation to Spring MVC

See gh-29825
This commit is contained in:
Rossen Stoyanchev
2023-06-08 15:39:22 +01:00
committed by rstoyanchev
parent cb04c3b335
commit bd054a4918
15 changed files with 1000 additions and 27 deletions

View File

@@ -0,0 +1,191 @@
/*
* 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.web.method;
import java.lang.reflect.Method;
import java.util.List;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Size;
import org.junit.jupiter.api.Test;
import org.springframework.util.ClassUtils;
import org.springframework.validation.annotation.Validated;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for {@link HandlerMethod}.
* @author Rossen Stoyanchev
*/
public class HandlerMethodTests {
@Test
void shouldValidateArgsWithConstraintsDirectlyOnClass() {
Object target = new MyClass();
testShouldValidateArguments(target, List.of("addIntValue", "addPersonAndIntValue"), true);
testShouldValidateArguments(target, List.of("addPerson", "getPerson", "getIntValue", "addPersonNotValidated"), false);
}
@Test
void shouldValidateArgsWithConstraintsOnInterface() {
Object target = new MyInterfaceImpl();
testShouldValidateArguments(target, List.of("addIntValue", "addPersonAndIntValue"), true);
testShouldValidateArguments(target, List.of("addPerson", "addPersonNotValidated", "getPerson", "getIntValue"), false);
}
@Test
void shouldValidateReturnValueWithConstraintsDirectlyOnClass() {
Object target = new MyClass();
testShouldValidateReturnValue(target, List.of("getPerson", "getIntValue"), true);
testShouldValidateReturnValue(target, List.of("addPerson", "addIntValue", "addPersonNotValidated"), false);
}
@Test
void shouldValidateReturnValueWithConstraintsOnInterface() {
Object target = new MyInterfaceImpl();
testShouldValidateReturnValue(target, List.of("getPerson", "getIntValue"), true);
testShouldValidateReturnValue(target, List.of("addPerson", "addIntValue", "addPersonNotValidated"), false);
}
@Test
void classLevelValidatedAnnotation() {
Object target = new MyValidatedClass();
testShouldValidateArguments(target, List.of("addPerson"), false);
testShouldValidateReturnValue(target, List.of("getPerson"), false);
}
private static void testShouldValidateArguments(Object target, List<String> methodNames, boolean expected) {
for (String methodName : methodNames) {
assertThat(getHandlerMethod(target, methodName).shouldValidateArguments()).isEqualTo(expected);
}
}
private static void testShouldValidateReturnValue(Object target, List<String> methodNames, boolean expected) {
for (String methodName : methodNames) {
assertThat(getHandlerMethod(target, methodName).shouldValidateReturnValue()).isEqualTo(expected);
}
}
private static HandlerMethod getHandlerMethod(Object target, String methodName) {
Method method = ClassUtils.getMethod(target.getClass(), methodName, (Class<?>[]) null);
return new HandlerMethod(target, method);
}
@SuppressWarnings("unused")
private record Person(@Size(min = 1, max = 10) String name) {
@Override
public String name() {
return this.name;
}
}
@SuppressWarnings("unused")
private static class MyClass {
public void addPerson(@Valid Person person) {
}
public void addIntValue(@Max(10) int value) {
}
public void addPersonAndIntValue(@Valid Person person, @Max(10) int value) {
}
public void addPersonNotValidated(Person person) {
}
@Valid
public Person getPerson() {
throw new UnsupportedOperationException();
}
@Max(10)
public int getIntValue() {
throw new UnsupportedOperationException();
}
}
@SuppressWarnings("unused")
private interface MyInterface {
void addPerson(@Valid Person person);
void addIntValue(@Max(10) int value);
void addPersonAndIntValue(@Valid Person person, @Max(10) int value);
void addPersonNotValidated(Person person);
@Valid
Person getPerson();
@Max(10)
int getIntValue();
}
@SuppressWarnings("unused")
private static class MyInterfaceImpl implements MyInterface {
@Override
public void addPerson(Person person) {
}
@Override
public void addIntValue(int value) {
}
@Override
public void addPersonAndIntValue(Person person, int value) {
}
@Override
public void addPersonNotValidated(Person person) {
}
@Override
public Person getPerson() {
throw new UnsupportedOperationException();
}
@Override
public int getIntValue() {
throw new UnsupportedOperationException();
}
}
@SuppressWarnings("unused")
@Validated
private static class MyValidatedClass {
public void addPerson(@Valid Person person) {
}
@Valid
public Person getPerson() {
throw new UnsupportedOperationException();
}
}
}

View File

@@ -162,10 +162,10 @@ public class ModelAttributeMethodProcessorTests {
public void resolveArgumentViaDefaultConstructor() throws Exception {
WebDataBinder dataBinder = new WebRequestDataBinder(null);
WebDataBinderFactory factory = mock();
given(factory.createBinder(any(), notNull(), eq("attrName"))).willReturn(dataBinder);
given(factory.createBinder(any(), notNull(), eq("attrName"), any())).willReturn(dataBinder);
this.processor.resolveArgument(this.paramNamedValidModelAttr, this.container, this.request, factory);
verify(factory).createBinder(any(), notNull(), eq("attrName"));
verify(factory).createBinder(any(), notNull(), eq("attrName"), any());
}
@Test
@@ -176,7 +176,7 @@ public class ModelAttributeMethodProcessorTests {
StubRequestDataBinder dataBinder = new StubRequestDataBinder(target, name);
WebDataBinderFactory factory = mock();
given(factory.createBinder(this.request, target, name)).willReturn(dataBinder);
given(factory.createBinder(this.request, target, name, this.paramNamedValidModelAttr)).willReturn(dataBinder);
this.processor.resolveArgument(this.paramNamedValidModelAttr, this.container, this.request, factory);
@@ -195,7 +195,7 @@ public class ModelAttributeMethodProcessorTests {
StubRequestDataBinder dataBinder = new StubRequestDataBinder(target, name);
WebDataBinderFactory factory = mock();
given(factory.createBinder(this.request, target, name)).willReturn(dataBinder);
given(factory.createBinder(this.request, target, name, this.paramNamedValidModelAttr)).willReturn(dataBinder);
this.processor.resolveArgument(this.paramNamedValidModelAttr, this.container, this.request, factory);
@@ -211,7 +211,7 @@ public class ModelAttributeMethodProcessorTests {
StubRequestDataBinder dataBinder = new StubRequestDataBinder(target, name);
WebDataBinderFactory factory = mock();
given(factory.createBinder(this.request, target, name)).willReturn(dataBinder);
given(factory.createBinder(this.request, target, name, this.paramBindingDisabledAttr)).willReturn(dataBinder);
this.processor.resolveArgument(this.paramBindingDisabledAttr, this.container, this.request, factory);
@@ -229,12 +229,12 @@ public class ModelAttributeMethodProcessorTests {
dataBinder.getBindingResult().reject("error");
WebDataBinderFactory binderFactory = mock();
given(binderFactory.createBinder(this.request, target, name)).willReturn(dataBinder);
given(binderFactory.createBinder(this.request, target, name, this.paramNonSimpleType)).willReturn(dataBinder);
assertThatExceptionOfType(MethodArgumentNotValidException.class).isThrownBy(() ->
this.processor.resolveArgument(this.paramNonSimpleType, this.container, this.request, binderFactory));
verify(binderFactory).createBinder(this.request, target, name);
verify(binderFactory).createBinder(this.request, target, name, this.paramNonSimpleType);
}
@Test // SPR-9378
@@ -249,7 +249,7 @@ public class ModelAttributeMethodProcessorTests {
StubRequestDataBinder dataBinder = new StubRequestDataBinder(testBean, name);
WebDataBinderFactory binderFactory = mock();
given(binderFactory.createBinder(this.request, testBean, name)).willReturn(dataBinder);
given(binderFactory.createBinder(this.request, testBean, name, this.paramModelAttr)).willReturn(dataBinder);
this.processor.resolveArgument(this.paramModelAttr, this.container, this.request, binderFactory);
@@ -278,7 +278,7 @@ public class ModelAttributeMethodProcessorTests {
ServletWebRequest requestWithParam = new ServletWebRequest(mockRequest);
WebDataBinderFactory factory = mock();
given(factory.createBinder(any(), any(), eq("testBeanWithConstructorArgs")))
given(factory.createBinder(any(), any(), eq("testBeanWithConstructorArgs"), any()))
.willAnswer(invocation -> {
WebRequestDataBinder binder = new WebRequestDataBinder(invocation.getArgument(1));
// Add conversion service which will convert "1,2" to a list
@@ -297,10 +297,10 @@ public class ModelAttributeMethodProcessorTests {
WebDataBinder dataBinder = new WebRequestDataBinder(target);
WebDataBinderFactory factory = mock();
given(factory.createBinder(this.request, target, expectedAttrName)).willReturn(dataBinder);
given(factory.createBinder(this.request, target, expectedAttrName, param)).willReturn(dataBinder);
this.processor.resolveArgument(param, this.container, this.request, factory);
verify(factory).createBinder(this.request, target, expectedAttrName);
verify(factory).createBinder(this.request, target, expectedAttrName, param);
}