/* * Copyright 2002-2016 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 * * http://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 java.net.URI; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.function.Predicate; import org.junit.Before; import org.junit.Test; import reactor.core.converter.RxJava1ObservableConverter; import reactor.core.converter.RxJava1SingleConverter; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.test.TestSubscriber; import rx.Observable; import rx.Single; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.codec.StringDecoder; import org.springframework.core.convert.support.MonoToCompletableFutureConverter; import org.springframework.core.convert.support.ReactorToRxJava1Converter; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.format.support.FormattingConversionService; import org.springframework.http.HttpMethod; import org.springframework.http.converter.reactive.CodecHttpMessageConverter; import org.springframework.http.converter.reactive.HttpMessageConverter; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.ui.ExtendedModelMap; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.reactive.result.ResolvableMethod; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebInputException; import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.MockWebSessionManager; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.springframework.core.ResolvableType.forClass; import static org.springframework.core.ResolvableType.forClassWithGenerics; /** * Unit tests for {@link RequestBodyArgumentResolver}.When adding a test also * consider whether the logic under test is in a parent class, then see: * {@link MessageConverterArgumentResolverTests}. * * @author Rossen Stoyanchev */ public class RequestBodyArgumentResolverTests { private RequestBodyArgumentResolver resolver = resolver(); private ServerWebExchange exchange; private MockServerHttpRequest request; private ResolvableMethod testMethod = ResolvableMethod.onClass(this.getClass()).name("handle"); @Before public void setUp() throws Exception { this.request = new MockServerHttpRequest(HttpMethod.POST, new URI("/path")); MockServerHttpResponse response = new MockServerHttpResponse(); this.exchange = new DefaultServerWebExchange(this.request, response, new MockWebSessionManager()); } private RequestBodyArgumentResolver resolver() { List<HttpMessageConverter<?>> converters = new ArrayList<>(); converters.add(new CodecHttpMessageConverter<>(new StringDecoder())); FormattingConversionService service = new DefaultFormattingConversionService(); service.addConverter(new MonoToCompletableFutureConverter()); service.addConverter(new ReactorToRxJava1Converter()); return new RequestBodyArgumentResolver(converters, service); } @Test public void supports() throws Exception { ResolvableType type = forClassWithGenerics(Mono.class, String.class); MethodParameter param = this.testMethod.resolveParam(type, requestBody(true)); assertTrue(this.resolver.supportsParameter(param)); MethodParameter parameter = this.testMethod.resolveParam(p -> !p.hasParameterAnnotations()); assertFalse(this.resolver.supportsParameter(parameter)); } @Test public void stringBody() throws Exception { String body = "line1"; ResolvableType type = forClass(String.class); MethodParameter param = this.testMethod.resolveParam(type, requestBody(true)); String value = resolveValue(param, body); assertEquals(body, value); } @Test(expected = ServerWebInputException.class) public void emptyBodyWithString() throws Exception { resolveValueWithEmptyBody(forClass(String.class), true); } @Test public void emptyBodyWithStringNotRequired() throws Exception { ResolvableType type = forClass(String.class); String body = resolveValueWithEmptyBody(type, false); assertNull(body); } @Test public void emptyBodyWithMono() throws Exception { ResolvableType type = forClassWithGenerics(Mono.class, String.class); TestSubscriber.subscribe(resolveValueWithEmptyBody(type, true)) .assertNoValues() .assertError(ServerWebInputException.class); TestSubscriber.subscribe(resolveValueWithEmptyBody(type, false)) .assertNoValues() .assertComplete(); } @Test public void emptyBodyWithFlux() throws Exception { ResolvableType type = forClassWithGenerics(Flux.class, String.class); TestSubscriber.subscribe(resolveValueWithEmptyBody(type, true)) .assertNoValues() .assertError(ServerWebInputException.class); TestSubscriber.subscribe(resolveValueWithEmptyBody(type, false)) .assertNoValues() .assertComplete(); } @Test public void emptyBodyWithSingle() throws Exception { ResolvableType type = forClassWithGenerics(Single.class, String.class); Single<String> single = resolveValueWithEmptyBody(type, true); TestSubscriber.subscribe(RxJava1SingleConverter.toPublisher(single)) .assertNoValues() .assertError(ServerWebInputException.class); single = resolveValueWithEmptyBody(type, false); TestSubscriber.subscribe(RxJava1SingleConverter.toPublisher(single)) .assertNoValues() .assertError(ServerWebInputException.class); } @Test public void emptyBodyWithObservable() throws Exception { ResolvableType type = forClassWithGenerics(Observable.class, String.class); Observable<String> observable = resolveValueWithEmptyBody(type, true); TestSubscriber.subscribe(RxJava1ObservableConverter.toPublisher(observable)) .assertNoValues() .assertError(ServerWebInputException.class); observable = resolveValueWithEmptyBody(type, false); TestSubscriber.subscribe(RxJava1ObservableConverter.toPublisher(observable)) .assertNoValues() .assertComplete(); } @Test public void emptyBodyWithCompletableFuture() throws Exception { ResolvableType type = forClassWithGenerics(CompletableFuture.class, String.class); CompletableFuture<String> future = resolveValueWithEmptyBody(type, true); future.whenComplete((text, ex) -> { assertNull(text); assertNotNull(ex); }); future = resolveValueWithEmptyBody(type, false); future.whenComplete((text, ex) -> { assertNotNull(text); assertNull(ex); }); } private <T> T resolveValue(MethodParameter param, String body) { this.request.writeWith(Flux.just(dataBuffer(body))); Mono<Object> result = this.resolver.readBody(param, true, this.exchange); Object value = result.block(Duration.ofSeconds(5)); assertNotNull(value); assertTrue("Unexpected return value type: " + value, param.getParameterType().isAssignableFrom(value.getClass())); //noinspection unchecked return (T) value; } private <T> T resolveValueWithEmptyBody(ResolvableType bodyType, boolean isRequired) { this.request.writeWith(Flux.empty()); MethodParameter param = this.testMethod.resolveParam(bodyType, requestBody(isRequired)); Mono<Object> result = this.resolver.resolveArgument(param, new ExtendedModelMap(), this.exchange); Object value = result.block(Duration.ofSeconds(5)); if (value != null) { assertTrue("Unexpected return value type: " + value, param.getParameterType().isAssignableFrom(value.getClass())); } //noinspection unchecked return (T) value; } private Predicate<MethodParameter> requestBody(boolean required) { return p -> { RequestBody annotation = p.getParameterAnnotation(RequestBody.class); return annotation != null && annotation.required() == required; }; } private DataBuffer dataBuffer(String body) { byte[] bytes = body.getBytes(Charset.forName("UTF-8")); ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); return new DefaultDataBufferFactory().wrap(byteBuffer); } @SuppressWarnings("unused") void handle( @RequestBody String string, @RequestBody Mono<String> mono, @RequestBody Flux<String> flux, @RequestBody Single<String> single, @RequestBody Observable<String> obs, @RequestBody CompletableFuture<String> future, @RequestBody(required = false) String stringNotRequired, @RequestBody(required = false) Mono<String> monoNotRequired, @RequestBody(required = false) Flux<String> fluxNotRequired, @RequestBody(required = false) Single<String> singleNotRequired, @RequestBody(required = false) Observable<String> obsNotRequired, @RequestBody(required = false) CompletableFuture<String> futureNotRequired, String notAnnotated) {} }