diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java index 73b5431dc9..7f41c3b6b9 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java @@ -103,8 +103,14 @@ public class ModelAttributeMethodArgumentResolver extends HandlerMethodArgumentR String name = ModelInitializer.getNameForParameter(parameter); - Mono dataBinderMono = initDataBinder( - name, (adapter != null ? parameter.nested() : parameter), context, exchange); + Mono 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 bindingResultSink = Sinks.unsafe().one(); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java index 18b27df971..fbc1ba74bb 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java @@ -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 valueExtractor) throws Exception { + Function 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 valueExtractor) - throws Exception { + private void testBindPojo(String modelKey, MethodParameter param, Function 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> valueMonoExtractor) - throws URISyntaxException { - + private void testValidationError(MethodParameter parameter, Function, Mono> valueMonoExtractor) { testValidationError(parameter, valueMonoExtractor, "age=invalid", "age", "invalid"); } - private void testValidationErrorWithoutBinding(MethodParameter parameter, Function, Mono> valueMonoExtractor) - throws URISyntaxException { - + private void testValidationErrorWithoutBinding(MethodParameter parameter, Function, Mono> valueMonoExtractor) { testValidationError(parameter, valueMonoExtractor, "name=Enigma", "name", null); } private void testValidationError(MethodParameter param, Function, 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)); diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverKotlinTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverKotlinTests.kt new file mode 100644 index 0000000000..2e09989d89 --- /dev/null +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverKotlinTests.kt @@ -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<*>>) { + testValidationError(parameter, valueMonoExtractor, "age=invalid", "age", "invalid") + } + + private fun testValidationError(param: MethodParameter, valueMonoExtractor: Function, 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) + +}