/* * 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.net.URI; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.CompletableFuture; import org.junit.Before; import org.junit.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import rx.Completable; import rx.Single; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.codec.ByteBufferEncoder; import org.springframework.core.codec.CharSequenceEncoder; import org.springframework.core.io.buffer.support.DataBufferTestUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.codec.EncoderHttpMessageWriter; import org.springframework.http.codec.ResourceHttpMessageWriter; import org.springframework.http.codec.HttpMessageWriter; import org.springframework.http.codec.json.Jackson2JsonEncoder; import org.springframework.http.codec.xml.Jaxb2XmlEncoder; import org.springframework.mock.http.server.reactive.test.MockServerWebExchange; import org.springframework.util.ObjectUtils; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.springframework.core.ResolvableType.forClassWithGenerics; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.http.ResponseEntity.notFound; import static org.springframework.http.ResponseEntity.ok; import static org.springframework.mock.http.server.reactive.test.MockServerHttpRequest.get; import static org.springframework.web.method.ResolvableMethod.on; import static org.springframework.web.reactive.HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE; /** * Unit tests for {@link ResponseEntityResultHandler}. When adding a test also * consider whether the logic under test is in a parent class, then see: * <ul> * <li>{@code MessageWriterResultHandlerTests}, * <li>{@code ContentNegotiatingResultHandlerSupportTests} * </ul> * @author Rossen Stoyanchev */ public class ResponseEntityResultHandlerTests { private ResponseEntityResultHandler resultHandler; @Before public void setup() throws Exception { this.resultHandler = createHandler(); } private ResponseEntityResultHandler createHandler(HttpMessageWriter<?>... writers) { List<HttpMessageWriter<?>> writerList; if (ObjectUtils.isEmpty(writers)) { writerList = new ArrayList<>(); writerList.add(new EncoderHttpMessageWriter<>(new ByteBufferEncoder())); writerList.add(new EncoderHttpMessageWriter<>(CharSequenceEncoder.textPlainOnly())); writerList.add(new ResourceHttpMessageWriter()); writerList.add(new EncoderHttpMessageWriter<>(new Jaxb2XmlEncoder())); writerList.add(new EncoderHttpMessageWriter<>(new Jackson2JsonEncoder())); writerList.add(new EncoderHttpMessageWriter<>(CharSequenceEncoder.allMimeTypes())); } else { writerList = Arrays.asList(writers); } RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder().build(); return new ResponseEntityResultHandler(writerList, resolver); } @Test @SuppressWarnings("ConstantConditions") public void supports() throws NoSuchMethodException { Object value = null; MethodParameter returnType = on(TestController.class).resolveReturnType(entity(String.class)); assertTrue(this.resultHandler.supports(handlerResult(value, returnType))); returnType = on(TestController.class).resolveReturnType(Mono.class, entity(String.class)); assertTrue(this.resultHandler.supports(handlerResult(value, returnType))); returnType = on(TestController.class).resolveReturnType(Single.class, entity(String.class)); assertTrue(this.resultHandler.supports(handlerResult(value, returnType))); returnType = on(TestController.class).resolveReturnType(CompletableFuture.class, entity(String.class)); assertTrue(this.resultHandler.supports(handlerResult(value, returnType))); } @Test @SuppressWarnings("ConstantConditions") public void doesNotSupport() throws NoSuchMethodException { Object value = null; MethodParameter returnType = on(TestController.class).resolveReturnType(String.class); assertFalse(this.resultHandler.supports(handlerResult(value, returnType))); returnType = on(TestController.class).resolveReturnType(Completable.class); assertFalse(this.resultHandler.supports(handlerResult(value, returnType))); // SPR-15464 returnType = on(TestController.class).resolveReturnType(Flux.class); assertFalse(this.resultHandler.supports(handlerResult(value, returnType))); } @Test public void defaultOrder() throws Exception { assertEquals(0, this.resultHandler.getOrder()); } @Test public void statusCode() throws Exception { ResponseEntity<Void> value = ResponseEntity.noContent().build(); MethodParameter returnType = on(TestController.class).resolveReturnType(entity(Void.class)); HandlerResult result = handlerResult(value, returnType); MockServerWebExchange exchange = get("/path").toExchange(); this.resultHandler.handleResult(exchange, result).block(Duration.ofSeconds(5)); assertEquals(HttpStatus.NO_CONTENT, exchange.getResponse().getStatusCode()); assertEquals(0, exchange.getResponse().getHeaders().size()); assertResponseBodyIsEmpty(exchange); } @Test public void headers() throws Exception { URI location = new URI("/path"); ResponseEntity<Void> value = ResponseEntity.created(location).build(); MethodParameter returnType = on(TestController.class).resolveReturnType(entity(Void.class)); HandlerResult result = handlerResult(value, returnType); MockServerWebExchange exchange = get("/path").toExchange(); this.resultHandler.handleResult(exchange, result).block(Duration.ofSeconds(5)); assertEquals(HttpStatus.CREATED, exchange.getResponse().getStatusCode()); assertEquals(1, exchange.getResponse().getHeaders().size()); assertEquals(location, exchange.getResponse().getHeaders().getLocation()); assertResponseBodyIsEmpty(exchange); } @Test public void handleResponseEntityWithNullBody() throws Exception { Object returnValue = Mono.just(notFound().build()); MethodParameter type = on(TestController.class).resolveReturnType(Mono.class, entity(String.class)); HandlerResult result = handlerResult(returnValue, type); MockServerWebExchange exchange = get("/path").toExchange(); this.resultHandler.handleResult(exchange, result).block(Duration.ofSeconds(5)); assertEquals(HttpStatus.NOT_FOUND, exchange.getResponse().getStatusCode()); assertResponseBodyIsEmpty(exchange); } @Test public void handleReturnTypes() throws Exception { Object returnValue = ok("abc"); MethodParameter returnType = on(TestController.class).resolveReturnType(entity(String.class)); testHandle(returnValue, returnType); returnValue = Mono.just(ok("abc")); returnType = on(TestController.class).resolveReturnType(Mono.class, entity(String.class)); testHandle(returnValue, returnType); returnValue = Mono.just(ok("abc")); returnType = on(TestController.class).resolveReturnType(Single.class, entity(String.class)); testHandle(returnValue, returnType); returnValue = Mono.just(ok("abc")); returnType = on(TestController.class).resolveReturnType(CompletableFuture.class, entity(String.class)); testHandle(returnValue, returnType); } @Test public void handleReturnValueLastModified() throws Exception { Instant currentTime = Instant.now().truncatedTo(ChronoUnit.SECONDS); Instant oneMinAgo = currentTime.minusSeconds(60); MockServerWebExchange exchange = get("/path").ifModifiedSince(currentTime.toEpochMilli()).toExchange(); ResponseEntity<String> entity = ok().lastModified(oneMinAgo.toEpochMilli()).body("body"); MethodParameter returnType = on(TestController.class).resolveReturnType(entity(String.class)); HandlerResult result = handlerResult(entity, returnType); this.resultHandler.handleResult(exchange, result).block(Duration.ofSeconds(5)); assertConditionalResponse(exchange, HttpStatus.NOT_MODIFIED, null, null, oneMinAgo); } @Test public void handleReturnValueEtag() throws Exception { String etagValue = "\"deadb33f8badf00d\""; MockServerWebExchange exchange = get("/path").ifNoneMatch(etagValue).toExchange(); ResponseEntity<String> entity = ok().eTag(etagValue).body("body"); MethodParameter returnType = on(TestController.class).resolveReturnType(entity(String.class)); HandlerResult result = handlerResult(entity, returnType); this.resultHandler.handleResult(exchange, result).block(Duration.ofSeconds(5)); assertConditionalResponse(exchange, HttpStatus.NOT_MODIFIED, null, etagValue, Instant.MIN); } @Test // SPR-14559 public void handleReturnValueEtagInvalidIfNoneMatch() throws Exception { MockServerWebExchange exchange = get("/path").ifNoneMatch("unquoted").toExchange(); ResponseEntity<String> entity = ok().eTag("\"deadb33f8badf00d\"").body("body"); MethodParameter returnType = on(TestController.class).resolveReturnType(entity(String.class)); HandlerResult result = handlerResult(entity, returnType); this.resultHandler.handleResult(exchange, result).block(Duration.ofSeconds(5)); assertEquals(HttpStatus.OK, exchange.getResponse().getStatusCode()); assertResponseBody(exchange, "body"); } @Test public void handleReturnValueETagAndLastModified() throws Exception { String eTag = "\"deadb33f8badf00d\""; Instant currentTime = Instant.now().truncatedTo(ChronoUnit.SECONDS); Instant oneMinAgo = currentTime.minusSeconds(60); MockServerWebExchange exchange = get("/path") .ifNoneMatch(eTag) .ifModifiedSince(currentTime.toEpochMilli()) .toExchange(); ResponseEntity<String> entity = ok().eTag(eTag).lastModified(oneMinAgo.toEpochMilli()).body("body"); MethodParameter returnType = on(TestController.class).resolveReturnType(entity(String.class)); HandlerResult result = handlerResult(entity, returnType); this.resultHandler.handleResult(exchange, result).block(Duration.ofSeconds(5)); assertConditionalResponse(exchange, HttpStatus.NOT_MODIFIED, null, eTag, oneMinAgo); } @Test public void handleReturnValueChangedETagAndLastModified() throws Exception { String etag = "\"deadb33f8badf00d\""; String newEtag = "\"changed-etag-value\""; Instant currentTime = Instant.now().truncatedTo(ChronoUnit.SECONDS); Instant oneMinAgo = currentTime.minusSeconds(60); MockServerWebExchange exchange = get("/path") .ifNoneMatch(etag) .ifModifiedSince(currentTime.toEpochMilli()) .toExchange(); ResponseEntity<String> entity = ok().eTag(newEtag).lastModified(oneMinAgo.toEpochMilli()).body("body"); MethodParameter returnType = on(TestController.class).resolveReturnType(entity(String.class)); HandlerResult result = handlerResult(entity, returnType); this.resultHandler.handleResult(exchange, result).block(Duration.ofSeconds(5)); assertConditionalResponse(exchange, HttpStatus.OK, "body", newEtag, oneMinAgo); } @Test // SPR-14877 public void handleMonoWithWildcardBodyType() throws Exception { MockServerWebExchange exchange = get("/path").toExchange(); exchange.getAttributes().put(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, Collections.singleton(APPLICATION_JSON)); MethodParameter type = on(TestController.class).resolveReturnType(Mono.class, ResponseEntity.class); HandlerResult result = new HandlerResult(new TestController(), Mono.just(ok().body("body")), type); this.resultHandler.handleResult(exchange, result).block(Duration.ofSeconds(5)); assertEquals(HttpStatus.OK, exchange.getResponse().getStatusCode()); assertResponseBody(exchange, "body"); } @Test // SPR-14877 public void handleMonoWithWildcardBodyTypeAndNullBody() throws Exception { MockServerWebExchange exchange = get("/path").toExchange(); exchange.getAttributes().put(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, Collections.singleton(APPLICATION_JSON)); MethodParameter returnType = on(TestController.class).resolveReturnType(Mono.class, ResponseEntity.class); HandlerResult result = new HandlerResult(new TestController(), Mono.just(notFound().build()), returnType); this.resultHandler.handleResult(exchange, result).block(Duration.ofSeconds(5)); assertEquals(HttpStatus.NOT_FOUND, exchange.getResponse().getStatusCode()); assertResponseBodyIsEmpty(exchange); } private void testHandle(Object returnValue, MethodParameter returnType) { MockServerWebExchange exchange = get("/path").toExchange(); HandlerResult result = handlerResult(returnValue, returnType); this.resultHandler.handleResult(exchange, result).block(Duration.ofSeconds(5)); assertEquals(HttpStatus.OK, exchange.getResponse().getStatusCode()); assertEquals("text/plain;charset=UTF-8", exchange.getResponse().getHeaders().getFirst("Content-Type")); assertResponseBody(exchange, "abc"); } private ResolvableType entity(Class<?> bodyType) { return forClassWithGenerics(ResponseEntity.class, bodyType); } private HandlerResult handlerResult(Object returnValue, MethodParameter returnType) { return new HandlerResult(new TestController(), returnValue, returnType); } private void assertResponseBody(MockServerWebExchange exchange, String responseBody) { StepVerifier.create(exchange.getResponse().getBody()) .consumeNextWith(buf -> assertEquals(responseBody, DataBufferTestUtils.dumpString(buf, StandardCharsets.UTF_8))) .expectComplete() .verify(); } private void assertResponseBodyIsEmpty(MockServerWebExchange exchange) { StepVerifier.create(exchange.getResponse().getBody()).expectComplete().verify(); } private void assertConditionalResponse(MockServerWebExchange exchange, HttpStatus status, String body, String etag, Instant lastModified) throws Exception { assertEquals(status, exchange.getResponse().getStatusCode()); if (body != null) { assertResponseBody(exchange, body); } else { assertResponseBodyIsEmpty(exchange); } if (etag != null) { assertEquals(1, exchange.getResponse().getHeaders().get(HttpHeaders.ETAG).size()); assertEquals(etag, exchange.getResponse().getHeaders().getETag()); } if (lastModified.isAfter(Instant.EPOCH)) { assertEquals(1, exchange.getResponse().getHeaders().get(HttpHeaders.LAST_MODIFIED).size()); assertEquals(lastModified.toEpochMilli(), exchange.getResponse().getHeaders().getLastModified()); } } @SuppressWarnings("unused") private static class TestController { ResponseEntity<String> responseEntityString() { return null; } ResponseEntity<Void> responseEntityVoid() { return null; } Mono<ResponseEntity<String>> mono() { return null; } Single<ResponseEntity<String>> single() { return null; } CompletableFuture<ResponseEntity<String>> completableFuture() { return null; } String string() { return null; } Completable completable() { return null; } Mono<ResponseEntity<?>> monoResponseEntityWildcard() { return null; } Flux<?> fluxWildcard() { return null; } } }