Skip to content

Commit

Permalink
Merge branch '6.0.x'
Browse files Browse the repository at this point in the history
Adapt spring-projectsgh-31045 fix to the main branch, and throw a
WebExchangeBindException instead of a ServerWebInputException.
  • Loading branch information
sdeleuze committed Aug 18, 2023
2 parents 3dcac0c + e6565c6 commit 334b154
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 30 deletions.
Expand Up @@ -103,8 +103,14 @@ public Mono<Object> resolveArgument(

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();
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -52,6 +51,7 @@
* @author Rossen Stoyanchev
* @author Juergen Hoeller
* @author Sam Brannen
* @author Sebastien Deleuze
*/
class ModelAttributeMethodArgumentResolverTests {

Expand Down Expand Up @@ -115,15 +115,15 @@ void supportsWithDefaultResolution() {
}

@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;
});
}

@Test
void createAndBindToMono() throws Exception {
void createAndBindToMono() {
MethodParameter parameter = this.testMethod
.annotNotPresent(ModelAttribute.class).arg(Mono.class, Pojo.class);

Expand All @@ -136,7 +136,7 @@ void createAndBindToMono() throws Exception {
}

@Test
void createAndBindToSingle() throws Exception {
void createAndBindToSingle() {
MethodParameter parameter = this.testMethod
.annotPresent(ModelAttribute.class).arg(Single.class, Pojo.class);

Expand All @@ -149,7 +149,7 @@ void createAndBindToSingle() throws Exception {
}

@Test
void createButDoNotBind() throws Exception {
void createButDoNotBind() {
MethodParameter parameter =
this.testMethod.annotPresent(ModelAttribute.class).arg(NonBindingPojo.class);

Expand All @@ -160,7 +160,7 @@ void createButDoNotBind() throws Exception {
}

@Test
void createButDoNotBindToMono() throws Exception {
void createButDoNotBindToMono() {
MethodParameter parameter =
this.testMethod.annotPresent(ModelAttribute.class).arg(Mono.class, NonBindingPojo.class);

Expand All @@ -173,7 +173,7 @@ void createButDoNotBindToMono() throws Exception {
}

@Test
void createButDoNotBindToSingle() throws Exception {
void createButDoNotBindToSingle() {
MethodParameter parameter =
this.testMethod.annotPresent(ModelAttribute.class).arg(Single.class, NonBindingPojo.class);

Expand All @@ -186,7 +186,7 @@ void createButDoNotBindToSingle() throws Exception {
}

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"))
Expand All @@ -205,7 +205,7 @@ private void createButDoNotBindToPojo(String modelKey, MethodParameter methodPar
}

@Test
void bindExisting() throws Exception {
void bindExisting() {
Pojo pojo = new Pojo();
pojo.setName("Jim");
this.bindContext.getModel().addAttribute(pojo);
Expand All @@ -220,7 +220,7 @@ void bindExisting() throws Exception {
}

@Test
void bindExistingMono() throws Exception {
void bindExistingMono() {
Pojo pojo = new Pojo();
pojo.setName("Jim");
this.bindContext.getModel().addAttribute("pojoMono", Mono.just(pojo));
Expand All @@ -235,7 +235,7 @@ void bindExistingMono() throws Exception {
}

@Test
void bindExistingSingle() throws Exception {
void bindExistingSingle() {
Pojo pojo = new Pojo();
pojo.setName("Jim");
this.bindContext.getModel().addAttribute("pojoSingle", Single.just(pojo));
Expand All @@ -250,7 +250,7 @@ void bindExistingSingle() throws Exception {
}

@Test
void bindExistingMonoToMono() throws Exception {
void bindExistingMonoToMono() {
Pojo pojo = new Pojo();
pojo.setName("Jim");
String modelKey = "pojoMono";
Expand All @@ -267,8 +267,7 @@ void bindExistingMonoToMono() throws Exception {
});
}

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"))
Expand All @@ -287,13 +286,19 @@ private void testBindPojo(String modelKey, MethodParameter param, Function<Objec
}

@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);

Expand All @@ -306,7 +311,7 @@ void validationErrorForMono() throws Exception {
}

@Test
void validationErrorForSingle() throws Exception {
void validationErrorForSingle() {
MethodParameter parameter = this.testMethod
.annotPresent(ModelAttribute.class).arg(Single.class, Pojo.class);

Expand All @@ -319,13 +324,13 @@ void validationErrorForSingle() throws Exception {
}

@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 -> {
Expand All @@ -346,20 +351,16 @@ void validationErrorWithoutBindingForSingle() throws Exception {
});
}

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);
Expand All @@ -376,7 +377,7 @@ private void testValidationError(MethodParameter param, Function<Mono<?>, Mono<?
}

@Test
void bindDataClass() throws Exception {
void bindDataClass() {
MethodParameter parameter = this.testMethod.annotNotPresent(ModelAttribute.class).arg(DataClass.class);

Object value = createResolver()
Expand Down Expand Up @@ -404,7 +405,7 @@ private ModelAttributeMethodArgumentResolver createResolver() {
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));
Expand Down
@@ -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)

}

0 comments on commit 334b154

Please sign in to comment.