Browse Source

Add methods to DefaultResponseCreator & MockRestResponseCreators

New methods in DefaultResponseCreator for adding headers and cookies,
for specifying the character encoding when setting a string body on
a response, which is useful when working in environments that do not
automatically assume UTF-8, such as integrating with legacy
applications from a new Spring one.

New methods in MockRestResponseCreators support extra commonly used
HTTP status codes, including some that occur when working in AWS,
CloudFlare, or when using gateways such as Kong, where resilient
applications should be able to respond to ratelimits, gateway errors,
and gateway timeouts that may occur if a remote service is down.

Added test cases for any changes made.

See gh-27280
pull/29220/head
Ashley Scopes 4 years ago committed by rstoyanchev
parent
commit
d14477eb84
  1. 68
      spring-test/src/main/java/org/springframework/test/web/client/response/DefaultResponseCreator.java
  2. 66
      spring-test/src/main/java/org/springframework/test/web/client/response/MockRestResponseCreators.java
  3. 230
      spring-test/src/test/java/org/springframework/test/web/client/response/DefaultResponseCreatorTests.java
  4. 84
      spring-test/src/test/java/org/springframework/test/web/client/response/ResponseCreatorsTests.java

68
spring-test/src/main/java/org/springframework/test/web/client/response/DefaultResponseCreator.java

@ -18,18 +18,25 @@ package org.springframework.test.web.client.response; @@ -18,18 +18,25 @@ package org.springframework.test.web.client.response;
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 @@ public class DefaultResponseCreator implements ResponseCreator { @@ -67,7 +74,6 @@ public class DefaultResponseCreator implements ResponseCreator {
this.statusCode = statusCode;
}
/**
* Set the body as a UTF-8 String.
*/
@ -76,6 +82,14 @@ public class DefaultResponseCreator implements ResponseCreator { @@ -76,6 +82,14 @@ public class DefaultResponseCreator implements ResponseCreator {
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 class DefaultResponseCreator implements ResponseCreator { @@ -85,7 +99,7 @@ public class DefaultResponseCreator implements ResponseCreator {
}
/**
* 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 class DefaultResponseCreator implements ResponseCreator { @@ -108,6 +122,26 @@ public class DefaultResponseCreator implements ResponseCreator {
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<String> 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 class DefaultResponseCreator implements ResponseCreator { @@ -116,6 +150,36 @@ public class DefaultResponseCreator implements ResponseCreator {
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<String, ResponseCookie> 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 {

66
spring-test/src/main/java/org/springframework/test/web/client/response/MockRestResponseCreators.java

@ -20,6 +20,7 @@ import java.io.IOException; @@ -20,6 +20,7 @@ import java.io.IOException;
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 abstract class MockRestResponseCreators { @@ -82,6 +83,13 @@ public abstract class MockRestResponseCreators {
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 abstract class MockRestResponseCreators { @@ -103,6 +111,43 @@ public abstract class MockRestResponseCreators {
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 abstract class MockRestResponseCreators { @@ -110,6 +155,27 @@ public abstract class MockRestResponseCreators {
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

230
spring-test/src/test/java/org/springframework/test/web/client/response/DefaultResponseCreatorTests.java

@ -0,0 +1,230 @@ @@ -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<String, ResponseCookie> 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<String, ResponseCookie> 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));
}
}

84
spring-test/src/test/java/org/springframework/test/web/client/response/ResponseCreatorsTests.java

@ -21,6 +21,7 @@ import java.net.URI; @@ -21,6 +21,7 @@ import java.net.URI;
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 @@ class ResponseCreatorsTests { @@ -79,6 +80,15 @@ class ResponseCreatorsTests {
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 @@ class ResponseCreatorsTests { @@ -109,6 +119,53 @@ class ResponseCreatorsTests {
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 @@ class ResponseCreatorsTests { @@ -119,6 +176,33 @@ class ResponseCreatorsTests {
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);

Loading…
Cancel
Save