/*
* Copyright 2002-2017 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.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import io.reactivex.Maybe;
import org.junit.Before;
import org.junit.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import rx.Observable;
import rx.RxReactiveStreams;
import rx.Single;
import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.codec.StringDecoder;
import org.springframework.http.codec.DecoderHttpMessageReader;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.method.ResolvableMethod;
import org.springframework.web.reactive.BindingContext;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebInputException;
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.web.method.MvcAnnotationPredicates.requestBody;
/**
* Unit tests for {@link RequestBodyArgumentResolver}. When adding a test also
* consider whether the logic under test is in a parent class, then see:
* {@link MessageReaderArgumentResolverTests}.
*
* @author Rossen Stoyanchev
*/
public class RequestBodyArgumentResolverTests {
private RequestBodyArgumentResolver resolver;
private ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handle").build();
@Before
public void setup() {
List<HttpMessageReader<?>> readers = new ArrayList<>();
readers.add(new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes(true)));
this.resolver = new RequestBodyArgumentResolver(readers, new ReactiveAdapterRegistry());
}
@Test
public void supports() throws Exception {
MethodParameter param;
param = this.testMethod.annot(requestBody()).arg(Mono.class, String.class);
assertTrue(this.resolver.supportsParameter(param));
param = this.testMethod.annotNotPresent(RequestBody.class).arg(String.class);
assertFalse(this.resolver.supportsParameter(param));
}
@Test
public void stringBody() throws Exception {
String body = "line1";
MethodParameter param = this.testMethod.annot(requestBody()).arg(String.class);
String value = resolveValue(param, body);
assertEquals(body, value);
}
@Test(expected = ServerWebInputException.class)
public void emptyBodyWithString() throws Exception {
MethodParameter param = this.testMethod.annot(requestBody()).arg(String.class);
resolveValueWithEmptyBody(param);
}
@Test
public void emptyBodyWithStringNotRequired() throws Exception {
MethodParameter param = this.testMethod.annot(requestBody().notRequired()).arg(String.class);
String body = resolveValueWithEmptyBody(param);
assertNull(body);
}
@Test
@SuppressWarnings("unchecked")
public void emptyBodyWithMono() throws Exception {
MethodParameter param = this.testMethod.annot(requestBody()).arg(Mono.class, String.class);
StepVerifier.create((Mono<Void>) resolveValueWithEmptyBody(param))
.expectNextCount(0)
.expectError(ServerWebInputException.class)
.verify();
param = this.testMethod.annot(requestBody().notRequired()).arg(Mono.class, String.class);
StepVerifier.create((Mono<Void>) resolveValueWithEmptyBody(param))
.expectNextCount(0)
.expectComplete()
.verify();
}
@Test
@SuppressWarnings("unchecked")
public void emptyBodyWithFlux() throws Exception {
MethodParameter param = this.testMethod.annot(requestBody()).arg(Flux.class, String.class);
StepVerifier.create((Flux<Void>) resolveValueWithEmptyBody(param))
.expectNextCount(0)
.expectError(ServerWebInputException.class)
.verify();
param = this.testMethod.annot(requestBody().notRequired()).arg(Flux.class, String.class);
StepVerifier.create((Flux<Void>) resolveValueWithEmptyBody(param))
.expectNextCount(0)
.expectComplete()
.verify();
}
@Test
public void emptyBodyWithSingle() throws Exception {
MethodParameter param = this.testMethod.annot(requestBody()).arg(Single.class, String.class);
Single<String> single = resolveValueWithEmptyBody(param);
StepVerifier.create(RxReactiveStreams.toPublisher(single))
.expectNextCount(0)
.expectError(ServerWebInputException.class)
.verify();
param = this.testMethod.annot(requestBody().notRequired()).arg(Single.class, String.class);
single = resolveValueWithEmptyBody(param);
StepVerifier.create(RxReactiveStreams.toPublisher(single))
.expectNextCount(0)
.expectError(ServerWebInputException.class)
.verify();
}
@Test
public void emptyBodyWithMaybe() throws Exception {
MethodParameter param = this.testMethod.annot(requestBody()).arg(Maybe.class, String.class);
Maybe<String> maybe = resolveValueWithEmptyBody(param);
StepVerifier.create(maybe.toFlowable())
.expectNextCount(0)
.expectError(ServerWebInputException.class)
.verify();
param = this.testMethod.annot(requestBody().notRequired()).arg(Maybe.class, String.class);
maybe = resolveValueWithEmptyBody(param);
StepVerifier.create(maybe.toFlowable())
.expectNextCount(0)
.expectComplete()
.verify();
}
@Test
public void emptyBodyWithObservable() throws Exception {
MethodParameter param = this.testMethod.annot(requestBody()).arg(Observable.class, String.class);
Observable<String> observable = resolveValueWithEmptyBody(param);
StepVerifier.create(RxReactiveStreams.toPublisher(observable))
.expectNextCount(0)
.expectError(ServerWebInputException.class)
.verify();
param = this.testMethod.annot(requestBody().notRequired()).arg(Observable.class, String.class);
observable = resolveValueWithEmptyBody(param);
StepVerifier.create(RxReactiveStreams.toPublisher(observable))
.expectNextCount(0)
.expectComplete()
.verify();
}
@Test
public void emptyBodyWithCompletableFuture() throws Exception {
MethodParameter param = this.testMethod.annot(requestBody()).arg(CompletableFuture.class, String.class);
CompletableFuture<String> future = resolveValueWithEmptyBody(param);
future.whenComplete((text, ex) -> {
assertNull(text);
assertNotNull(ex);
});
param = this.testMethod.annot(requestBody().notRequired()).arg(CompletableFuture.class, String.class);
future = resolveValueWithEmptyBody(param);
future.whenComplete((text, ex) -> {
assertNotNull(text);
assertNull(ex);
});
}
@SuppressWarnings("unchecked")
private <T> T resolveValue(MethodParameter param, String body) {
ServerWebExchange exchange = MockServerHttpRequest.post("/path").body(body).toExchange();
Mono<Object> result = this.resolver.readBody(param, true, new BindingContext(), exchange);
Object value = result.block(Duration.ofSeconds(5));
assertNotNull(value);
assertTrue("Unexpected return value type: " + value,
param.getParameterType().isAssignableFrom(value.getClass()));
//no inspection unchecked
return (T) value;
}
@SuppressWarnings("unchecked")
private <T> T resolveValueWithEmptyBody(MethodParameter param) {
ServerWebExchange exchange = MockServerHttpRequest.post("/path").build().toExchange();
Mono<Object> result = this.resolver.resolveArgument(param, new BindingContext(), exchange);
Object value = result.block(Duration.ofSeconds(5));
if (value != null) {
assertTrue("Unexpected parameter type: " + value,
param.getParameterType().isAssignableFrom(value.getClass()));
}
//no inspection unchecked
return (T) value;
}
@SuppressWarnings("unused")
void handle(
@RequestBody String string,
@RequestBody Mono<String> mono,
@RequestBody Flux<String> flux,
@RequestBody Single<String> single,
@RequestBody io.reactivex.Single<String> rxJava2Single,
@RequestBody Maybe<String> rxJava2Maybe,
@RequestBody Observable<String> obs,
@RequestBody io.reactivex.Observable<String> rxjava2Obs,
@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) io.reactivex.Single<String> rxJava2SingleNotRequired,
@RequestBody(required = false) Maybe<String> rxJava2MaybeNotRequired,
@RequestBody(required = false) Observable<String> obsNotRequired,
@RequestBody(required = false) io.reactivex.Observable<String> rxjava2ObsNotRequired,
@RequestBody(required = false) CompletableFuture<String> futureNotRequired,
String notAnnotated) {}
}