Merge branch '6.0.x'
Adapt gh-31045 fix to the main branch, and throw a WebExchangeBindException instead of a ServerWebInputException.
This commit is contained in:
@@ -103,8 +103,14 @@ public class ModelAttributeMethodArgumentResolver extends HandlerMethodArgumentR
|
||||
|
||||
String name = ModelInitializer.getNameForParameter(parameter);
|
||||
|
||||
Mono<WebExchangeDataBinder> dataBinderMono = initDataBinder(
|
||||
name, (adapter != null ? parameter.nested() : parameter), context, exchange);
|
||||
Mono<WebExchangeDataBinder> dataBinderMono =
|
||||
initDataBinder(name, (adapter != null ? parameter.nested() : parameter), context, exchange)
|
||||
.doOnNext(binder -> {
|
||||
BindingResult errors = binder.getBindingResult();
|
||||
if (errors.hasErrors()) {
|
||||
throw new WebExchangeBindException(parameter, errors);
|
||||
}
|
||||
});
|
||||
|
||||
// unsafe() is OK: source is Reactive Streams Publisher
|
||||
Sinks.One<BindingResult> bindingResultSink = Sinks.unsafe().one();
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package org.springframework.web.reactive.result.method.annotation;
|
||||
|
||||
import java.net.URISyntaxException;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
@@ -52,6 +51,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @author Sebastien Deleuze
|
||||
*/
|
||||
class ModelAttributeMethodArgumentResolverTests {
|
||||
|
||||
@@ -115,7 +115,7 @@ class ModelAttributeMethodArgumentResolverTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void createAndBind() throws Exception {
|
||||
void createAndBind() {
|
||||
testBindPojo("pojo", this.testMethod.annotPresent(ModelAttribute.class).arg(Pojo.class), value -> {
|
||||
assertThat(value.getClass()).isEqualTo(Pojo.class);
|
||||
return (Pojo) value;
|
||||
@@ -123,7 +123,7 @@ class ModelAttributeMethodArgumentResolverTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void createAndBindToMono() throws Exception {
|
||||
void createAndBindToMono() {
|
||||
MethodParameter parameter = this.testMethod
|
||||
.annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class);
|
||||
|
||||
@@ -136,7 +136,7 @@ class ModelAttributeMethodArgumentResolverTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void createAndBindToSingle() throws Exception {
|
||||
void createAndBindToSingle() {
|
||||
MethodParameter parameter = this.testMethod
|
||||
.annotPresent(ModelAttribute.class).arg(Single.class, Pojo.class);
|
||||
|
||||
@@ -149,7 +149,7 @@ class ModelAttributeMethodArgumentResolverTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void createButDoNotBind() throws Exception {
|
||||
void createButDoNotBind() {
|
||||
MethodParameter parameter =
|
||||
this.testMethod.annotPresent(ModelAttribute.class).arg(NonBindingPojo.class);
|
||||
|
||||
@@ -160,7 +160,7 @@ class ModelAttributeMethodArgumentResolverTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void createButDoNotBindToMono() throws Exception {
|
||||
void createButDoNotBindToMono() {
|
||||
MethodParameter parameter =
|
||||
this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, NonBindingPojo.class);
|
||||
|
||||
@@ -173,7 +173,7 @@ class ModelAttributeMethodArgumentResolverTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void createButDoNotBindToSingle() throws Exception {
|
||||
void createButDoNotBindToSingle() {
|
||||
MethodParameter parameter =
|
||||
this.testMethod.annotPresent(ModelAttribute.class).arg(Single.class, NonBindingPojo.class);
|
||||
|
||||
@@ -186,7 +186,7 @@ class ModelAttributeMethodArgumentResolverTests {
|
||||
}
|
||||
|
||||
private void createButDoNotBindToPojo(String modelKey, MethodParameter methodParameter,
|
||||
Function<Object, NonBindingPojo> valueExtractor) throws Exception {
|
||||
Function<Object, NonBindingPojo> valueExtractor) {
|
||||
|
||||
Object value = createResolver()
|
||||
.resolveArgument(methodParameter, this.bindContext, postForm("name=Enigma"))
|
||||
@@ -205,7 +205,7 @@ class ModelAttributeMethodArgumentResolverTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void bindExisting() throws Exception {
|
||||
void bindExisting() {
|
||||
Pojo pojo = new Pojo();
|
||||
pojo.setName("Jim");
|
||||
this.bindContext.getModel().addAttribute(pojo);
|
||||
@@ -220,7 +220,7 @@ class ModelAttributeMethodArgumentResolverTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void bindExistingMono() throws Exception {
|
||||
void bindExistingMono() {
|
||||
Pojo pojo = new Pojo();
|
||||
pojo.setName("Jim");
|
||||
this.bindContext.getModel().addAttribute("pojoMono", Mono.just(pojo));
|
||||
@@ -235,7 +235,7 @@ class ModelAttributeMethodArgumentResolverTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void bindExistingSingle() throws Exception {
|
||||
void bindExistingSingle() {
|
||||
Pojo pojo = new Pojo();
|
||||
pojo.setName("Jim");
|
||||
this.bindContext.getModel().addAttribute("pojoSingle", Single.just(pojo));
|
||||
@@ -250,7 +250,7 @@ class ModelAttributeMethodArgumentResolverTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void bindExistingMonoToMono() throws Exception {
|
||||
void bindExistingMonoToMono() {
|
||||
Pojo pojo = new Pojo();
|
||||
pojo.setName("Jim");
|
||||
String modelKey = "pojoMono";
|
||||
@@ -267,8 +267,7 @@ class ModelAttributeMethodArgumentResolverTests {
|
||||
});
|
||||
}
|
||||
|
||||
private void testBindPojo(String modelKey, MethodParameter param, Function<Object, Pojo> valueExtractor)
|
||||
throws Exception {
|
||||
private void testBindPojo(String modelKey, MethodParameter param, Function<Object, Pojo> valueExtractor) {
|
||||
|
||||
Object value = createResolver()
|
||||
.resolveArgument(param, this.bindContext, postForm("name= Robert&age=25"))
|
||||
@@ -287,13 +286,19 @@ class ModelAttributeMethodArgumentResolverTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void validationErrorForPojo() throws Exception {
|
||||
void validationErrorForPojo() {
|
||||
MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(Pojo.class);
|
||||
testValidationError(parameter, Function.identity());
|
||||
}
|
||||
|
||||
@Test
|
||||
void validationErrorForMono() throws Exception {
|
||||
void validationErrorForDataClass() {
|
||||
MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(DataClass.class);
|
||||
testValidationError(parameter, Function.identity());
|
||||
}
|
||||
|
||||
@Test
|
||||
void validationErrorForMono() {
|
||||
MethodParameter parameter = this.testMethod
|
||||
.annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class);
|
||||
|
||||
@@ -306,7 +311,7 @@ class ModelAttributeMethodArgumentResolverTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void validationErrorForSingle() throws Exception {
|
||||
void validationErrorForSingle() {
|
||||
MethodParameter parameter = this.testMethod
|
||||
.annotPresent(ModelAttribute.class).arg(Single.class, Pojo.class);
|
||||
|
||||
@@ -319,13 +324,13 @@ class ModelAttributeMethodArgumentResolverTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void validationErrorWithoutBindingForPojo() throws Exception {
|
||||
void validationErrorWithoutBindingForPojo() {
|
||||
MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(ValidatedPojo.class);
|
||||
testValidationErrorWithoutBinding(parameter, Function.identity());
|
||||
}
|
||||
|
||||
@Test
|
||||
void validationErrorWithoutBindingForMono() throws Exception {
|
||||
void validationErrorWithoutBindingForMono() {
|
||||
MethodParameter parameter = this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, ValidatedPojo.class);
|
||||
|
||||
testValidationErrorWithoutBinding(parameter, resolvedArgumentMono -> {
|
||||
@@ -346,20 +351,16 @@ class ModelAttributeMethodArgumentResolverTests {
|
||||
});
|
||||
}
|
||||
|
||||
private void testValidationError(MethodParameter parameter, Function<Mono<?>, Mono<?>> valueMonoExtractor)
|
||||
throws URISyntaxException {
|
||||
|
||||
private void testValidationError(MethodParameter parameter, Function<Mono<?>, Mono<?>> valueMonoExtractor) {
|
||||
testValidationError(parameter, valueMonoExtractor, "age=invalid", "age", "invalid");
|
||||
}
|
||||
|
||||
private void testValidationErrorWithoutBinding(MethodParameter parameter, Function<Mono<?>, Mono<?>> valueMonoExtractor)
|
||||
throws URISyntaxException {
|
||||
|
||||
private void testValidationErrorWithoutBinding(MethodParameter parameter, Function<Mono<?>, Mono<?>> valueMonoExtractor) {
|
||||
testValidationError(parameter, valueMonoExtractor, "name=Enigma", "name", null);
|
||||
}
|
||||
|
||||
private void testValidationError(MethodParameter param, Function<Mono<?>, Mono<?>> valueMonoExtractor,
|
||||
String formData, String field, String rejectedValue) throws URISyntaxException {
|
||||
String formData, String field, String rejectedValue) {
|
||||
|
||||
Mono<?> mono = createResolver().resolveArgument(param, this.bindContext, postForm(formData));
|
||||
mono = valueMonoExtractor.apply(mono);
|
||||
@@ -376,7 +377,7 @@ class ModelAttributeMethodArgumentResolverTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void bindDataClass() throws Exception {
|
||||
void bindDataClass() {
|
||||
MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(DataClass.class);
|
||||
|
||||
Object value = createResolver()
|
||||
@@ -404,7 +405,7 @@ class ModelAttributeMethodArgumentResolverTests {
|
||||
return new ModelAttributeMethodArgumentResolver(ReactiveAdapterRegistry.getSharedInstance(), false);
|
||||
}
|
||||
|
||||
private ServerWebExchange postForm(String formData) throws URISyntaxException {
|
||||
private ServerWebExchange postForm(String formData) {
|
||||
return MockServerWebExchange.from(MockServerHttpRequest.post("/")
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.body(formData));
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* 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.reactive.result.method.annotation
|
||||
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.core.MethodParameter
|
||||
import org.springframework.core.ReactiveAdapterRegistry
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean
|
||||
import org.springframework.web.bind.annotation.ModelAttribute
|
||||
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer
|
||||
import org.springframework.web.bind.support.WebExchangeBindException
|
||||
import org.springframework.web.reactive.BindingContext
|
||||
import org.springframework.web.server.ServerWebExchange
|
||||
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest
|
||||
import org.springframework.web.testfixture.method.ResolvableMethod
|
||||
import org.springframework.web.testfixture.server.MockServerWebExchange
|
||||
import reactor.core.publisher.Mono
|
||||
import reactor.test.StepVerifier
|
||||
import java.util.function.Function
|
||||
|
||||
/**
|
||||
* Kotlin test fixture for [ModelAttributeMethodArgumentResolver].
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
*/
|
||||
class ModelAttributeMethodArgumentResolverKotlinTests {
|
||||
|
||||
private val testMethod = ResolvableMethod.on(javaClass).named("handle").build()
|
||||
|
||||
private lateinit var bindContext: BindingContext
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
val validator = LocalValidatorFactoryBean()
|
||||
validator.afterPropertiesSet()
|
||||
val initializer = ConfigurableWebBindingInitializer()
|
||||
initializer.validator = validator
|
||||
this.bindContext = BindingContext(initializer)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validationErrorForDataClass() {
|
||||
val parameter = this.testMethod.annotNotPresent(ModelAttribute::class.java).arg(DataClass::class.java)
|
||||
testValidationError(parameter, Function.identity())
|
||||
}
|
||||
|
||||
private fun testValidationError(parameter: MethodParameter, valueMonoExtractor: Function<Mono<*>, Mono<*>>) {
|
||||
testValidationError(parameter, valueMonoExtractor, "age=invalid", "age", "invalid")
|
||||
}
|
||||
|
||||
private fun testValidationError(param: MethodParameter, valueMonoExtractor: Function<Mono<*>, Mono<*>>,
|
||||
formData: String, field: String, rejectedValue: String) {
|
||||
var mono: Mono<*> = createResolver().resolveArgument(param, this.bindContext, postForm(formData))
|
||||
mono = valueMonoExtractor.apply(mono)
|
||||
StepVerifier.create(mono)
|
||||
.consumeErrorWith { ex: Throwable ->
|
||||
Assertions.assertThat(ex).isInstanceOf(WebExchangeBindException::class.java)
|
||||
val bindException = ex as WebExchangeBindException
|
||||
Assertions.assertThat(bindException.errorCount).isEqualTo(1)
|
||||
Assertions.assertThat(bindException.hasFieldErrors(field)).isTrue()
|
||||
Assertions.assertThat(bindException.getFieldError(field)!!.rejectedValue)
|
||||
.isEqualTo(rejectedValue)
|
||||
}
|
||||
.verify()
|
||||
}
|
||||
|
||||
private fun createResolver(): ModelAttributeMethodArgumentResolver {
|
||||
return ModelAttributeMethodArgumentResolver(ReactiveAdapterRegistry.getSharedInstance(), false)
|
||||
}
|
||||
|
||||
private fun postForm(formData: String): ServerWebExchange {
|
||||
return MockServerWebExchange.from(
|
||||
MockServerHttpRequest.post("/")
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.body(formData)
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
private fun handle(dataClassNotAnnotated: DataClass) {
|
||||
}
|
||||
|
||||
private class DataClass(val name: String, val age: Int, val count: Int)
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user