WebFlux test support for server endpoints
Issue: SPR-14590
This commit is contained in:
@@ -67,10 +67,10 @@ public abstract class AssertionErrors {
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert two objects are equal raise an {@link AssertionError} if not.
|
||||
* Assert two objects are equal and raise an {@link AssertionError} if not.
|
||||
* <p>For example:
|
||||
* <pre class="code">
|
||||
* assertEquals("Response header [" + name + "]", actual, expected);
|
||||
* assertEquals("Response header [" + name + "]", expected, actual);
|
||||
* </pre>
|
||||
* @param message describes the value being checked
|
||||
* @param expected the expected value
|
||||
@@ -82,4 +82,21 @@ public abstract class AssertionErrors {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert two objects are not equal and raise an {@link AssertionError} otherwise.
|
||||
* <p>For example:
|
||||
* <pre class="code">
|
||||
* assertNotEquals("Response header [" + name + "]", expected, actual);
|
||||
* </pre>
|
||||
* @param message describes the value being checked
|
||||
* @param expected the expected value
|
||||
* @param actual the actual value
|
||||
*/
|
||||
public static void assertNotEquals(String message, Object expected, Object actual) {
|
||||
if (ObjectUtils.nullSafeEquals(expected, actual)) {
|
||||
throw new AssertionError(message + " was not expected to be:" +
|
||||
"<" + ObjectUtils.nullSafeToString(actual) + ">");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* 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.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.format.FormatterRegistry;
|
||||
import org.springframework.http.codec.HttpMessageReader;
|
||||
import org.springframework.http.codec.HttpMessageWriter;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.validation.Validator;
|
||||
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
|
||||
import org.springframework.web.reactive.config.CorsRegistry;
|
||||
import org.springframework.web.reactive.config.DelegatingWebFluxConfiguration;
|
||||
import org.springframework.web.reactive.config.PathMatchConfigurer;
|
||||
import org.springframework.web.reactive.config.ViewResolverRegistry;
|
||||
import org.springframework.web.reactive.config.WebFluxConfigurer;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link WebTestClient.ControllerSpec}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
class DefaultControllerSpec implements WebTestClient.ControllerSpec {
|
||||
|
||||
private final List<Object> controllers;
|
||||
|
||||
private final TestWebFluxConfigurer configurer = new TestWebFluxConfigurer();
|
||||
|
||||
|
||||
public DefaultControllerSpec(Object... controllers) {
|
||||
Assert.isTrue(!ObjectUtils.isEmpty(controllers), "At least one controller is required");
|
||||
this.controllers = Arrays.asList(controllers);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public DefaultControllerSpec contentTypeResolver(Consumer<RequestedContentTypeResolverBuilder> consumer) {
|
||||
this.configurer.contentTypeResolverConsumer = consumer;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultControllerSpec corsMappings(Consumer<CorsRegistry> consumer) {
|
||||
this.configurer.corsRegistryConsumer = consumer;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultControllerSpec pathMatching(Consumer<PathMatchConfigurer> consumer) {
|
||||
this.configurer.pathMatchConsumer = consumer;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultControllerSpec messageReaders(Consumer<List<HttpMessageReader<?>>> consumer) {
|
||||
this.configurer.readersConsumer = consumer;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultControllerSpec messageWriters(Consumer<List<HttpMessageWriter<?>>> consumer) {
|
||||
this.configurer.writersConsumer = consumer;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultControllerSpec formatters(Consumer<FormatterRegistry> consumer) {
|
||||
this.configurer.formattersConsumer = consumer;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultControllerSpec validator(Validator validator) {
|
||||
this.configurer.validator = validator;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultControllerSpec viewResolvers(Consumer<ViewResolverRegistry> consumer) {
|
||||
this.configurer.viewResolversConsumer = consumer;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebTestClient.WebClientSpec webClientSpec() {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
this.controllers.forEach(controller -> registerBean(context, controller));
|
||||
context.register(DelegatingWebFluxConfiguration.class);
|
||||
context.registerBean(WebFluxConfigurer.class, () -> this.configurer);
|
||||
context.refresh();
|
||||
return WebTestClient.bindToApplicationContext(context);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> void registerBean(AnnotationConfigApplicationContext context, T bean) {
|
||||
context.registerBean((Class<T>) bean.getClass(), () -> bean);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebTestClient build() {
|
||||
return webClientSpec().build();
|
||||
}
|
||||
|
||||
|
||||
private class TestWebFluxConfigurer implements WebFluxConfigurer {
|
||||
|
||||
private Consumer<RequestedContentTypeResolverBuilder> contentTypeResolverConsumer;
|
||||
|
||||
private Consumer<CorsRegistry> corsRegistryConsumer;
|
||||
|
||||
private Consumer<PathMatchConfigurer> pathMatchConsumer;
|
||||
|
||||
private Consumer<List<HttpMessageReader<?>>> readersConsumer;
|
||||
|
||||
private Consumer<List<HttpMessageWriter<?>>> writersConsumer;
|
||||
|
||||
private Consumer<FormatterRegistry> formattersConsumer;
|
||||
|
||||
private Validator validator;
|
||||
|
||||
private Consumer<ViewResolverRegistry> viewResolversConsumer;
|
||||
|
||||
|
||||
@Override
|
||||
public void configureContentTypeResolver(RequestedContentTypeResolverBuilder builder) {
|
||||
if (this.contentTypeResolverConsumer != null) {
|
||||
this.contentTypeResolverConsumer.accept(builder);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
if (this.corsRegistryConsumer != null) {
|
||||
this.corsRegistryConsumer.accept(registry);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configurePathMatching(PathMatchConfigurer configurer) {
|
||||
if (this.pathMatchConsumer != null) {
|
||||
this.pathMatchConsumer.accept(configurer);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extendMessageReaders(List<HttpMessageReader<?>> readers) {
|
||||
if (this.readersConsumer != null) {
|
||||
this.readersConsumer.accept(readers);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extendMessageWriters(List<HttpMessageWriter<?>> writers) {
|
||||
if (this.writersConsumer != null) {
|
||||
this.writersConsumer.accept(writers);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addFormatters(FormatterRegistry registry) {
|
||||
if (this.formattersConsumer != null) {
|
||||
this.formattersConsumer.accept(registry);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Validator> getValidator() {
|
||||
return Optional.ofNullable(this.validator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureViewResolvers(ViewResolverRegistry registry) {
|
||||
if (this.viewResolversConsumer != null) {
|
||||
this.viewResolversConsumer.accept(registry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 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.util.Map;
|
||||
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
import org.springframework.web.reactive.function.client.ExchangeStrategies;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.util.UriBuilderFactory;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link WebTestClient.WebClientSpec}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
class DefaultWebClientSpec implements WebTestClient.WebClientSpec {
|
||||
|
||||
private final WebClient.Builder builder = WebClient.builder();
|
||||
|
||||
private final ClientHttpConnector connector;
|
||||
|
||||
|
||||
public DefaultWebClientSpec() {
|
||||
this(new ReactorClientHttpConnector());
|
||||
}
|
||||
|
||||
public DefaultWebClientSpec(HttpHandler httpHandler) {
|
||||
this(new HttpHandlerConnector(httpHandler));
|
||||
}
|
||||
|
||||
public DefaultWebClientSpec(ClientHttpConnector connector) {
|
||||
this.connector = connector;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public WebTestClient.WebClientSpec baseUrl(String baseUrl) {
|
||||
this.builder.baseUrl(baseUrl);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebTestClient.WebClientSpec defaultUriVariables(Map<String, ?> defaultUriVariables) {
|
||||
this.builder.defaultUriVariables(defaultUriVariables);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebTestClient.WebClientSpec uriBuilderFactory(UriBuilderFactory uriBuilderFactory) {
|
||||
this.builder.uriBuilderFactory(uriBuilderFactory);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebTestClient.WebClientSpec defaultHeader(String headerName, String... headerValues) {
|
||||
this.builder.defaultHeader(headerName, headerValues);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebTestClient.WebClientSpec defaultCookie(String cookieName, String... cookieValues) {
|
||||
this.builder.defaultCookie(cookieName, cookieValues);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebTestClient.WebClientSpec exchangeStrategies(ExchangeStrategies strategies) {
|
||||
this.builder.exchangeStrategies(strategies);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebTestClient.Builder builder() {
|
||||
return new DefaultWebTestClientBuilder(this.builder, this.connector);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebTestClient build() {
|
||||
return builder().build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
/*
|
||||
* 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.nio.charset.Charset;
|
||||
import java.time.Duration;
|
||||
import java.time.ZonedDateTime;
|
||||
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 org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
import org.springframework.http.client.reactive.ClientHttpRequest;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.reactive.function.BodyInserter;
|
||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.util.UriBuilder;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link WebTestClient}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
class DefaultWebTestClient implements WebTestClient {
|
||||
|
||||
private final WebClient webClient;
|
||||
|
||||
private final Duration responseTimeout;
|
||||
|
||||
private final WiretapConnectorListener connectorListener;
|
||||
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
private DefaultWebTestClient(DefaultWebTestClient webTestClient, ExchangeFilterFunction filter) {
|
||||
this.webClient = webTestClient.webClient.filter(filter);
|
||||
this.connectorListener = webTestClient.connectorListener;
|
||||
this.responseTimeout = webTestClient.responseTimeout;
|
||||
}
|
||||
|
||||
|
||||
private Duration getTimeout() {
|
||||
return this.responseTimeout;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public UriSpec get() {
|
||||
return toUriSpec(WebClient::get);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UriSpec head() {
|
||||
return toUriSpec(WebClient::head);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UriSpec post() {
|
||||
return toUriSpec(WebClient::post);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UriSpec put() {
|
||||
return toUriSpec(WebClient::put);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UriSpec patch() {
|
||||
return toUriSpec(WebClient::patch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UriSpec delete() {
|
||||
return toUriSpec(WebClient::delete);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UriSpec options() {
|
||||
return toUriSpec(WebClient::options);
|
||||
}
|
||||
|
||||
private UriSpec toUriSpec(Function<WebClient, WebClient.UriSpec> function) {
|
||||
return new DefaultUriSpec(function.apply(this.webClient));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public WebTestClient filter(ExchangeFilterFunction filter) {
|
||||
return new DefaultWebTestClient(this, filter);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private class DefaultUriSpec implements UriSpec {
|
||||
|
||||
private final WebClient.UriSpec uriSpec;
|
||||
|
||||
|
||||
DefaultUriSpec(WebClient.UriSpec spec) {
|
||||
this.uriSpec = spec;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HeaderSpec uri(URI uri) {
|
||||
return new DefaultHeaderSpec(this.uriSpec.uri(uri));
|
||||
}
|
||||
|
||||
@Override
|
||||
public HeaderSpec uri(String uriTemplate, Object... uriVariables) {
|
||||
return new DefaultHeaderSpec(this.uriSpec.uri(uriTemplate, uriVariables));
|
||||
}
|
||||
|
||||
@Override
|
||||
public HeaderSpec uri(String uriTemplate, Map<String, ?> uriVariables) {
|
||||
return new DefaultHeaderSpec(this.uriSpec.uri(uriTemplate, uriVariables));
|
||||
}
|
||||
|
||||
@Override
|
||||
public HeaderSpec uri(Function<UriBuilder, URI> uriBuilder) {
|
||||
return new DefaultHeaderSpec(this.uriSpec.uri(uriBuilder));
|
||||
}
|
||||
}
|
||||
|
||||
private class DefaultHeaderSpec implements WebTestClient.HeaderSpec {
|
||||
|
||||
private final WebClient.HeaderSpec headerSpec;
|
||||
|
||||
private final String requestId;
|
||||
|
||||
|
||||
DefaultHeaderSpec(WebClient.HeaderSpec spec) {
|
||||
this.headerSpec = spec;
|
||||
this.requestId = connectorListener.registerRequestId(spec);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public DefaultHeaderSpec header(String headerName, String... headerValues) {
|
||||
this.headerSpec.header(headerName, headerValues);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultHeaderSpec headers(HttpHeaders headers) {
|
||||
this.headerSpec.headers(headers);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultHeaderSpec accept(MediaType... acceptableMediaTypes) {
|
||||
this.headerSpec.accept(acceptableMediaTypes);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultHeaderSpec acceptCharset(Charset... acceptableCharsets) {
|
||||
this.headerSpec.acceptCharset(acceptableCharsets);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultHeaderSpec contentType(MediaType contentType) {
|
||||
this.headerSpec.contentType(contentType);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultHeaderSpec contentLength(long contentLength) {
|
||||
this.headerSpec.contentLength(contentLength);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultHeaderSpec cookie(String name, String value) {
|
||||
this.headerSpec.cookie(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultHeaderSpec cookies(MultiValueMap<String, String> cookies) {
|
||||
this.headerSpec.cookies(cookies);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultHeaderSpec ifModifiedSince(ZonedDateTime ifModifiedSince) {
|
||||
this.headerSpec.ifModifiedSince(ifModifiedSince);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultHeaderSpec ifNoneMatch(String... ifNoneMatches) {
|
||||
this.headerSpec.ifNoneMatch(ifNoneMatches);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ExchangeActions exchange() {
|
||||
return getExchangeActions(this.headerSpec.exchange());
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> ExchangeActions exchange(BodyInserter<T, ? super ClientHttpRequest> inserter) {
|
||||
return getExchangeActions(this.headerSpec.exchange(inserter));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T, S extends Publisher<T>> ExchangeActions exchange(S publisher, Class<T> elementClass) {
|
||||
return getExchangeActions(this.headerSpec.exchange(publisher, elementClass));
|
||||
}
|
||||
|
||||
private ExchangeActions getExchangeActions(Mono<ClientResponse> responseMono) {
|
||||
ClientResponse response = responseMono.block(getTimeout());
|
||||
ExchangeInfo info = getExchangeInfo(response);
|
||||
return new ExchangeActions(info);
|
||||
}
|
||||
|
||||
private ExchangeInfo getExchangeInfo(ClientResponse clientResponse) {
|
||||
WiretapConnector.Info wiretapInfo = connectorListener.retrieveRequest(this.requestId);
|
||||
ClientHttpRequest request = wiretapInfo.getRequest();
|
||||
return new ExchangeInfo(request.getMethod(), request.getURI(), request.getHeaders(),
|
||||
clientResponse, getTimeout());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static class WiretapConnectorListener implements Consumer<WiretapConnector.Info> {
|
||||
|
||||
private static final String REQUEST_ID_HEADER_NAME = "request-id";
|
||||
|
||||
|
||||
private final AtomicLong index = new AtomicLong();
|
||||
|
||||
private final Map<String, WiretapConnector.Info> exchanges = new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
public String registerRequestId(WebClient.HeaderSpec headerSpec) {
|
||||
String requestId = String.valueOf(this.index.incrementAndGet());
|
||||
headerSpec.header(REQUEST_ID_HEADER_NAME, requestId);
|
||||
return requestId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(WiretapConnector.Info info) {
|
||||
Optional.ofNullable(info.getRequest().getHeaders().getFirst(REQUEST_ID_HEADER_NAME))
|
||||
.ifPresent(id -> this.exchanges.put(id, info));
|
||||
}
|
||||
|
||||
public WiretapConnector.Info retrieveRequest(String requestId) {
|
||||
WiretapConnector.Info info = this.exchanges.remove(requestId);
|
||||
Assert.notNull(info, "No match for \"request-id\"=" + requestId);
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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.time.Duration;
|
||||
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link WebTestClient.Builder}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
class DefaultWebTestClientBuilder implements WebTestClient.Builder {
|
||||
|
||||
private final WebClient.Builder webClientBuilder;
|
||||
|
||||
private final ClientHttpConnector connector;
|
||||
|
||||
private Duration responseTimeout;
|
||||
|
||||
|
||||
public DefaultWebTestClientBuilder(WebClient.Builder builder, ClientHttpConnector connector) {
|
||||
this.webClientBuilder = builder;
|
||||
this.connector = connector;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public WebTestClient.Builder responseTimeout(Duration timeout) {
|
||||
this.responseTimeout = timeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebTestClient build() {
|
||||
return new DefaultWebTestClient(this.webClientBuilder, this.connector, this.responseTimeout);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* 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.nio.ByteBuffer;
|
||||
import java.time.Duration;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.test.util.AssertionErrors;
|
||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
|
||||
/**
|
||||
* An API to apply assertion and other actions against a performed exchange.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public final class ExchangeActions {
|
||||
|
||||
private final ExchangeInfo exchangeInfo;
|
||||
|
||||
|
||||
public ExchangeActions(ExchangeInfo info) {
|
||||
this.exchangeInfo = info;
|
||||
}
|
||||
|
||||
|
||||
private ClientResponse getResponse() {
|
||||
return this.exchangeInfo.getResponse();
|
||||
}
|
||||
|
||||
private Duration getResponseTimeout() {
|
||||
return this.exchangeInfo.getResponseTimeout();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Assert the status of the response.
|
||||
* @return further options for asserting the status of the response
|
||||
*/
|
||||
public ResponseStatusAssertions assertStatus() {
|
||||
return new ResponseStatusAssertions(this, this.exchangeInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert specific, commonly used response headers.
|
||||
* @return further options for asserting headers
|
||||
*/
|
||||
public ResponseHeadersAssertions assertHeaders() {
|
||||
return new ResponseHeadersAssertions(this, getResponse().headers().asHttpHeaders());
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert options for any response header specified by name.
|
||||
* @return options for asserting headers
|
||||
*/
|
||||
public StringMultiValueMapEntryAssertions assertHeader(String headerName) {
|
||||
HttpHeaders headers = getResponse().headers().asHttpHeaders();
|
||||
return new StringMultiValueMapEntryAssertions(this, headerName, headers, "Response header");
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response is empty.
|
||||
*/
|
||||
public void assertNoContent() {
|
||||
Flux<?> body = getResponse().bodyToFlux(ByteBuffer.class);
|
||||
StepVerifier.create(body).expectComplete().verify(getResponseTimeout());
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the content of the response.
|
||||
* @param entityType the type of entity to decode the response as
|
||||
* @param <T> the type of entity
|
||||
* @return further options for asserting response entities
|
||||
*/
|
||||
public <T> ResponseContentAssertions<T> assertEntity(Class<T> entityType) {
|
||||
return new ResponseContentAssertions<T>(this.exchangeInfo, ResolvableType.forClass(entityType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Variant of {@link #assertEntity(Class)} with a {@link ResolvableType}.
|
||||
* @param entityType the type of entity to decode the response as
|
||||
* @return further options for asserting response entities
|
||||
*/
|
||||
public <T> ResponseContentAssertions<T> assertEntity(ResolvableType entityType) {
|
||||
return new ResponseContentAssertions<T>(this.exchangeInfo, entityType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log debug information about the exchange.
|
||||
*/
|
||||
public LoggingExchangeConsumer log() {
|
||||
return new LoggingExchangeConsumer(this, this.exchangeInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply custom assertions on the performed exchange with the help of
|
||||
* {@link AssertionErrors} or an assertion library such as AssertJ.
|
||||
* <p>Consider using statically imported methods to improve readability
|
||||
* @param consumer consumer that will apply assertions.
|
||||
*/
|
||||
public ExchangeActions andAssert(Consumer<ExchangeInfo> consumer) {
|
||||
consumer.accept(this.exchangeInfo);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply custom actions on the performed exchange.
|
||||
* <p>Consider using statically imported methods to improve readability
|
||||
* @param consumer consumer that will apply the custom action
|
||||
*/
|
||||
public ExchangeActions andDo(Consumer<ExchangeInfo> consumer) {
|
||||
consumer.accept(this.exchangeInfo);
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 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.time.Duration;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
|
||||
/**
|
||||
* Contains information about a performed exchange.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class ExchangeInfo {
|
||||
|
||||
private final HttpMethod method;
|
||||
|
||||
private final URI url;
|
||||
|
||||
private final HttpHeaders requestHeaders;
|
||||
|
||||
private final ClientResponse response;
|
||||
|
||||
private final Duration responseTimeout;
|
||||
|
||||
|
||||
public ExchangeInfo(HttpMethod httpMethod, URI uri, HttpHeaders requestHeaders,
|
||||
ClientResponse response, Duration responseTimeout) {
|
||||
|
||||
this.method = httpMethod;
|
||||
this.url = uri;
|
||||
this.requestHeaders = requestHeaders;
|
||||
this.response = response;
|
||||
this.responseTimeout = responseTimeout;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the HTTP method of the exchange.
|
||||
*/
|
||||
public HttpMethod getHttpMethod() {
|
||||
return this.method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the URI of the exchange.
|
||||
*/
|
||||
public URI getUrl() {
|
||||
return this.url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the request headers of the exchange.
|
||||
*/
|
||||
public HttpHeaders getRequestHeaders() {
|
||||
return this.requestHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link ClientResponse} for the exchange.
|
||||
*/
|
||||
public ClientResponse getResponse() {
|
||||
return this.response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured timeout for blocking on response data.
|
||||
*/
|
||||
public Duration getResponseTimeout() {
|
||||
return this.responseTimeout;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
HttpStatus status = getResponse().statusCode();
|
||||
return "\n\n" +
|
||||
formatValue("Request", getHttpMethod() + " " + getUrl()) +
|
||||
formatValue("Status", status + " " + status.getReasonPhrase()) +
|
||||
formatHeading("Response Headers") +
|
||||
formatHeaders(getResponse().headers().asHttpHeaders()) +
|
||||
formatHeading("Request Headers") +
|
||||
formatHeaders(getRequestHeaders());
|
||||
}
|
||||
|
||||
private String formatHeading(String heading) {
|
||||
return "\n" + String.format("%s", heading) + "\n";
|
||||
}
|
||||
|
||||
private String formatValue(String label, Object value) {
|
||||
return String.format("%18s: %s", label, value) + "\n";
|
||||
}
|
||||
|
||||
private String formatHeaders(HttpHeaders headers) {
|
||||
return headers.entrySet().stream()
|
||||
.map(entry -> formatValue(entry.getKey(), entry.getValue()))
|
||||
.collect(Collectors.joining());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* 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.Optional;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.MonoProcessor;
|
||||
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.http.HttpCookie;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
import org.springframework.http.client.reactive.ClientHttpRequest;
|
||||
import org.springframework.http.client.reactive.ClientHttpResponse;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.mock.http.client.reactive.MockClientHttpRequest;
|
||||
import org.springframework.mock.http.client.reactive.MockClientHttpResponse;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpResponse;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
/**
|
||||
* Connector that handles requests by invoking an {@link HttpHandler} rather
|
||||
* than making actual requests to a network socket.
|
||||
*
|
||||
* <p>Internally the connector uses and adapts<br>
|
||||
* {@link MockClientHttpRequest} and {@link MockClientHttpResponse} to<br>
|
||||
* {@link MockServerHttpRequest} and {@link MockServerHttpResponse}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class HttpHandlerConnector implements ClientHttpConnector {
|
||||
|
||||
private static Log logger = LogFactory.getLog(HttpHandlerConnector.class);
|
||||
|
||||
|
||||
private final HttpHandler handler;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor with the {@link HttpHandler} to handle requests with.
|
||||
*/
|
||||
public HttpHandlerConnector(HttpHandler handler) {
|
||||
Assert.notNull(handler, "HttpHandler is required");
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<ClientHttpResponse> connect(HttpMethod httpMethod, URI uri,
|
||||
Function<? super ClientHttpRequest, Mono<Void>> requestCallback) {
|
||||
|
||||
MonoProcessor<ClientHttpResponse> result = MonoProcessor.create();
|
||||
|
||||
MockClientHttpRequest mockClientRequest = new MockClientHttpRequest(httpMethod, uri);
|
||||
MockServerHttpResponse mockServerResponse = new MockServerHttpResponse();
|
||||
|
||||
mockClientRequest.setWriteHandler(requestBody -> {
|
||||
log("Invoking HttpHandler for ", httpMethod, uri);
|
||||
ServerHttpRequest mockServerRequest = adaptRequest(mockClientRequest, requestBody);
|
||||
this.handler.handle(mockServerRequest, mockServerResponse).subscribe(aVoid -> {}, result::onError);
|
||||
return Mono.empty();
|
||||
});
|
||||
|
||||
mockServerResponse.setWriteHandler(responseBody -> {
|
||||
log("Creating client response for ", httpMethod, uri);
|
||||
result.onNext(adaptResponse(mockServerResponse, responseBody));
|
||||
return Mono.empty();
|
||||
});
|
||||
|
||||
log("Writing client request for ", httpMethod, uri);
|
||||
requestCallback.apply(mockClientRequest).subscribe(aVoid -> {}, result::onError);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void log(String message, HttpMethod httpMethod, URI uri) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(String.format("%s %s \"%s\"", message, httpMethod, uri));
|
||||
}
|
||||
}
|
||||
|
||||
private ServerHttpRequest adaptRequest(MockClientHttpRequest request, Publisher<DataBuffer> body) {
|
||||
HttpMethod method = request.getMethod();
|
||||
URI uri = request.getURI();
|
||||
HttpHeaders headers = request.getHeaders();
|
||||
MultiValueMap<String, HttpCookie> cookies = request.getCookies();
|
||||
return MockServerHttpRequest.method(method, uri).headers(headers).cookies(cookies).body(body);
|
||||
}
|
||||
|
||||
private ClientHttpResponse adaptResponse(MockServerHttpResponse response, Flux<DataBuffer> body) {
|
||||
HttpStatus status = Optional.ofNullable(response.getStatusCode()).orElse(HttpStatus.OK);
|
||||
MockClientHttpResponse clientResponse = new MockClientHttpResponse(status);
|
||||
clientResponse.getHeaders().putAll(response.getHeaders());
|
||||
clientResponse.getCookies().putAll(response.getCookies());
|
||||
clientResponse.setBody(body);
|
||||
return clientResponse;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* 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.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.Writer;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
/**
|
||||
* Provides options for logging information about the performed exchange.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class LoggingExchangeConsumer {
|
||||
|
||||
private static Log logger = LogFactory.getLog(LoggingExchangeConsumer.class);
|
||||
|
||||
|
||||
private final ExchangeActions exchangeActions;
|
||||
|
||||
private final ExchangeInfo exchangeInfo;
|
||||
|
||||
|
||||
public LoggingExchangeConsumer(ExchangeActions exchangeActions, ExchangeInfo exchangeInfo) {
|
||||
this.exchangeActions = exchangeActions;
|
||||
this.exchangeInfo = exchangeInfo;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Log with {@link System#out}.
|
||||
*/
|
||||
public ExchangeActions toConsole() {
|
||||
System.out.println(this.exchangeInfo.toString());
|
||||
return this.exchangeActions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log with a given {@link OutputStream}.
|
||||
*/
|
||||
public ExchangeActions toOutputStream(OutputStream stream) {
|
||||
return toWriter(new PrintWriter(stream, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Log with a given {@link Writer}.
|
||||
*/
|
||||
public ExchangeActions toWriter(Writer writer) {
|
||||
try {
|
||||
writer.write(this.exchangeInfo.toString());
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalStateException("Failed to print exchange info", ex);
|
||||
}
|
||||
return this.exchangeActions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log if TRACE level logging is enabled.
|
||||
*/
|
||||
public ExchangeActions ifTraceEnabled() {
|
||||
return doLog(Log::isTraceEnabled, Log::trace);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log if DEBUG level logging is enabled.
|
||||
*/
|
||||
public ExchangeActions ifDebugEnabled() {
|
||||
return doLog(Log::isDebugEnabled, Log::debug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log if INFO level logging is enabled.
|
||||
*/
|
||||
public ExchangeActions ifInfoEnabled() {
|
||||
return doLog(Log::isInfoEnabled, Log::info);
|
||||
}
|
||||
|
||||
private ExchangeActions doLog(Predicate<Log> logLevelPredicate, BiConsumer<Log, String> logAction) {
|
||||
if (logLevelPredicate.test(logger)) {
|
||||
logAction.accept(logger, this.exchangeInfo.toString());
|
||||
}
|
||||
return this.exchangeActions;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
import static org.springframework.test.util.AssertionErrors.assertEquals;
|
||||
import static org.springframework.test.util.AssertionErrors.assertTrue;
|
||||
import static org.springframework.test.util.AssertionErrors.fail;
|
||||
|
||||
/**
|
||||
* Assertions on the values of a {@link MultiValueMap} entry.
|
||||
*
|
||||
* @param <V> the type of values in the map.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class MultiValueMapEntryAssertions<V> {
|
||||
|
||||
private final ExchangeActions exchangeActions;
|
||||
|
||||
private final String name;
|
||||
|
||||
private final MultiValueMap<String, V> map;
|
||||
|
||||
private final String errorMessagePrefix;
|
||||
|
||||
|
||||
MultiValueMapEntryAssertions(ExchangeActions actions, String name,
|
||||
MultiValueMap<String, V> map, String errorMessagePrefix) {
|
||||
|
||||
this.exchangeActions = actions;
|
||||
this.name = name;
|
||||
this.map = map;
|
||||
this.errorMessagePrefix = errorMessagePrefix + " " + this.name;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The given values are equal to the actual values.
|
||||
* @param values the values to match
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public ExchangeActions isEqualTo(V... values) {
|
||||
List<V> actual = this.map.get(this.name);
|
||||
assertEquals(this.errorMessagePrefix, Arrays.asList(values), actual);
|
||||
return this.exchangeActions;
|
||||
}
|
||||
|
||||
/**
|
||||
* The list of actual values contains the given values.
|
||||
* @param values the values to match
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public ExchangeActions hasValues(V... values) {
|
||||
List<V> actual = this.map.get(this.name);
|
||||
List<V> expected = Arrays.asList(values);
|
||||
String message = getErrorMessagePrefix() + " does not contain " + expected;
|
||||
assertTrue(message, actual.containsAll(expected));
|
||||
return this.exchangeActions;
|
||||
}
|
||||
|
||||
|
||||
// Protected methods for sub-classes
|
||||
|
||||
protected ExchangeActions getExchangeActions() {
|
||||
return this.exchangeActions;
|
||||
}
|
||||
|
||||
protected String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
protected MultiValueMap<String, V> getMap() {
|
||||
return this.map;
|
||||
}
|
||||
|
||||
protected String getErrorMessagePrefix() {
|
||||
return this.errorMessagePrefix;
|
||||
}
|
||||
|
||||
protected V getValue(int index) {
|
||||
List<V> actualValues = getMap().get(getName());
|
||||
if (actualValues == null || index >= actualValues.size()) {
|
||||
fail(getErrorMessagePrefix() + " does not have values at index[" + index + "]: " + actualValues);
|
||||
}
|
||||
return actualValues.get(index);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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 reactor.core.publisher.Flux;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
|
||||
import static org.springframework.web.reactive.function.BodyExtractors.toFlux;
|
||||
|
||||
/**
|
||||
* Provides options for asserting the content of the response.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class ResponseContentAssertions<T> extends ResponseEntityAssertions<T> {
|
||||
|
||||
|
||||
ResponseContentAssertions(ExchangeInfo exchangeInfo, ResolvableType entityType) {
|
||||
super(exchangeInfo, entityType);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Assert the response as a collection of entities.
|
||||
*/
|
||||
public ResponseEntityCollectionAssertions<T> collection() {
|
||||
return new ResponseEntityCollectionAssertions<>(getExchangeInfo(), getEntityType());
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response using a {@link StepVerifier}.
|
||||
*/
|
||||
public StepVerifier.FirstStep<T> stepVerifier() {
|
||||
Flux<T> flux = getExchangeInfo().getResponse().body(toFlux(getEntityType()));
|
||||
return StepVerifier.create(flux);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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.util.function.Consumer;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.test.util.AssertionErrors;
|
||||
|
||||
import static org.springframework.test.util.AssertionErrors.assertEquals;
|
||||
import static org.springframework.web.reactive.function.BodyExtractors.toMono;
|
||||
|
||||
/**
|
||||
* Provides methods for asserting the response body as a single entity.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class ResponseEntityAssertions<T> {
|
||||
|
||||
private final ExchangeInfo exchangeInfo;
|
||||
|
||||
private final ResolvableType entityType;
|
||||
|
||||
private T entity;
|
||||
|
||||
|
||||
ResponseEntityAssertions(ExchangeInfo info, ResolvableType entityType) {
|
||||
this.exchangeInfo = info;
|
||||
this.entityType = entityType;
|
||||
}
|
||||
|
||||
|
||||
protected ExchangeInfo getExchangeInfo() {
|
||||
return this.exchangeInfo;
|
||||
}
|
||||
|
||||
protected ResolvableType getEntityType() {
|
||||
return this.entityType;
|
||||
}
|
||||
|
||||
private T resolveEntity() {
|
||||
if (this.entity == null) {
|
||||
Mono<T> mono = this.exchangeInfo.getResponse().body(toMono(entityType));
|
||||
this.entity = mono.block(this.exchangeInfo.getResponseTimeout());
|
||||
}
|
||||
return this.entity;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Assert the response entity is equal to the given expected entity.
|
||||
*/
|
||||
public ResponseEntityAssertions<T> isEqualTo(T expected) {
|
||||
assertEquals("Response body", expected, resolveEntity());
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response entity is not equal to the given expected entity.
|
||||
*/
|
||||
public ResponseEntityAssertions<T> isNotEqualTo(T expected) {
|
||||
assertEquals("Response body", expected, resolveEntity());
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply custom assertions on the response entity with the help of
|
||||
* {@link AssertionErrors} or an assertion library such as AssertJ.
|
||||
* <p>Consider using statically imported methods for creating the assertion
|
||||
* consumer to improve readability of tests.
|
||||
* @param assertionConsumer consumer that will apply assertions.
|
||||
*/
|
||||
public ResponseEntityAssertions<T> andAssert(Consumer<T> assertionConsumer) {
|
||||
assertionConsumer.accept(resolveEntity());
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply custom actions on the response entity collection.
|
||||
* <p>Consider using statically imported methods for creating the assertion
|
||||
* consumer to improve readability of tests.
|
||||
* @param consumer consumer that will apply the custom action
|
||||
*/
|
||||
public ResponseEntityAssertions<T> andDo(Consumer<T> consumer) {
|
||||
consumer.accept(resolveEntity());
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 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.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.test.util.AssertionErrors;
|
||||
|
||||
import static org.springframework.test.util.AssertionErrors.assertEquals;
|
||||
import static org.springframework.test.util.AssertionErrors.assertTrue;
|
||||
import static org.springframework.web.reactive.function.BodyExtractors.toFlux;
|
||||
|
||||
/**
|
||||
* Provides methods for asserting the response body as a collection of entities.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class ResponseEntityCollectionAssertions<T> {
|
||||
|
||||
private final Collection<T> entities;
|
||||
|
||||
|
||||
ResponseEntityCollectionAssertions(ExchangeInfo info, ResolvableType entityType) {
|
||||
Flux<T> flux = info.getResponse().body(toFlux(entityType));
|
||||
this.entities = flux.collectList().block(info.getResponseTimeout());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Assert the number of entities.
|
||||
*/
|
||||
public ResponseEntityCollectionAssertions<T> hasSize(int size) {
|
||||
assertEquals("Response entities count", size, this.entities.size());
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response contains all of the given entities.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public ResponseEntityCollectionAssertions<T> contains(T... entities) {
|
||||
Arrays.stream(entities).forEach(entity ->
|
||||
assertTrue("Response does not contain " + entity, this.entities.contains(entity)));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response does not contain any of the given entities.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public ResponseEntityCollectionAssertions<T> doesNotContain(T... entities) {
|
||||
Arrays.stream(entities).forEach(entity ->
|
||||
assertTrue("Response should not contain " + entity, !this.entities.contains(entity)));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply custom assertions on the response entity collection with the help of
|
||||
* {@link AssertionErrors} or an assertion library such as AssertJ.
|
||||
* <p>Consider using statically imported methods for creating the assertion
|
||||
* consumer to improve readability of tests.
|
||||
* @param assertionConsumer consumer that will apply assertions.
|
||||
*/
|
||||
public ResponseEntityCollectionAssertions<T> andAssert(Consumer<Collection<T>> assertionConsumer) {
|
||||
assertionConsumer.accept(this.entities);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply custom actions on the response entity collection.
|
||||
* <p>Consider using statically imported methods for creating the assertion
|
||||
* consumer to improve readability of tests.
|
||||
* @param consumer consumer that will apply the custom action
|
||||
*/
|
||||
public ResponseEntityCollectionAssertions<T> andDo(Consumer<Collection<T>> consumer) {
|
||||
consumer.accept(this.entities);
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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 org.springframework.http.CacheControl;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import static org.springframework.test.util.AssertionErrors.assertEquals;
|
||||
|
||||
/**
|
||||
* Provides methods for asserting specific, commonly used response headers.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class ResponseHeadersAssertions {
|
||||
|
||||
private final ExchangeActions exchangeActions;
|
||||
|
||||
private final HttpHeaders headers;
|
||||
|
||||
|
||||
ResponseHeadersAssertions(ExchangeActions actions, HttpHeaders headers) {
|
||||
this.exchangeActions = actions;
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
|
||||
public ExchangeActions cacheControl(CacheControl cacheControl) {
|
||||
String actual = this.headers.getCacheControl();
|
||||
assertEquals("Response header Cache-Control", cacheControl.getHeaderValue(), actual);
|
||||
return this.exchangeActions;
|
||||
}
|
||||
|
||||
public ExchangeActions contentDisposition(ContentDisposition contentDisposition) {
|
||||
ContentDisposition actual = this.headers.getContentDisposition();
|
||||
assertEquals("Response header Content-Disposition", contentDisposition, actual);
|
||||
return this.exchangeActions;
|
||||
}
|
||||
|
||||
public ExchangeActions contentLength(long contentLength) {
|
||||
long actual = this.headers.getContentLength();
|
||||
assertEquals("Response header Content-Length", contentLength, actual);
|
||||
return this.exchangeActions;
|
||||
}
|
||||
|
||||
public ExchangeActions contentType(MediaType mediaType) {
|
||||
MediaType actual = this.headers.getContentType();
|
||||
assertEquals("Response header Content-Type", mediaType, actual);
|
||||
return this.exchangeActions;
|
||||
}
|
||||
|
||||
public ExchangeActions expires(int expires) {
|
||||
long actual = this.headers.getExpires();
|
||||
assertEquals("Response header Expires", expires, actual);
|
||||
return this.exchangeActions;
|
||||
}
|
||||
|
||||
public ExchangeActions lastModified(int lastModified) {
|
||||
long actual = this.headers.getLastModified();
|
||||
assertEquals("Response header Last-Modified", lastModified, actual);
|
||||
return this.exchangeActions;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,594 @@
|
||||
/*
|
||||
* 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 org.springframework.http.HttpStatus;
|
||||
|
||||
import static org.springframework.test.util.AssertionErrors.assertEquals;
|
||||
|
||||
/**
|
||||
* Provides methods for asserting the response status.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class ResponseStatusAssertions {
|
||||
|
||||
private final ExchangeActions exchangeActions;
|
||||
|
||||
private final HttpStatus httpStatus;
|
||||
|
||||
|
||||
ResponseStatusAssertions(ExchangeActions actions, ExchangeInfo info) {
|
||||
this.exchangeActions = actions;
|
||||
this.httpStatus = info.getResponse().statusCode();
|
||||
}
|
||||
|
||||
|
||||
public ExchangeActions is(int status) {
|
||||
assertEquals("Response status", status, this.httpStatus.value());
|
||||
return this.exchangeActions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is in the 1xx range.
|
||||
*/
|
||||
public ExchangeActions is1xxInformational() {
|
||||
String message = "Range for response status value " + this.httpStatus;
|
||||
assertEquals(message, HttpStatus.Series.INFORMATIONAL, this.httpStatus.series());
|
||||
return this.exchangeActions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is in the 2xx range.
|
||||
*/
|
||||
public ExchangeActions is2xxSuccessful() {
|
||||
String message = "Range for response status value " + this.httpStatus;
|
||||
assertEquals(message, HttpStatus.Series.SUCCESSFUL, this.httpStatus.series());
|
||||
return this.exchangeActions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is in the 3xx range.
|
||||
*/
|
||||
public ExchangeActions is3xxRedirection() {
|
||||
String message = "Range for response status value " + this.httpStatus;
|
||||
assertEquals(message, HttpStatus.Series.REDIRECTION, this.httpStatus.series());
|
||||
return this.exchangeActions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is in the 4xx range.
|
||||
*/
|
||||
public ExchangeActions is4xxClientError() {
|
||||
String message = "Range for response status value " + this.httpStatus;
|
||||
assertEquals(message, HttpStatus.Series.CLIENT_ERROR, this.httpStatus.series());
|
||||
return this.exchangeActions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is in the 5xx range.
|
||||
*/
|
||||
public ExchangeActions is5xxServerError() {
|
||||
String message = "Range for response status value " + this.httpStatus;
|
||||
assertEquals(message, HttpStatus.Series.SERVER_ERROR, this.httpStatus.series());
|
||||
return this.exchangeActions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response error message.
|
||||
*/
|
||||
public ExchangeActions reason(String reason) {
|
||||
assertEquals("Response status reason", reason, this.httpStatus.getReasonPhrase());
|
||||
return this.exchangeActions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.CONTINUE} (100).
|
||||
*/
|
||||
public ExchangeActions isContinue() {
|
||||
return doMatch(HttpStatus.CONTINUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.SWITCHING_PROTOCOLS} (101).
|
||||
*/
|
||||
public ExchangeActions isSwitchingProtocols() {
|
||||
return doMatch(HttpStatus.SWITCHING_PROTOCOLS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.PROCESSING} (102).
|
||||
*/
|
||||
public ExchangeActions isProcessing() {
|
||||
return doMatch(HttpStatus.PROCESSING);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.CHECKPOINT} (103).
|
||||
*/
|
||||
public ExchangeActions isCheckpoint() {
|
||||
return doMatch(HttpStatus.valueOf(103));
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.OK} (200).
|
||||
*/
|
||||
public ExchangeActions isOk() {
|
||||
return doMatch(HttpStatus.OK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.CREATED} (201).
|
||||
*/
|
||||
public ExchangeActions isCreated() {
|
||||
return doMatch(HttpStatus.CREATED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.ACCEPTED} (202).
|
||||
*/
|
||||
public ExchangeActions isAccepted() {
|
||||
return doMatch(HttpStatus.ACCEPTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.NON_AUTHORITATIVE_INFORMATION} (203).
|
||||
*/
|
||||
public ExchangeActions isNonAuthoritativeInformation() {
|
||||
return doMatch(HttpStatus.NON_AUTHORITATIVE_INFORMATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.NO_CONTENT} (204).
|
||||
*/
|
||||
public ExchangeActions isNoContent() {
|
||||
return doMatch(HttpStatus.NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.RESET_CONTENT} (205).
|
||||
*/
|
||||
public ExchangeActions isResetContent() {
|
||||
return doMatch(HttpStatus.RESET_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.PARTIAL_CONTENT} (206).
|
||||
*/
|
||||
public ExchangeActions isPartialContent() {
|
||||
return doMatch(HttpStatus.PARTIAL_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.MULTI_STATUS} (207).
|
||||
*/
|
||||
public ExchangeActions isMultiStatus() {
|
||||
return doMatch(HttpStatus.MULTI_STATUS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.ALREADY_REPORTED} (208).
|
||||
*/
|
||||
public ExchangeActions isAlreadyReported() {
|
||||
return doMatch(HttpStatus.ALREADY_REPORTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.IM_USED} (226).
|
||||
*/
|
||||
public ExchangeActions isImUsed() {
|
||||
return doMatch(HttpStatus.IM_USED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.MULTIPLE_CHOICES} (300).
|
||||
*/
|
||||
public ExchangeActions isMultipleChoices() {
|
||||
return doMatch(HttpStatus.MULTIPLE_CHOICES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.MOVED_PERMANENTLY} (301).
|
||||
*/
|
||||
public ExchangeActions isMovedPermanently() {
|
||||
return doMatch(HttpStatus.MOVED_PERMANENTLY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.FOUND} (302).
|
||||
*/
|
||||
public ExchangeActions isFound() {
|
||||
return doMatch(HttpStatus.FOUND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.MOVED_TEMPORARILY} (302).
|
||||
* @see #isFound()
|
||||
* @deprecated in favor of {@link #isFound()}
|
||||
*/
|
||||
@Deprecated
|
||||
public ExchangeActions isMovedTemporarily() {
|
||||
return doMatch(HttpStatus.MOVED_TEMPORARILY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.SEE_OTHER} (303).
|
||||
*/
|
||||
public ExchangeActions isSeeOther() {
|
||||
return doMatch(HttpStatus.SEE_OTHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.NOT_MODIFIED} (304).
|
||||
*/
|
||||
public ExchangeActions isNotModified() {
|
||||
return doMatch(HttpStatus.NOT_MODIFIED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.USE_PROXY} (305).
|
||||
* @deprecated matching the deprecation of {@code HttpStatus.USE_PROXY}
|
||||
*/
|
||||
@Deprecated
|
||||
public ExchangeActions isUseProxy() {
|
||||
return doMatch(HttpStatus.USE_PROXY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.TEMPORARY_REDIRECT} (307).
|
||||
*/
|
||||
public ExchangeActions isTemporaryRedirect() {
|
||||
return doMatch(HttpStatus.TEMPORARY_REDIRECT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.PERMANENT_REDIRECT} (308).
|
||||
*/
|
||||
public ExchangeActions isPermanentRedirect() {
|
||||
return doMatch(HttpStatus.valueOf(308));
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.BAD_REQUEST} (400).
|
||||
*/
|
||||
public ExchangeActions isBadRequest() {
|
||||
return doMatch(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.UNAUTHORIZED} (401).
|
||||
*/
|
||||
public ExchangeActions isUnauthorized() {
|
||||
return doMatch(HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.PAYMENT_REQUIRED} (402).
|
||||
*/
|
||||
public ExchangeActions isPaymentRequired() {
|
||||
return doMatch(HttpStatus.PAYMENT_REQUIRED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.FORBIDDEN} (403).
|
||||
*/
|
||||
public ExchangeActions isForbidden() {
|
||||
return doMatch(HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.NOT_FOUND} (404).
|
||||
*/
|
||||
public ExchangeActions isNotFound() {
|
||||
return doMatch(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.METHOD_NOT_ALLOWED} (405).
|
||||
*/
|
||||
public ExchangeActions isMethodNotAllowed() {
|
||||
return doMatch(HttpStatus.METHOD_NOT_ALLOWED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.NOT_ACCEPTABLE} (406).
|
||||
*/
|
||||
public ExchangeActions isNotAcceptable() {
|
||||
return doMatch(HttpStatus.NOT_ACCEPTABLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.PROXY_AUTHENTICATION_REQUIRED} (407).
|
||||
*/
|
||||
public ExchangeActions isProxyAuthenticationRequired() {
|
||||
return doMatch(HttpStatus.PROXY_AUTHENTICATION_REQUIRED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.REQUEST_TIMEOUT} (408).
|
||||
*/
|
||||
public ExchangeActions isRequestTimeout() {
|
||||
return doMatch(HttpStatus.REQUEST_TIMEOUT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.CONFLICT} (409).
|
||||
*/
|
||||
public ExchangeActions isConflict() {
|
||||
return doMatch(HttpStatus.CONFLICT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.GONE} (410).
|
||||
*/
|
||||
public ExchangeActions isGone() {
|
||||
return doMatch(HttpStatus.GONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.LENGTH_REQUIRED} (411).
|
||||
*/
|
||||
public ExchangeActions isLengthRequired() {
|
||||
return doMatch(HttpStatus.LENGTH_REQUIRED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.PRECONDITION_FAILED} (412).
|
||||
*/
|
||||
public ExchangeActions isPreconditionFailed() {
|
||||
return doMatch(HttpStatus.PRECONDITION_FAILED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.PAYLOAD_TOO_LARGE} (413).
|
||||
* @since 4.1
|
||||
*/
|
||||
public ExchangeActions isPayloadTooLarge() {
|
||||
return doMatch(HttpStatus.PAYLOAD_TOO_LARGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.REQUEST_ENTITY_TOO_LARGE} (413).
|
||||
* @deprecated matching the deprecation of {@code HttpStatus.REQUEST_ENTITY_TOO_LARGE}
|
||||
* @see #isPayloadTooLarge()
|
||||
*/
|
||||
@Deprecated
|
||||
public ExchangeActions isRequestEntityTooLarge() {
|
||||
return doMatch(HttpStatus.REQUEST_ENTITY_TOO_LARGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.REQUEST_URI_TOO_LONG} (414).
|
||||
* @since 4.1
|
||||
*/
|
||||
public ExchangeActions isUriTooLong() {
|
||||
return doMatch(HttpStatus.URI_TOO_LONG);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.REQUEST_URI_TOO_LONG} (414).
|
||||
* @deprecated matching the deprecation of {@code HttpStatus.REQUEST_URI_TOO_LONG}
|
||||
* @see #isUriTooLong()
|
||||
*/
|
||||
@Deprecated
|
||||
public ExchangeActions isRequestUriTooLong() {
|
||||
return doMatch(HttpStatus.REQUEST_URI_TOO_LONG);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.UNSUPPORTED_MEDIA_TYPE} (415).
|
||||
*/
|
||||
public ExchangeActions isUnsupportedMediaType() {
|
||||
return doMatch(HttpStatus.UNSUPPORTED_MEDIA_TYPE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE} (416).
|
||||
*/
|
||||
public ExchangeActions isRequestedRangeNotSatisfiable() {
|
||||
return doMatch(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.EXPECTATION_FAILED} (417).
|
||||
*/
|
||||
public ExchangeActions isExpectationFailed() {
|
||||
return doMatch(HttpStatus.EXPECTATION_FAILED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.I_AM_A_TEAPOT} (418).
|
||||
*/
|
||||
public ExchangeActions isIAmATeapot() {
|
||||
return doMatch(HttpStatus.valueOf(418));
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.INSUFFICIENT_SPACE_ON_RESOURCE} (419).
|
||||
* @deprecated matching the deprecation of {@code HttpStatus.INSUFFICIENT_SPACE_ON_RESOURCE}
|
||||
*/
|
||||
@Deprecated
|
||||
public ExchangeActions isInsufficientSpaceOnResource() {
|
||||
return doMatch(HttpStatus.INSUFFICIENT_SPACE_ON_RESOURCE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.METHOD_FAILURE} (420).
|
||||
* @deprecated matching the deprecation of {@code HttpStatus.METHOD_FAILURE}
|
||||
*/
|
||||
@Deprecated
|
||||
public ExchangeActions isMethodFailure() {
|
||||
return doMatch(HttpStatus.METHOD_FAILURE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.DESTINATION_LOCKED} (421).
|
||||
* @deprecated matching the deprecation of {@code HttpStatus.DESTINATION_LOCKED}
|
||||
*/
|
||||
@Deprecated
|
||||
public ExchangeActions isDestinationLocked() {
|
||||
return doMatch(HttpStatus.DESTINATION_LOCKED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.UNPROCESSABLE_ENTITY} (422).
|
||||
*/
|
||||
public ExchangeActions isUnprocessableEntity() {
|
||||
return doMatch(HttpStatus.UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.LOCKED} (423).
|
||||
*/
|
||||
public ExchangeActions isLocked() {
|
||||
return doMatch(HttpStatus.LOCKED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.FAILED_DEPENDENCY} (424).
|
||||
*/
|
||||
public ExchangeActions isFailedDependency() {
|
||||
return doMatch(HttpStatus.FAILED_DEPENDENCY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.UPGRADE_REQUIRED} (426).
|
||||
*/
|
||||
public ExchangeActions isUpgradeRequired() {
|
||||
return doMatch(HttpStatus.UPGRADE_REQUIRED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.PRECONDITION_REQUIRED} (428).
|
||||
*/
|
||||
public ExchangeActions isPreconditionRequired() {
|
||||
return doMatch(HttpStatus.valueOf(428));
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.TOO_MANY_REQUESTS} (429).
|
||||
*/
|
||||
public ExchangeActions isTooManyRequests() {
|
||||
return doMatch(HttpStatus.valueOf(429));
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE} (431).
|
||||
*/
|
||||
public ExchangeActions isRequestHeaderFieldsTooLarge() {
|
||||
return doMatch(HttpStatus.valueOf(431));
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.UNAVAILABLE_FOR_LEGAL_REASONS} (451).
|
||||
* @since 4.3
|
||||
*/
|
||||
public ExchangeActions isUnavailableForLegalReasons() {
|
||||
return doMatch(HttpStatus.valueOf(451));
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.INTERNAL_SERVER_ERROR} (500).
|
||||
*/
|
||||
public ExchangeActions isInternalServerError() {
|
||||
return doMatch(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.NOT_IMPLEMENTED} (501).
|
||||
*/
|
||||
public ExchangeActions isNotImplemented() {
|
||||
return doMatch(HttpStatus.NOT_IMPLEMENTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.BAD_GATEWAY} (502).
|
||||
*/
|
||||
public ExchangeActions isBadGateway() {
|
||||
return doMatch(HttpStatus.BAD_GATEWAY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.SERVICE_UNAVAILABLE} (503).
|
||||
*/
|
||||
public ExchangeActions isServiceUnavailable() {
|
||||
return doMatch(HttpStatus.SERVICE_UNAVAILABLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.GATEWAY_TIMEOUT} (504).
|
||||
*/
|
||||
public ExchangeActions isGatewayTimeout() {
|
||||
return doMatch(HttpStatus.GATEWAY_TIMEOUT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.HTTP_VERSION_NOT_SUPPORTED} (505).
|
||||
*/
|
||||
public ExchangeActions isHttpVersionNotSupported() {
|
||||
return doMatch(HttpStatus.HTTP_VERSION_NOT_SUPPORTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.VARIANT_ALSO_NEGOTIATES} (506).
|
||||
*/
|
||||
public ExchangeActions isVariantAlsoNegotiates() {
|
||||
return doMatch(HttpStatus.VARIANT_ALSO_NEGOTIATES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.INSUFFICIENT_STORAGE} (507).
|
||||
*/
|
||||
public ExchangeActions isInsufficientStorage() {
|
||||
return doMatch(HttpStatus.INSUFFICIENT_STORAGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.LOOP_DETECTED} (508).
|
||||
*/
|
||||
public ExchangeActions isLoopDetected() {
|
||||
return doMatch(HttpStatus.LOOP_DETECTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.BANDWIDTH_LIMIT_EXCEEDED} (509).
|
||||
*/
|
||||
public ExchangeActions isBandwidthLimitExceeded() {
|
||||
return doMatch(HttpStatus.valueOf(509));
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.NOT_EXTENDED} (510).
|
||||
*/
|
||||
public ExchangeActions isNotExtended() {
|
||||
return doMatch(HttpStatus.NOT_EXTENDED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response status code is {@code HttpStatus.NETWORK_AUTHENTICATION_REQUIRED} (511).
|
||||
*/
|
||||
public ExchangeActions isNetworkAuthenticationRequired() {
|
||||
return doMatch(HttpStatus.valueOf(511));
|
||||
}
|
||||
|
||||
private ExchangeActions doMatch(final HttpStatus status) {
|
||||
assertEquals("Status", status, this.httpStatus);
|
||||
return this.exchangeActions;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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.util.Arrays;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
import static org.springframework.test.util.AssertionErrors.assertTrue;
|
||||
|
||||
/**
|
||||
* Extension of {@link MultiValueMapEntryAssertions} for String values.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class StringMultiValueMapEntryAssertions extends MultiValueMapEntryAssertions<String> {
|
||||
|
||||
|
||||
public StringMultiValueMapEntryAssertions(ExchangeActions actions, String name,
|
||||
MultiValueMap<String, String> map, String errorMessagePrefix) {
|
||||
|
||||
super(actions, name, map, errorMessagePrefix);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The specified value {@link String#contains contains} the given sub-strings.
|
||||
* @param values the values to match
|
||||
*/
|
||||
public ExchangeActions valueContains(CharSequence... values) {
|
||||
String actual = getValue(0);
|
||||
Arrays.stream(values).forEach(value -> {
|
||||
String message = getErrorMessagePrefix() + " does not contain " + value;
|
||||
assertTrue(message, actual.contains(value));
|
||||
});
|
||||
return getExchangeActions();
|
||||
}
|
||||
|
||||
/**
|
||||
* The specified value does not {@link String#contains contain} the given sub-strings.
|
||||
* @param values the values to match
|
||||
*/
|
||||
public ExchangeActions valueDoesNotContain(CharSequence... values) {
|
||||
String actual = getValue(0);
|
||||
Arrays.stream(values).forEach(value -> {
|
||||
String message = getErrorMessagePrefix() + " contains " + value + " but shouldn't";
|
||||
assertTrue(message, !actual.contains(value));
|
||||
});
|
||||
return getExchangeActions();
|
||||
}
|
||||
|
||||
/**
|
||||
* The specified value matches the given regex pattern value.
|
||||
* @param pattern the values to be compiled with {@link Pattern}
|
||||
*/
|
||||
public ExchangeActions valueMatches(String pattern) {
|
||||
String actual = getValue(0);
|
||||
boolean match = Pattern.compile(pattern).matcher(actual).matches();
|
||||
String message = getErrorMessagePrefix() + " with value " + actual + " does not match " + pattern;
|
||||
assertTrue(message, match);
|
||||
return getExchangeActions();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,477 @@
|
||||
/*
|
||||
* 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.nio.charset.Charset;
|
||||
import java.time.Duration;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.format.FormatterRegistry;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.reactive.ClientHttpRequest;
|
||||
import org.springframework.http.codec.HttpMessageReader;
|
||||
import org.springframework.http.codec.HttpMessageWriter;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.validation.Validator;
|
||||
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
|
||||
import org.springframework.web.reactive.config.CorsRegistry;
|
||||
import org.springframework.web.reactive.config.PathMatchConfigurer;
|
||||
import org.springframework.web.reactive.config.ViewResolverRegistry;
|
||||
import org.springframework.web.reactive.config.WebFluxConfigurer;
|
||||
import org.springframework.web.reactive.function.BodyInserter;
|
||||
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
|
||||
import org.springframework.web.reactive.function.client.ExchangeFunction;
|
||||
import org.springframework.web.reactive.function.client.ExchangeStrategies;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||
import org.springframework.web.server.adapter.HttpWebHandlerAdapter;
|
||||
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
|
||||
import org.springframework.web.util.UriBuilder;
|
||||
import org.springframework.web.util.UriBuilderFactory;
|
||||
|
||||
/**
|
||||
* Main entry point for testing WebFlux server endpoints with an API similar to
|
||||
* that of {@link WebClient}, and actually delegating to a {@code WebClient}
|
||||
* instance, but with a focus on testing.
|
||||
*
|
||||
* <p>The {@code WebTestClient} has 3 setup options without a running server:
|
||||
* <ul>
|
||||
* <li>{@link #bindToController}
|
||||
* <li>{@link #bindToApplicationContext}
|
||||
* <li>{@link #bindToRouterFunction}
|
||||
* </ul>
|
||||
* <p>and 1 option for actual requests on a socket:
|
||||
* <ul>
|
||||
* <li>{@link #bindToServer()}
|
||||
* </ul>
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public interface WebTestClient {
|
||||
|
||||
/**
|
||||
* Prepare an HTTP GET request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
UriSpec get();
|
||||
|
||||
/**
|
||||
* Prepare an HTTP HEAD request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
UriSpec head();
|
||||
|
||||
/**
|
||||
* Prepare an HTTP POST request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
UriSpec post();
|
||||
|
||||
/**
|
||||
* Prepare an HTTP PUT request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
UriSpec put();
|
||||
|
||||
/**
|
||||
* Prepare an HTTP PATCH request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
UriSpec patch();
|
||||
|
||||
/**
|
||||
* Prepare an HTTP DELETE request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
UriSpec delete();
|
||||
|
||||
/**
|
||||
* Prepare an HTTP OPTIONS request.
|
||||
* @return a spec for specifying the target URL
|
||||
*/
|
||||
UriSpec options();
|
||||
|
||||
|
||||
/**
|
||||
* Filter the client with the given {@code ExchangeFilterFunction}.
|
||||
* @param filterFunction the filter to apply to this client
|
||||
* @return the filtered client
|
||||
* @see ExchangeFilterFunction#apply(ExchangeFunction)
|
||||
*/
|
||||
WebTestClient filter(ExchangeFilterFunction filterFunction);
|
||||
|
||||
|
||||
// Static, factory methods
|
||||
|
||||
/**
|
||||
* Integration testing without a server, targeting specific annotated,
|
||||
* WebFlux controllers. The default configuration is the same as for
|
||||
* {@link org.springframework.web.reactive.config.EnableWebFlux @EnableWebFlux}
|
||||
* but can also be further customized through the returned spec.
|
||||
* @param controllers the controllers to test
|
||||
* @return spec for controller configuration and test client builder
|
||||
*/
|
||||
static ControllerSpec bindToController(Object... controllers) {
|
||||
return new DefaultControllerSpec(controllers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Integration testing without a server, with WebFlux infrastructure detected
|
||||
* from an {@link ApplicationContext} such as {@code @EnableWebFlux}
|
||||
* Java config and annotated controller Spring beans.
|
||||
* @param applicationContext the context
|
||||
* @return the {@link WebTestClient} builder
|
||||
* @see org.springframework.web.reactive.config.EnableWebFlux
|
||||
*/
|
||||
static WebClientSpec bindToApplicationContext(ApplicationContext applicationContext) {
|
||||
HttpHandler httpHandler = WebHttpHandlerBuilder.applicationContext(applicationContext).build();
|
||||
return new DefaultWebClientSpec(httpHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Integration testing without a server, targeting WebFlux functional endpoints.
|
||||
* @param routerFunction the RouterFunction to test
|
||||
* @return the {@link WebTestClient} builder
|
||||
*/
|
||||
static WebClientSpec bindToRouterFunction(RouterFunction<?> routerFunction) {
|
||||
HttpWebHandlerAdapter httpHandler = RouterFunctions.toHttpHandler(routerFunction);
|
||||
return new DefaultWebClientSpec(httpHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete end-to-end integration tests with actual requests to a running server.
|
||||
* @return the {@link WebTestClient} builder
|
||||
*/
|
||||
static WebClientSpec bindToServer() {
|
||||
return new DefaultWebClientSpec();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Specification for customizing controller configuration equivalent to, and
|
||||
* internally delegating to, a {@link WebFluxConfigurer}.
|
||||
*/
|
||||
interface ControllerSpec {
|
||||
|
||||
/**
|
||||
* Customize content type resolution.
|
||||
* @see WebFluxConfigurer#configureContentTypeResolver
|
||||
*/
|
||||
ControllerSpec contentTypeResolver(Consumer<RequestedContentTypeResolverBuilder> consumer);
|
||||
|
||||
/**
|
||||
* Configure CORS support.
|
||||
* @see WebFluxConfigurer#addCorsMappings
|
||||
*/
|
||||
ControllerSpec corsMappings(Consumer<CorsRegistry> consumer);
|
||||
|
||||
/**
|
||||
* Configure path matching options.
|
||||
* @see WebFluxConfigurer#configurePathMatching
|
||||
*/
|
||||
ControllerSpec pathMatching(Consumer<PathMatchConfigurer> consumer);
|
||||
|
||||
/**
|
||||
* Modify or extend the list of built-in message readers.
|
||||
* @see WebFluxConfigurer#configureMessageReaders
|
||||
*/
|
||||
ControllerSpec messageReaders(Consumer<List<HttpMessageReader<?>>> readers);
|
||||
|
||||
/**
|
||||
* Modify or extend the list of built-in message writers.
|
||||
* @see WebFluxConfigurer#configureMessageWriters
|
||||
*/
|
||||
ControllerSpec messageWriters(Consumer<List<HttpMessageWriter<?>>> writers);
|
||||
|
||||
/**
|
||||
* Register formatters and converters to use for type conversion.
|
||||
* @see WebFluxConfigurer#addFormatters
|
||||
*/
|
||||
ControllerSpec formatters(Consumer<FormatterRegistry> consumer);
|
||||
|
||||
/**
|
||||
* Configure a global Validator.
|
||||
* @see WebFluxConfigurer#getValidator()
|
||||
*/
|
||||
ControllerSpec validator(Validator validator);
|
||||
|
||||
/**
|
||||
* Configure view resolution.
|
||||
* @see WebFluxConfigurer#configureViewResolvers
|
||||
*/
|
||||
ControllerSpec viewResolvers(Consumer<ViewResolverRegistry> consumer);
|
||||
|
||||
/**
|
||||
* Proceed to configure the {@link WebClient} to test with.
|
||||
*/
|
||||
WebClientSpec webClientSpec();
|
||||
|
||||
/**
|
||||
* Shortcut to build the {@link WebTestClient}.
|
||||
*/
|
||||
WebTestClient build();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Steps for customizing the {@link WebClient} used to test with
|
||||
* internally delegating to a {@link WebClient.Builder}.
|
||||
*/
|
||||
interface WebClientSpec {
|
||||
|
||||
/**
|
||||
* Configure a base URI as described in
|
||||
* {@link org.springframework.web.reactive.function.client.WebClient#create(String)
|
||||
* WebClient.create(String)}.
|
||||
* @see #defaultUriVariables(Map)
|
||||
* @see #uriBuilderFactory(UriBuilderFactory)
|
||||
*/
|
||||
WebClientSpec baseUrl(String baseUrl);
|
||||
|
||||
/**
|
||||
* Configure default URI variable values that will be used when expanding
|
||||
* URI templates using a {@link Map}.
|
||||
* @param defaultUriVariables the default values to use
|
||||
* @see #baseUrl(String)
|
||||
* @see #uriBuilderFactory(UriBuilderFactory)
|
||||
*/
|
||||
WebClientSpec defaultUriVariables(Map<String, ?> defaultUriVariables);
|
||||
|
||||
/**
|
||||
* Provide a pre-configured {@link UriBuilderFactory} instance. This is
|
||||
* an alternative to and effectively overrides the following:
|
||||
* <ul>
|
||||
* <li>{@link #baseUrl(String)}
|
||||
* <li>{@link #defaultUriVariables(Map)}.
|
||||
* </ul>
|
||||
* @param uriBuilderFactory the URI builder factory to use
|
||||
* @see #baseUrl(String)
|
||||
* @see #defaultUriVariables(Map)
|
||||
*/
|
||||
WebClientSpec uriBuilderFactory(UriBuilderFactory uriBuilderFactory);
|
||||
|
||||
/**
|
||||
* Add the given header to all requests that haven't added it.
|
||||
* @param headerName the header name
|
||||
* @param headerValues the header values
|
||||
*/
|
||||
WebClientSpec defaultHeader(String headerName, String... headerValues);
|
||||
|
||||
/**
|
||||
* Add the given header to all requests that haven't added it.
|
||||
* @param cookieName the cookie name
|
||||
* @param cookieValues the cookie values
|
||||
*/
|
||||
WebClientSpec defaultCookie(String cookieName, String... cookieValues);
|
||||
|
||||
/**
|
||||
* Configure the {@link ExchangeStrategies} to use.
|
||||
* <p>By default {@link ExchangeStrategies#withDefaults()} is used.
|
||||
* @param strategies the strategies to use
|
||||
*/
|
||||
WebClientSpec exchangeStrategies(ExchangeStrategies strategies);
|
||||
|
||||
/**
|
||||
* Proceed to building the {@link WebTestClient}.
|
||||
*/
|
||||
Builder builder();
|
||||
|
||||
/**
|
||||
* Shortcut to build the {@link WebTestClient}.
|
||||
*/
|
||||
WebTestClient build();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Build steps to create a {@link WebTestClient}.
|
||||
*/
|
||||
interface Builder {
|
||||
|
||||
/**
|
||||
* Max amount of time to wait for responses.
|
||||
* <p>By default 5 seconds.
|
||||
* @param timeout the response timeout value
|
||||
*/
|
||||
Builder responseTimeout(Duration timeout);
|
||||
|
||||
|
||||
/**
|
||||
* Build the {@link WebTestClient} instance.
|
||||
*/
|
||||
WebTestClient build();
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Contract for specifying the URI for a request.
|
||||
*/
|
||||
interface UriSpec {
|
||||
|
||||
/**
|
||||
* Specify the URI using an absolute, fully constructed {@link URI}.
|
||||
*/
|
||||
HeaderSpec uri(URI uri);
|
||||
|
||||
/**
|
||||
* Specify the URI for the request using a URI template and URI variables.
|
||||
* If a {@link UriBuilderFactory} was configured for the client (e.g.
|
||||
* with a base URI) it will be used to expand the URI template.
|
||||
*/
|
||||
HeaderSpec uri(String uri, Object... uriVariables);
|
||||
|
||||
/**
|
||||
* Specify the URI for the request using a URI template and URI variables.
|
||||
* If a {@link UriBuilderFactory} was configured for the client (e.g.
|
||||
* with a base URI) it will be used to expand the URI template.
|
||||
*/
|
||||
HeaderSpec uri(String uri, Map<String, ?> uriVariables);
|
||||
|
||||
/**
|
||||
* Build the URI for the request with a {@link UriBuilder} obtained
|
||||
* through the {@link UriBuilderFactory} configured for this client.
|
||||
*/
|
||||
HeaderSpec uri(Function<UriBuilder, URI> uriFunction);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Contract for specifying request headers leading up to the exchange.
|
||||
*/
|
||||
interface HeaderSpec {
|
||||
|
||||
/**
|
||||
* Set the list of acceptable {@linkplain MediaType media types}, as
|
||||
* specified by the {@code Accept} header.
|
||||
* @param acceptableMediaTypes the acceptable media types
|
||||
* @return this builder
|
||||
*/
|
||||
HeaderSpec accept(MediaType... acceptableMediaTypes);
|
||||
|
||||
/**
|
||||
* Set the list of acceptable {@linkplain Charset charsets}, as specified
|
||||
* by the {@code Accept-Charset} header.
|
||||
* @param acceptableCharsets the acceptable charsets
|
||||
* @return this builder
|
||||
*/
|
||||
HeaderSpec acceptCharset(Charset... acceptableCharsets);
|
||||
|
||||
/**
|
||||
* Set the length of the body in bytes, as specified by the
|
||||
* {@code Content-Length} header.
|
||||
* @param contentLength the content length
|
||||
* @return this builder
|
||||
* @see HttpHeaders#setContentLength(long)
|
||||
*/
|
||||
HeaderSpec contentLength(long contentLength);
|
||||
|
||||
/**
|
||||
* Set the {@linkplain MediaType media type} of the body, as specified
|
||||
* by the {@code Content-Type} header.
|
||||
* @param contentType the content type
|
||||
* @return this builder
|
||||
* @see HttpHeaders#setContentType(MediaType)
|
||||
*/
|
||||
HeaderSpec contentType(MediaType contentType);
|
||||
|
||||
/**
|
||||
* Add a cookie with the given name and value.
|
||||
* @param name the cookie name
|
||||
* @param value the cookie value
|
||||
* @return this builder
|
||||
*/
|
||||
HeaderSpec cookie(String name, String value);
|
||||
|
||||
/**
|
||||
* Copy the given cookies into the entity's cookies map.
|
||||
*
|
||||
* @param cookies the existing cookies to copy from
|
||||
* @return this builder
|
||||
*/
|
||||
HeaderSpec cookies(MultiValueMap<String, String> cookies);
|
||||
|
||||
/**
|
||||
* Set the value of the {@code If-Modified-Since} header.
|
||||
* <p>The date should be specified as the number of milliseconds since
|
||||
* January 1, 1970 GMT.
|
||||
* @param ifModifiedSince the new value of the header
|
||||
* @return this builder
|
||||
*/
|
||||
HeaderSpec ifModifiedSince(ZonedDateTime ifModifiedSince);
|
||||
|
||||
/**
|
||||
* Set the values of the {@code If-None-Match} header.
|
||||
* @param ifNoneMatches the new value of the header
|
||||
* @return this builder
|
||||
*/
|
||||
HeaderSpec ifNoneMatch(String... ifNoneMatches);
|
||||
|
||||
/**
|
||||
* Add the given, single header value under the given name.
|
||||
* @param headerName the header name
|
||||
* @param headerValues the header value(s)
|
||||
* @return this builder
|
||||
*/
|
||||
HeaderSpec header(String headerName, String... headerValues);
|
||||
|
||||
/**
|
||||
* Copy the given headers into the entity's headers map.
|
||||
* @param headers the existing headers to copy from
|
||||
* @return this builder
|
||||
*/
|
||||
HeaderSpec headers(HttpHeaders headers);
|
||||
|
||||
/**
|
||||
* Perform the request without a request body.
|
||||
* @return options for asserting the response with
|
||||
*/
|
||||
ExchangeActions exchange();
|
||||
|
||||
/**
|
||||
* Set the body of the request to the given {@code BodyInserter} and
|
||||
* perform the request.
|
||||
* @param inserter the {@code BodyInserter} that writes to the request
|
||||
* @param <T> the type contained in the body
|
||||
* @return options for asserting the response with
|
||||
*/
|
||||
<T> ExchangeActions exchange(BodyInserter<T, ? super ClientHttpRequest> inserter);
|
||||
|
||||
/**
|
||||
* Set the body of the request to the given {@code Publisher} and
|
||||
* perform the request.
|
||||
* @param publisher the {@code Publisher} to write to the request
|
||||
* @param elementClass the class of elements contained in the publisher
|
||||
* @param <T> the type of the elements contained in the publisher
|
||||
* @param <S> the type of the {@code Publisher}
|
||||
* @return options for asserting the response with
|
||||
*/
|
||||
<T, S extends Publisher<T>> ExchangeActions exchange(S publisher, Class<T> elementClass);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* 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.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 ClientHttpRequest request;
|
||||
|
||||
private final ClientHttpResponse response;
|
||||
|
||||
|
||||
public Info(ClientHttpRequest request, ClientHttpResponse response) {
|
||||
this.request = request;
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
|
||||
public ClientHttpRequest getRequest() {
|
||||
return this.request;
|
||||
}
|
||||
|
||||
public ClientHttpResponse getResponse() {
|
||||
return this.response;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Support for testing Spring WebFlux server endpoints via
|
||||
* {@link org.springframework.test.web.reactive.server.WebTestClient}.
|
||||
*/
|
||||
package org.springframework.test.web.reactive.server;
|
||||
Reference in New Issue
Block a user