/* * 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 java.util.function.UnaryOperator; import org.reactivestreams.Publisher; import org.springframework.context.ApplicationContext; import org.springframework.core.ResolvableType; 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.ServerCodecConfigurer; 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.result.method.annotation.ArgumentResolverConfigurer; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; 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<RequestHeadersSpec<?>> get(); /** * Prepare an HTTP HEAD request. * @return a spec for specifying the target URL */ UriSpec<RequestHeadersSpec<?>> head(); /** * Prepare an HTTP POST request. * @return a spec for specifying the target URL */ UriSpec<RequestBodySpec> post(); /** * Prepare an HTTP PUT request. * @return a spec for specifying the target URL */ UriSpec<RequestBodySpec> put(); /** * Prepare an HTTP PATCH request. * @return a spec for specifying the target URL */ UriSpec<RequestBodySpec> patch(); /** * Prepare an HTTP DELETE request. * @return a spec for specifying the target URL */ UriSpec<RequestHeadersSpec<?>> delete(); /** * Prepare an HTTP OPTIONS request. * @return a spec for specifying the target URL */ UriSpec<RequestHeadersSpec<?>> 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); /** * Filter the client applying the given transformation function on the * {@code ServerWebExchange} to every request. * <p><strong>Note:</strong> this option is applicable only when testing * without an actual running server. * @param mutator the transformation function * @return the filtered client */ WebTestClient exchangeMutator(UnaryOperator<ServerWebExchange> mutator); // 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 setting up controller configuration */ 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 MockServerSpec<?> bindToApplicationContext(ApplicationContext applicationContext) { return new ApplicationContextSpec(applicationContext); } /** * Integration testing without a server targeting WebFlux functional endpoints. * @param routerFunction the RouterFunction to test * @return the {@link WebTestClient} builder */ static MockServerSpec<?> bindToRouterFunction(RouterFunction<?> routerFunction) { return new RouterFunctionSpec(routerFunction); } /** * Integration testing without a server targeting the given HttpHandler. * @param httpHandler the handler to test * @return the {@link WebTestClient} builder */ static Builder bindToHttpHandler(HttpHandler httpHandler) { return new DefaultWebTestClientBuilder(httpHandler, null); } /** * Complete end-to-end integration tests with actual requests to a running server. * @return the {@link WebTestClient} builder */ static Builder bindToServer() { return new DefaultWebTestClientBuilder(); } /** * Base specification for setting up tests without a server. */ interface MockServerSpec<B extends MockServerSpec<B>> { /** * Configure a transformation function on {@code ServerWebExchange} to * be applied at the start of server-side, request processing. * @param mutator the transforming function. * @see ServerWebExchange#mutate() */ <T extends B> T exchangeMutator(UnaryOperator<ServerWebExchange> mutator); /** * Configure {@link WebFilter}'s for server request processing. * @param filter one or more filters */ <T extends B> T webFilter(WebFilter... filter); /** * Proceed to configure and build the test client. */ Builder configureClient(); /** * Shortcut to build the test client. */ WebTestClient build(); } /** * Specification for customizing controller configuration equivalent to, and * internally delegating to, a {@link WebFluxConfigurer}. */ interface ControllerSpec extends MockServerSpec<ControllerSpec> { /** * Register one or more * {@link org.springframework.web.bind.annotation.ControllerAdvice * ControllerAdvice} instances to be used in tests. */ ControllerSpec controllerAdvice(Object... controllerAdvice); /** * 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); /** * Configure resolvers for custom controller method arguments. * @see WebFluxConfigurer#configureHttpMessageCodecs */ ControllerSpec argumentResolvers(Consumer<ArgumentResolverConfigurer> configurer); /** * Configure custom HTTP message readers and writers or override built-in ones. * @see WebFluxConfigurer#configureHttpMessageCodecs */ ControllerSpec httpMessageCodecs(Consumer<ServerCodecConfigurer> configurer); /** * 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); } /** * Steps for customizing the {@link WebClient} used to test with * internally delegating to a {@link WebClient.Builder}. */ interface Builder { /** * Configure a base URI as described in * {@link org.springframework.web.reactive.function.client.WebClient#create(String) * WebClient.create(String)}. */ Builder baseUrl(String baseUrl); /** * Provide a pre-configured {@link UriBuilderFactory} instance as an * alternative to and effectively overriding {@link #baseUrl(String)}. */ Builder 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 */ Builder 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 */ Builder 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 */ Builder exchangeStrategies(ExchangeStrategies strategies); /** * 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(); } /** * Specification for providing the URI of a request. */ interface UriSpec<S extends RequestHeadersSpec<?>> { /** * Specify the URI using an absolute, fully constructed {@link URI}. * @return spec to add headers or perform the exchange */ S 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. * @return spec to add headers or perform the exchange */ S 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. * @return spec to add headers or perform the exchange */ S 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. * @return spec to add headers or perform the exchange */ S uri(Function<UriBuilder, URI> uriFunction); } /** * Specification for adding request headers and performing an exchange. */ interface RequestHeadersSpec<S extends RequestHeadersSpec<S>> { /** * Set the list of acceptable {@linkplain MediaType media types}, as * specified by the {@code Accept} header. * @param acceptableMediaTypes the acceptable media types * @return the same instance */ S 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 the same instance */ S acceptCharset(Charset... acceptableCharsets); /** * Add a cookie with the given name and value. * @param name the cookie name * @param value the cookie value * @return the same instance */ S cookie(String name, String value); /** * Copy the given cookies into the entity's cookies map. * * @param cookies the existing cookies to copy from * @return the same instance */ S 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 the same instance */ S ifModifiedSince(ZonedDateTime ifModifiedSince); /** * Set the values of the {@code If-None-Match} header. * @param ifNoneMatches the new value of the header * @return the same instance */ S 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 the same instance */ S header(String headerName, String... headerValues); /** * Copy the given headers into the entity's headers map. * @param headers the existing headers to copy from * @return the same instance */ S headers(HttpHeaders headers); /** * Perform the exchange without a request body. * @return spec for decoding the response */ ResponseSpec exchange(); } interface RequestBodySpec extends RequestHeadersSpec<RequestBodySpec> { /** * Set the length of the body in bytes, as specified by the * {@code Content-Length} header. * @param contentLength the content length * @return the same instance * @see HttpHeaders#setContentLength(long) */ RequestBodySpec 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 the same instance * @see HttpHeaders#setContentType(MediaType) */ RequestBodySpec contentType(MediaType contentType); /** * Set the body of the request to the given {@code BodyInserter}. * @param inserter the inserter * @return spec for decoding the response * @see org.springframework.web.reactive.function.BodyInserters */ RequestHeadersSpec<?> body(BodyInserter<?, ? super ClientHttpRequest> inserter); /** * Set the body of the request to the given asynchronous {@code Publisher}. * @param publisher the request body data * @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 spec for decoding the response */ <T, S extends Publisher<T>> RequestHeadersSpec<?> body(S publisher, Class<T> elementClass); /** * Set the body of the request to the given synchronous {@code Object} and * perform the request. * @param body the {@code Object} to write to the request * @return a {@code Mono} with the response */ RequestHeadersSpec<?> syncBody(Object body); } /** * Spec for declaring expectations on the response. */ interface ResponseSpec { /** * Declare expectations on the response status. */ StatusAssertions expectStatus(); /** * Declared expectations on the headers of the response. */ HeaderAssertions expectHeader(); /** * Declare expectations on the response body decoded to {@code <B>}. * @param bodyType the expected body type */ <B> BodySpec<B, ?> expectBody(Class<B> bodyType); /** * Variant of {@link #expectBody(Class)} for a body type with generics. */ <B> BodySpec<B, ?> expectBody(ResolvableType bodyType); /** * Declare expectations on the response body decoded to {@code List<E>}. * @param elementType the expected List element type */ <E> ListBodySpec<E> expectBodyList(Class<E> elementType); /** * Variant of {@link #expectBodyList(Class)} for element types with generics. */ <E> ListBodySpec<E> expectBodyList(ResolvableType elementType); /** * Declare expectations on the response body content. */ BodyContentSpec expectBody(); /** * Return the exchange result with the body decoded to {@code Flux<T>}. * Use this option for infinite streams and consume the stream with * the {@code StepVerifier} from the Reactor Add-Ons. * * @see <a href="https://github.com/reactor/reactor-addons"> * https://github.com/reactor/reactor-addons</a> */ <T> FluxExchangeResult<T> returnResult(Class<T> elementType); /** * Variant of {@link #returnResult(Class)} for element types with generics. */ <T> FluxExchangeResult<T> returnResult(ResolvableType elementType); } /** * Spec for expectations on the response body decoded to a single Object. */ interface BodySpec<B, S extends BodySpec<B, S>> { /** * Assert the extracted body is equal to the given value. */ <T extends S> T isEqualTo(B expected); /** * Assert the extracted body with the given {@link Consumer}. */ <T extends S> T consumeWith(Consumer<B> consumer); /** * Return the exchange result with the decoded body. */ EntityExchangeResult<B> returnResult(); } /** * Spec for expectations on the response body decoded to a List. */ interface ListBodySpec<E> extends BodySpec<List<E>, ListBodySpec<E>> { /** * Assert the extracted list of values is of the given size. * @param size the expected size */ ListBodySpec<E> hasSize(int size); /** * Assert the extracted list of values contains the given elements. * @param elements the elements to check */ @SuppressWarnings("unchecked") ListBodySpec<E> contains(E... elements); /** * Assert the extracted list of values doesn't contain the given elements. * @param elements the elements to check */ @SuppressWarnings("unchecked") ListBodySpec<E> doesNotContain(E... elements); } /** * Spec for expectations on the response body content. */ interface BodyContentSpec { /** * Assert the response body is empty and return the exchange result. */ EntityExchangeResult<Void> isEmpty(); /** * Parse the expected and actual response content as JSON and perform a * "lenient" comparison verifying the same attribute-value pairs. * <p>Use of this option requires the * <a href="http://jsonassert.skyscreamer.org/">JSONassert<a/> library * on to be on the classpath. * @param expectedJson the expected JSON content. */ BodyContentSpec json(String expectedJson); /** * Access to response body assertions using a * <a href="https://github.com/jayway/JsonPath">JsonPath</a> expression * to inspect a specific subset of the body. * <p>The JSON path expression can be a parameterized string using * formatting specifiers as defined in {@link String#format}. * @param expression the JsonPath expression * @param args arguments to parameterize the expression */ JsonPathAssertions jsonPath(String expression, Object... args); /** * Assert the response body content converted to a String with the given * {@link Consumer}. The String is created with the {@link Charset} from * the "content-type" response header or {@code UTF-8} otherwise. * @param consumer the consumer for the response body; the input String * may be {@code null} if there was no response body. */ BodyContentSpec consumeAsStringWith(Consumer<String> consumer); /** * Assert the response body content with the given {@link Consumer}. * @param consumer the consumer for the response body; the input * {@code byte[]} may be {@code null} if there was no response body. */ BodyContentSpec consumeWith(Consumer<byte[]> consumer); /** * Return the exchange result with body content as {@code byte[]}. */ EntityExchangeResult<byte[]> returnResult(); } }