diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/AbstractMockServerSpec.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/AbstractMockServerSpec.java index 70b16c9bc0..bc087733d9 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/AbstractMockServerSpec.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/AbstractMockServerSpec.java @@ -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> implements WebTestClient.MockServerSpec { - private Function exchangeMutator; + private final ExchangeMutatorWebFilter exchangeMutatorFilter = new ExchangeMutatorWebFilter(); @Override - public T exchangeMutator(Function mutator) { - this.exchangeMutator = this.exchangeMutator != null ? this.exchangeMutator.andThen(mutator) : mutator; + public T exchangeMutator(UnaryOperator mutator) { + this.exchangeMutatorFilter.register(mutator); return self(); } @SuppressWarnings("unchecked") - protected T self() { + private 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() { diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/ApplicationContextSpec.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/ApplicationContextSpec.java index 414b501e75..d39490b6b7 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/ApplicationContextSpec.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/ApplicationContextSpec.java @@ -38,7 +38,7 @@ class ApplicationContextSpec extends AbstractMockServerSpec 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 { diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java index dc2db6bc17..66d49080fa 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java @@ -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); } } diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/ExchangeMutatorWebFilter.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/ExchangeMutatorWebFilter.java new file mode 100644 index 0000000000..5c589615dc --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/ExchangeMutatorWebFilter.java @@ -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> globalMutators = new ArrayList<>(4); + + private final Map> requestMutators = new ConcurrentHashMap<>(4); + + + /** + * Register a global transformation function to apply to all requests. + * @param mutator the transformation function + */ + public void register(UnaryOperator 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 mutator) { + this.requestMutators.put(requestId, mutator); + } + + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + + for (UnaryOperator mutator : this.globalMutators) { + exchange = mutator.apply(exchange); + } + + String requestId = WiretapConnector.getRequestId(exchange.getRequest().getHeaders()); + UnaryOperator mutator = this.requestMutators.remove(requestId); + if (mutator != null) { + exchange = mutator.apply(exchange); + } + + return chain.filter(exchange); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/RouterFunctionSpec.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/RouterFunctionSpec.java index 7159b4da14..ca5a2ef2ee 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/RouterFunctionSpec.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/RouterFunctionSpec.java @@ -44,7 +44,7 @@ public class RouterFunctionSpec extends AbstractMockServerSpecNote: this option is applicable only when testing + * without an actual running server. + * @param mutator the transformation function + * @return the filtered client + */ + WebTestClient exchangeMutator(UnaryOperator 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 exchangeMutator(Function function); + T exchangeMutator(UnaryOperator mutator); /** * Proceed to configure and build the test client. diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java index 35cdf70d22..a2b94a958d 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java @@ -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. */ diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/ApplicationContextTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/ApplicationContextTests.java index f5cf748368..daa4a9b36c 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/ApplicationContextTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/ApplicationContextTests.java @@ -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 identityMutator(String userName) { + private UnaryOperator 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 diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/ControllerTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/ControllerTests.java index 1501894f86..6c4407c744 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/ControllerTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/ControllerTests.java @@ -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 identityMutator(String userName) { + private UnaryOperator 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 {