From fd51893a44e2fe9766b9c7e78d3f151561ef3a2d Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 25 May 2017 11:37:33 -0400 Subject: [PATCH] Refactor exchange mutator As a follow-up to the recent commit 246e72 some slight modifications to MockServerExchangeMutator (renamed to ExchnageMutatorWebFilter). Aside from the name change, the main difference is that "per request" exchange processors are now simply applied via WebTestClient#filter(..). Issue: SPR-15570 --- .../server/ExchangeMutatorWebFilter.java | 112 ++++++++++++++++ .../server/MockServerExchangeMutator.java | 126 ------------------ .../web/reactive/server/WebTestClient.java | 6 +- .../ExchangeMutatorWebFilterTests.java | 100 ++++++++++++++ .../samples/bind/ApplicationContextTests.java | 98 ++------------ .../server/samples/bind/ControllerTests.java | 98 ++------------ .../server/samples/bind/HttpServerTests.java | 2 +- .../samples/bind/RouterFunctionTests.java | 2 +- 8 files changed, 235 insertions(+), 309 deletions(-) create mode 100644 spring-test/src/main/java/org/springframework/test/web/reactive/server/ExchangeMutatorWebFilter.java delete mode 100644 spring-test/src/main/java/org/springframework/test/web/reactive/server/MockServerExchangeMutator.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ExchangeMutatorWebFilterTests.java 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..b5467923af --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/ExchangeMutatorWebFilter.java @@ -0,0 +1,112 @@ +/* + * 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 java.util.function.UnaryOperator; + +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}. + * + *

Register the {@code WebFilter} while setting up the mock server through + * one of the following: + *

+ * + *

Example usage: + * + *

+ * ExchangeMutatorWebFilter mutator = new ExchangeMutatorWebFilter(exchange -> ...);
+ * WebTestClient client = WebTestClient.bindToController(new MyController()).webFilter(mutator).build();
+ * 
+ * + *

It is also possible to apply "per request" transformations: + * + *

+ * ExchangeMutatorWebFilter mutator = new ExchangeMutatorWebFilter(exchange -> ...);
+ * WebTestClient client = WebTestClient.bindToController(new MyController()).webFilter(mutator).build();
+ * client.filter(mutator.perClient(exchange -> ...)).get().uri("/").exchange();
+ * 
+ * + * @author Rossen Stoyanchev + * @since 5.0 + */ +public class ExchangeMutatorWebFilter implements WebFilter { + + private final Function processor; + + private final Map> perRequestProcessors = + new ConcurrentHashMap<>(4); + + + public ExchangeMutatorWebFilter(UnaryOperator processor) { + Assert.notNull(processor, "'processor' is required"); + this.processor = processor; + } + + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + exchange = getProcessor(exchange).apply(exchange); + return chain.filter(exchange); + } + + private Function getProcessor(ServerWebExchange exchange) { + String id = getRequestId(exchange.getRequest().getHeaders()); + Function 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(UnaryOperator 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); + }; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/MockServerExchangeMutator.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/MockServerExchangeMutator.java deleted file mode 100644 index 3ebebfc4af..0000000000 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/MockServerExchangeMutator.java +++ /dev/null @@ -1,126 +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 java.util.function.UnaryOperator; - -import reactor.core.publisher.Mono; - -import org.springframework.http.HttpHeaders; -import org.springframework.util.Assert; -import org.springframework.web.server.ServerWebExchange; -import org.springframework.web.server.WebFilter; -import org.springframework.web.server.WebFilterChain; - -/** - * Built-in {@link WebFilter} for applying {@code ServerWebExchange} - * transformations during requests from the {@code WebTestClient} to a mock - * server -- i.e. when one of the following is in use: - *
    - *
  • {@link WebTestClient#bindToController} - *
  • {@link WebTestClient#bindToRouterFunction} - *
  • {@link WebTestClient#bindToApplicationContext} - *
  • {@link WebTestClient#bindToWebHandler} - *
- * - *

Example of registering a "global" transformation: - *

- *
- * MockServerExchangeMutator mutator = new MockServerExchangeMutator(exchange -> ...);
- * WebTestClient client = WebTestClient.bindToController(new MyController()).webFilter(mutator).build()
- * 
- * - *

Example of registering "per client" transformations: - *

- *
- * MockServerExchangeMutator mutator = new MockServerExchangeMutator(exchange -> ...);
- * WebTestClient client = WebTestClient.bindToController(new MyController()).webFilter(mutator).build()
- *
- * WebTestClient clientA = mutator.filterClient(client, exchange -> ...);
- * // Use client A...
- *
- * WebTestClient clientB = mutator.filterClient(client, exchange -> ...);
- * // Use client B...
- * 
- * - * @author Rossen Stoyanchev - * @since 5.0 - */ -public class MockServerExchangeMutator implements WebFilter { - - private final Function mutator; - - private final Map> perRequestMutators = - new ConcurrentHashMap<>(4); - - - public MockServerExchangeMutator(Function mutator) { - Assert.notNull(mutator, "'mutator' is required"); - this.mutator = mutator; - } - - - @Override - public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { - return chain.filter(getMutatorsFor(exchange).apply(exchange)); - } - - private Function getMutatorsFor(ServerWebExchange exchange) { - String id = getRequestId(exchange.getRequest().getHeaders()); - Function clientMutator = this.perRequestMutators.remove(id); - return (clientMutator != null ? this.mutator.andThen(clientMutator) : this.mutator); - } - - 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 a filter to the given client in order to apply - * {@code ServerWebExchange} transformations only to requests executed - * through the returned client instance. See examples in the - * {@link MockServerExchangeMutator class-level Javadoc}. - * - * @param mutator the per-request mutator to use - * @param mutators additional per-request mutators to use - * @return a new client instance filtered with {@link WebTestClient#filter} - */ - @SafeVarargs - public final WebTestClient filterClient(WebTestClient client, - UnaryOperator mutator, UnaryOperator... mutators) { - - return client.filter((request, next) -> { - String id = getRequestId(request.headers()); - registerPerRequestMutator(id, mutator); - for (UnaryOperator current : mutators) { - registerPerRequestMutator(id, current); - } - return next.exchange(request); - }); - } - - private void registerPerRequestMutator(String id, UnaryOperator m) { - this.perRequestMutators.compute(id, (s, value) -> value != null ? value.andThen(m) : m); - } - -} diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java index 009eae64f8..dfa496af93 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java @@ -201,10 +201,10 @@ public interface WebTestClient { * *

This could be used for example to apply {@code ServerWebExchange} * transformations such as setting the Principal (for all requests or a - * subset) via {@link MockServerExchangeMutator}. + * subset) via {@link ExchangeMutatorWebFilter}. * * @param filter one or more filters - * @see MockServerExchangeMutator + * @see ExchangeMutatorWebFilter */ T webFilter(WebFilter... filter); @@ -536,7 +536,7 @@ public interface WebTestClient { */ BodySpec expectBody(ResolvableType bodyType); - /** + /** * Declare expectations on the response body decoded to {@code List}. * @param elementType the expected List element type */ diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ExchangeMutatorWebFilterTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ExchangeMutatorWebFilterTests.java new file mode 100644 index 0000000000..f2de0a72de --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ExchangeMutatorWebFilterTests.java @@ -0,0 +1,100 @@ +/* + * 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 + .filter(this.exchangeMutator.perClient(userIdentity("Giovanni"))) + .get().uri("/userIdentity") + .exchange() + .expectStatus().isOk() + .expectBody(String.class).isEqualTo("Hello Giovanni!"); + } + + + private UnaryOperator 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; + } + } + +} 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 239bdc7999..e1bb1705f0 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,29 +16,20 @@ package org.springframework.test.web.reactive.server.samples.bind; -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.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.test.web.reactive.server.MockServerExchangeMutator; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestAttribute; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.reactive.config.EnableWebFlux; -import org.springframework.web.server.ServerWebExchange; -import org.springframework.web.server.WebFilter; - -import static org.junit.Assert.assertEquals; /** - * Binding to server infrastructure declared in a Spring ApplicationContext. + * Sample tests demonstrating "mock" server tests binding to server infrastructure + * declared in a Spring ApplicationContext. * * @author Rossen Stoyanchev * @since 5.0 @@ -47,8 +38,6 @@ public class ApplicationContextTests { private WebTestClient client; - private MockServerExchangeMutator exchangeMutator; - @Before public void setUp() throws Exception { @@ -57,65 +46,15 @@ public class ApplicationContextTests { context.register(WebConfig.class); context.refresh(); - this.exchangeMutator = new MockServerExchangeMutator(principal("Pablo")); - - WebFilter userPrefixFilter = (exchange, chain) -> { - Mono user = exchange.getPrincipal().map(p -> new TestUser("Mr. " + p.getName())); - return chain.filter(exchange.mutate().principal(user).build()); - }; - - this.client = WebTestClient.bindToApplicationContext(context) - .webFilter(this.exchangeMutator, userPrefixFilter) - .build(); - } - - - @Test - public void bodyContent() throws Exception { - this.client.get().uri("/principal") - .exchange() - .expectStatus().isOk() - .expectBody(String.class).isEqualTo("Hello Mr. Pablo!"); + this.client = WebTestClient.bindToApplicationContext(context).build(); } @Test - public void bodyContentWithConsumer() throws Exception { - this.client.get().uri("/principal") + public void test() throws Exception { + this.client.get().uri("/test") .exchange() .expectStatus().isOk() - .expectBody(String.class) - .consumeWith(result -> assertEquals("Hello Mr. Pablo!", result.getResponseBody())); - } - - @Test - public void perRequestExchangeMutator() throws Exception { - this.exchangeMutator.filterClient(this.client, principal("Giovanni")) - .get().uri("/principal") - .exchange() - .expectStatus().isOk() - .expectBody(String.class).isEqualTo("Hello Mr. Giovanni!"); - } - - @Test - public void perRequestMultipleExchangeMutators() throws Exception { - this.exchangeMutator - .filterClient(this.client, attribute("attr1", "foo"), attribute("attr2", "bar")) - .get().uri("/attributes") - .exchange() - .expectStatus().isOk() - .expectBody(String.class).isEqualTo("foo+bar"); - } - - - private UnaryOperator principal(String userName) { - return exchange -> exchange.mutate().principal(Mono.just(new TestUser(userName))).build(); - } - - private UnaryOperator attribute(String attrName, String attrValue) { - return exchange -> { - exchange.getAttributes().put(attrName, attrValue); - return exchange; - }; + .expectBody(String.class).isEqualTo("It works!"); } @@ -133,28 +72,9 @@ public class ApplicationContextTests { @RestController static class TestController { - @GetMapping("/principal") - public String handle(Principal principal) { - return "Hello " + principal.getName() + "!"; - } - - @GetMapping("/attributes") - public String handle(@RequestAttribute String attr1, @RequestAttribute String attr2) { - return attr1 + "+" + attr2; - } - } - - private static class TestUser implements Principal { - - private final String name; - - TestUser(String name) { - this.name = name; - } - - @Override - public String getName() { - return this.name; + @GetMapping("/test") + public String handle() { + return "It works!"; } } 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 43ba532bc8..a088d97d33 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,125 +16,45 @@ package org.springframework.test.web.reactive.server.samples.bind; -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.MockServerExchangeMutator; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestAttribute; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.server.ServerWebExchange; -import org.springframework.web.server.WebFilter; - -import static org.junit.Assert.assertEquals; /** - * Bind to annotated controllers. + * Sample tests demonstrating "mock" server tests binding to an annotated + * controller. * * @author Rossen Stoyanchev - * @since 5.0 */ public class ControllerTests { private WebTestClient client; - private MockServerExchangeMutator exchangeMutator; - @Before public void setUp() throws Exception { - - this.exchangeMutator = new MockServerExchangeMutator(principal("Pablo")); - - WebFilter userPrefixFilter = (exchange, chain) -> { - Mono user = exchange.getPrincipal().map(p -> new TestUser("Mr. " + p.getName())); - return chain.filter(exchange.mutate().principal(user).build()); - }; - - this.client = WebTestClient.bindToController(new TestController()) - .webFilter(this.exchangeMutator, userPrefixFilter) - .build(); + this.client = WebTestClient.bindToController(new TestController()).build(); } - @Test - public void bodyContent() throws Exception { - this.client.get().uri("/principal") - .exchange() - .expectStatus().isOk() - .expectBody(String.class).isEqualTo("Hello Mr. Pablo!"); - } - - @Test - public void bodyContentWithConsumer() throws Exception { - this.client.get().uri("/principal") - .exchange() - .expectStatus().isOk() - .expectBody(String.class) - .consumeWith(result -> assertEquals("Hello Mr. Pablo!", result.getResponseBody())); - } - - @Test - public void perRequestExchangeMutator() throws Exception { - this.exchangeMutator.filterClient(this.client, principal("Giovanni")) - .get().uri("/principal") - .exchange() - .expectStatus().isOk() - .expectBody(String.class).isEqualTo("Hello Mr. Giovanni!"); - } @Test - public void perRequestMultipleExchangeMutators() throws Exception { - this.exchangeMutator - .filterClient(this.client, attribute("attr1", "foo"), attribute("attr2", "bar")) - .get().uri("/attributes") + public void test() throws Exception { + this.client.get().uri("/test") .exchange() .expectStatus().isOk() - .expectBody(String.class).isEqualTo("foo+bar"); - } - - - private UnaryOperator principal(String userName) { - return exchange -> exchange.mutate().principal(Mono.just(new TestUser(userName))).build(); - } - - private UnaryOperator attribute(String attrName, String attrValue) { - return exchange -> { - exchange.getAttributes().put(attrName, attrValue); - return exchange; - }; + .expectBody(String.class).isEqualTo("It works!"); } @RestController static class TestController { - @GetMapping("/principal") - public String handle(Principal principal) { - return "Hello " + principal.getName() + "!"; - } - - @GetMapping("/attributes") - public String handle(@RequestAttribute String attr1, @RequestAttribute String attr2) { - return attr1 + "+" + attr2; - } - } - - private static class TestUser implements Principal { - - private final String name; - - TestUser(String name) { - this.name = name; - } - - @Override - public String getName() { - return this.name; + @GetMapping("/test") + public String handle() { + return "It works!"; } } diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/HttpServerTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/HttpServerTests.java index 8abf7b3a3a..026a8e5e9a 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/HttpServerTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/HttpServerTests.java @@ -30,7 +30,7 @@ import static org.springframework.web.reactive.function.server.RequestPredicates import static org.springframework.web.reactive.function.server.RouterFunctions.route; /** - * Bind to a running server, making actual requests over a socket. + * Sample tests demonstrating live server integration tests. * * @author Rossen Stoyanchev * @since 5.0 diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/RouterFunctionTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/RouterFunctionTests.java index dd3800256b..4878a5f274 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/RouterFunctionTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/RouterFunctionTests.java @@ -27,7 +27,7 @@ import static org.springframework.web.reactive.function.server.RequestPredicates import static org.springframework.web.reactive.function.server.RouterFunctions.route; /** - * Bind to a {@link RouterFunction} and functional endpoints. + * Sample tests demonstrating "mock" server tests binding to a RouterFunction. * * @author Rossen Stoyanchev * @since 5.0