Per-request exchange mutating for WebTestClient
Issue: SPR-15250
This commit is contained in:
parent
f36e3d4a0d
commit
f84580c32d
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class ApplicationContextSpec extends AbstractMockServerSpec<ApplicationContextSp
|
|||
|
||||
|
||||
@Override
|
||||
protected WebHttpHandlerBuilder createHttpHandlerBuilder() {
|
||||
protected WebHttpHandlerBuilder initHttpHandlerBuilder() {
|
||||
return WebHttpHandlerBuilder.applicationContext(this.applicationContext);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ class DefaultControllerSpec extends AbstractMockServerSpec<WebTestClient.Control
|
|||
|
||||
|
||||
@Override
|
||||
protected WebHttpHandlerBuilder createHttpHandlerBuilder() {
|
||||
protected WebHttpHandlerBuilder initHttpHandlerBuilder() {
|
||||
return WebHttpHandlerBuilder.applicationContext(initApplicationContext());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -44,7 +44,7 @@ public class RouterFunctionSpec extends AbstractMockServerSpec<RouterFunctionSpe
|
|||
|
||||
|
||||
@Override
|
||||
protected WebHttpHandlerBuilder createHttpHandlerBuilder() {
|
||||
protected WebHttpHandlerBuilder initHttpHandlerBuilder() {
|
||||
return WebHttpHandlerBuilder.applicationContext(initApplicationContext());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue