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);
}

View File

@ -0,0 +1,148 @@
/*
* 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.security.Principal;
import org.junit.Before;
import org.junit.Test;
import reactor.core.publisher.Mono;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.lang.Nullable;
import org.springframework.test.web.reactive.server.MockServerConfigurer;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.reactive.server.WebTestClientConfigurer;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
/**
* Samples tests that demonstrate applying ServerWebExchange initialization.
* @author Rossen Stoyanchev
*/
public class ExchangeMutatorTests {
private WebTestClient webTestClient;
@Before
public void setUp() throws Exception {
this.webTestClient = WebTestClient.bindToController(new TestController())
.apply(globalIdentity("Pablo"))
.build();
}
@Test
public void useGloballyConfiguredIdentity() throws Exception {
this.webTestClient.get().uri("/userIdentity")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).isEqualTo("Hello Pablo!");
}
@Test
public void useLocallyConfiguredIdentity() throws Exception {
withIdentity(this.webTestClient, "Giovanni")
.get().uri("/userIdentity")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).isEqualTo("Hello Giovanni!");
}
private static MockServerConfigurer globalIdentity(String userName) {
return new IdentityConfigurer(userName);
}
private static WebTestClient withIdentity(WebTestClient client, String userName) {
return client.mutate().apply(new IdentityConfigurer(userName)).build();
}
@RestController
static class TestController {
@GetMapping("/userIdentity")
public String handle(Principal principal) {
return "Hello " + principal.getName() + "!";
}
}
private static class TestUser implements Principal {
private final String name;
TestUser(String name) {
this.name = name;
}
@Override
public String getName() {
return this.name;
}
}
private static class IdentityConfigurer implements MockServerConfigurer, WebTestClientConfigurer {
private final IdentityFilter filter;
public IdentityConfigurer(String userName) {
this.filter = new IdentityFilter(userName);
}
@Override
public void beforeServerCreated(WebHttpHandlerBuilder builder) {
builder.filters(filters -> filters.add(0, this.filter));
}
@Override
public void afterConfigurerAdded(WebTestClient.Builder builder,
@Nullable WebHttpHandlerBuilder httpHandlerBuilder,
@Nullable ClientHttpConnector connector) {
Assert.notNull(httpHandlerBuilder, "Not a mock server");
httpHandlerBuilder.filters(filters -> {
filters.removeIf(filter -> filter instanceof IdentityFilter);
filters.add(0, this.filter);
});
}
}
private static class IdentityFilter implements WebFilter {
private final Mono<Principal> userMono;
IdentityFilter(String userName) {
this.userMono = Mono.just(new TestUser(userName));
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
exchange = exchange.mutate().principal(this.userMono).build();
return chain.filter(exchange);
}
}
}

View File

@ -1,106 +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.samples;
import java.security.Principal;
import java.util.function.UnaryOperator;
import org.junit.Before;
import org.junit.Test;
import reactor.core.publisher.Mono;
import org.springframework.test.web.reactive.server.ExchangeMutatorWebFilter;
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.server.ServerWebExchange;
/**
* Samples tests that demonstrate applying ServerWebExchange initialization.
* @author Rossen Stoyanchev
*/
public class ExchangeMutatorWebFilterTests {
private ExchangeMutatorWebFilter exchangeMutator;
private WebTestClient webTestClient;
@Before
public void setUp() throws Exception {
this.exchangeMutator = new ExchangeMutatorWebFilter(userIdentity("Pablo"));
this.webTestClient = WebTestClient.bindToController(new TestController())
.webFilter(this.exchangeMutator)
.build();
}
@Test
public void globalMutator() throws Exception {
this.webTestClient.get().uri("/userIdentity")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).isEqualTo("Hello Pablo!");
}
@Test
public void perRequestMutators() throws Exception {
this.webTestClient = WebTestClient.bindToController(new TestController())
.webFilter(this.exchangeMutator)
.configureClient()
.filter(this.exchangeMutator.perClient(userIdentity("Giovanni")))
.build();
this.webTestClient
.get().uri("/userIdentity")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).isEqualTo("Hello Giovanni!");
}
private UnaryOperator<ServerWebExchange> userIdentity(String userName) {
return exchange -> exchange.mutate().principal(Mono.just(new TestUser(userName))).build();
}
@RestController
static class TestController {
@GetMapping("/userIdentity")
public String handle(Principal principal) {
return "Hello " + principal.getName() + "!";
}
}
private static class TestUser implements Principal {
private final String name;
TestUser(String name) {
this.name = name;
}
@Override
public String getName() {
return this.name;
}
}
}

View File

@ -96,6 +96,19 @@ public class WebHttpHandlerBuilder {
this.webHandler = webHandler;
}
/**
* Copy constructor.
*/
private WebHttpHandlerBuilder(WebHttpHandlerBuilder other) {
this.webHandler = other.webHandler;
this.filters.addAll(other.filters);
this.exceptionHandlers.addAll(other.exceptionHandlers);
this.sessionManager = other.sessionManager;
this.codecConfigurer = other.codecConfigurer;
this.localeContextResolver = other.localeContextResolver;
}
/**
* Static factory method to create a new builder instance.
@ -263,6 +276,14 @@ public class WebHttpHandlerBuilder {
return adapted;
}
/**
* Clone this {@link WebHttpHandlerBuilder}.
* @return the cloned builder instance
*/
public WebHttpHandlerBuilder cloneBuilder() {
return new WebHttpHandlerBuilder(this);
}
private static class SortedBeanContainer {

View File

@ -16,6 +16,7 @@
package org.springframework.web.server.handler;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@ -40,7 +41,7 @@ public class ExceptionHandlingWebHandler extends WebHandlerDecorator {
public ExceptionHandlingWebHandler(WebHandler delegate, List<WebExceptionHandler> handlers) {
super(delegate);
this.exceptionHandlers = Collections.unmodifiableList(handlers);
this.exceptionHandlers = Collections.unmodifiableList(new ArrayList<>(handlers));
}