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") + public void test() throws Exception { + this.client.get().uri("/test") .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") - .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