WebTestClient polish and minor refactoring
This commit is contained in:
parent
d924538211
commit
20be40bf64
|
|
@ -22,12 +22,9 @@ import java.time.ZonedDateTime;
|
|||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
|
@ -36,7 +33,6 @@ import reactor.core.publisher.Mono;
|
|||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
import org.springframework.http.client.reactive.ClientHttpRequest;
|
||||
|
|
@ -48,9 +44,9 @@ import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
|
|||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.util.UriBuilder;
|
||||
|
||||
import static java.util.stream.Collectors.toList;
|
||||
import static org.springframework.test.util.AssertionErrors.assertEquals;
|
||||
import static org.springframework.test.util.AssertionErrors.assertTrue;
|
||||
import static org.springframework.test.util.AssertionErrors.fail;
|
||||
import static org.springframework.web.reactive.function.BodyExtractors.toDataBuffers;
|
||||
import static org.springframework.web.reactive.function.BodyExtractors.toFlux;
|
||||
import static org.springframework.web.reactive.function.BodyExtractors.toMono;
|
||||
|
|
@ -65,33 +61,30 @@ class DefaultWebTestClient implements WebTestClient {
|
|||
|
||||
private final WebClient webClient;
|
||||
|
||||
private final Duration responseTimeout;
|
||||
private final WebTestClientConnector webTestClientConnector;
|
||||
|
||||
private final WiretapConnectorListener connectorListener;
|
||||
private final Duration timeout;
|
||||
|
||||
private final AtomicLong requestIndex = new AtomicLong();
|
||||
|
||||
|
||||
DefaultWebTestClient(WebClient.Builder webClientBuilder, ClientHttpConnector connector, Duration timeout) {
|
||||
Assert.notNull(webClientBuilder, "WebClient.Builder is required");
|
||||
|
||||
WiretapConnector wiretapConnector = new WiretapConnector(connector);
|
||||
webClientBuilder.clientConnector(wiretapConnector);
|
||||
|
||||
this.connectorListener = new WiretapConnectorListener();
|
||||
wiretapConnector.addListener(this.connectorListener);
|
||||
|
||||
this.webClient = webClientBuilder.build();
|
||||
this.responseTimeout = (timeout != null ? timeout : Duration.ofSeconds(5));
|
||||
this.webTestClientConnector = new WebTestClientConnector(connector);
|
||||
this.webClient = webClientBuilder.clientConnector(this.webTestClientConnector).build();
|
||||
this.timeout = (timeout != null ? timeout : Duration.ofSeconds(5));
|
||||
}
|
||||
|
||||
private DefaultWebTestClient(DefaultWebTestClient webTestClient, ExchangeFilterFunction filter) {
|
||||
this.webClient = webTestClient.webClient.filter(filter);
|
||||
this.connectorListener = webTestClient.connectorListener;
|
||||
this.responseTimeout = webTestClient.responseTimeout;
|
||||
this.timeout = webTestClient.timeout;
|
||||
this.webTestClientConnector = webTestClient.webTestClientConnector;
|
||||
}
|
||||
|
||||
|
||||
private Duration getTimeout() {
|
||||
return this.responseTimeout;
|
||||
return this.timeout;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -141,7 +134,6 @@ class DefaultWebTestClient implements WebTestClient {
|
|||
}
|
||||
|
||||
|
||||
|
||||
private class DefaultUriSpec implements UriSpec {
|
||||
|
||||
private final WebClient.UriSpec uriSpec;
|
||||
|
|
@ -181,7 +173,8 @@ class DefaultWebTestClient implements WebTestClient {
|
|||
|
||||
DefaultHeaderSpec(WebClient.HeaderSpec spec) {
|
||||
this.headerSpec = spec;
|
||||
this.requestId = connectorListener.registerRequestId(spec);
|
||||
this.requestId = String.valueOf(requestIndex.incrementAndGet());
|
||||
this.headerSpec.header(WebTestClientConnector.REQUEST_ID_HEADER_NAME, this.requestId);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -247,29 +240,25 @@ class DefaultWebTestClient implements WebTestClient {
|
|||
|
||||
@Override
|
||||
public ResponseSpec exchange() {
|
||||
return createResponseSpec(this.headerSpec.exchange());
|
||||
return toResponseSpec(this.headerSpec.exchange());
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> ResponseSpec exchange(BodyInserter<T, ? super ClientHttpRequest> inserter) {
|
||||
return createResponseSpec(this.headerSpec.exchange(inserter));
|
||||
return toResponseSpec(this.headerSpec.exchange(inserter));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T, S extends Publisher<T>> ResponseSpec exchange(S publisher, Class<T> elementClass) {
|
||||
return createResponseSpec(this.headerSpec.exchange(publisher, elementClass));
|
||||
return toResponseSpec(this.headerSpec.exchange(publisher, elementClass));
|
||||
}
|
||||
|
||||
protected DefaultResponseSpec createResponseSpec(Mono<ClientResponse> responseMono) {
|
||||
private DefaultResponseSpec toResponseSpec(Mono<ClientResponse> responseMono) {
|
||||
ClientResponse response = responseMono.block(getTimeout());
|
||||
WiretapConnector.Info info = connectorListener.retrieveRequest(this.requestId);
|
||||
HttpMethod method = info.getMethod();
|
||||
URI url = info.getUrl();
|
||||
HttpHeaders headers = info.getRequestHeaders();
|
||||
ExchangeResult<Flux<DataBuffer>> result = ExchangeResult.fromResponse(method, url, headers, response);
|
||||
ClientHttpRequest request = webTestClientConnector.claimRequest(this.requestId);
|
||||
ExchangeResult<Flux<DataBuffer>> result = ExchangeResult.create(request, response);
|
||||
return new DefaultResponseSpec(result, response);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private abstract class ResponseSpecSupport {
|
||||
|
|
@ -284,11 +273,6 @@ class DefaultWebTestClient implements WebTestClient {
|
|||
this.response = response;
|
||||
}
|
||||
|
||||
public ResponseSpecSupport(ResponseSpecSupport responseSpec) {
|
||||
this.exchangeResult = responseSpec.getExchangeResult();
|
||||
this.response = responseSpec.getResponse();
|
||||
}
|
||||
|
||||
|
||||
protected ExchangeResult<Flux<DataBuffer>> getExchangeResult() {
|
||||
return this.exchangeResult;
|
||||
|
|
@ -298,10 +282,6 @@ class DefaultWebTestClient implements WebTestClient {
|
|||
return this.response;
|
||||
}
|
||||
|
||||
protected HttpHeaders getResponseHeaders() {
|
||||
return getExchangeResult().getResponseHeaders();
|
||||
}
|
||||
|
||||
protected <T> ExchangeResult<T> createResultWithDecodedBody(T body) {
|
||||
return ExchangeResult.withDecodedBody(this.exchangeResult, body);
|
||||
}
|
||||
|
|
@ -311,19 +291,28 @@ class DefaultWebTestClient implements WebTestClient {
|
|||
private class DefaultResponseSpec extends ResponseSpecSupport implements ResponseSpec {
|
||||
|
||||
|
||||
public DefaultResponseSpec(ExchangeResult<Flux<DataBuffer>> result, ClientResponse response) {
|
||||
super(result, response);
|
||||
public DefaultResponseSpec(ExchangeResult<Flux<DataBuffer>> exchangeResult, ClientResponse response) {
|
||||
super(exchangeResult, response);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public StatusAssertions expectStatus() {
|
||||
return new StatusAssertions(getResponse().statusCode(), this);
|
||||
return new StatusAssertions(getExchangeResult(), this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HeaderAssertions expectHeader() {
|
||||
return new HeaderAssertions(getResponseHeaders(), this);
|
||||
return new HeaderAssertions(getExchangeResult(), this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeBodySpec expectBody(Class<?> elementType) {
|
||||
return expectBody(ResolvableType.forClass(elementType));
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeBodySpec expectBody(ResolvableType elementType) {
|
||||
return new DefaultTypeBodySpec(this, elementType);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -331,16 +320,6 @@ class DefaultWebTestClient implements WebTestClient {
|
|||
return new DefaultBodySpec(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ElementBodySpec expectBody(Class<?> elementType) {
|
||||
return expectBody(ResolvableType.forClass(elementType));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ElementBodySpec expectBody(ResolvableType elementType) {
|
||||
return new DefaultElementBodySpec(this, elementType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseSpec consumeWith(Consumer<ExchangeResult<Flux<DataBuffer>>> consumer) {
|
||||
consumer.accept(getExchangeResult());
|
||||
|
|
@ -353,97 +332,13 @@ class DefaultWebTestClient implements WebTestClient {
|
|||
}
|
||||
}
|
||||
|
||||
private class DefaultBodySpec extends ResponseSpecSupport implements BodySpec {
|
||||
|
||||
|
||||
public DefaultBodySpec(ResponseSpecSupport responseSpec) {
|
||||
super(responseSpec);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ExchangeResult<Void> isEmpty() {
|
||||
DataBuffer buffer = getResponse().body(toDataBuffers()).blockFirst(getTimeout());
|
||||
assertTrue("Expected empty body", buffer == null);
|
||||
return createResultWithDecodedBody(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapBodySpec map(Class<?> keyType, Class<?> valueType) {
|
||||
return map(ResolvableType.forClass(keyType), ResolvableType.forClass(valueType));
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapBodySpec map(ResolvableType keyType, ResolvableType valueType) {
|
||||
return new DefaultMapBodySpec(this, keyType, valueType);
|
||||
}
|
||||
}
|
||||
|
||||
private class DefaultMapBodySpec extends ResponseSpecSupport implements MapBodySpec {
|
||||
|
||||
private final Map<?, ?> body;
|
||||
|
||||
|
||||
public DefaultMapBodySpec(ResponseSpecSupport spec, ResolvableType keyType, ResolvableType valueType) {
|
||||
super(spec);
|
||||
ResolvableType mapType = ResolvableType.forClassWithGenerics(Map.class, keyType, valueType);
|
||||
this.body = (Map<?, ?>) spec.getResponse().body(toMono(mapType)).block(getTimeout());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public <K, V> ExchangeResult<Map<K, V>> isEqualTo(Map<K, V> expected) {
|
||||
return returnResult();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapBodySpec hasSize(int size) {
|
||||
assertEquals("Response body map size", size, this.body.size());
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapBodySpec contains(Object key, Object value) {
|
||||
assertEquals("Response body map value for key " + key, value, this.body.get(key));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapBodySpec containsKeys(Object... keys) {
|
||||
List<Object> missing = Arrays.stream(keys)
|
||||
.filter(key -> !this.body.containsKey(key))
|
||||
.collect(Collectors.toList());
|
||||
if (!missing.isEmpty()) {
|
||||
fail("Response body map does not contain keys " + Arrays.toString(keys));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapBodySpec containsValues(Object... values) {
|
||||
List<Object> missing = Arrays.stream(values)
|
||||
.filter(value -> !this.body.containsValue(value))
|
||||
.collect(Collectors.toList());
|
||||
if (!missing.isEmpty()) {
|
||||
fail("Response body map does not contain values " + Arrays.toString(values));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <K, V> ExchangeResult<Map<K, V>> returnResult() {
|
||||
return createResultWithDecodedBody((Map<K, V>) this.body);
|
||||
}
|
||||
}
|
||||
|
||||
private class DefaultElementBodySpec extends ResponseSpecSupport implements ElementBodySpec {
|
||||
private class DefaultTypeBodySpec extends ResponseSpecSupport implements TypeBodySpec {
|
||||
|
||||
private final ResolvableType elementType;
|
||||
|
||||
|
||||
public DefaultElementBodySpec(ResponseSpecSupport spec, ResolvableType elementType) {
|
||||
super(spec);
|
||||
public DefaultTypeBodySpec(DefaultResponseSpec spec, ResolvableType elementType) {
|
||||
super(spec.getExchangeResult(), spec.getResponse());
|
||||
this.elementType = elementType;
|
||||
}
|
||||
|
||||
|
|
@ -455,7 +350,7 @@ class DefaultWebTestClient implements WebTestClient {
|
|||
|
||||
@Override
|
||||
public ListBodySpec list() {
|
||||
return new DefaultListBodySpec(this, this.elementType, -1);
|
||||
return list(-1);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -470,14 +365,13 @@ class DefaultWebTestClient implements WebTestClient {
|
|||
}
|
||||
}
|
||||
|
||||
private class DefaultSingleValueBodySpec extends ResponseSpecSupport
|
||||
implements SingleValueBodySpec {
|
||||
private class DefaultSingleValueBodySpec extends ResponseSpecSupport implements SingleValueBodySpec {
|
||||
|
||||
private final Object body;
|
||||
|
||||
|
||||
public DefaultSingleValueBodySpec(ResponseSpecSupport spec, ResolvableType elementType) {
|
||||
super(spec);
|
||||
public DefaultSingleValueBodySpec(DefaultTypeBodySpec spec, ResolvableType elementType) {
|
||||
super(spec.getExchangeResult(), spec.getResponse());
|
||||
this.body = getResponse().body(toMono(elementType)).block(getTimeout());
|
||||
}
|
||||
|
||||
|
|
@ -495,14 +389,13 @@ class DefaultWebTestClient implements WebTestClient {
|
|||
}
|
||||
}
|
||||
|
||||
private class DefaultListBodySpec extends ResponseSpecSupport
|
||||
implements ListBodySpec {
|
||||
private class DefaultListBodySpec extends ResponseSpecSupport implements ListBodySpec {
|
||||
|
||||
private final List<?> body;
|
||||
|
||||
|
||||
public DefaultListBodySpec(ResponseSpecSupport spec, ResolvableType elementType, int elementCount) {
|
||||
super(spec);
|
||||
public DefaultListBodySpec(DefaultTypeBodySpec spec, ResolvableType elementType, int elementCount) {
|
||||
super(spec.getExchangeResult(), spec.getResponse());
|
||||
Flux<?> flux = getResponse().body(toFlux(elementType));
|
||||
if (elementCount >= 0) {
|
||||
flux = flux.take(elementCount);
|
||||
|
|
@ -545,33 +438,79 @@ class DefaultWebTestClient implements WebTestClient {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private static class WiretapConnectorListener implements Consumer<WiretapConnector.Info> {
|
||||
|
||||
private static final String REQUEST_ID_HEADER_NAME = "request-id";
|
||||
private class DefaultBodySpec extends ResponseSpecSupport implements BodySpec {
|
||||
|
||||
|
||||
private final AtomicLong index = new AtomicLong();
|
||||
|
||||
private final Map<String, WiretapConnector.Info> exchanges = new ConcurrentHashMap<>();
|
||||
public DefaultBodySpec(DefaultResponseSpec spec) {
|
||||
super(spec.getExchangeResult(), spec.getResponse());
|
||||
}
|
||||
|
||||
|
||||
public String registerRequestId(WebClient.HeaderSpec headerSpec) {
|
||||
String requestId = String.valueOf(this.index.incrementAndGet());
|
||||
headerSpec.header(REQUEST_ID_HEADER_NAME, requestId);
|
||||
return requestId;
|
||||
@Override
|
||||
public ExchangeResult<Void> isEmpty() {
|
||||
DataBuffer buffer = getResponse().body(toDataBuffers()).blockFirst(getTimeout());
|
||||
assertTrue("Expected empty body", buffer == null);
|
||||
return createResultWithDecodedBody(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(WiretapConnector.Info info) {
|
||||
Optional.ofNullable(info.getRequestHeaders().getFirst(REQUEST_ID_HEADER_NAME))
|
||||
.ifPresent(id -> this.exchanges.put(id, info));
|
||||
public MapBodySpec map(Class<?> keyType, Class<?> valueType) {
|
||||
return map(ResolvableType.forClass(keyType), ResolvableType.forClass(valueType));
|
||||
}
|
||||
|
||||
public WiretapConnector.Info retrieveRequest(String requestId) {
|
||||
WiretapConnector.Info info = this.exchanges.remove(requestId);
|
||||
Assert.notNull(info, "No match for request-id=" + requestId);
|
||||
return info;
|
||||
@Override
|
||||
public MapBodySpec map(ResolvableType keyType, ResolvableType valueType) {
|
||||
return new DefaultMapBodySpec(this, keyType, valueType);
|
||||
}
|
||||
}
|
||||
|
||||
private class DefaultMapBodySpec extends ResponseSpecSupport implements MapBodySpec {
|
||||
|
||||
private final Map<?, ?> body;
|
||||
|
||||
|
||||
public DefaultMapBodySpec(DefaultBodySpec spec, ResolvableType keyType, ResolvableType valueType) {
|
||||
super(spec.getExchangeResult(), spec.getResponse());
|
||||
ResolvableType mapType = ResolvableType.forClassWithGenerics(Map.class, keyType, valueType);
|
||||
this.body = (Map<?, ?>) spec.getResponse().body(toMono(mapType)).block(getTimeout());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public <K, V> ExchangeResult<Map<K, V>> isEqualTo(Map<K, V> expected) {
|
||||
return returnResult();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapBodySpec hasSize(int size) {
|
||||
assertEquals("Response body map size", size, this.body.size());
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapBodySpec contains(Object key, Object value) {
|
||||
assertEquals("Response body map value for key " + key, value, this.body.get(key));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapBodySpec containsKeys(Object... keys) {
|
||||
List<?> missing = Arrays.stream(keys).filter(k -> !this.body.containsKey(k)).collect(toList());
|
||||
assertTrue("Response body map does not contain keys " + missing, missing.isEmpty());
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapBodySpec containsValues(Object... values) {
|
||||
List<?> missing = Arrays.stream(values).filter(v -> !this.body.containsValue(v)).collect(toList());
|
||||
assertTrue("Response body map does not contain values " + missing, missing.isEmpty());
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <K, V> ExchangeResult<Map<K, V>> returnResult() {
|
||||
return createResultWithDecodedBody((Map<K, V>) this.body);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import org.springframework.core.io.buffer.DataBuffer;
|
|||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.client.reactive.ClientHttpRequest;
|
||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
|
||||
import static org.springframework.web.reactive.function.BodyExtractors.toDataBuffers;
|
||||
|
|
@ -111,19 +112,16 @@ public class ExchangeResult<T> {
|
|||
|
||||
|
||||
/**
|
||||
* Create an instance from a ClientResponse (body not yet consumed).
|
||||
* Create from ClientHttpRequest and ClientResponse (body not yet consumed).
|
||||
*/
|
||||
static ExchangeResult<Flux<DataBuffer>> fromResponse(HttpMethod method, URI url,
|
||||
HttpHeaders requestHeaders, ClientResponse response) {
|
||||
|
||||
HttpStatus status = response.statusCode();
|
||||
HttpHeaders responseHeaders = response.headers().asHttpHeaders();
|
||||
Flux<DataBuffer> body = response.body(toDataBuffers());
|
||||
return new ExchangeResult<>(method, url, requestHeaders, status, responseHeaders, body);
|
||||
static ExchangeResult<Flux<DataBuffer>> create(ClientHttpRequest request, ClientResponse response) {
|
||||
return new ExchangeResult<>(request.getMethod(), request.getURI(), request.getHeaders(),
|
||||
response.statusCode(), response.headers().asHttpHeaders(),
|
||||
response.body(toDataBuffers()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-create the result with a generic type matching the decoded body.
|
||||
* Re-create with decoded body (possibly still not consumed).
|
||||
*/
|
||||
static <T> ExchangeResult<T> withDecodedBody(ExchangeResult<?> result, T body) {
|
||||
return new ExchangeResult<>(result.getMethod(), result.getUrl(), result.getRequestHeaders(),
|
||||
|
|
|
|||
|
|
@ -29,33 +29,42 @@ import static org.springframework.test.util.AssertionErrors.assertEquals;
|
|||
import static org.springframework.test.util.AssertionErrors.assertTrue;
|
||||
|
||||
/**
|
||||
* Provides methods for HTTP header assertions.
|
||||
* Assertions on headers of the response.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
* @see ResponseAssertions#header()
|
||||
* @see WebTestClient.ResponseSpec#expectHeader()
|
||||
*/
|
||||
public class HeaderAssertions {
|
||||
|
||||
private final HttpHeaders headers;
|
||||
private final ExchangeResult<?> exchangeResult;
|
||||
|
||||
private final WebTestClient.ResponseSpec responseSpec;
|
||||
|
||||
|
||||
public HeaderAssertions(HttpHeaders headers, WebTestClient.ResponseSpec responseSpec) {
|
||||
this.headers = headers;
|
||||
HeaderAssertions(ExchangeResult<?> exchangeResult, WebTestClient.ResponseSpec responseSpec) {
|
||||
this.exchangeResult = exchangeResult;
|
||||
this.responseSpec = responseSpec;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Expect a header with the given name to match the specified values.
|
||||
*/
|
||||
public WebTestClient.ResponseSpec valueEquals(String headerName, String... values) {
|
||||
List<String> actual = this.headers.get(headerName);
|
||||
List<String> actual = getHeaders().get(headerName);
|
||||
assertEquals("Response header [" + headerName + "]", Arrays.asList(values), actual);
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect a header with the given name whose first value matches the
|
||||
* provided regex pattern.
|
||||
* @param headerName the header name
|
||||
* @param pattern String pattern to pass to {@link Pattern#compile(String)}
|
||||
*/
|
||||
public WebTestClient.ResponseSpec valueMatches(String headerName, String pattern) {
|
||||
List<String> values = this.headers.get(headerName);
|
||||
List<String> values = getHeaders().get(headerName);
|
||||
String value = CollectionUtils.isEmpty(values) ? "" : values.get(0);
|
||||
boolean match = Pattern.compile(pattern).matcher(value).matches();
|
||||
String message = "Response header " + headerName + "=\'" + value + "\' does not match " + pattern;
|
||||
|
|
@ -63,40 +72,65 @@ public class HeaderAssertions {
|
|||
return this.responseSpec;
|
||||
}
|
||||
|
||||
public WebTestClient.ResponseSpec cacheControlEquals(CacheControl cacheControl) {
|
||||
String actual = this.headers.getCacheControl();
|
||||
/**
|
||||
* Expect a "Cache-Control" header with the given value.
|
||||
*/
|
||||
public WebTestClient.ResponseSpec cacheControl(CacheControl cacheControl) {
|
||||
String actual = getHeaders().getCacheControl();
|
||||
assertEquals("Response header Cache-Control", cacheControl.getHeaderValue(), actual);
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
public WebTestClient.ResponseSpec contentDispositionEquals(ContentDisposition contentDisposition) {
|
||||
ContentDisposition actual = this.headers.getContentDisposition();
|
||||
/**
|
||||
* Expect a "Content-Disposition" header with the given value.
|
||||
*/
|
||||
public WebTestClient.ResponseSpec contentDisposition(ContentDisposition contentDisposition) {
|
||||
ContentDisposition actual = getHeaders().getContentDisposition();
|
||||
assertEquals("Response header Content-Disposition", contentDisposition, actual);
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
public WebTestClient.ResponseSpec contentLengthEquals(long contentLength) {
|
||||
long actual = this.headers.getContentLength();
|
||||
/**
|
||||
* Expect a "Content-Length" header with the given value.
|
||||
*/
|
||||
public WebTestClient.ResponseSpec contentLength(long contentLength) {
|
||||
long actual = getHeaders().getContentLength();
|
||||
assertEquals("Response header Content-Length", contentLength, actual);
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
public WebTestClient.ResponseSpec contentTypeEquals(MediaType mediaType) {
|
||||
MediaType actual = this.headers.getContentType();
|
||||
/**
|
||||
* Expect a "Content-Type" header with the given value.
|
||||
*/
|
||||
public WebTestClient.ResponseSpec contentType(MediaType mediaType) {
|
||||
MediaType actual = getHeaders().getContentType();
|
||||
assertEquals("Response header Content-Type", mediaType, actual);
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
public WebTestClient.ResponseSpec expiresEquals(int expires) {
|
||||
long actual = this.headers.getExpires();
|
||||
/**
|
||||
* Expect an "Expires" header with the given value.
|
||||
*/
|
||||
public WebTestClient.ResponseSpec expires(int expires) {
|
||||
long actual = getHeaders().getExpires();
|
||||
assertEquals("Response header Expires", expires, actual);
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
public WebTestClient.ResponseSpec lastModifiedEquals(int lastModified) {
|
||||
long actual = this.headers.getLastModified();
|
||||
/**
|
||||
* Expect a "Last-Modified" header with the given value.
|
||||
*/
|
||||
public WebTestClient.ResponseSpec lastModified(int lastModified) {
|
||||
long actual = getHeaders().getLastModified();
|
||||
assertEquals("Response header Last-Modified", lastModified, actual);
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
|
||||
// Private methods
|
||||
|
||||
private HttpHeaders getHeaders() {
|
||||
return this.exchangeResult.getResponseHeaders();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,22 +20,22 @@ import org.springframework.http.HttpStatus;
|
|||
import static org.springframework.test.util.AssertionErrors.assertEquals;
|
||||
|
||||
/**
|
||||
* Assertions on the status of a response.
|
||||
* Assertions on the response status.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
* @see ResponseAssertions#status()
|
||||
* @see WebTestClient.ResponseSpec#expectStatus()
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class StatusAssertions {
|
||||
|
||||
private final HttpStatus httpStatus;
|
||||
private final ExchangeResult<?> exchangeResult;
|
||||
|
||||
private final WebTestClient.ResponseSpec responseSpec;
|
||||
|
||||
|
||||
StatusAssertions(HttpStatus status, WebTestClient.ResponseSpec responseSpec) {
|
||||
this.httpStatus = status;
|
||||
StatusAssertions(ExchangeResult<?> exchangeResult, WebTestClient.ResponseSpec responseSpec) {
|
||||
this.exchangeResult = exchangeResult;
|
||||
this.responseSpec = responseSpec;
|
||||
}
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ public class StatusAssertions {
|
|||
* Assert the response status as an {@link HttpStatus}.
|
||||
*/
|
||||
public WebTestClient.ResponseSpec isEqualTo(HttpStatus status) {
|
||||
assertEquals("Response status", status, this.httpStatus);
|
||||
assertEquals("Response status", status, getStatus());
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ public class StatusAssertions {
|
|||
* Assert the response status as an integer.
|
||||
*/
|
||||
public WebTestClient.ResponseSpec isEqualTo(int status) {
|
||||
assertEquals("Response status", status, this.httpStatus.value());
|
||||
assertEquals("Response status", status, getStatus().value());
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
|
|
@ -60,7 +60,7 @@ public class StatusAssertions {
|
|||
* Assert the response status code is {@code HttpStatus.OK} (200).
|
||||
*/
|
||||
public WebTestClient.ResponseSpec isOk() {
|
||||
assertEquals("Status", HttpStatus.OK, this.httpStatus);
|
||||
assertEquals("Status", HttpStatus.OK, getStatus());
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
|
|
@ -68,7 +68,7 @@ public class StatusAssertions {
|
|||
* Assert the response status code is {@code HttpStatus.CREATED} (201).
|
||||
*/
|
||||
public WebTestClient.ResponseSpec isCreated() {
|
||||
assertEquals("Status", HttpStatus.CREATED, this.httpStatus);
|
||||
assertEquals("Status", HttpStatus.CREATED, getStatus());
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
|
|
@ -76,7 +76,7 @@ public class StatusAssertions {
|
|||
* Assert the response status code is {@code HttpStatus.ACCEPTED} (202).
|
||||
*/
|
||||
public WebTestClient.ResponseSpec isAccepted() {
|
||||
assertEquals("Status", HttpStatus.ACCEPTED, this.httpStatus);
|
||||
assertEquals("Status", HttpStatus.ACCEPTED, getStatus());
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
|
|
@ -84,7 +84,7 @@ public class StatusAssertions {
|
|||
* Assert the response status code is {@code HttpStatus.NO_CONTENT} (204).
|
||||
*/
|
||||
public WebTestClient.ResponseSpec isNoContent() {
|
||||
assertEquals("Status", HttpStatus.NO_CONTENT, this.httpStatus);
|
||||
assertEquals("Status", HttpStatus.NO_CONTENT, getStatus());
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
|
|
@ -92,7 +92,7 @@ public class StatusAssertions {
|
|||
* Assert the response status code is {@code HttpStatus.FOUND} (302).
|
||||
*/
|
||||
public WebTestClient.ResponseSpec isFound() {
|
||||
assertEquals("Status", HttpStatus.FOUND, this.httpStatus);
|
||||
assertEquals("Status", HttpStatus.FOUND, getStatus());
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
|
|
@ -100,7 +100,7 @@ public class StatusAssertions {
|
|||
* Assert the response status code is {@code HttpStatus.SEE_OTHER} (303).
|
||||
*/
|
||||
public WebTestClient.ResponseSpec isSeeOther() {
|
||||
assertEquals("Status", HttpStatus.SEE_OTHER, this.httpStatus);
|
||||
assertEquals("Status", HttpStatus.SEE_OTHER, getStatus());
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
|
|
@ -108,7 +108,7 @@ public class StatusAssertions {
|
|||
* Assert the response status code is {@code HttpStatus.NOT_MODIFIED} (304).
|
||||
*/
|
||||
public WebTestClient.ResponseSpec isNotModified() {
|
||||
assertEquals("Status", HttpStatus.NOT_MODIFIED, this.httpStatus);
|
||||
assertEquals("Status", HttpStatus.NOT_MODIFIED, getStatus());
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
|
|
@ -116,7 +116,7 @@ public class StatusAssertions {
|
|||
* Assert the response status code is {@code HttpStatus.TEMPORARY_REDIRECT} (307).
|
||||
*/
|
||||
public WebTestClient.ResponseSpec isTemporaryRedirect() {
|
||||
assertEquals("Status", HttpStatus.TEMPORARY_REDIRECT, this.httpStatus);
|
||||
assertEquals("Status", HttpStatus.TEMPORARY_REDIRECT, getStatus());
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
|
|
@ -124,7 +124,7 @@ public class StatusAssertions {
|
|||
* Assert the response status code is {@code HttpStatus.PERMANENT_REDIRECT} (308).
|
||||
*/
|
||||
public WebTestClient.ResponseSpec isPermanentRedirect() {
|
||||
assertEquals("Status", HttpStatus.PERMANENT_REDIRECT, this.httpStatus);
|
||||
assertEquals("Status", HttpStatus.PERMANENT_REDIRECT, getStatus());
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
|
|
@ -132,7 +132,15 @@ public class StatusAssertions {
|
|||
* Assert the response status code is {@code HttpStatus.BAD_REQUEST} (400).
|
||||
*/
|
||||
public WebTestClient.ResponseSpec isBadRequest() {
|
||||
assertEquals("Status", HttpStatus.BAD_REQUEST, this.httpStatus);
|
||||
assertEquals("Status", HttpStatus.BAD_REQUEST, getStatus());
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.UNAUTHORIZED} (401).
|
||||
*/
|
||||
public WebTestClient.ResponseSpec isUnauthorized() {
|
||||
assertEquals("Status", HttpStatus.UNAUTHORIZED, getStatus());
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
|
|
@ -140,7 +148,7 @@ public class StatusAssertions {
|
|||
* Assert the response status code is {@code HttpStatus.NOT_FOUND} (404).
|
||||
*/
|
||||
public WebTestClient.ResponseSpec isNotFound() {
|
||||
assertEquals("Status", HttpStatus.NOT_FOUND, this.httpStatus);
|
||||
assertEquals("Status", HttpStatus.NOT_FOUND, getStatus());
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
|
|
@ -148,7 +156,7 @@ public class StatusAssertions {
|
|||
* Assert the response error message.
|
||||
*/
|
||||
public WebTestClient.ResponseSpec reasonEquals(String reason) {
|
||||
assertEquals("Response status reason", reason, this.httpStatus.getReasonPhrase());
|
||||
assertEquals("Response status reason", reason, getStatus().getReasonPhrase());
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
|
|
@ -156,8 +164,8 @@ public class StatusAssertions {
|
|||
* Assert the response status code is in the 1xx range.
|
||||
*/
|
||||
public WebTestClient.ResponseSpec is1xxInformational() {
|
||||
String message = "Range for response status value " + this.httpStatus;
|
||||
assertEquals(message, HttpStatus.Series.INFORMATIONAL, this.httpStatus.series());
|
||||
String message = "Range for response status value " + getStatus();
|
||||
assertEquals(message, HttpStatus.Series.INFORMATIONAL, getStatus().series());
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
|
|
@ -165,8 +173,8 @@ public class StatusAssertions {
|
|||
* Assert the response status code is in the 2xx range.
|
||||
*/
|
||||
public WebTestClient.ResponseSpec is2xxSuccessful() {
|
||||
String message = "Range for response status value " + this.httpStatus;
|
||||
assertEquals(message, HttpStatus.Series.SUCCESSFUL, this.httpStatus.series());
|
||||
String message = "Range for response status value " + getStatus();
|
||||
assertEquals(message, HttpStatus.Series.SUCCESSFUL, getStatus().series());
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
|
|
@ -174,8 +182,8 @@ public class StatusAssertions {
|
|||
* Assert the response status code is in the 3xx range.
|
||||
*/
|
||||
public WebTestClient.ResponseSpec is3xxRedirection() {
|
||||
String message = "Range for response status value " + this.httpStatus;
|
||||
assertEquals(message, HttpStatus.Series.REDIRECTION, this.httpStatus.series());
|
||||
String message = "Range for response status value " + getStatus();
|
||||
assertEquals(message, HttpStatus.Series.REDIRECTION, getStatus().series());
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
|
|
@ -183,8 +191,8 @@ public class StatusAssertions {
|
|||
* Assert the response status code is in the 4xx range.
|
||||
*/
|
||||
public WebTestClient.ResponseSpec is4xxClientError() {
|
||||
String message = "Range for response status value " + this.httpStatus;
|
||||
assertEquals(message, HttpStatus.Series.CLIENT_ERROR, this.httpStatus.series());
|
||||
String message = "Range for response status value " + getStatus();
|
||||
assertEquals(message, HttpStatus.Series.CLIENT_ERROR, getStatus().series());
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
|
|
@ -192,9 +200,16 @@ public class StatusAssertions {
|
|||
* Assert the response status code is in the 5xx range.
|
||||
*/
|
||||
public WebTestClient.ResponseSpec is5xxServerError() {
|
||||
String message = "Range for response status value " + this.httpStatus;
|
||||
assertEquals(message, HttpStatus.Series.SERVER_ERROR, this.httpStatus.series());
|
||||
String message = "Range for response status value " + getStatus();
|
||||
assertEquals(message, HttpStatus.Series.SERVER_ERROR, getStatus().series());
|
||||
return this.responseSpec;
|
||||
}
|
||||
|
||||
|
||||
// Private methods
|
||||
|
||||
private HttpStatus getStatus() {
|
||||
return this.exchangeResult.getStatus();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -448,7 +448,7 @@ public interface WebTestClient {
|
|||
}
|
||||
|
||||
/**
|
||||
* Specification for expectations on the response.
|
||||
* Specification for processing the response and applying expectations.
|
||||
*/
|
||||
interface ResponseSpec {
|
||||
|
||||
|
|
@ -458,40 +458,123 @@ public interface WebTestClient {
|
|||
StatusAssertions expectStatus();
|
||||
|
||||
/**
|
||||
* Assertions on the response headers.
|
||||
* Assertions on the headers of the response.
|
||||
*/
|
||||
HeaderAssertions expectHeader();
|
||||
|
||||
/**
|
||||
* Assertions on the response body.
|
||||
* Assertions on the body of the response extracted to one or more
|
||||
* elements of the given type.
|
||||
*/
|
||||
TypeBodySpec expectBody(Class<?> elementType);
|
||||
|
||||
/**
|
||||
* Variant of {@link #expectBody(Class)} for use with generic types.
|
||||
*/
|
||||
TypeBodySpec expectBody(ResolvableType elementType);
|
||||
|
||||
/**
|
||||
* Access to additional assertions on the response body --
|
||||
* isEmpty, map, and others.
|
||||
*/
|
||||
BodySpec expectBody();
|
||||
|
||||
/**
|
||||
* Assertions on the response body where the body is to be decoded to
|
||||
* one or more elements of the given type.
|
||||
*/
|
||||
ElementBodySpec expectBody(Class<?> elementType);
|
||||
|
||||
/**
|
||||
* Alternative to {@link #expectBody(Class)} for generic types.
|
||||
*/
|
||||
ElementBodySpec expectBody(ResolvableType elementType);
|
||||
|
||||
/**
|
||||
* Consume the result of the exchange and continue with expectations.
|
||||
* Consume the {@link ExchangeResult} and continue with expectations.
|
||||
* The {@code ExchangeResult} is parameterized with data buffers since
|
||||
* the body is not yet consumed nor decoded at this level.
|
||||
*/
|
||||
ResponseSpec consumeWith(Consumer<ExchangeResult<Flux<DataBuffer>>> consumer);
|
||||
|
||||
/**
|
||||
* Return a container for the result of the exchange with the body
|
||||
* not yet decoded nor consumed.
|
||||
* Return a container for the result of the exchange. The returned
|
||||
* {@code ExchangeResult} is parameterized with data buffers since
|
||||
* the body is not yet consumed nor decoded at this level.
|
||||
*/
|
||||
ExchangeResult<Flux<DataBuffer>> returnResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Specification for expectations on the body of the response.
|
||||
* Specification for extracting entities from the response body.
|
||||
*/
|
||||
interface TypeBodySpec {
|
||||
|
||||
/**
|
||||
* Extract a single value from the response.
|
||||
*/
|
||||
SingleValueBodySpec value();
|
||||
|
||||
/**
|
||||
* Extract a list of values from the response.
|
||||
*/
|
||||
ListBodySpec list();
|
||||
|
||||
/**
|
||||
* Extract a list of values consuming the first N elements.
|
||||
*/
|
||||
ListBodySpec list(int elementCount);
|
||||
|
||||
/**
|
||||
* Return a container for the result of the exchange parameterized with
|
||||
* the {@code Flux} of decoded objects (not yet consumed).
|
||||
*/
|
||||
<T> ExchangeResult<Flux<T>> returnResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Specification to assert a single value extracted from the response body.
|
||||
*/
|
||||
interface SingleValueBodySpec {
|
||||
|
||||
/**
|
||||
* Assert the extracted body is equal to the given value.
|
||||
*/
|
||||
<T> ExchangeResult<T> isEqualTo(Object expected);
|
||||
|
||||
/**
|
||||
* Return a container for the result of the exchange parameterized with
|
||||
* the extracted response entity.
|
||||
*/
|
||||
<T> ExchangeResult<T> returnResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Specification to assert a list of values extracted from the response.
|
||||
*/
|
||||
interface ListBodySpec {
|
||||
|
||||
/**
|
||||
* Assert the extracted body is equal to the given list.
|
||||
*/
|
||||
<T> ExchangeResult<List<T>> isEqualTo(List<T> expected);
|
||||
|
||||
/**
|
||||
* Assert the extracted list of values is of the given size.
|
||||
* @param size the expected size
|
||||
*/
|
||||
ListBodySpec hasSize(int size);
|
||||
|
||||
/**
|
||||
* Assert the extracted list of values contains the given elements.
|
||||
* @param elements the elements to check
|
||||
*/
|
||||
ListBodySpec contains(Object... elements);
|
||||
|
||||
/**
|
||||
* Assert the extracted list of values doesn't contain the given elements.
|
||||
* @param elements the elements to check
|
||||
*/
|
||||
ListBodySpec doesNotContain(Object... elements);
|
||||
|
||||
/**
|
||||
* Return a container for the result of the exchange parameterized with
|
||||
* the extracted list of response entities.
|
||||
*/
|
||||
<T> ExchangeResult<List<T>> returnResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Specification to apply additional assertions on the response body.
|
||||
*/
|
||||
interface BodySpec {
|
||||
|
||||
|
|
@ -502,48 +585,48 @@ public interface WebTestClient {
|
|||
ExchangeResult<Void> isEmpty();
|
||||
|
||||
/**
|
||||
* Decode the response body as a Map with the given key and value type.
|
||||
* Extract the response body as a Map with the given key and value type.
|
||||
*/
|
||||
MapBodySpec map(Class<?> keyType, Class<?> valueType);
|
||||
|
||||
/**
|
||||
* Alternative to {@link #map(Class, Class)} for generic types.
|
||||
* Variant of {@link #map(Class, Class)} for use with generic types.
|
||||
*/
|
||||
MapBodySpec map(ResolvableType keyType, ResolvableType valueType);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Specification for expectations on the body of the response decoded as a map.
|
||||
* Specification to assert response the body extracted as a map.
|
||||
*/
|
||||
interface MapBodySpec {
|
||||
|
||||
/**
|
||||
* Assert the decoded body is equal to the given list of elements.
|
||||
* Assert the extracted map is equal to the given list of elements.
|
||||
*/
|
||||
<K, V> ExchangeResult<Map<K, V>> isEqualTo(Map<K, V> expected);
|
||||
|
||||
/**
|
||||
* Assert the decoded map has the given size.
|
||||
* Assert the extracted map has the given size.
|
||||
* @param size the expected size
|
||||
*/
|
||||
MapBodySpec hasSize(int size);
|
||||
|
||||
/**
|
||||
* Assert the decoded map contains the given key value pair.
|
||||
* Assert the extracted map contains the given key value pair.
|
||||
* @param key the key to check
|
||||
* @param value the value to check
|
||||
*/
|
||||
MapBodySpec contains(Object key, Object value);
|
||||
|
||||
/**
|
||||
* Assert the decoded map contains the given keys.
|
||||
* Assert the extracted map contains the given keys.
|
||||
* @param keys the keys to check
|
||||
*/
|
||||
MapBodySpec containsKeys(Object... keys);
|
||||
|
||||
/**
|
||||
* Assert the decoded map contains the given values.
|
||||
* Assert the extracted map contains the given values.
|
||||
* @param values the keys to check
|
||||
*/
|
||||
MapBodySpec containsValues(Object... values);
|
||||
|
|
@ -554,85 +637,4 @@ public interface WebTestClient {
|
|||
<K, V> ExchangeResult<Map<K, V>> returnResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Specification for expectations on the body of the response to be decoded
|
||||
* as one or more elements of a specific type.
|
||||
*/
|
||||
interface ElementBodySpec {
|
||||
|
||||
/**
|
||||
* Decode the response as a single element.
|
||||
*/
|
||||
SingleValueBodySpec value();
|
||||
|
||||
/**
|
||||
* Decode the response as a Flux of objects and collect it to a list.
|
||||
*/
|
||||
ListBodySpec list();
|
||||
|
||||
/**
|
||||
* Decode the response as a Flux of objects consuming only the specified
|
||||
* number of elements.
|
||||
*/
|
||||
ListBodySpec list(int elementCount);
|
||||
|
||||
/**
|
||||
* Decode the response as a Flux of objects and return a container for
|
||||
* the result where the response body not yet consumed.
|
||||
*/
|
||||
<T> ExchangeResult<Flux<T>> returnResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Specification for expectations on the body of the response decoded as a
|
||||
* single element of a specific type.
|
||||
*/
|
||||
interface SingleValueBodySpec {
|
||||
|
||||
/**
|
||||
* Assert the decoded body is equal to the given value.
|
||||
*/
|
||||
<T> ExchangeResult<T> isEqualTo(Object expected);
|
||||
|
||||
/**
|
||||
* Return a container for the result of the exchange.
|
||||
*/
|
||||
<T> ExchangeResult<T> returnResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Specification for expectations on the body of the response decoded as a
|
||||
* list of elements of a specific type.
|
||||
*/
|
||||
interface ListBodySpec {
|
||||
|
||||
/**
|
||||
* Assert the decoded body is equal to the given list of elements.
|
||||
*/
|
||||
<T> ExchangeResult<List<T>> isEqualTo(List<T> expected);
|
||||
|
||||
/**
|
||||
* Assert the decoded list of values is of the given size.
|
||||
* @param size the expected size
|
||||
*/
|
||||
ListBodySpec hasSize(int size);
|
||||
|
||||
/**
|
||||
* Assert the decoded list of values contains the given elements.
|
||||
* @param elements the elements to check
|
||||
*/
|
||||
ListBodySpec contains(Object... elements);
|
||||
|
||||
/**
|
||||
* Assert the decoded list of values does not contain the given elements.
|
||||
* @param elements the elements to check
|
||||
*/
|
||||
ListBodySpec doesNotContain(Object... elements);
|
||||
|
||||
/**
|
||||
* Return a container for the result of the exchange.
|
||||
*/
|
||||
<T> ExchangeResult<List<T>> returnResult();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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.test.web.reactive.server;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Function;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
import org.springframework.http.client.reactive.ClientHttpRequest;
|
||||
import org.springframework.http.client.reactive.ClientHttpResponse;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Decorate any other {@link ClientHttpConnector} with the purpose of
|
||||
* intercepting, capturing, and exposing {@code ClientHttpRequest}s reflecting
|
||||
* the exact and complete details sent to the server.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
* @see HttpHandlerConnector
|
||||
*/
|
||||
class WebTestClientConnector implements ClientHttpConnector {
|
||||
|
||||
public static final String REQUEST_ID_HEADER_NAME = "request-id";
|
||||
|
||||
|
||||
private final ClientHttpConnector delegate;
|
||||
|
||||
private final Map<String, ClientHttpRequest> capturedRequests = new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
public WebTestClientConnector(ClientHttpConnector delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<ClientHttpResponse> connect(HttpMethod method, URI uri,
|
||||
Function<? super ClientHttpRequest, Mono<Void>> requestCallback) {
|
||||
|
||||
AtomicReference<ClientHttpRequest> requestRef = new AtomicReference<>();
|
||||
|
||||
return this.delegate
|
||||
.connect(method, uri, request -> {
|
||||
requestRef.set(request);
|
||||
return requestCallback.apply(request);
|
||||
})
|
||||
.doOnNext(response -> {
|
||||
ClientHttpRequest request = requestRef.get();
|
||||
String id = request.getHeaders().getFirst(REQUEST_ID_HEADER_NAME);
|
||||
if (id != null) {
|
||||
this.capturedRequests.put(id, request);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the request with the given "request-id" header.
|
||||
*/
|
||||
public ClientHttpRequest claimRequest(String requestId) {
|
||||
ClientHttpRequest request = this.capturedRequests.get(requestId);
|
||||
Assert.notNull(request, "No matching request [" + requestId + "]. Did connect return a response yet?");
|
||||
return request;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
/*
|
||||
* 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.test.web.reactive.server;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
import org.springframework.http.client.reactive.ClientHttpRequest;
|
||||
import org.springframework.http.client.reactive.ClientHttpResponse;
|
||||
|
||||
/**
|
||||
* Decorates a {@link ClientHttpConnector} in order to capture executed requests
|
||||
* and responses and notify one or more registered listeners. This is helpful
|
||||
* for access to the actual {@link ClientHttpRequest} sent and the
|
||||
* {@link ClientHttpResponse} returned by the server.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
class WiretapConnector implements ClientHttpConnector {
|
||||
|
||||
private final ClientHttpConnector delegate;
|
||||
|
||||
private final List<Consumer<Info>> listeners;
|
||||
|
||||
|
||||
public WiretapConnector(ClientHttpConnector delegate) {
|
||||
this.delegate = delegate;
|
||||
this.listeners = new CopyOnWriteArrayList<>();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Register a listener to consume exchanged requests and responses.
|
||||
*/
|
||||
public void addListener(Consumer<Info> consumer) {
|
||||
this.listeners.add(consumer);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<ClientHttpResponse> connect(HttpMethod method, URI uri,
|
||||
Function<? super ClientHttpRequest, Mono<Void>> requestCallback) {
|
||||
|
||||
AtomicReference<ClientHttpRequest> requestRef = new AtomicReference<>();
|
||||
|
||||
return this.delegate
|
||||
.connect(method, uri, request -> {
|
||||
requestRef.set(request);
|
||||
return requestCallback.apply(request);
|
||||
})
|
||||
.doOnNext(response -> {
|
||||
Info info = new Info(requestRef.get(), response);
|
||||
this.listeners.forEach(consumer -> consumer.accept(info));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public static class Info {
|
||||
|
||||
private final HttpMethod method;
|
||||
|
||||
private final URI url;
|
||||
|
||||
private final HttpHeaders requestHeaders;
|
||||
|
||||
private final ClientHttpResponse response;
|
||||
|
||||
|
||||
public Info(ClientHttpRequest request, ClientHttpResponse response) {
|
||||
this.method = request.getMethod();
|
||||
this.url = request.getURI();
|
||||
this.requestHeaders = request.getHeaders();
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
|
||||
public HttpMethod getMethod() {
|
||||
return this.method;
|
||||
}
|
||||
|
||||
public URI getUrl() {
|
||||
return this.url;
|
||||
}
|
||||
|
||||
public HttpHeaders getRequestHeaders() {
|
||||
return this.requestHeaders;
|
||||
}
|
||||
|
||||
public ClientHttpResponse getResponse() {
|
||||
return this.response;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -17,7 +17,6 @@
|
|||
package org.springframework.test.web.reactive.server;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.junit.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
|
@ -35,35 +34,32 @@ import org.springframework.web.reactive.function.client.ExchangeFunctions;
|
|||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link WiretapConnector}.
|
||||
* Unit tests for {@link WebTestClientConnector}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class WiretapConnectorTests {
|
||||
public class WebTestClientConnectorTests {
|
||||
|
||||
@Test
|
||||
public void listener() throws Exception {
|
||||
public void captureAndClaim() throws Exception {
|
||||
|
||||
ClientHttpRequest request = new MockClientHttpRequest(HttpMethod.GET, "/test");
|
||||
ClientHttpResponse response = new MockClientHttpResponse(HttpStatus.OK);
|
||||
ClientHttpConnector connector = (method, uri, fn) -> fn.apply(request).then(Mono.just(response));
|
||||
|
||||
AtomicReference<WiretapConnector.Info> infoRef = new AtomicReference<>();
|
||||
WiretapConnector wiretapConnector = new WiretapConnector(connector);
|
||||
wiretapConnector.addListener(infoRef::set);
|
||||
ClientRequest clientRequest = ClientRequest.method(HttpMethod.GET, URI.create("/test"))
|
||||
.header(WebTestClientConnector.REQUEST_ID_HEADER_NAME, "1").build();
|
||||
|
||||
ExchangeFunction exchangeFn = ExchangeFunctions.create(wiretapConnector);
|
||||
ClientRequest clientRequest = ClientRequest.method(HttpMethod.GET, URI.create("/test")).build();
|
||||
exchangeFn.exchange(clientRequest).blockMillis(0);
|
||||
WebTestClientConnector webTestClientConnector = new WebTestClientConnector(connector);
|
||||
ExchangeFunction function = ExchangeFunctions.create(webTestClientConnector);
|
||||
function.exchange(clientRequest).blockMillis(0);
|
||||
|
||||
WiretapConnector.Info info = infoRef.get();
|
||||
assertNotNull(info);
|
||||
assertEquals(HttpMethod.GET, info.getMethod());
|
||||
assertEquals("/test", info.getUrl().toString());
|
||||
assertSame(response, info.getResponse());
|
||||
ClientHttpRequest actual = webTestClientConnector.claimRequest("1");
|
||||
assertNotNull(actual);
|
||||
assertEquals(HttpMethod.GET, actual.getMethod());
|
||||
assertEquals("/test", actual.getURI().toString());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -66,7 +66,7 @@ public class ResponseEntityTests {
|
|||
this.client.get().uri("/persons/John")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectHeader().contentTypeEquals(MediaType.APPLICATION_JSON_UTF8)
|
||||
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
|
||||
.expectBody(Person.class).value().isEqualTo(new Person("John"));
|
||||
}
|
||||
|
||||
|
|
@ -79,7 +79,7 @@ public class ResponseEntityTests {
|
|||
this.client.get().uri("/persons")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectHeader().contentTypeEquals(MediaType.APPLICATION_JSON_UTF8)
|
||||
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
|
||||
.expectBody(Person.class).list().isEqualTo(expected);
|
||||
}
|
||||
|
||||
|
|
@ -106,7 +106,7 @@ public class ResponseEntityTests {
|
|||
.accept(TEXT_EVENT_STREAM)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectHeader().contentTypeEquals(TEXT_EVENT_STREAM)
|
||||
.expectHeader().contentType(TEXT_EVENT_STREAM)
|
||||
.expectBody(Person.class)
|
||||
.returnResult();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue