diff --git a/spring-test/src/main/java/org/springframework/test/web/client/response/DefaultResponseCreator.java b/spring-test/src/main/java/org/springframework/test/web/client/response/DefaultResponseCreator.java index 081001fe8ab7..c3dc650e442f 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/response/DefaultResponseCreator.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/response/DefaultResponseCreator.java @@ -18,18 +18,25 @@ import java.io.IOException; import java.net.URI; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; +import org.springframework.http.ResponseCookie; import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpResponse; import org.springframework.lang.Nullable; import org.springframework.mock.http.client.MockClientHttpResponse; import org.springframework.test.web.client.ResponseCreator; import org.springframework.util.Assert; +import org.springframework.util.MultiValueMap; /** * A {@code ResponseCreator} with builder-style methods for adding response details. @@ -67,7 +74,6 @@ protected DefaultResponseCreator(HttpStatusCode statusCode) { this.statusCode = statusCode; } - /** * Set the body as a UTF-8 String. */ @@ -76,6 +82,14 @@ public DefaultResponseCreator body(String content) { return this; } + /** + * Set the body from a string using the given character set. + */ + public DefaultResponseCreator body(String content, Charset charset) { + this.content = content.getBytes(charset); + return this; + } + /** * Set the body as a byte array. */ @@ -85,7 +99,7 @@ public DefaultResponseCreator body(byte[] content) { } /** - * Set the body as a {@link Resource}. + * Set the body from a {@link Resource}. */ public DefaultResponseCreator body(Resource resource) { this.contentResource = resource; @@ -108,6 +122,26 @@ public DefaultResponseCreator location(URI location) { return this; } + /** + * Add a single header. + */ + public DefaultResponseCreator header(String name, String value) { + // This is really just an alias, but it makes the interface more fluent. + return headers(name, value); + } + + /** + * Add one or more headers. + */ + public DefaultResponseCreator headers(String name, String ... value) { + List valueList = Stream.of(value) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + this.headers.addAll(name, valueList); + return this; + } + /** * Copy all given headers. */ @@ -116,6 +150,36 @@ public DefaultResponseCreator headers(HttpHeaders headers) { return this; } + /** + * Add a single cookie. + */ + public DefaultResponseCreator cookie(ResponseCookie cookie) { + // This is really just an alias, but it makes the interface more fluent. + return cookies(cookie); + } + + /** + * Add one or more cookies. + */ + public DefaultResponseCreator cookies(ResponseCookie... cookies) { + for (ResponseCookie cookie : cookies) { + this.headers.add(HttpHeaders.SET_COOKIE, cookie.toString()); + } + + return this; + } + + /** + * Copy all given cookies. + */ + public DefaultResponseCreator cookies(MultiValueMap cookies) { + cookies.values() + .stream() + .flatMap(List::stream) + .forEach(cookie -> this.headers.add(HttpHeaders.SET_COOKIE, cookie.toString())); + + return this; + } @Override public ClientHttpResponse createResponse(@Nullable ClientHttpRequest request) throws IOException { diff --git a/spring-test/src/main/java/org/springframework/test/web/client/response/MockRestResponseCreators.java b/spring-test/src/main/java/org/springframework/test/web/client/response/MockRestResponseCreators.java index 6c3a5b2ac15a..949b32e40118 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/response/MockRestResponseCreators.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/response/MockRestResponseCreators.java @@ -20,6 +20,7 @@ import java.net.URI; import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; @@ -82,6 +83,13 @@ public static DefaultResponseCreator withCreatedEntity(URI location) { return new DefaultResponseCreator(HttpStatus.CREATED).location(location); } + /** + * {@code ResponseCreator} for a 202 response (ACCEPTED). + */ + public static DefaultResponseCreator withAccepted() { + return new DefaultResponseCreator(HttpStatus.ACCEPTED); + } + /** * {@code ResponseCreator} for a 204 response (NO_CONTENT). */ @@ -103,6 +111,43 @@ public static DefaultResponseCreator withUnauthorizedRequest() { return new DefaultResponseCreator(HttpStatus.UNAUTHORIZED); } + /** + * {@code ResponseCreator} for a 403 response (FORBIDDEN). + */ + public static DefaultResponseCreator withForbiddenRequest() { + return new DefaultResponseCreator(HttpStatus.FORBIDDEN); + } + + /** + * {@code ResponseCreator} for a 404 response (NOT_FOUND). + */ + public static DefaultResponseCreator withResourceNotFound() { + return new DefaultResponseCreator(HttpStatus.NOT_FOUND); + } + + /** + * {@code ResponseCreator} for a 409 response (CONFLICT). + */ + public static DefaultResponseCreator withRequestConflict() { + return new DefaultResponseCreator(HttpStatus.CONFLICT); + } + + /** + * {@code ResponseCreator} for a 429 ratelimited response (TOO_MANY_REQUESTS). + */ + public static DefaultResponseCreator withTooManyRequests() { + return new DefaultResponseCreator(HttpStatus.TOO_MANY_REQUESTS); + } + + /** + * {@code ResponseCreator} for a 429 ratelimited response (TOO_MANY_REQUESTS) with a {@code Retry-After} header + * in seconds. + */ + public static DefaultResponseCreator withTooManyRequests(int retryAfter) { + return new DefaultResponseCreator(HttpStatus.TOO_MANY_REQUESTS) + .header(HttpHeaders.RETRY_AFTER, Integer.toString(retryAfter)); + } + /** * {@code ResponseCreator} for a 500 response (SERVER_ERROR). */ @@ -110,6 +155,27 @@ public static DefaultResponseCreator withServerError() { return new DefaultResponseCreator(HttpStatus.INTERNAL_SERVER_ERROR); } + /** + * {@code ResponseCreator} for a 502 response (BAD_GATEWAY). + */ + public static DefaultResponseCreator withBadGateway() { + return new DefaultResponseCreator(HttpStatus.BAD_GATEWAY); + } + + /** + * {@code ResponseCreator} for a 503 response (SERVICE_UNAVAILABLE). + */ + public static DefaultResponseCreator withServiceUnavailable() { + return new DefaultResponseCreator(HttpStatus.SERVICE_UNAVAILABLE); + } + + /** + * {@code ResponseCreator} for a 504 response (GATEWAY_TIMEOUT). + */ + public static DefaultResponseCreator withGatewayTimeout() { + return new DefaultResponseCreator(HttpStatus.GATEWAY_TIMEOUT); + } + /** * {@code ResponseCreator} with a specific HTTP status. * @param status the response status diff --git a/spring-test/src/test/java/org/springframework/test/web/client/response/DefaultResponseCreatorTests.java b/spring-test/src/test/java/org/springframework/test/web/client/response/DefaultResponseCreatorTests.java new file mode 100644 index 000000000000..a11607e40e11 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/client/response/DefaultResponseCreatorTests.java @@ -0,0 +1,230 @@ +/* + * Copyright 2002-2021 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.test.web.client.response; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; + +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseCookie; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.mock.http.client.MockClientHttpRequest; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.util.UriComponentsBuilder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assumptions.assumeThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for the {@link DefaultResponseCreator} factory methods. + * + * @author Ashley Scopes + */ +class DefaultResponseCreatorTests { + @ParameterizedTest(name = "expect status to be set [{0}]") + @ValueSource(ints = {200, 401, 429}) + void expectStatus(int statusValue) throws IOException { + HttpStatus status = HttpStatus.valueOf(statusValue); + ClientHttpResponse response = createResponse(new DefaultResponseCreator(status)); + assertThat(response.getStatusCode()).isEqualTo(status); + } + + @Test + void setBodyFromString() throws IOException { + // Use unicode codepoint for "thinking" emoji to help verify correct encoding is used internally. + ClientHttpResponse response = createResponse(new DefaultResponseCreator(HttpStatus.OK) + .body("hello, world! \uD83E\uDD14")); + + assertThat(IOUtils.toByteArray(response.getBody())) + .isEqualTo("hello, world! \uD83E\uDD14".getBytes(StandardCharsets.UTF_8)); + } + + @ParameterizedTest(name = "setBodyFromStringWithCharset [{0}]") + @ValueSource(strings = {"Cp1047", "UTF-8", "UTF-16", "US-ASCII", "ISO-8859-1"}) + void setBodyFromStringWithCharset(String charset) throws IOException { + + assumeThat(Charset.isSupported(charset)) + .overridingErrorMessage("charset %s is not supported by this JVM", charset) + .isTrue(); + + Charset charsetObj = Charset.forName(charset); + + String content = "hello! €½$~@><·─"; + + ClientHttpResponse response = createResponse(new DefaultResponseCreator(HttpStatus.OK) + .body(content, charsetObj)); + + ByteBuffer expectBuff = charsetObj.encode(content); + byte[] expect = new byte[expectBuff.remaining()]; + expectBuff.get(expect); + + assertThat(IOUtils.toByteArray(response.getBody())).isEqualTo(expect); + } + + @Test + void setBodyFromByteArray() throws IOException { + byte[] body = { 0, 9, 18, 27, 36, 45, 54, 63, 72, 81, 90 }; + ClientHttpResponse response = createResponse(new DefaultResponseCreator(HttpStatus.OK).body(body)); + assertThat(IOUtils.toByteArray(response.getBody())).isEqualTo(body); + } + + @Test + void setBodyFromResource() throws IOException { + byte[] resourceContent = {7, 14, 21, 28, 35}; + + Resource resource = mock(Resource.class); + given(resource.getInputStream()).willReturn(new ByteArrayInputStream(resourceContent)); + + ClientHttpResponse response = createResponse(new DefaultResponseCreator(HttpStatus.OK).body(resource)); + + then(resource).should().getInputStream(); + + assertThat(IOUtils.toByteArray(response.getBody())).isEqualTo(resourceContent); + } + + @Test + void setContentType() throws IOException { + ClientHttpResponse response = createResponse(new DefaultResponseCreator(HttpStatus.OK) + .contentType(MediaType.APPLICATION_JSON)); + + assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON); + } + + @Test + void setLocation() throws IOException { + URI uri = UriComponentsBuilder + .fromUriString("https://docs.spring.io/spring-framework/docs/current/reference/html/testing.html") + .build() + .toUri(); + + ClientHttpResponse response = createResponse(new DefaultResponseCreator(HttpStatus.OK).location(uri)); + assertThat(response.getHeaders().getLocation()).isEqualTo(uri); + } + + @Test + void setHeader() throws IOException { + + ClientHttpResponse response = createResponse(new DefaultResponseCreator(HttpStatus.OK) + .header("foo", "bar") + .header("baz", "bork") + .headers("lorem", "ipsum", "dolor", "sit", "amet")); + + HttpHeaders headers = response.getHeaders(); + assertThat(headers.get("foo")).isNotNull().isEqualTo(Collections.singletonList("bar")); + assertThat(headers.get("baz")).isNotNull().isEqualTo(Collections.singletonList("bork")); + assertThat(headers.get("lorem")).isNotNull().isEqualTo(Arrays.asList("ipsum", "dolor", "sit", "amet")); + } + + @Test + void setHeaders() throws IOException { + + HttpHeaders firstHeaders = new HttpHeaders(); + firstHeaders.setContentType(MediaType.APPLICATION_JSON); + firstHeaders.setOrigin("https://github.com"); + + HttpHeaders secondHeaders = new HttpHeaders(); + secondHeaders.setAllow(Collections.singleton(HttpMethod.PUT)); + + ClientHttpResponse response = createResponse(new DefaultResponseCreator(HttpStatus.OK) + .headers(firstHeaders) + .headers(secondHeaders)); + + HttpHeaders responseHeaders = response.getHeaders(); + + assertThat(responseHeaders.getContentType()).isEqualTo(MediaType.APPLICATION_JSON); + assertThat(responseHeaders.getOrigin()).isEqualTo("https://github.com"); + assertThat(responseHeaders.getAllow()).isEqualTo(Collections.singleton(HttpMethod.PUT)); + } + + @Test + void setCookie() throws IOException { + ResponseCookie firstCookie = ResponseCookie.from("user-id", "1234").build(); + ResponseCookie secondCookie = ResponseCookie.from("group-id", "5432").build(); + ResponseCookie thirdCookie = ResponseCookie.from("cookie-cookie", "cookies").build(); + ResponseCookie fourthCookie = ResponseCookie.from("foobar", "bazbork").build(); + + ClientHttpResponse response = createResponse(new DefaultResponseCreator(HttpStatus.OK) + .cookie(firstCookie) + .cookie(secondCookie) + .cookies(thirdCookie, fourthCookie)); + + HttpHeaders responseHeaders = response.getHeaders(); + + assertThat(responseHeaders.get(HttpHeaders.SET_COOKIE)) + .isNotNull() + .containsExactly( + firstCookie.toString(), + secondCookie.toString(), + thirdCookie.toString(), + fourthCookie.toString() + ); + } + + @Test + void setCookies() throws IOException { + ResponseCookie firstCookie = ResponseCookie.from("user-id", "1234").build(); + ResponseCookie secondCookie = ResponseCookie.from("group-id", "5432").build(); + MultiValueMap firstCookies = new LinkedMultiValueMap<>(); + firstCookies.add(firstCookie.getName(), firstCookie); + firstCookies.add(secondCookie.getName(), secondCookie); + + ResponseCookie thirdCookie = ResponseCookie.from("cookie-cookie", "cookies").build(); + ResponseCookie fourthCookie = ResponseCookie.from("foobar", "bazbork").build(); + MultiValueMap secondCookies = new LinkedMultiValueMap<>(); + firstCookies.add(thirdCookie.getName(), thirdCookie); + firstCookies.add(fourthCookie.getName(), fourthCookie); + + ClientHttpResponse response = createResponse(new DefaultResponseCreator(HttpStatus.OK) + .cookies(firstCookies) + .cookies(secondCookies)); + + HttpHeaders responseHeaders = response.getHeaders(); + + assertThat(responseHeaders.get(HttpHeaders.SET_COOKIE)) + .isNotNull() + .containsExactly( + firstCookie.toString(), + secondCookie.toString(), + thirdCookie.toString(), + fourthCookie.toString() + ); + } + + private static ClientHttpResponse createResponse(DefaultResponseCreator creator) throws IOException { + URI uri = UriComponentsBuilder.fromUriString("/foo/bar").build().toUri(); + return creator.createResponse(new MockClientHttpRequest(HttpMethod.POST, uri)); + } +} diff --git a/spring-test/src/test/java/org/springframework/test/web/client/response/ResponseCreatorsTests.java b/spring-test/src/test/java/org/springframework/test/web/client/response/ResponseCreatorsTests.java index ba50da304f4e..9a043b73e31c 100644 --- a/spring-test/src/test/java/org/springframework/test/web/client/response/ResponseCreatorsTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/client/response/ResponseCreatorsTests.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; @@ -79,6 +80,15 @@ void created() throws Exception { assertThat(StreamUtils.copyToByteArray(response.getBody()).length).isEqualTo(0); } + @Test + void accepted() throws Exception { + DefaultResponseCreator responseCreator = MockRestResponseCreators.withAccepted(); + MockClientHttpResponse response = (MockClientHttpResponse) responseCreator.createResponse(null); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED); + assertThat(StreamUtils.copyToByteArray(response.getBody()).length).isEqualTo(0); + } + @Test void noContent() throws Exception { DefaultResponseCreator responseCreator = MockRestResponseCreators.withNoContent(); @@ -109,6 +119,53 @@ void unauthorized() throws Exception { assertThat(StreamUtils.copyToByteArray(response.getBody()).length).isEqualTo(0); } + @Test + void forbiddenRequest() throws Exception { + DefaultResponseCreator responseCreator = MockRestResponseCreators.withForbiddenRequest(); + MockClientHttpResponse response = (MockClientHttpResponse) responseCreator.createResponse(null); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + assertThat(StreamUtils.copyToByteArray(response.getBody()).length).isEqualTo(0); + } + + @Test + void resourceNotFound() throws Exception { + DefaultResponseCreator responseCreator = MockRestResponseCreators.withResourceNotFound(); + MockClientHttpResponse response = (MockClientHttpResponse) responseCreator.createResponse(null); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + assertThat(StreamUtils.copyToByteArray(response.getBody()).length).isEqualTo(0); + } + + @Test + void requestConflict() throws Exception { + DefaultResponseCreator responseCreator = MockRestResponseCreators.withRequestConflict(); + MockClientHttpResponse response = (MockClientHttpResponse) responseCreator.createResponse(null); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); + assertThat(StreamUtils.copyToByteArray(response.getBody()).length).isEqualTo(0); + } + + @Test + void tooManyRequests() throws Exception { + DefaultResponseCreator responseCreator = MockRestResponseCreators.withTooManyRequests(); + MockClientHttpResponse response = (MockClientHttpResponse) responseCreator.createResponse(null); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.TOO_MANY_REQUESTS); + assertThat(response.getHeaders()).doesNotContainKey(HttpHeaders.RETRY_AFTER); + assertThat(StreamUtils.copyToByteArray(response.getBody()).length).isEqualTo(0); + } + + @Test + void tooManyRequestsWithRetryAfter() throws Exception { + DefaultResponseCreator responseCreator = MockRestResponseCreators.withTooManyRequests(512); + MockClientHttpResponse response = (MockClientHttpResponse) responseCreator.createResponse(null); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.TOO_MANY_REQUESTS); + assertThat(response.getHeaders().getFirst(HttpHeaders.RETRY_AFTER)).isEqualTo("512"); + assertThat(StreamUtils.copyToByteArray(response.getBody()).length).isEqualTo(0); + } + @Test void serverError() throws Exception { DefaultResponseCreator responseCreator = MockRestResponseCreators.withServerError(); @@ -119,6 +176,33 @@ void serverError() throws Exception { assertThat(StreamUtils.copyToByteArray(response.getBody()).length).isEqualTo(0); } + @Test + void badGateway() throws Exception { + DefaultResponseCreator responseCreator = MockRestResponseCreators.withBadGateway(); + MockClientHttpResponse response = (MockClientHttpResponse) responseCreator.createResponse(null); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_GATEWAY); + assertThat(StreamUtils.copyToByteArray(response.getBody()).length).isEqualTo(0); + } + + @Test + void serviceUnavailable() throws Exception { + DefaultResponseCreator responseCreator = MockRestResponseCreators.withServiceUnavailable(); + MockClientHttpResponse response = (MockClientHttpResponse) responseCreator.createResponse(null); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SERVICE_UNAVAILABLE); + assertThat(StreamUtils.copyToByteArray(response.getBody()).length).isEqualTo(0); + } + + @Test + void gatewayTimeout() throws Exception { + DefaultResponseCreator responseCreator = MockRestResponseCreators.withGatewayTimeout(); + MockClientHttpResponse response = (MockClientHttpResponse) responseCreator.createResponse(null); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.GATEWAY_TIMEOUT); + assertThat(StreamUtils.copyToByteArray(response.getBody()).length).isEqualTo(0); + } + @Test void withStatus() throws Exception { DefaultResponseCreator responseCreator = MockRestResponseCreators.withStatus(HttpStatus.FORBIDDEN);