Add WebTestClientConfigurer

Issue: SPR-15674
This commit is contained in:
Rossen Stoyanchev
2017-06-23 14:45:46 -04:00
parent 4db0ce12e1
commit 8fc3b3bc37
11 changed files with 260 additions and 253 deletions

View File

@@ -18,7 +18,6 @@ package org.springframework.test.web.reactive.server;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.springframework.web.server.WebFilter;
@@ -61,7 +60,7 @@ abstract class AbstractMockServerSpec<B extends WebTestClient.MockServerSpec<B>>
WebHttpHandlerBuilder builder = initHttpHandlerBuilder();
builder.filters(theFilters -> theFilters.addAll(0, this.filters));
this.configurers.forEach(configurer -> configurer.beforeServerCreated(builder));
return new DefaultWebTestClientBuilder(builder.build());
return new DefaultWebTestClientBuilder(builder);
}
/**

View File

@@ -71,15 +71,18 @@ class DefaultWebTestClient implements WebTestClient {
private final Duration timeout;
private final WebTestClient.Builder builder;
private final AtomicLong requestIndex = new AtomicLong();
DefaultWebTestClient(WebClient.Builder clientBuilder, ClientHttpConnector connector,
@Nullable Duration timeout) {
@Nullable Duration timeout, WebTestClient.Builder webTestClientBuilder) {
Assert.notNull(clientBuilder, "WebClient.Builder is required");
this.wiretapConnector = new WiretapConnector(connector);
this.webClient = clientBuilder.clientConnector(this.wiretapConnector).build();
this.timeout = (timeout != null ? timeout : Duration.ofSeconds(5));
this.builder = webTestClientBuilder;
}
@@ -125,8 +128,7 @@ class DefaultWebTestClient implements WebTestClient {
@Override
public Builder mutate() {
return new DefaultWebTestClientBuilder(this.wiretapConnector.getDelegate(),
this.webClient.mutate(), this.timeout);
return this.builder;
}
private <S extends RequestHeadersSpec<?>> UriSpec<S> toUriSpec(

View File

@@ -23,12 +23,13 @@ import java.util.function.Consumer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
import org.springframework.web.util.UriBuilderFactory;
/**
@@ -41,29 +42,32 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder {
private final WebClient.Builder webClientBuilder;
private final WebHttpHandlerBuilder httpHandlerBuilder;
private final ClientHttpConnector connector;
private Duration responseTimeout;
DefaultWebTestClientBuilder() {
this(new ReactorClientHttpConnector());
this(null, null, new ReactorClientHttpConnector(), null);
}
DefaultWebTestClientBuilder(HttpHandler httpHandler) {
this(new HttpHandlerConnector(httpHandler));
DefaultWebTestClientBuilder(WebHttpHandlerBuilder httpHandlerBuilder) {
this(null, httpHandlerBuilder, null, null);
}
DefaultWebTestClientBuilder(ClientHttpConnector connector) {
this(connector, null, null);
}
DefaultWebTestClientBuilder(ClientHttpConnector connector,
@Nullable WebClient.Builder webClientBuilder,
DefaultWebTestClientBuilder(@Nullable WebClient.Builder webClientBuilder,
@Nullable WebHttpHandlerBuilder httpHandlerBuilder,
@Nullable ClientHttpConnector connector,
@Nullable Duration responseTimeout) {
this.connector = connector;
Assert.isTrue(httpHandlerBuilder != null || connector !=null,
"Either WebHttpHandlerBuilder or ClientHttpConnector must be provided");
this.webClientBuilder = (webClientBuilder != null ? webClientBuilder : WebClient.builder());
this.httpHandlerBuilder = (httpHandlerBuilder != null ? httpHandlerBuilder.cloneBuilder() : null);
this.connector = connector;
this.responseTimeout = responseTimeout;
}
@@ -129,9 +133,24 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder {
return this;
}
@Override
public WebTestClient.Builder apply(WebTestClientConfigurer configurer) {
configurer.afterConfigurerAdded(this, this.httpHandlerBuilder, this.connector);
return this;
}
@Override
public WebTestClient build() {
return new DefaultWebTestClient(this.webClientBuilder, this.connector, this.responseTimeout);
ClientHttpConnector connectorToUse = (this.connector != null ? this.connector :
new HttpHandlerConnector(this.httpHandlerBuilder.build()));
DefaultWebTestClientBuilder webTestClientBuilder = new DefaultWebTestClientBuilder(
this.webClientBuilder.build().mutate(), this.httpHandlerBuilder,
this.connector, this.responseTimeout);
return new DefaultWebTestClient(this.webClientBuilder,
connectorToUse, this.responseTimeout, webTestClientBuilder);
}
}

View File

@@ -1,123 +0,0 @@
/*
* Copyright 2002-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.reactive.server;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpHeaders;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
/**
* Apply {@code ServerWebExchange} transformations during "mock" server tests
* with the {@code WebTestClient}.
*
* <p>Register the {@code WebFilter} while setting up the mock server through
* one of the following:
* <ul>
* <li>{@link WebTestClient#bindToController}
* <li>{@link WebTestClient#bindToRouterFunction}
* <li>{@link WebTestClient#bindToApplicationContext}
* <li>{@link WebTestClient#bindToWebHandler}
* </ul>
*
* <p>Example usage:
*
* <pre class="code">
* Function&lt;ServerWebExchange, ServerWebExchange&gt; fn1 = ...;
* Function&lt;ServerWebExchange, ServerWebExchange&gt; fn2 = ...;
*
* ExchangeMutatorWebFilter mutator = new ExchangeMutatorWebFilter(fn1().andThen(fn2()));
* WebTestClient client = WebTestClient.bindToController(new MyController()).webFilter(mutator).build();
* </pre>
*
*
* <p>It is also possible to apply "per request" transformations:
*
* <pre class="code">
* ExchangeMutatorWebFilter mutator = new ExchangeMutatorWebFilter();
* WebTestClient client = WebTestClient.bindToController(new MyController()).webFilter(mutator).build();
*
* Function&lt;ServerWebExchange, ServerWebExchange&gt; fn1 = ...;
* Function&lt;ServerWebExchange, ServerWebExchange&gt; fn2 = ...;
*
* client.filter(mutator.perClient(fn1().andThen(fn2()))).get().uri("/").exchange();
* </pre>
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public class ExchangeMutatorWebFilter implements WebFilter {
private final Function<ServerWebExchange, ServerWebExchange> processor;
private final Map<String, Function<ServerWebExchange, ServerWebExchange>> perRequestProcessors =
new ConcurrentHashMap<>(4);
public ExchangeMutatorWebFilter() {
this(exchange -> exchange);
}
public ExchangeMutatorWebFilter(Function<ServerWebExchange, ServerWebExchange> processor) {
Assert.notNull(processor, "'processor' is required");
this.processor = processor;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
exchange = getProcessor(exchange).apply(exchange);
return chain.filter(exchange);
}
private Function<ServerWebExchange, ServerWebExchange> getProcessor(ServerWebExchange exchange) {
String id = getRequestId(exchange.getRequest().getHeaders());
Function<ServerWebExchange, ServerWebExchange> clientMutator = this.perRequestProcessors.remove(id);
return (clientMutator != null ? this.processor.andThen(clientMutator) : this.processor);
}
private String getRequestId(HttpHeaders headers) {
String id = headers.getFirst(WebTestClient.WEBTESTCLIENT_REQUEST_ID);
Assert.notNull(id, "No \"" + WebTestClient.WEBTESTCLIENT_REQUEST_ID + "\" header");
return id;
}
/**
* Apply the given processor only to requests performed through the client
* instance filtered with the returned filter. See class-level Javadoc for
* sample code.
* @param processor the exchange processor to use
* @return client filter for use with {@link WebTestClient#filter}
*/
public ExchangeFilterFunction perClient(Function<ServerWebExchange, ServerWebExchange> processor) {
return (request, next) -> {
String id = getRequestId(request.headers());
this.perRequestProcessors.compute(id,
(s, value) -> value != null ? value.andThen(processor) : processor);
return next.exchange(request);
};
}
}

View File

@@ -38,6 +38,7 @@ import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
*
* @author Rossen Stoyanchev
* @since 5.0
* @see WebTestClientConfigurer
*/
public interface MockServerConfigurer {

View File

@@ -193,13 +193,7 @@ public interface WebTestClient {
/**
* Register one or more {@link WebFilter} instances to apply to the
* mock server.
*
* <p>This could be used for example to apply {@code ServerWebExchange}
* transformations such as setting the Principal (for all requests or a
* subset) via {@link ExchangeMutatorWebFilter}.
*
* @param filter one or more filters
* @see ExchangeMutatorWebFilter
*/
<T extends B> T webFilter(WebFilter... filter);
@@ -380,6 +374,12 @@ public interface WebTestClient {
*/
Builder responseTimeout(Duration timeout);
/**
*
* @param configurer
* @return
*/
Builder apply(WebTestClientConfigurer configurer);
/**
* Build the {@link WebTestClient} instance.

View File

@@ -0,0 +1,45 @@
/*
* 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.client.reactive.ClientHttpConnector;
import org.springframework.lang.Nullable;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
/**
* Contract that frameworks or applications can use to pre-package a set of
* customizations to a {@link WebTestClient.Builder} and expose that
* as a shortcut.
*
* @author Rossen Stoyanchev
* @since 5.0
* @see MockServerConfigurer
*/
public interface WebTestClientConfigurer {
/**
* Invoked once only, immediately (i.e. before this method returns).
* @param builder the WebTestClient builder to make changes to
* @param httpHandlerBuilder the builder for the "mock server" HttpHandler
* this client was configured for "mock server" testing
* @param connector the connector for "live" integration tests if this
* server was configured for live integration testing
*/
void afterConfigurerAdded(WebTestClient.Builder builder,
@Nullable WebHttpHandlerBuilder httpHandlerBuilder,
@Nullable ClientHttpConnector connector);
}