27 changed files with 3414 additions and 2 deletions
@ -0,0 +1,199 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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; |
||||
@ -0,0 +1,66 @@
@@ -0,0 +1,66 @@
|
||||
/* |
||||
* 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.samples; |
||||
|
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
|
||||
import org.springframework.test.web.reactive.server.WebTestClient; |
||||
import org.springframework.web.bind.annotation.GetMapping; |
||||
import org.springframework.web.bind.annotation.RestController; |
||||
|
||||
/** |
||||
* Tests with error status codes or error conditions. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
*/ |
||||
@SuppressWarnings("unused") |
||||
public class ErrorTests { |
||||
|
||||
private WebTestClient client; |
||||
|
||||
|
||||
@Before |
||||
public void setUp() throws Exception { |
||||
this.client = WebTestClient.bindToController(new TestController()).build(); |
||||
} |
||||
|
||||
|
||||
@Test |
||||
public void notFound() throws Exception { |
||||
this.client.get().uri("/invalid") |
||||
.exchange() |
||||
.assertStatus().isNotFound(); |
||||
} |
||||
|
||||
@Test |
||||
public void serverException() throws Exception { |
||||
this.client.get().uri("/server-error") |
||||
.exchange() |
||||
.assertStatus().isInternalServerError(); |
||||
} |
||||
|
||||
|
||||
@RestController |
||||
static class TestController { |
||||
|
||||
@GetMapping("/server-error") |
||||
void handleAndThrowException() { |
||||
throw new IllegalStateException("server error"); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,63 @@
@@ -0,0 +1,63 @@
|
||||
/* |
||||
* 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.samples; |
||||
|
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
|
||||
import org.springframework.http.ResponseEntity; |
||||
import org.springframework.test.web.reactive.server.WebTestClient; |
||||
import org.springframework.web.bind.annotation.GetMapping; |
||||
import org.springframework.web.bind.annotation.RequestHeader; |
||||
import org.springframework.web.bind.annotation.RestController; |
||||
|
||||
/** |
||||
* Tests with custom headers. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
*/ |
||||
@SuppressWarnings("unused") |
||||
public class HeaderTests { |
||||
|
||||
private WebTestClient client; |
||||
|
||||
|
||||
@Before |
||||
public void setUp() throws Exception { |
||||
this.client = WebTestClient.bindToController(new TestController()).build(); |
||||
} |
||||
|
||||
|
||||
@Test |
||||
public void customHeader() throws Exception { |
||||
this.client.get().uri("/header").header("h1", "ping") |
||||
.exchange() |
||||
.assertStatus().isOk() |
||||
.assertHeader("h1").isEqualTo("ping-pong"); |
||||
} |
||||
|
||||
|
||||
@RestController |
||||
static class TestController { |
||||
|
||||
@GetMapping("header") |
||||
ResponseEntity<Void> handleHeader(@RequestHeader("h1") String myHeader) { |
||||
String value = myHeader + "-pong"; |
||||
return ResponseEntity.ok().header("h1", value).build(); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,159 @@
@@ -0,0 +1,159 @@
|
||||
/* |
||||
* 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.samples; |
||||
|
||||
import java.net.URI; |
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator; |
||||
import com.fasterxml.jackson.annotation.JsonProperty; |
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
import reactor.core.publisher.Flux; |
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.http.ResponseEntity; |
||||
import org.springframework.test.web.reactive.server.WebTestClient; |
||||
import org.springframework.web.bind.annotation.GetMapping; |
||||
import org.springframework.web.bind.annotation.PathVariable; |
||||
import org.springframework.web.bind.annotation.PostMapping; |
||||
import org.springframework.web.bind.annotation.RequestBody; |
||||
import org.springframework.web.bind.annotation.RequestMapping; |
||||
import org.springframework.web.bind.annotation.RestController; |
||||
|
||||
import static org.hamcrest.CoreMatchers.endsWith; |
||||
import static org.junit.Assert.assertThat; |
||||
import static org.springframework.http.MediaType.TEXT_EVENT_STREAM; |
||||
|
||||
/** |
||||
* Annotated controllers accepting and returning typed Objects. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
*/ |
||||
@SuppressWarnings("unused") |
||||
public class ResponseEntityTests { |
||||
|
||||
private WebTestClient client; |
||||
|
||||
|
||||
@Before |
||||
public void setUp() throws Exception { |
||||
this.client = WebTestClient.bindToController(new PersonController()).build(); |
||||
} |
||||
|
||||
|
||||
@Test |
||||
public void entity() throws Exception { |
||||
this.client.get().uri("/persons/John") |
||||
.exchange() |
||||
.assertStatus().isOk() |
||||
.assertHeaders().contentType(MediaType.APPLICATION_JSON_UTF8) |
||||
.assertEntity(Person.class).isEqualTo(new Person("John")); |
||||
} |
||||
|
||||
@Test |
||||
public void entityCollection() throws Exception { |
||||
this.client.get().uri("/persons") |
||||
.exchange() |
||||
.assertStatus().isOk() |
||||
.assertHeaders().contentType(MediaType.APPLICATION_JSON_UTF8) |
||||
.assertEntity(Person.class).collection() |
||||
.hasSize(3) |
||||
.contains(new Person("Jane"), new Person("Jason"), new Person("John")); |
||||
} |
||||
|
||||
@Test |
||||
public void entityStream() throws Exception { |
||||
this.client.get().uri("/persons").accept(TEXT_EVENT_STREAM) |
||||
.exchange() |
||||
.assertStatus().isOk() |
||||
.assertHeaders().contentType(TEXT_EVENT_STREAM) |
||||
.assertEntity(Person.class).stepVerifier() |
||||
.expectNext(new Person("N0"), new Person("N1"), new Person("N2")) |
||||
.expectNextCount(4) |
||||
.consumeNextWith(person -> assertThat(person.getName(), endsWith("7"))) |
||||
.thenCancel() |
||||
.verify(); |
||||
} |
||||
|
||||
@Test |
||||
public void saveEntity() throws Exception { |
||||
this.client.post().uri("/persons") |
||||
.exchange(Mono.just(new Person("John")), Person.class) |
||||
.assertStatus().isCreated() |
||||
.assertHeader("location").isEqualTo("/persons/John") |
||||
.assertNoContent(); |
||||
} |
||||
|
||||
|
||||
@RestController |
||||
@RequestMapping("/persons") |
||||
static class PersonController { |
||||
|
||||
@GetMapping("/{name}") |
||||
Person getPerson(@PathVariable String name) { |
||||
return new Person(name); |
||||
} |
||||
|
||||
@GetMapping |
||||
Flux<Person> getPersons() { |
||||
return Flux.just(new Person("Jane"), new Person("Jason"), new Person("John")); |
||||
} |
||||
|
||||
@GetMapping(produces = "text/event-stream") |
||||
Flux<Person> getPersonStream() { |
||||
return Flux.intervalMillis(100).onBackpressureBuffer(10).map(index -> new Person("N" + index)); |
||||
} |
||||
|
||||
@PostMapping |
||||
ResponseEntity<String> savePerson(@RequestBody Person person) { |
||||
return ResponseEntity.created(URI.create("/persons/" + person.getName())).build(); |
||||
} |
||||
} |
||||
|
||||
static class Person { |
||||
|
||||
private final String name; |
||||
|
||||
@JsonCreator |
||||
public Person(@JsonProperty("name") String name) { |
||||
this.name = name; |
||||
} |
||||
|
||||
public String getName() { |
||||
return this.name; |
||||
} |
||||
|
||||
@Override |
||||
public boolean equals(Object other) { |
||||
if (this == other) return true; |
||||
if (other == null || getClass() != other.getClass()) return false; |
||||
Person person = (Person) other; |
||||
return getName().equals(person.getName()); |
||||
} |
||||
|
||||
@Override |
||||
public int hashCode() { |
||||
return getName().hashCode(); |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return "Person[name='" + name + "']"; |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,79 @@
@@ -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.samples.bind; |
||||
|
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.test.web.reactive.server.WebTestClient; |
||||
import org.springframework.web.bind.annotation.GetMapping; |
||||
import org.springframework.web.bind.annotation.RestController; |
||||
import org.springframework.web.reactive.config.EnableWebFlux; |
||||
|
||||
/** |
||||
* Binding to server infrastructure declared in a Spring ApplicationContext. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
*/ |
||||
@SuppressWarnings("unused") |
||||
public class ApplicationContextTests { |
||||
|
||||
private WebTestClient client; |
||||
|
||||
|
||||
@Before |
||||
public void setUp() throws Exception { |
||||
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); |
||||
context.register(WebConfig.class); |
||||
context.refresh(); |
||||
|
||||
this.client = WebTestClient.bindToApplicationContext(context).build(); |
||||
} |
||||
|
||||
@Test |
||||
public void test() throws Exception { |
||||
this.client.get().uri("/test") |
||||
.exchange() |
||||
.assertStatus().isOk() |
||||
.assertEntity(String .class).isEqualTo("It works!"); |
||||
} |
||||
|
||||
|
||||
@Configuration |
||||
@EnableWebFlux |
||||
static class WebConfig { |
||||
|
||||
@Bean |
||||
public TestController controller() { |
||||
return new TestController(); |
||||
} |
||||
|
||||
} |
||||
|
||||
@RestController |
||||
static class TestController { |
||||
|
||||
@GetMapping("/test") |
||||
public String handle() { |
||||
return "It works!"; |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,59 @@
@@ -0,0 +1,59 @@
|
||||
/* |
||||
* 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.samples.bind; |
||||
|
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
|
||||
import org.springframework.test.web.reactive.server.WebTestClient; |
||||
import org.springframework.web.bind.annotation.GetMapping; |
||||
import org.springframework.web.bind.annotation.RestController; |
||||
|
||||
/** |
||||
* Bind to annotated controllers. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
*/ |
||||
@SuppressWarnings("unused") |
||||
public class ControllerTests { |
||||
|
||||
private WebTestClient client; |
||||
|
||||
|
||||
@Before |
||||
public void setUp() throws Exception { |
||||
this.client = WebTestClient.bindToController(new TestController()).build(); |
||||
} |
||||
|
||||
|
||||
@Test |
||||
public void test() throws Exception { |
||||
this.client.get().uri("/test") |
||||
.exchange() |
||||
.assertStatus().isOk() |
||||
.assertEntity(String .class).isEqualTo("It works!"); |
||||
} |
||||
|
||||
|
||||
@RestController |
||||
static class TestController { |
||||
|
||||
@GetMapping("/test") |
||||
public String handle() { |
||||
return "It works!"; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,75 @@
@@ -0,0 +1,75 @@
|
||||
/* |
||||
* 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.samples.bind; |
||||
|
||||
import org.junit.After; |
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.http.server.reactive.HttpHandler; |
||||
import org.springframework.http.server.reactive.bootstrap.ReactorHttpServer; |
||||
import org.springframework.test.web.reactive.server.WebTestClient; |
||||
import org.springframework.web.reactive.function.server.RouterFunctions; |
||||
import org.springframework.web.reactive.function.server.ServerResponse; |
||||
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.GET; |
||||
import static org.springframework.web.reactive.function.server.RouterFunctions.route; |
||||
|
||||
/** |
||||
* Bind to a running server, making actual requests over a socket. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
*/ |
||||
public class HttpServerTests { |
||||
|
||||
private ReactorHttpServer server; |
||||
|
||||
private WebTestClient client; |
||||
|
||||
|
||||
@Before |
||||
public void setUp() throws Exception { |
||||
|
||||
HttpHandler httpHandler = RouterFunctions.toHttpHandler( |
||||
route(GET("/test"), request -> |
||||
ServerResponse.ok().body(Mono.just("It works!"), String.class))); |
||||
|
||||
this.server = new ReactorHttpServer(); |
||||
this.server.setHandler(httpHandler); |
||||
this.server.afterPropertiesSet(); |
||||
this.server.start(); |
||||
|
||||
this.client = WebTestClient.bindToServer() |
||||
.baseUrl("http://localhost:" + this.server.getPort()) |
||||
.build(); |
||||
} |
||||
|
||||
@After |
||||
public void tearDown() throws Exception { |
||||
this.server.stop(); |
||||
} |
||||
|
||||
|
||||
@Test |
||||
public void test() throws Exception { |
||||
this.client.get().uri("/test") |
||||
.exchange() |
||||
.assertStatus().isOk() |
||||
.assertEntity(String .class).isEqualTo("It works!"); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,56 @@
@@ -0,0 +1,56 @@
|
||||
/* |
||||
* 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.samples.bind; |
||||
|
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.test.web.reactive.server.WebTestClient; |
||||
import org.springframework.web.reactive.function.server.RouterFunction; |
||||
import org.springframework.web.reactive.function.server.ServerResponse; |
||||
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.GET; |
||||
import static org.springframework.web.reactive.function.server.RouterFunctions.route; |
||||
|
||||
/** |
||||
* Bind to a {@link RouterFunction} and functional endpoints. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
*/ |
||||
public class RouterFunctionTests { |
||||
|
||||
private WebTestClient testClient; |
||||
|
||||
|
||||
@Before |
||||
public void setUp() throws Exception { |
||||
|
||||
RouterFunction<?> route = route(GET("/test"), request -> |
||||
ServerResponse.ok().body(Mono.just("It works!"), String.class)); |
||||
|
||||
this.testClient = WebTestClient.bindToRouterFunction(route).build(); |
||||
} |
||||
|
||||
@Test |
||||
public void test() throws Exception { |
||||
this.testClient.get().uri("/test") |
||||
.exchange() |
||||
.assertStatus().isOk() |
||||
.assertEntity(String .class).isEqualTo("It works!"); |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue