Per-request exchange mutating for WebTestClient

Issue: SPR-15250
This commit is contained in:
Rossen Stoyanchev 2017-03-10 17:01:50 -05:00
parent f36e3d4a0d
commit f84580c32d
11 changed files with 174 additions and 40 deletions

View File

@ -15,8 +15,9 @@
*/
package org.springframework.test.web.reactive.server;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
@ -29,37 +30,28 @@ import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
abstract class AbstractMockServerSpec<B extends WebTestClient.MockServerSpec<B>>
implements WebTestClient.MockServerSpec<B> {
private Function<ServerWebExchange, ServerWebExchange> exchangeMutator;
private final ExchangeMutatorWebFilter exchangeMutatorFilter = new ExchangeMutatorWebFilter();
@Override
public <T extends B> T exchangeMutator(Function<ServerWebExchange, ServerWebExchange> mutator) {
this.exchangeMutator = this.exchangeMutator != null ? this.exchangeMutator.andThen(mutator) : mutator;
public <T extends B> T exchangeMutator(UnaryOperator<ServerWebExchange> mutator) {
this.exchangeMutatorFilter.register(mutator);
return self();
}
@SuppressWarnings("unchecked")
protected <T extends B> T self() {
private <T extends B> T self() {
return (T) this;
}
@Override
public WebTestClient.Builder configureClient() {
WebHttpHandlerBuilder handlerBuilder = createHttpHandlerBuilder();
if (this.exchangeMutator != null) {
handlerBuilder.prependFilter((exchange, chain) -> {
exchange = this.exchangeMutator.apply(exchange);
return chain.filter(exchange);
});
}
return new DefaultWebTestClientBuilder(handlerBuilder.build());
HttpHandler handler = initHttpHandlerBuilder().prependFilter(this.exchangeMutatorFilter).build();
return new DefaultWebTestClientBuilder(handler, this.exchangeMutatorFilter);
}
protected abstract WebHttpHandlerBuilder createHttpHandlerBuilder();
protected abstract WebHttpHandlerBuilder initHttpHandlerBuilder();
@Override
public WebTestClient build() {

View File

@ -38,7 +38,7 @@ class ApplicationContextSpec extends AbstractMockServerSpec<ApplicationContextSp
@Override
protected WebHttpHandlerBuilder createHttpHandlerBuilder() {
protected WebHttpHandlerBuilder initHttpHandlerBuilder() {
return WebHttpHandlerBuilder.applicationContext(this.applicationContext);
}

View File

@ -115,7 +115,7 @@ class DefaultControllerSpec extends AbstractMockServerSpec<WebTestClient.Control
@Override
protected WebHttpHandlerBuilder createHttpHandlerBuilder() {
protected WebHttpHandlerBuilder initHttpHandlerBuilder() {
return WebHttpHandlerBuilder.applicationContext(initApplicationContext());
}

View File

@ -25,6 +25,7 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
@ -42,6 +43,7 @@ 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.server.ServerWebExchange;
import org.springframework.web.util.UriBuilder;
import static java.util.stream.Collectors.toList;
@ -63,23 +65,29 @@ class DefaultWebTestClient implements WebTestClient {
private final WiretapConnector wiretapConnector;
private final ExchangeMutatorWebFilter exchangeMutatorWebFilter;
private final Duration timeout;
private final AtomicLong requestIndex = new AtomicLong();
DefaultWebTestClient(WebClient.Builder webClientBuilder, ClientHttpConnector connector, Duration timeout) {
DefaultWebTestClient(WebClient.Builder webClientBuilder, ClientHttpConnector connector,
ExchangeMutatorWebFilter webFilter, Duration timeout) {
Assert.notNull(webClientBuilder, "WebClient.Builder is required");
this.wiretapConnector = new WiretapConnector(connector);
this.webClient = webClientBuilder.clientConnector(this.wiretapConnector).build();
this.exchangeMutatorWebFilter = webFilter;
this.timeout = (timeout != null ? timeout : Duration.ofSeconds(5));
}
private DefaultWebTestClient(DefaultWebTestClient webTestClient, ExchangeFilterFunction filter) {
this.webClient = webTestClient.webClient.filter(filter);
this.timeout = webTestClient.timeout;
this.wiretapConnector = webTestClient.wiretapConnector;
this.exchangeMutatorWebFilter = webTestClient.exchangeMutatorWebFilter;
this.timeout = webTestClient.timeout;
}
@ -133,6 +141,20 @@ class DefaultWebTestClient implements WebTestClient {
return new DefaultWebTestClient(this, filter);
}
@Override
public WebTestClient exchangeMutator(UnaryOperator<ServerWebExchange> mutator) {
Assert.notNull(this.exchangeMutatorWebFilter,
"This option is applicable only for tests without an actual running server");
return filter((request, next) -> {
String requestId = request.headers().getFirst(WiretapConnector.REQUEST_ID_HEADER_NAME);
Assert.notNull(requestId, "No request-id header");
this.exchangeMutatorWebFilter.register(requestId, mutator);
return next.exchange(request);
});
}
private class DefaultUriSpec implements UriSpec {

View File

@ -36,19 +36,23 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder {
private final ClientHttpConnector connector;
private final ExchangeMutatorWebFilter exchangeMutatorFilter;
private Duration responseTimeout;
public DefaultWebTestClientBuilder() {
DefaultWebTestClientBuilder() {
this(new ReactorClientHttpConnector());
}
public DefaultWebTestClientBuilder(HttpHandler httpHandler) {
this(new HttpHandlerConnector(httpHandler));
DefaultWebTestClientBuilder(ClientHttpConnector connector) {
this.connector = connector;
this.exchangeMutatorFilter = null;
}
public DefaultWebTestClientBuilder(ClientHttpConnector connector) {
this.connector = connector;
DefaultWebTestClientBuilder(HttpHandler httpHandler, ExchangeMutatorWebFilter exchangeMutatorFilter) {
this.connector = new HttpHandlerConnector(httpHandler);
this.exchangeMutatorFilter = exchangeMutatorFilter;
}
@ -90,7 +94,8 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder {
@Override
public WebTestClient build() {
return new DefaultWebTestClient(this.webClientBuilder, this.connector, this.responseTimeout);
return new DefaultWebTestClient(this.webClientBuilder, this.connector,
this.exchangeMutatorFilter, this.responseTimeout);
}
}

View File

@ -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 java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.UnaryOperator;
import reactor.core.publisher.Mono;
import org.springframework.util.Assert;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
/**
* WebFilter that applies global and request-specific transformation on
* {@link ServerWebExchange}.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
class ExchangeMutatorWebFilter implements WebFilter {
private volatile List<UnaryOperator<ServerWebExchange>> globalMutators = new ArrayList<>(4);
private final Map<String, UnaryOperator<ServerWebExchange>> requestMutators = new ConcurrentHashMap<>(4);
/**
* Register a global transformation function to apply to all requests.
* @param mutator the transformation function
*/
public void register(UnaryOperator<ServerWebExchange> mutator) {
Assert.notNull(mutator, "'mutator' is required");
this.globalMutators.add(mutator);
}
/**
* Register a per-request transformation function.
* @param requestId the "request-id" header value identifying the request
* @param mutator the transformation function
*/
public void register(String requestId, UnaryOperator<ServerWebExchange> mutator) {
this.requestMutators.put(requestId, mutator);
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
for (UnaryOperator<ServerWebExchange> mutator : this.globalMutators) {
exchange = mutator.apply(exchange);
}
String requestId = WiretapConnector.getRequestId(exchange.getRequest().getHeaders());
UnaryOperator<ServerWebExchange> mutator = this.requestMutators.remove(requestId);
if (mutator != null) {
exchange = mutator.apply(exchange);
}
return chain.filter(exchange);
}
}

View File

@ -44,7 +44,7 @@ public class RouterFunctionSpec extends AbstractMockServerSpec<RouterFunctionSpe
@Override
protected WebHttpHandlerBuilder createHttpHandlerBuilder() {
protected WebHttpHandlerBuilder initHttpHandlerBuilder() {
return WebHttpHandlerBuilder.applicationContext(initApplicationContext());
}

View File

@ -23,6 +23,7 @@ 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;
@ -123,6 +124,16 @@ public interface WebTestClient {
*/
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
@ -176,10 +187,10 @@ public interface WebTestClient {
/**
* Configure a transformation function on {@code ServerWebExchange} to
* be applied at the start of server-side, request processing.
* @param function the transforming function.
* @param mutator the transforming function.
* @see ServerWebExchange#mutate()
*/
<T extends B> T exchangeMutator(Function<ServerWebExchange, ServerWebExchange> function);
<T extends B> T exchangeMutator(UnaryOperator<ServerWebExchange> mutator);
/**
* Proceed to configure and build the test client.

View File

@ -23,6 +23,7 @@ import java.util.function.Function;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.ClientHttpRequest;
@ -68,7 +69,7 @@ class WiretapConnector implements ClientHttpConnector {
})
.map(response -> {
WiretapClientHttpRequest wrappedRequest = requestRef.get();
String requestId = wrappedRequest.getHeaders().getFirst(REQUEST_ID_HEADER_NAME);
String requestId = getRequestId(wrappedRequest.getHeaders());
Assert.notNull(requestId, "No request-id header");
WiretapClientHttpResponse wrappedResponse = new WiretapClientHttpResponse(response);
ExchangeResult result = new ExchangeResult(wrappedRequest, wrappedResponse);
@ -77,6 +78,12 @@ class WiretapConnector implements ClientHttpConnector {
});
}
public static String getRequestId(HttpHeaders headers) {
String requestId = headers.getFirst(REQUEST_ID_HEADER_NAME);
Assert.notNull(requestId, "No request-id header");
return requestId;
}
/**
* Retrieve the {@code ExchangeResult} for the given "request-id" header value.
*/

View File

@ -16,7 +16,7 @@
package org.springframework.test.web.reactive.server.samples.bind;
import java.security.Principal;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import org.junit.Before;
import org.junit.Test;
@ -53,11 +53,11 @@ public class ApplicationContextTests {
context.refresh();
this.client = WebTestClient.bindToApplicationContext(context)
.exchangeMutator(identityMutator("Pablo"))
.exchangeMutator(identitySetup("Pablo"))
.build();
}
private Function<ServerWebExchange, ServerWebExchange> identityMutator(String userName) {
private UnaryOperator<ServerWebExchange> identitySetup(String userName) {
return exchange -> {
Principal user = mock(Principal.class);
when(user.getName()).thenReturn(userName);
@ -67,13 +67,22 @@ public class ApplicationContextTests {
@Test
public void test() throws Exception {
public void basic() throws Exception {
this.client.get().uri("/test")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).value().isEqualTo("Hello Pablo!");
}
@Test
public void perRequestIdentityOverride() throws Exception {
this.client.exchangeMutator(identitySetup("Giovani"))
.get().uri("/test")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).value().isEqualTo("Hello Giovani!");
}
@Configuration
@EnableWebFlux

View File

@ -16,7 +16,7 @@
package org.springframework.test.web.reactive.server.samples.bind;
import java.security.Principal;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import org.junit.Before;
import org.junit.Test;
@ -43,13 +43,12 @@ public class ControllerTests {
@Before
public void setUp() throws Exception {
this.client = WebTestClient.bindToController(new TestController())
.exchangeMutator(identityMutator("Pablo"))
.exchangeMutator(identitySetup("Pablo"))
.build();
}
private Function<ServerWebExchange, ServerWebExchange> identityMutator(String userName) {
private UnaryOperator<ServerWebExchange> identitySetup(String userName) {
return exchange -> {
Principal user = mock(Principal.class);
when(user.getName()).thenReturn(userName);
@ -59,13 +58,22 @@ public class ControllerTests {
@Test
public void test() throws Exception {
public void basic() throws Exception {
this.client.get().uri("/test")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).value().isEqualTo("Hello Pablo!");
}
@Test
public void perRequestIdentityOverride() throws Exception {
this.client.exchangeMutator(identitySetup("Giovani"))
.get().uri("/test")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).value().isEqualTo("Hello Giovani!");
}
@RestController
static class TestController {