From 4116e6dd184a6944ab4ee29bef504597ebe4482c Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 17 Aug 2020 07:53:39 +0100 Subject: [PATCH 1/7] Pull up method to ConfiugrableMockMvcBuilder See gh-19647 --- .../servlet/setup/AbstractMockMvcBuilder.java | 1 + .../setup/ConfigurableMockMvcBuilder.java | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/AbstractMockMvcBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/AbstractMockMvcBuilder.java index ab9b89c0aa4..de05563d704 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/AbstractMockMvcBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/AbstractMockMvcBuilder.java @@ -107,6 +107,7 @@ public abstract class AbstractMockMvcBuilder return self(); } + @Override public final T addDispatcherServletCustomizer(DispatcherServletCustomizer customizer) { this.dispatcherServletCustomizers.add(customizer); return self(); diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/ConfigurableMockMvcBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/ConfigurableMockMvcBuilder.java index 31dbd4169b9..a619ee7a588 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/ConfigurableMockMvcBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/ConfigurableMockMvcBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 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. @@ -18,6 +18,7 @@ package org.springframework.test.web.servlet.setup; import javax.servlet.Filter; +import org.springframework.test.web.servlet.DispatcherServletCustomizer; import org.springframework.test.web.servlet.MockMvcBuilder; import org.springframework.test.web.servlet.RequestBuilder; import org.springframework.test.web.servlet.ResultHandler; @@ -37,7 +38,7 @@ public interface ConfigurableMockMvcBuilder * mockMvcBuilder.addFilters(springSecurityFilterChain); * - *

is the equivalent of the following web.xml configuration: + *

It is the equivalent of the following web.xml configuration: *

 	 * <filter-mapping>
 	 *     <filter-name>springSecurityFilterChain</filter-name>
@@ -52,9 +53,9 @@ public interface ConfigurableMockMvcBuilder
-	 * mockMvcBuilder.addFilters(myResourceFilter, "/resources/*");
+	 * mockMvcBuilder.addFilter(myResourceFilter, "/resources/*");
 	 * 
- *

is the equivalent of: + *

It is the equivalent of: *

 	 * <filter-mapping>
 	 *     <filter-name>myResourceFilter</filter-name>
@@ -105,6 +106,13 @@ public interface ConfigurableMockMvcBuilder T dispatchOptions(boolean dispatchOptions);
 
+	/**
+	 * A more advanced variant of {@link #dispatchOptions(boolean)} that allows
+	 * customizing any {@link org.springframework.web.servlet.DispatcherServlet}
+	 * property.
+	 */
+	 T addDispatcherServletCustomizer(DispatcherServletCustomizer customizer);
+
 	/**
 	 * Add a {@code MockMvcConfigurer} that automates MockMvc setup and
 	 * configures it for some specific purpose (e.g. security).

From dd7369df4820b3418ccfdeaf5ee79e5ebdf63940 Mon Sep 17 00:00:00 2001
From: Rossen Stoyanchev 
Date: Mon, 17 Aug 2020 14:53:19 +0100
Subject: [PATCH 2/7] WiretapConnector.Info is private

The claimRequest method was not intended to be public and couldn't
have been used since the Info type it returned was package private.
This change completely hides the Info.

See gh-19647
---
 .../reactive/server/DefaultWebTestClient.java | 12 ++++----
 .../web/reactive/server/WiretapConnector.java | 30 +++++++++++--------
 .../server/WiretapConnectorTests.java         |  5 ++--
 3 files changed, 24 insertions(+), 23 deletions(-)

diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java
index 65e2e366149..c9dce22f2a7 100644
--- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java
+++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2020 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.
@@ -306,8 +306,8 @@ class DefaultWebTestClient implements WebTestClient {
 		public ResponseSpec exchange() {
 			ClientResponse clientResponse = this.bodySpec.exchange().block(getTimeout());
 			Assert.state(clientResponse != null, "No ClientResponse");
-			WiretapConnector.Info info = wiretapConnector.claimRequest(this.requestId);
-			return new DefaultResponseSpec(info, clientResponse, this.uriTemplate, getTimeout());
+			ExchangeResult result = wiretapConnector.getExchangeResult(this.requestId, this.uriTemplate, getTimeout());
+			return new DefaultResponseSpec(result, clientResponse, getTimeout());
 		}
 	}
 
@@ -321,10 +321,8 @@ class DefaultWebTestClient implements WebTestClient {
 		private final Duration timeout;
 
 
-		DefaultResponseSpec(WiretapConnector.Info wiretapInfo, ClientResponse response,
-				@Nullable String uriTemplate, Duration timeout) {
-
-			this.exchangeResult = wiretapInfo.createExchangeResult(timeout, uriTemplate);
+		DefaultResponseSpec(ExchangeResult exchangeResult, ClientResponse response, Duration timeout) {
+			this.exchangeResult = exchangeResult;
 			this.response = response;
 			this.timeout = timeout;
 		}
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 b904da963d2..fcc8fc48f97 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
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2018 the original author or authors.
+ * Copyright 2002-2020 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.
@@ -53,7 +53,7 @@ class WiretapConnector implements ClientHttpConnector {
 
 	private final ClientHttpConnector delegate;
 
-	private final Map exchanges = new ConcurrentHashMap<>();
+	private final Map exchanges = new ConcurrentHashMap<>();
 
 
 	WiretapConnector(ClientHttpConnector delegate) {
@@ -79,43 +79,47 @@ class WiretapConnector implements ClientHttpConnector {
 					String requestId = wrappedRequest.getHeaders().getFirst(header);
 					Assert.state(requestId != null, () -> "No \"" + header + "\" header");
 					WiretapClientHttpResponse wrappedResponse = new WiretapClientHttpResponse(response);
-					this.exchanges.put(requestId, new Info(wrappedRequest, wrappedResponse));
+					this.exchanges.put(requestId, new ClientExchangeInfo(wrappedRequest, wrappedResponse));
 					return wrappedResponse;
 				});
 	}
 
 	/**
-	 * Retrieve the {@link Info} for the given "request-id" header value.
+	 * Create the {@link ExchangeResult} for the given "request-id" header value.
 	 */
-	public Info claimRequest(String requestId) {
-		Info info = this.exchanges.remove(requestId);
+	ExchangeResult getExchangeResult(String requestId, @Nullable String uriTemplate, Duration timeout) {
+		ClientExchangeInfo info = this.exchanges.remove(requestId);
 		Assert.state(info != null, () -> {
 			String header = WebTestClient.WEBTESTCLIENT_REQUEST_ID;
 			return "No match for " + header + "=" + requestId;
 		});
-		return info;
+		return new ExchangeResult(info.getRequest(), info.getResponse(),
+				info.getRequest().getRecorder().getContent(),
+				info.getResponse().getRecorder().getContent(),
+				timeout, uriTemplate);
 	}
 
 
 	/**
 	 * Holder for {@link WiretapClientHttpRequest} and {@link WiretapClientHttpResponse}.
 	 */
-	class Info {
+	private static class ClientExchangeInfo {
 
 		private final WiretapClientHttpRequest request;
 
 		private final WiretapClientHttpResponse response;
 
-
-		public Info(WiretapClientHttpRequest request, WiretapClientHttpResponse response) {
+		public ClientExchangeInfo(WiretapClientHttpRequest request, WiretapClientHttpResponse response) {
 			this.request = request;
 			this.response = response;
 		}
 
+		public WiretapClientHttpRequest getRequest() {
+			return this.request;
+		}
 
-		public ExchangeResult createExchangeResult(Duration timeout, @Nullable String uriTemplate) {
-			return new ExchangeResult(this.request, this.response, this.request.getRecorder().getContent(),
-					this.response.getRecorder().getContent(), timeout, uriTemplate);
+		public WiretapClientHttpResponse getResponse() {
+			return this.response;
 		}
 	}
 
diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/WiretapConnectorTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/WiretapConnectorTests.java
index 60a3ba15e34..000f2f14174 100644
--- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/WiretapConnectorTests.java
+++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/WiretapConnectorTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2020 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.
@@ -57,8 +57,7 @@ public class WiretapConnectorTests {
 		ExchangeFunction function = ExchangeFunctions.create(wiretapConnector);
 		function.exchange(clientRequest).block(ofMillis(0));
 
-		WiretapConnector.Info actual = wiretapConnector.claimRequest("1");
-		ExchangeResult result = actual.createExchangeResult(Duration.ZERO, null);
+		ExchangeResult result = wiretapConnector.getExchangeResult("1", null, Duration.ZERO);
 		assertThat(result.getMethod()).isEqualTo(HttpMethod.GET);
 		assertThat(result.getUrl().toString()).isEqualTo("/test");
 	}

From cb02b0e776337d3667db8b915d4c7ac8adeb5931 Mon Sep 17 00:00:00 2001
From: Rossen Stoyanchev 
Date: Wed, 19 Aug 2020 17:33:27 +0100
Subject: [PATCH 3/7] WebTestClient releases body on returnResult(Void.class)

The original behavior was to ignore the body which came with odd
warnings in the Javadoc and potential leaks that could be reported
from tests causing unnecessary concern.

This change causes the body to be released and effectively still
ignores it but minus the potential leaks.

See gh-19647
---
 .../reactive/server/DefaultWebTestClient.java |  9 +++++-
 .../web/reactive/server/WebTestClient.java    | 28 ++++++-------------
 2 files changed, 17 insertions(+), 20 deletions(-)

diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java
index c9dce22f2a7..56a41deaa15 100644
--- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java
+++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java
@@ -378,7 +378,14 @@ class DefaultWebTestClient implements WebTestClient {
 
 		@Override
 		public  FluxExchangeResult returnResult(Class elementClass) {
-			Flux body = this.response.bodyToFlux(elementClass);
+			Flux body;
+			if (elementClass.equals(Void.class)) {
+				this.response.releaseBody().block();
+				body = Flux.empty();
+			}
+			else {
+				body = this.response.bodyToFlux(elementClass);
+			}
 			return new FluxExchangeResult<>(this.exchangeResult, body);
 		}
 
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 6b55d52075e..ed9b1ecde7a 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
@@ -172,7 +172,7 @@ public interface WebTestClient {
 	// Static factory methods
 
 	/**
-	 * Use this server setup to test one `@Controller` at a time.
+	 * Use this server setup to test one {@code @Controller} at a time.
 	 * This option loads the default configuration of
 	 * {@link org.springframework.web.reactive.config.EnableWebFlux @EnableWebFlux}.
 	 * There are builder methods to customize the Java config. The resulting
@@ -229,8 +229,8 @@ public interface WebTestClient {
 	}
 
 	/**
-	 * This server setup option allows you to connect to a running server via
-	 * Reactor Netty.
+	 * This server setup option allows you to connect to a live server through
+	 * a Reactor Netty client connector.
 	 * 

 	 * WebTestClient client = WebTestClient.bindToServer()
 	 *         .baseUrl("http://localhost:8080")
@@ -244,11 +244,6 @@ public interface WebTestClient {
 
 	/**
 	 * A variant of {@link #bindToServer()} with a pre-configured connector.
-	 * 

-	 * WebTestClient client = WebTestClient.bindToServer()
-	 *         .baseUrl("http://localhost:8080")
-	 *         .build();
-	 * 
* @return chained API to customize client config * @since 5.0.2 */ @@ -802,18 +797,13 @@ public interface WebTestClient { BodyContentSpec expectBody(); /** - * Exit the chained API and consume the response body externally. This - * is useful for testing infinite streams (e.g. SSE) where you need to - * to assert decoded objects as they come and then cancel at some point - * when test objectives are met. Consider using {@code StepVerifier} - * from {@literal "reactor-test"} to assert the {@code Flux} stream - * of decoded objects. + * Exit the chained flow in order to consume the response body + * externally, e.g. via {@link reactor.test.StepVerifier}. * - *

Note: Do not use this option for cases where there - * is no content (e.g. 204, 4xx) or you're not interested in the content. - * For such cases you can use {@code expectBody().isEmpty()} or - * {@code expectBody(Void.class)} which ensures that resources are - * released regardless of whether the response has content or not. + *

Note that when {@code Void.class} is passed in, the response body + * is consumed and released. If no content is expected, then consider + * using {@code .expectBody().isEmpty()} instead which asserts that + * there is no content. */ FluxExchangeResult returnResult(Class elementClass); From f500ab0f9b0185cb5ccf784622de753a052b8421 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 19 Aug 2020 17:47:49 +0100 Subject: [PATCH 4/7] Add mechanism to expose mock server results WebTestClient is an actual client and generally it's only possible to assert the client response (i.e. what goes over HTTP). However, in a mock server scenario technically we have access to the server request and response and can make those available for further assertions. This will be helpful for the WebTestClient integration with MockMvc where many more assertions can be performed on the server request and response when needed. See gh-19647 --- .../web/reactive/server/ExchangeResult.java | 30 ++++++++++++++-- .../server/MockServerClientHttpResponse.java | 35 +++++++++++++++++++ .../web/reactive/server/WiretapConnector.java | 19 ++++++---- .../reactive/server/HeaderAssertionTests.java | 2 +- .../reactive/server/StatusAssertionTests.java | 2 +- 5 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 spring-test/src/main/java/org/springframework/test/web/reactive/server/MockServerClientHttpResponse.java diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/ExchangeResult.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/ExchangeResult.java index 77e514dd757..ff1c20d5020 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/ExchangeResult.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/ExchangeResult.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -72,6 +72,9 @@ public class ExchangeResult { @Nullable private final String uriTemplate; + @Nullable + final Object mockServerResult; + /** * Create an instance with an HTTP request and response along with promises @@ -83,9 +86,11 @@ public class ExchangeResult { * @param responseBody capture of serialized response body content * @param timeout how long to wait for content to materialize * @param uriTemplate the URI template used to set up the request, if any + * @param serverResult the result of a mock server exchange if applicable. */ ExchangeResult(ClientHttpRequest request, ClientHttpResponse response, - Mono requestBody, Mono responseBody, Duration timeout, @Nullable String uriTemplate) { + Mono requestBody, Mono responseBody, Duration timeout, @Nullable String uriTemplate, + @Nullable Object serverResult) { Assert.notNull(request, "ClientHttpRequest is required"); Assert.notNull(response, "ClientHttpResponse is required"); @@ -98,6 +103,7 @@ public class ExchangeResult { this.responseBody = responseBody; this.timeout = timeout; this.uriTemplate = uriTemplate; + this.mockServerResult = serverResult; } /** @@ -110,6 +116,7 @@ public class ExchangeResult { this.responseBody = other.responseBody; this.timeout = other.timeout; this.uriTemplate = other.uriTemplate; + this.mockServerResult = other.mockServerResult; } @@ -195,6 +202,16 @@ public class ExchangeResult { return this.responseBody.block(this.timeout); } + /** + * Return the result from the mock server exchange, if applicable, for + * further assertions on the state of the server response. + * @since 5.3 + * @see org.springframework.test.web.servlet.client.MockMvcTestClient#resultActionsFor(ExchangeResult) + */ + @Nullable + public Object getMockServerResult() { + return this.mockServerResult; + } /** * Execute the given Runnable, catch any {@link AssertionError}, decorate @@ -222,7 +239,8 @@ public class ExchangeResult { "< " + getStatus() + " " + getStatus().getReasonPhrase() + "\n" + "< " + formatHeaders(getResponseHeaders(), "\n< ") + "\n" + "\n" + - formatBody(getResponseHeaders().getContentType(), this.responseBody) +"\n"; + formatBody(getResponseHeaders().getContentType(), this.responseBody) +"\n" + + formatMockServerResult(); } private String formatHeaders(HttpHeaders headers, String delimiter) { @@ -252,4 +270,10 @@ public class ExchangeResult { .block(this.timeout); } + private String formatMockServerResult() { + return (this.mockServerResult != null ? + "\n====================== MockMvc (Server) ===============================\n" + + this.mockServerResult + "\n" : ""); + } + } diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/MockServerClientHttpResponse.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/MockServerClientHttpResponse.java new file mode 100644 index 00000000000..8fe12d92659 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/MockServerClientHttpResponse.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2020 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 + * + * https://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 org.springframework.http.client.reactive.ClientHttpResponse; + +/** + * Simple {@link ClientHttpResponse} extension that also exposes a result object + * from the underlying mock server exchange for further assertions on the state + * of the server response after the request is performed. + * + * @author Rossen Stoyanchev + * @since 5.3 + */ +public interface MockServerClientHttpResponse extends ClientHttpResponse { + + /** + * Return the result object with the server request and response. + */ + Object getServerResult(); + +} 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 fcc8fc48f97..76329b6a6b0 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 @@ -88,15 +88,16 @@ class WiretapConnector implements ClientHttpConnector { * Create the {@link ExchangeResult} for the given "request-id" header value. */ ExchangeResult getExchangeResult(String requestId, @Nullable String uriTemplate, Duration timeout) { - ClientExchangeInfo info = this.exchanges.remove(requestId); - Assert.state(info != null, () -> { + ClientExchangeInfo clientInfo = this.exchanges.remove(requestId); + Assert.state(clientInfo != null, () -> { String header = WebTestClient.WEBTESTCLIENT_REQUEST_ID; return "No match for " + header + "=" + requestId; }); - return new ExchangeResult(info.getRequest(), info.getResponse(), - info.getRequest().getRecorder().getContent(), - info.getResponse().getRecorder().getContent(), - timeout, uriTemplate); + return new ExchangeResult(clientInfo.getRequest(), clientInfo.getResponse(), + clientInfo.getRequest().getRecorder().getContent(), + clientInfo.getResponse().getRecorder().getContent(), + timeout, uriTemplate, + clientInfo.getResponse().getMockServerResult()); } @@ -279,6 +280,12 @@ class WiretapConnector implements ClientHttpConnector { public Flux getBody() { return Flux.from(this.recorder.getPublisherToUse()); } + + @Nullable + public Object getMockServerResult() { + return (getDelegate() instanceof MockServerClientHttpResponse ? + ((MockServerClientHttpResponse) getDelegate()).getServerResult() : null); + } } } diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/HeaderAssertionTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/HeaderAssertionTests.java index 8cec305eb36..dd5cebe838f 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/HeaderAssertionTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/HeaderAssertionTests.java @@ -244,7 +244,7 @@ class HeaderAssertionTests { MonoProcessor emptyContent = MonoProcessor.fromSink(Sinks.one()); emptyContent.onComplete(); - ExchangeResult result = new ExchangeResult(request, response, emptyContent, emptyContent, Duration.ZERO, null); + ExchangeResult result = new ExchangeResult(request, response, emptyContent, emptyContent, Duration.ZERO, null, null); return new HeaderAssertions(result, mock(WebTestClient.ResponseSpec.class)); } diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/StatusAssertionTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/StatusAssertionTests.java index 9f06a724664..c9fd9b68d06 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/StatusAssertionTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/StatusAssertionTests.java @@ -160,7 +160,7 @@ class StatusAssertionTests { MonoProcessor emptyContent = MonoProcessor.fromSink(Sinks.one()); emptyContent.onComplete(); - ExchangeResult result = new ExchangeResult(request, response, emptyContent, emptyContent, Duration.ZERO, null); + ExchangeResult result = new ExchangeResult(request, response, emptyContent, emptyContent, Duration.ZERO, null, null); return new StatusAssertions(result, mock(WebTestClient.ResponseSpec.class)); } From 6e8bb6c4a98e2671b62f3e21250685cfd1e16cee Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 19 Aug 2020 17:52:11 +0100 Subject: [PATCH 5/7] WebTestClient header assertion improvements Provides parity with similar options in MockMvc: - compare header using a long value - compare header using a date/time value - dedicated method for "Location" header (redirect) - let Hamcrest assert a header even when missing See gh-19647 --- .../web/reactive/server/HeaderAssertions.java | 67 +++++++++++++++++-- 1 file changed, 61 insertions(+), 6 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java index efb0d904ece..82813c6894a 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java @@ -16,6 +16,7 @@ package org.springframework.test.web.reactive.server; +import java.net.URI; import java.util.Arrays; import java.util.List; import java.util.function.Consumer; @@ -31,6 +32,10 @@ import org.springframework.lang.Nullable; import org.springframework.test.util.AssertionErrors; import org.springframework.util.CollectionUtils; +import static org.springframework.test.util.AssertionErrors.assertEquals; +import static org.springframework.test.util.AssertionErrors.assertNotNull; +import static org.springframework.test.util.AssertionErrors.assertTrue; + /** * Assertions on headers of the response. * @@ -57,7 +62,42 @@ public class HeaderAssertions { * Expect a header with the given name to match the specified values. */ public WebTestClient.ResponseSpec valueEquals(String headerName, String... values) { - return assertHeader(headerName, Arrays.asList(values), getHeaders().get(headerName)); + return assertHeader(headerName, Arrays.asList(values), getHeaders().getOrEmpty(headerName)); + } + + /** + * Expect a header with the given name to match the given long value. + * @since 5.3 + */ + public WebTestClient.ResponseSpec valueEquals(String headerName, long value) { + String actual = getHeaders().getFirst(headerName); + this.exchangeResult.assertWithDiagnostics(() -> + assertTrue("Response does not contain header '" + headerName + "'", actual != null)); + return assertHeader(headerName, value, Long.parseLong(actual)); + } + + /** + * Expect a header with the given name to match the specified long value + * parsed into a date using the preferred date format described in RFC 7231. + *

An {@link AssertionError} is thrown if the response does not contain + * the specified header, or if the supplied {@code value} does not match the + * primary header value. + * @since 5.3 + */ + public WebTestClient.ResponseSpec valueEqualsDate(String headerName, long value) { + this.exchangeResult.assertWithDiagnostics(() -> { + String headerValue = getHeaders().getFirst(headerName); + assertNotNull("Response does not contain header '" + headerName + "'", headerValue); + + HttpHeaders headers = new HttpHeaders(); + headers.setDate("expected", value); + headers.set("actual", headerValue); + + assertEquals("Response header '" + headerName + "'='" + headerValue + "' " + + "does not match expected value '" + headers.getFirst("expected") + "'", + headers.getFirstDate("expected"), headers.getFirstDate("actual")); + }); + return this.responseSpec; } /** @@ -106,8 +146,11 @@ public class HeaderAssertions { * @since 5.1 */ public WebTestClient.ResponseSpec value(String name, Matcher matcher) { - String value = getRequiredValue(name); - this.exchangeResult.assertWithDiagnostics(() -> MatcherAssert.assertThat(value, matcher)); + String value = getHeaders().getFirst(name); + this.exchangeResult.assertWithDiagnostics(() -> { + String message = getMessage(name); + MatcherAssert.assertThat(message, value, matcher); + }); return this.responseSpec; } @@ -118,8 +161,11 @@ public class HeaderAssertions { * @since 5.3 */ public WebTestClient.ResponseSpec values(String name, Matcher> matcher) { - List values = getRequiredValues(name); - this.exchangeResult.assertWithDiagnostics(() -> MatcherAssert.assertThat(values, matcher)); + List values = getHeaders().get(name); + this.exchangeResult.assertWithDiagnostics(() -> { + String message = getMessage(name); + MatcherAssert.assertThat(message, values, matcher); + }); return this.responseSpec; } @@ -154,7 +200,8 @@ public class HeaderAssertions { private List getRequiredValues(String name) { List values = getHeaders().get(name); if (CollectionUtils.isEmpty(values)) { - AssertionErrors.fail(getMessage(name) + " not found"); + this.exchangeResult.assertWithDiagnostics(() -> + AssertionErrors.fail(getMessage(name) + " not found")); } return values; } @@ -249,6 +296,14 @@ public class HeaderAssertions { return assertHeader("Last-Modified", lastModified, getHeaders().getLastModified()); } + /** + * Expect a "Location" header with the given value. + * @since 5.3 + */ + public WebTestClient.ResponseSpec location(String location) { + return assertHeader("Location", URI.create(location), getHeaders().getLocation()); + } + private HttpHeaders getHeaders() { return this.exchangeResult.getResponseHeaders(); From 128acaff8a0318e67cc6bf1ff77f36e877abbe8a Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 19 Aug 2020 21:11:53 +0100 Subject: [PATCH 6/7] WebTestClient cookie assertion support See gh-19647 --- .../reactive/MockClientHttpResponse.java | 2 +- .../web/reactive/server/CookieAssertions.java | 220 ++++++++++++++++++ .../reactive/server/DefaultWebTestClient.java | 5 + .../web/reactive/server/WebTestClient.java | 6 + .../reactive/server/CookieAssertionTests.java | 138 +++++++++++ 5 files changed, 370 insertions(+), 1 deletion(-) create mode 100644 spring-test/src/main/java/org/springframework/test/web/reactive/server/CookieAssertions.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/reactive/server/CookieAssertionTests.java diff --git a/spring-test/src/main/java/org/springframework/mock/http/client/reactive/MockClientHttpResponse.java b/spring-test/src/main/java/org/springframework/mock/http/client/reactive/MockClientHttpResponse.java index 8c584848089..c2e950025f4 100644 --- a/spring-test/src/main/java/org/springframework/mock/http/client/reactive/MockClientHttpResponse.java +++ b/spring-test/src/main/java/org/springframework/mock/http/client/reactive/MockClientHttpResponse.java @@ -80,7 +80,7 @@ public class MockClientHttpResponse implements ClientHttpResponse { public HttpHeaders getHeaders() { if (!getCookies().isEmpty() && this.headers.get(HttpHeaders.SET_COOKIE) == null) { getCookies().values().stream().flatMap(Collection::stream) - .forEach(cookie -> getHeaders().add(HttpHeaders.SET_COOKIE, cookie.toString())); + .forEach(cookie -> this.headers.add(HttpHeaders.SET_COOKIE, cookie.toString())); } return this.headers; } diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/CookieAssertions.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/CookieAssertions.java new file mode 100644 index 00000000000..9d1cad06598 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/CookieAssertions.java @@ -0,0 +1,220 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.time.Duration; +import java.util.function.Consumer; + +import org.hamcrest.Matcher; +import org.hamcrest.MatcherAssert; + +import org.springframework.http.ResponseCookie; +import org.springframework.test.util.AssertionErrors; + +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Assertions on cookies of the response. + * @author Rossen Stoyanchev + */ +public class CookieAssertions { + + private final ExchangeResult exchangeResult; + + private final WebTestClient.ResponseSpec responseSpec; + + + public CookieAssertions(ExchangeResult exchangeResult, WebTestClient.ResponseSpec responseSpec) { + this.exchangeResult = exchangeResult; + this.responseSpec = responseSpec; + } + + + /** + * Expect a header with the given name to match the specified values. + */ + public WebTestClient.ResponseSpec valueEquals(String name, String value) { + this.exchangeResult.assertWithDiagnostics(() -> { + String message = getMessage(name); + AssertionErrors.assertEquals(message, value, getCookie(name).getValue()); + }); + return this.responseSpec; + } + + /** + * Assert the first value of the response cookie with a Hamcrest {@link Matcher}. + */ + public WebTestClient.ResponseSpec value(String name, Matcher matcher) { + String value = getCookie(name).getValue(); + this.exchangeResult.assertWithDiagnostics(() -> { + String message = getMessage(name); + MatcherAssert.assertThat(message, value, matcher); + }); + return this.responseSpec; + } + + /** + * Consume the value of the response cookie. + */ + public WebTestClient.ResponseSpec value(String name, Consumer consumer) { + String value = getCookie(name).getValue(); + this.exchangeResult.assertWithDiagnostics(() -> consumer.accept(value)); + return this.responseSpec; + } + + /** + * Expect that the cookie with the given name is present. + */ + public WebTestClient.ResponseSpec exists(String name) { + getCookie(name); + return this.responseSpec; + } + + /** + * Expect that the cookie with the given name is not present. + */ + public WebTestClient.ResponseSpec doesNotExist(String name) { + ResponseCookie cookie = this.exchangeResult.getResponseCookies().getFirst(name); + if (cookie != null) { + String message = getMessage(name) + " exists with value=[" + cookie.getValue() + "]"; + this.exchangeResult.assertWithDiagnostics(() -> AssertionErrors.fail(message)); + } + return this.responseSpec; + } + + /** + * Assert a cookie's maxAge attribute. + */ + public WebTestClient.ResponseSpec maxAge(String name, Duration expected) { + Duration maxAge = getCookie(name).getMaxAge(); + this.exchangeResult.assertWithDiagnostics(() -> { + String message = getMessage(name) + " maxAge"; + AssertionErrors.assertEquals(message, expected, maxAge); + }); + return this.responseSpec; + } + + /** + * Assert a cookie's maxAge attribute with a Hamcrest {@link Matcher}. + */ + public WebTestClient.ResponseSpec maxAge(String name, Matcher matcher) { + long maxAge = getCookie(name).getMaxAge().getSeconds(); + this.exchangeResult.assertWithDiagnostics(() -> { + String message = getMessage(name) + " maxAge"; + assertThat(message, maxAge, matcher); + }); + return this.responseSpec; + } + + /** + * Assert a cookie's path attribute. + */ + public WebTestClient.ResponseSpec path(String name, String expected) { + String path = getCookie(name).getPath(); + this.exchangeResult.assertWithDiagnostics(() -> { + String message = getMessage(name) + " path"; + AssertionErrors.assertEquals(message, expected, path); + }); + return this.responseSpec; + } + + /** + * Assert a cookie's path attribute with a Hamcrest {@link Matcher}. + */ + public WebTestClient.ResponseSpec path(String name, Matcher matcher) { + String path = getCookie(name).getPath(); + this.exchangeResult.assertWithDiagnostics(() -> { + String message = getMessage(name) + " path"; + assertThat(message, path, matcher); + }); + return this.responseSpec; + } + + /** + * Assert a cookie's domain attribute. + */ + public WebTestClient.ResponseSpec domain(String name, String expected) { + String path = getCookie(name).getDomain(); + this.exchangeResult.assertWithDiagnostics(() -> { + String message = getMessage(name) + " domain"; + AssertionErrors.assertEquals(message, expected, path); + }); + return this.responseSpec; + } + + /** + * Assert a cookie's domain attribute with a Hamcrest {@link Matcher}. + */ + public WebTestClient.ResponseSpec domain(String name, Matcher matcher) { + String domain = getCookie(name).getDomain(); + this.exchangeResult.assertWithDiagnostics(() -> { + String message = getMessage(name) + " domain"; + assertThat(message, domain, matcher); + }); + return this.responseSpec; + } + + /** + * Assert a cookie's secure attribute. + */ + public WebTestClient.ResponseSpec secure(String name, boolean expected) { + boolean isSecure = getCookie(name).isSecure(); + this.exchangeResult.assertWithDiagnostics(() -> { + String message = getMessage(name) + " secure"; + AssertionErrors.assertEquals(message, expected, isSecure); + }); + return this.responseSpec; + } + + /** + * Assert a cookie's httpOnly attribute. + */ + public WebTestClient.ResponseSpec httpOnly(String name, boolean expected) { + boolean isHttpOnly = getCookie(name).isHttpOnly(); + this.exchangeResult.assertWithDiagnostics(() -> { + String message = getMessage(name) + " secure"; + AssertionErrors.assertEquals(message, expected, isHttpOnly); + }); + return this.responseSpec; + } + + /** + * Assert a cookie's sameSite attribute. + */ + public WebTestClient.ResponseSpec sameSite(String name, String expected) { + String sameSite = getCookie(name).getSameSite(); + this.exchangeResult.assertWithDiagnostics(() -> { + String message = getMessage(name) + " secure"; + AssertionErrors.assertEquals(message, expected, sameSite); + }); + return this.responseSpec; + } + + + private ResponseCookie getCookie(String name) { + ResponseCookie cookie = this.exchangeResult.getResponseCookies().getFirst(name); + if (cookie == null) { + String message = "No cookie with name '" + name + "'"; + this.exchangeResult.assertWithDiagnostics(() -> AssertionErrors.fail(message)); + } + return cookie; + } + + private String getMessage(String cookie) { + return "Response cookie '" + cookie + "'"; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java index 56a41deaa15..2c3bff24944 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java @@ -337,6 +337,11 @@ class DefaultWebTestClient implements WebTestClient { return new HeaderAssertions(this.exchangeResult, this); } + @Override + public CookieAssertions expectCookie() { + return new CookieAssertions(this.exchangeResult, this); + } + @Override public BodySpec expectBody(Class bodyType) { B body = this.response.bodyToMono(bodyType).block(this.timeout); 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 ed9b1ecde7a..4018ac1acde 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 @@ -764,6 +764,12 @@ public interface WebTestClient { */ HeaderAssertions expectHeader(); + /** + * Assertions on the cookies of the response. + * @since 5.3 + */ + CookieAssertions expectCookie(); + /** * Consume and decode the response body to a single object of type * {@code } and then apply assertions. diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/CookieAssertionTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/CookieAssertionTests.java new file mode 100644 index 00000000000..426dc7b3445 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/CookieAssertionTests.java @@ -0,0 +1,138 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.net.URI; +import java.time.Duration; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.MonoProcessor; +import reactor.core.publisher.Sinks; + +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseCookie; +import org.springframework.mock.http.client.reactive.MockClientHttpRequest; +import org.springframework.mock.http.client.reactive.MockClientHttpResponse; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link CookieAssertions} + * @author Rossen Stoyanchev + */ +public class CookieAssertionTests { + + private final ResponseCookie cookie = ResponseCookie.from("foo", "bar") + .maxAge(Duration.ofMinutes(30)) + .domain("foo.com") + .path("/foo") + .secure(true) + .httpOnly(true) + .sameSite("Lax") + .build(); + + private final CookieAssertions assertions = cookieAssertions(cookie); + + + @Test + void valueEquals() { + assertions.valueEquals("foo", "bar"); + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.valueEquals("what?!", "bar")); + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.valueEquals("foo", "what?!")); + } + + @Test + void value() { + assertions.value("foo", equalTo("bar")); + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.value("foo", equalTo("what?!"))); + } + + @Test + void exists() { + assertions.exists("foo"); + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.exists("what?!")); + } + + @Test + void doesNotExist() { + assertions.doesNotExist("what?!"); + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.doesNotExist("foo")); + } + + @Test + void maxAge() { + assertions.maxAge("foo", Duration.ofMinutes(30)); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertions.maxAge("foo", Duration.ofMinutes(29))); + + assertions.maxAge("foo", equalTo(Duration.ofMinutes(30).getSeconds())); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertions.maxAge("foo", equalTo(Duration.ofMinutes(29).getSeconds()))); + } + + @Test + void domain() { + assertions.domain("foo", "foo.com"); + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.domain("foo", "what.com")); + + assertions.domain("foo", equalTo("foo.com")); + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.domain("foo", equalTo("what.com"))); + } + + @Test + void path() { + assertions.path("foo", "/foo"); + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.path("foo", "/what")); + + assertions.path("foo", equalTo("/foo")); + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.path("foo", equalTo("/what"))); + } + + @Test + void secure() { + assertions.secure("foo", true); + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.secure("foo", false)); + } + + @Test + void httpOnly() { + assertions.httpOnly("foo", true); + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.httpOnly("foo", false)); + } + + @Test + void sameSite() { + assertions.sameSite("foo", "Lax"); + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.sameSite("foo", "Strict")); + } + + + private CookieAssertions cookieAssertions(ResponseCookie cookie) { + MockClientHttpRequest request = new MockClientHttpRequest(HttpMethod.GET, URI.create("/")); + MockClientHttpResponse response = new MockClientHttpResponse(HttpStatus.OK); + response.getCookies().add(cookie.getName(), cookie); + + MonoProcessor emptyContent = MonoProcessor.fromSink(Sinks.one()); + emptyContent.onComplete(); + + ExchangeResult result = new ExchangeResult(request, response, emptyContent, emptyContent, Duration.ZERO, null, null); + return new CookieAssertions(result, mock(WebTestClient.ResponseSpec.class)); + } + +} From 3426e6274c1b9aeaf12727ef0b47c46c5a4ae1b6 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 19 Aug 2020 21:12:04 +0100 Subject: [PATCH 7/7] Add MockMvcTestClient See gh-19647 --- .../client/AbstractMockMvcServerSpec.java | 105 +++++ .../client/ApplicationContextMockMvcSpec.java | 43 ++ .../servlet/client/MockMvcHttpConnector.java | 301 +++++++++++++ .../web/servlet/client/MockMvcTestClient.java | 380 ++++++++++++++++ .../servlet/client/StandaloneMockMvcSpec.java | 176 ++++++++ .../test/web/servlet/client/package-info.java | 13 + .../AsyncControllerJavaConfigTests.java | 119 +++++ .../client/context/JavaConfigTests.java | 148 +++++++ .../client/context/WebAppResourceTests.java | 98 +++++ .../client/context/XmlConfigTests.java | 84 ++++ .../samples/client/standalone/AsyncTests.java | 221 ++++++++++ .../standalone/ExceptionHandlerTests.java | 238 ++++++++++ .../client/standalone/FilterTests.java | 323 ++++++++++++++ .../standalone/FrameworkExtensionTests.java | 112 +++++ .../standalone/MultipartControllerTests.java | 405 ++++++++++++++++++ .../standalone/ReactiveReturnTypeTests.java | 71 +++ .../client/standalone/RedirectTests.java | 163 +++++++ .../standalone/RequestParameterTests.java | 62 +++ .../client/standalone/ResponseBodyTests.java | 90 ++++ .../standalone/ViewResolutionTests.java | 183 ++++++++ .../PrintingResultHandlerSmokeTests.java | 84 ++++ .../resultmatches/ContentAssertionTests.java | 145 +++++++ .../resultmatches/CookieAssertionTests.java | 120 ++++++ .../FlashAttributeAssertionTests.java | 103 +++++ .../resultmatches/HandlerAssertionTests.java | 106 +++++ .../resultmatches/HeaderAssertionTests.java | 272 ++++++++++++ .../resultmatches/JsonPathAssertionTests.java | 152 +++++++ .../resultmatches/ModelAssertionTests.java | 135 ++++++ .../RequestAttributeAssertionTests.java | 92 ++++ .../SessionAttributeAssertionTests.java | 106 +++++ .../resultmatches/StatusAssertionTests.java | 106 +++++ .../resultmatches/UrlAssertionTests.java | 90 ++++ .../resultmatches/ViewNameAssertionTests.java | 72 ++++ .../XmlContentAssertionTests.java | 120 ++++++ .../resultmatches/XpathAssertionTests.java | 236 ++++++++++ .../samples/context/PersonController.java | 2 +- .../samples/standalone/AsyncTests.java | 56 +-- .../standalone/ExceptionHandlerTests.java | 12 +- .../PrintingResultHandlerSmokeTests.java | 5 +- .../FlashAttributeAssertionTests.java | 2 +- .../resultmatchers/ModelAssertionTests.java | 2 +- .../RequestAttributeAssertionTests.java | 2 +- .../SessionAttributeAssertionTests.java | 2 +- 43 files changed, 5299 insertions(+), 58 deletions(-) create mode 100644 spring-test/src/main/java/org/springframework/test/web/servlet/client/AbstractMockMvcServerSpec.java create mode 100644 spring-test/src/main/java/org/springframework/test/web/servlet/client/ApplicationContextMockMvcSpec.java create mode 100644 spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcHttpConnector.java create mode 100644 spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcTestClient.java create mode 100644 spring-test/src/main/java/org/springframework/test/web/servlet/client/StandaloneMockMvcSpec.java create mode 100644 spring-test/src/main/java/org/springframework/test/web/servlet/client/package-info.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/context/AsyncControllerJavaConfigTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/context/JavaConfigTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/context/WebAppResourceTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/context/XmlConfigTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/AsyncTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ExceptionHandlerTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/FilterTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/FrameworkExtensionTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/MultipartControllerTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ReactiveReturnTypeTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/RedirectTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/RequestParameterTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ResponseBodyTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ViewResolutionTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resulthandlers/PrintingResultHandlerSmokeTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/ContentAssertionTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/CookieAssertionTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/FlashAttributeAssertionTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/HandlerAssertionTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/HeaderAssertionTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/JsonPathAssertionTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/ModelAssertionTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/RequestAttributeAssertionTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/SessionAttributeAssertionTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/StatusAssertionTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/UrlAssertionTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/ViewNameAssertionTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/XmlContentAssertionTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/XpathAssertionTests.java diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/client/AbstractMockMvcServerSpec.java b/spring-test/src/main/java/org/springframework/test/web/servlet/client/AbstractMockMvcServerSpec.java new file mode 100644 index 00000000000..0e8dd4c2aa7 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/client/AbstractMockMvcServerSpec.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.servlet.client; + +import javax.servlet.Filter; + +import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.DispatcherServletCustomizer; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.RequestBuilder; +import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; +import org.springframework.test.web.servlet.setup.MockMvcConfigurer; + +/** + * Base class for implementations of {@link MockMvcTestClient.MockMvcServerSpec} + * that simply delegates to a {@link ConfigurableMockMvcBuilder} supplied by + * the concrete sub-classes. + * + * @author Rossen Stoyanchev + * @since 5.3 + * @param the type of the concrete sub-class spec + */ +abstract class AbstractMockMvcServerSpec> + implements MockMvcTestClient.MockMvcServerSpec { + + @Override + public T filters(Filter... filters) { + getMockMvcBuilder().addFilters(filters); + return self(); + } + + public final T filter(Filter filter, String... urlPatterns) { + getMockMvcBuilder().addFilter(filter, urlPatterns); + return self(); + } + + @Override + public T defaultRequest(RequestBuilder requestBuilder) { + getMockMvcBuilder().defaultRequest(requestBuilder); + return self(); + } + + @Override + public T alwaysExpect(ResultMatcher resultMatcher) { + getMockMvcBuilder().alwaysExpect(resultMatcher); + return self(); + } + + @Override + public T dispatchOptions(boolean dispatchOptions) { + getMockMvcBuilder().dispatchOptions(dispatchOptions); + return self(); + } + + @Override + public T dispatcherServletCustomizer(DispatcherServletCustomizer customizer) { + getMockMvcBuilder().addDispatcherServletCustomizer(customizer); + return self(); + } + + @Override + public T apply(MockMvcConfigurer configurer) { + getMockMvcBuilder().apply(configurer); + return self(); + } + + @SuppressWarnings("unchecked") + private T self() { + return (T) this; + } + + /** + * Return the concrete {@link ConfigurableMockMvcBuilder} to delegate + * configuration methods and to use to create the {@link MockMvc}. + */ + protected abstract ConfigurableMockMvcBuilder getMockMvcBuilder(); + + @Override + public WebTestClient.Builder configureClient() { + MockMvc mockMvc = getMockMvcBuilder().build(); + ClientHttpConnector connector = new MockMvcHttpConnector(mockMvc); + return WebTestClient.bindToServer(connector); + } + + @Override + public WebTestClient build() { + return configureClient().build(); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/client/ApplicationContextMockMvcSpec.java b/spring-test/src/main/java/org/springframework/test/web/servlet/client/ApplicationContextMockMvcSpec.java new file mode 100644 index 00000000000..1c6c3577798 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/client/ApplicationContextMockMvcSpec.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.servlet.client; + +import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; +import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +/** + * Simple wrapper around a {@link DefaultMockMvcBuilder}. + * + * @author Rossen Stoyanchev + * @since 5.3 + */ +class ApplicationContextMockMvcSpec extends AbstractMockMvcServerSpec { + + private final DefaultMockMvcBuilder mockMvcBuilder; + + + public ApplicationContextMockMvcSpec(WebApplicationContext context) { + this.mockMvcBuilder = MockMvcBuilders.webAppContextSetup(context); + } + + @Override + protected ConfigurableMockMvcBuilder getMockMvcBuilder() { + return this.mockMvcBuilder; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcHttpConnector.java b/spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcHttpConnector.java new file mode 100644 index 00000000000..705e89432b7 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcHttpConnector.java @@ -0,0 +1,301 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.servlet.client; + +import java.io.StringWriter; +import java.net.URI; +import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + +import javax.servlet.http.Cookie; + +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.core.io.buffer.DefaultDataBuffer; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.http.HttpCookie; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ReactiveHttpInputMessage; +import org.springframework.http.ResponseCookie; +import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.http.client.reactive.ClientHttpRequest; +import org.springframework.http.client.reactive.ClientHttpResponse; +import org.springframework.http.codec.multipart.DefaultPartHttpMessageReader; +import org.springframework.http.codec.multipart.FilePart; +import org.springframework.http.codec.multipart.Part; +import org.springframework.lang.Nullable; +import org.springframework.mock.http.client.reactive.MockClientHttpRequest; +import org.springframework.mock.http.client.reactive.MockClientHttpResponse; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockPart; +import org.springframework.test.web.reactive.server.MockServerClientHttpResponse; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.RequestBuilder; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.FlashMap; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; + +/** + * Connector that handles requests by invoking a {@link MockMvc} rather than + * making actual requests over HTTP. + * + * @author Rossen Stoyanchev + * @since 5.3 + */ +public class MockMvcHttpConnector implements ClientHttpConnector { + + private static final DefaultPartHttpMessageReader MULTIPART_READER = new DefaultPartHttpMessageReader(); + + private static final Duration TIMEOUT = Duration.ofSeconds(5); + + + private final MockMvc mockMvc; + + + public MockMvcHttpConnector(MockMvc mockMvc) { + this.mockMvc = mockMvc; + } + + + @Override + public Mono connect( + HttpMethod method, URI uri, Function> requestCallback) { + + RequestBuilder requestBuilder = adaptRequest(method, uri, requestCallback); + try { + MvcResult mvcResult = this.mockMvc.perform(requestBuilder).andReturn(); + if (mvcResult.getRequest().isAsyncStarted()) { + mvcResult.getAsyncResult(); + mvcResult = this.mockMvc.perform(asyncDispatch(mvcResult)).andReturn(); + } + return Mono.just(adaptResponse(mvcResult)); + } + catch (Exception ex) { + return Mono.error(ex); + } + } + + private RequestBuilder adaptRequest( + HttpMethod httpMethod, URI uri, Function> requestCallback) { + + MockClientHttpRequest httpRequest = new MockClientHttpRequest(httpMethod, uri); + + AtomicReference contentRef = new AtomicReference<>(); + httpRequest.setWriteHandler(dataBuffers -> + DataBufferUtils.join(dataBuffers) + .doOnNext(buffer -> { + byte[] bytes = new byte[buffer.readableByteCount()]; + buffer.read(bytes); + DataBufferUtils.release(buffer); + contentRef.set(bytes); + }) + .then()); + + // Initialize the client request + requestCallback.apply(httpRequest).block(TIMEOUT); + + MockHttpServletRequestBuilder requestBuilder = + initRequestBuilder(httpMethod, uri, httpRequest, contentRef.get()); + + requestBuilder.headers(httpRequest.getHeaders()); + for (List cookies : httpRequest.getCookies().values()) { + for (HttpCookie cookie : cookies) { + requestBuilder.cookie(new Cookie(cookie.getName(), cookie.getValue())); + } + } + + return requestBuilder; + } + + private MockHttpServletRequestBuilder initRequestBuilder( + HttpMethod httpMethod, URI uri, MockClientHttpRequest httpRequest, @Nullable byte[] bytes) { + + String contentType = httpRequest.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE); + if (!StringUtils.startsWithIgnoreCase(contentType, "multipart/")) { + MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.request(httpMethod, uri); + if (!ObjectUtils.isEmpty(bytes)) { + requestBuilder.content(bytes); + } + return requestBuilder; + } + + // Parse the multipart request in order to adapt to Servlet Part's + MockMultipartHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.multipart(uri); + + Assert.notNull(bytes, "No multipart content"); + ReactiveHttpInputMessage inputMessage = MockServerHttpRequest.post(uri.toString()) + .headers(httpRequest.getHeaders()) + .body(Mono.just(DefaultDataBufferFactory.sharedInstance.wrap(bytes))); + + MULTIPART_READER.read(ResolvableType.forClass(Part.class), inputMessage, Collections.emptyMap()) + .flatMap(part -> + DataBufferUtils.join(part.content()) + .doOnNext(buffer -> { + byte[] partBytes = new byte[buffer.readableByteCount()]; + buffer.read(partBytes); + DataBufferUtils.release(buffer); + + // Adapt to javax.servlet.http.Part... + MockPart mockPart = (part instanceof FilePart ? + new MockPart(part.name(), ((FilePart) part).filename(), partBytes) : + new MockPart(part.name(), partBytes)); + mockPart.getHeaders().putAll(part.headers()); + requestBuilder.part(mockPart); + })) + .blockLast(TIMEOUT); + + return requestBuilder; + } + + private MockClientHttpResponse adaptResponse(MvcResult mvcResult) { + MockClientHttpResponse clientResponse = new MockMvcServerClientHttpResponse(mvcResult); + MockHttpServletResponse servletResponse = mvcResult.getResponse(); + for (String header : servletResponse.getHeaderNames()) { + for (String value : servletResponse.getHeaders(header)) { + clientResponse.getHeaders().add(header, value); + } + } + if (servletResponse.getForwardedUrl() != null) { + clientResponse.getHeaders().add("Forwarded-Url", servletResponse.getForwardedUrl()); + } + for (Cookie cookie : servletResponse.getCookies()) { + ResponseCookie httpCookie = + ResponseCookie.fromClientResponse(cookie.getName(), cookie.getValue()) + .maxAge(Duration.ofSeconds(cookie.getMaxAge())) + .domain(cookie.getDomain()) + .path(cookie.getPath()) + .secure(cookie.getSecure()) + .httpOnly(cookie.isHttpOnly()) + .build(); + clientResponse.getCookies().add(httpCookie.getName(), httpCookie); + } + byte[] bytes = servletResponse.getContentAsByteArray(); + DefaultDataBuffer dataBuffer = DefaultDataBufferFactory.sharedInstance.wrap(bytes); + clientResponse.setBody(Mono.just(dataBuffer)); + return clientResponse; + } + + + private static class MockMvcServerClientHttpResponse + extends MockClientHttpResponse implements MockServerClientHttpResponse { + + private final MvcResult mvcResult; + + + public MockMvcServerClientHttpResponse(MvcResult result) { + super(result.getResponse().getStatus()); + this.mvcResult = new PrintingMvcResult(result); + } + + @Override + public Object getServerResult() { + return this.mvcResult; + } + } + + + private static class PrintingMvcResult implements MvcResult { + + private final MvcResult mvcResult; + + public PrintingMvcResult(MvcResult mvcResult) { + this.mvcResult = mvcResult; + } + + @Override + public MockHttpServletRequest getRequest() { + return this.mvcResult.getRequest(); + } + + @Override + public MockHttpServletResponse getResponse() { + return this.mvcResult.getResponse(); + } + + @Nullable + @Override + public Object getHandler() { + return this.mvcResult.getHandler(); + } + + @Nullable + @Override + public HandlerInterceptor[] getInterceptors() { + return this.mvcResult.getInterceptors(); + } + + @Nullable + @Override + public ModelAndView getModelAndView() { + return this.mvcResult.getModelAndView(); + } + + @Nullable + @Override + public Exception getResolvedException() { + return this.mvcResult.getResolvedException(); + } + + @Override + public FlashMap getFlashMap() { + return this.mvcResult.getFlashMap(); + } + + @Override + public Object getAsyncResult() { + return this.mvcResult.getAsyncResult(); + } + + @Override + public Object getAsyncResult(long timeToWait) { + return this.mvcResult.getAsyncResult(timeToWait); + } + + @Override + public String toString() { + StringWriter writer = new StringWriter(); + try { + MockMvcResultHandlers.print(writer).handle(this); + } + catch (Exception ex) { + writer.append("Unable to format ") + .append(String.valueOf(this)) + .append(": ") + .append(ex.getMessage()); + } + return writer.toString(); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcTestClient.java b/spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcTestClient.java new file mode 100644 index 00000000000..8e481389e52 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcTestClient.java @@ -0,0 +1,380 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.servlet.client; + +import java.util.function.Supplier; + +import javax.servlet.Filter; + +import org.springframework.format.support.FormattingConversionService; +import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.lang.Nullable; +import org.springframework.test.web.reactive.server.ExchangeResult; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.DispatcherServletCustomizer; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.RequestBuilder; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.ResultHandler; +import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; +import org.springframework.test.web.servlet.setup.MockMvcConfigurer; +import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder; +import org.springframework.util.Assert; +import org.springframework.validation.Validator; +import org.springframework.web.accept.ContentNegotiationManager; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.HandlerMethodReturnValueHandler; +import org.springframework.web.servlet.FlashMapManager; +import org.springframework.web.servlet.HandlerExceptionResolver; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.servlet.View; +import org.springframework.web.servlet.ViewResolver; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.util.pattern.PathPatternParser; + +/** + * The main class for testing Spring MVC applications via {@link WebTestClient} + * with {@link MockMvc} for server request handling. + * + *

Provides static factory methods and specs to initialize {@code MockMvc} + * to which the {@code WebTestClient} connects to. For example: + *

+ * WebTestClient client = MockMvcTestClient.bindToController(myController)
+ *         .controllerAdvice(myControllerAdvice)
+ *         .validator(myValidator)
+ *         .build()
+ * 
+ * + *

The client itself can also be configured. For example: + *

+ * WebTestClient client = MockMvcTestClient.bindToController(myController)
+ *         .validator(myValidator)
+ *         .configureClient()
+ *         .baseUrl("/path")
+ *         .build();
+ * 
+ * + * @author Rossen Stoyanchev + * @since 5.3 + */ +public interface MockMvcTestClient { + + /** + * Begin creating a {@link WebTestClient} by providing the {@code @Controller} + * instance(s) to handle requests with. + *

Internally this is delegated to and equivalent to using + * {@link org.springframework.test.web.servlet.setup.MockMvcBuilders#standaloneSetup(Object...)}. + * to initialize {@link MockMvc}. + */ + static ControllerSpec bindToController(Object... controllers) { + return new StandaloneMockMvcSpec(controllers); + } + + /** + * Begin creating a {@link WebTestClient} by providing a + * {@link WebApplicationContext} with Spring MVC infrastructure and + * controllers. + *

Internally this is delegated to and equivalent to using + * {@link org.springframework.test.web.servlet.setup.MockMvcBuilders#webAppContextSetup(WebApplicationContext)} + * to initialize {@code MockMvc}. + */ + static MockMvcServerSpec bindToApplicationContext(WebApplicationContext context) { + return new ApplicationContextMockMvcSpec(context); + } + + /** + * Begin creating a {@link WebTestClient} by providing an already + * initialized {@link MockMvc} instance to use as the server. + */ + static WebTestClient.Builder bindTo(MockMvc mockMvc) { + ClientHttpConnector connector = new MockMvcHttpConnector(mockMvc); + return WebTestClient.bindToServer(connector); + } + + /** + * This method can be used to apply further assertions on a given + * {@link ExchangeResult} based the state of the server response. + * + *

Normally {@link WebTestClient} is used to assert the client response + * including HTTP status, headers, and body. That is all that is available + * when making a live request over HTTP. However when the server is + * {@link MockMvc}, many more assertions are possible against the server + * response, e.g. model attributes, flash attributes, etc. + * + *

Example: + *

+	 * EntityExchangeResult<Void> result =
+	 * 		webTestClient.post().uri("/people/123")
+	 * 				.exchange()
+	 * 				.expectStatus().isFound()
+	 * 				.expectHeader().location("/persons/Joe")
+	 * 				.expectBody().isEmpty();
+	 *
+	 * MockMvcTestClient.resultActionsFor(result)
+	 * 		.andExpect(model().size(1))
+	 * 		.andExpect(model().attributeExists("name"))
+	 * 		.andExpect(flash().attributeCount(1))
+	 * 		.andExpect(flash().attribute("message", "success!"));
+	 * 
+ * + *

Note: this method works only if the {@link WebTestClient} used to + * perform the request was initialized through one of bind method in this + * class, and therefore requests are handled by {@link MockMvc}. + */ + static ResultActions resultActionsFor(ExchangeResult exchangeResult) { + Object serverResult = exchangeResult.getMockServerResult(); + Assert.notNull(serverResult, "No MvcResult"); + Assert.isInstanceOf(MvcResult.class, serverResult); + return new ResultActions() { + @Override + public ResultActions andExpect(ResultMatcher matcher) throws Exception { + matcher.match((MvcResult) serverResult); + return this; + } + @Override + public ResultActions andDo(ResultHandler handler) throws Exception { + handler.handle((MvcResult) serverResult); + return this; + } + @Override + public MvcResult andReturn() { + return (MvcResult) serverResult; + } + }; + } + + + /** + * Base specification for configuring {@link MockMvc}, and a simple facade + * around {@link ConfigurableMockMvcBuilder}. + * + * @param a self reference to the builder type + */ + interface MockMvcServerSpec> { + + /** + * Add a global filter. + *

This is delegated to + * {@link ConfigurableMockMvcBuilder#addFilters(Filter...)}. + */ + T filters(Filter... filters); + + /** + * Add a filter for specific URL patterns. + *

This is delegated to + * {@link ConfigurableMockMvcBuilder#addFilter(Filter, String...)}. + */ + T filter(Filter filter, String... urlPatterns); + + /** + * Define default request properties that should be merged into all + * performed requests such that input from the client request override + * the default properties defined here. + *

This is delegated to + * {@link ConfigurableMockMvcBuilder#defaultRequest(RequestBuilder)}. + */ + T defaultRequest(RequestBuilder requestBuilder); + + /** + * Define a global expectation that should always be applied to + * every response. + *

This is delegated to + * {@link ConfigurableMockMvcBuilder#alwaysExpect(ResultMatcher)}. + */ + T alwaysExpect(ResultMatcher resultMatcher); + + /** + * Whether to handle HTTP OPTIONS requests. + *

This is delegated to + * {@link ConfigurableMockMvcBuilder#dispatchOptions(boolean)}. + */ + T dispatchOptions(boolean dispatchOptions); + + /** + * Allow customization of {@code DispatcherServlet}. + *

This is delegated to + * {@link ConfigurableMockMvcBuilder#addDispatcherServletCustomizer(DispatcherServletCustomizer)}. + */ + T dispatcherServletCustomizer(DispatcherServletCustomizer customizer); + + /** + * Add a {@code MockMvcConfigurer} that automates MockMvc setup. + *

This is delegated to + * {@link ConfigurableMockMvcBuilder#apply(MockMvcConfigurer)}. + */ + T apply(MockMvcConfigurer configurer); + + /** + * Proceed to configure and build the test client. + */ + WebTestClient.Builder configureClient(); + + /** + * Shortcut to build the test client. + */ + WebTestClient build(); + } + + + /** + * Specification for configuring {@link MockMvc} to test one or more + * controllers directly, and a simple facade around + * {@link StandaloneMockMvcBuilder}. + */ + interface ControllerSpec extends MockMvcServerSpec { + + /** + * Register {@link org.springframework.web.bind.annotation.ControllerAdvice} + *

This is delegated to + * {@link StandaloneMockMvcBuilder#setControllerAdvice(Object...)}. + */ + ControllerSpec controllerAdvice(Object... controllerAdvice); + + /** + * Set the message converters to use. + *

This is delegated to + * {@link StandaloneMockMvcBuilder#setMessageConverters(HttpMessageConverter[])}. + */ + ControllerSpec messageConverters(HttpMessageConverter... messageConverters); + + /** + * Provide a custom {@link Validator}. + *

This is delegated to + * {@link StandaloneMockMvcBuilder#setValidator(Validator)}. + */ + ControllerSpec validator(Validator validator); + + /** + * Provide a conversion service. + *

This is delegated to + * {@link StandaloneMockMvcBuilder#setConversionService(FormattingConversionService)}. + */ + ControllerSpec conversionService(FormattingConversionService conversionService); + + /** + * Add global interceptors. + *

This is delegated to + * {@link StandaloneMockMvcBuilder#addInterceptors(HandlerInterceptor...)}. + */ + ControllerSpec interceptors(HandlerInterceptor... interceptors); + + /** + * Add interceptors for specific patterns. + *

This is delegated to + * {@link StandaloneMockMvcBuilder#addMappedInterceptors(String[], HandlerInterceptor...)}. + */ + ControllerSpec mappedInterceptors( + @Nullable String[] pathPatterns, HandlerInterceptor... interceptors); + + /** + * Set a ContentNegotiationManager. + *

This is delegated to + * {@link StandaloneMockMvcBuilder#setContentNegotiationManager(ContentNegotiationManager)}. + */ + ControllerSpec contentNegotiationManager(ContentNegotiationManager manager); + + /** + * Specify the timeout value for async execution. + *

This is delegated to + * {@link StandaloneMockMvcBuilder#setAsyncRequestTimeout(long)}. + */ + ControllerSpec asyncRequestTimeout(long timeout); + + /** + * Provide custom argument resolvers. + *

This is delegated to + * {@link StandaloneMockMvcBuilder#setCustomArgumentResolvers(HandlerMethodArgumentResolver...)}. + */ + ControllerSpec customArgumentResolvers(HandlerMethodArgumentResolver... argumentResolvers); + + /** + * Provide custom return value handlers. + *

This is delegated to + * {@link StandaloneMockMvcBuilder#setCustomReturnValueHandlers(HandlerMethodReturnValueHandler...)}. + */ + ControllerSpec customReturnValueHandlers(HandlerMethodReturnValueHandler... handlers); + + /** + * Set the HandlerExceptionResolver types to use. + *

This is delegated to + * {@link StandaloneMockMvcBuilder#setHandlerExceptionResolvers(HandlerExceptionResolver...)}. + */ + ControllerSpec handlerExceptionResolvers(HandlerExceptionResolver... exceptionResolvers); + + /** + * Set up view resolution. + *

This is delegated to + * {@link StandaloneMockMvcBuilder#setViewResolvers(ViewResolver...)}. + */ + ControllerSpec viewResolvers(ViewResolver... resolvers); + + /** + * Set up a single {@link ViewResolver} with a fixed view. + *

This is delegated to + * {@link StandaloneMockMvcBuilder#setSingleView(View)}. + */ + ControllerSpec singleView(View view); + + /** + * Provide the LocaleResolver to use. + *

This is delegated to + * {@link StandaloneMockMvcBuilder#setLocaleResolver(LocaleResolver)}. + */ + ControllerSpec localeResolver(LocaleResolver localeResolver); + + /** + * Provide a custom FlashMapManager. + *

This is delegated to + * {@link StandaloneMockMvcBuilder#setFlashMapManager(FlashMapManager)}. + */ + ControllerSpec flashMapManager(FlashMapManager flashMapManager); + + /** + * Enable URL path matching with parsed + * {@link org.springframework.web.util.pattern.PathPattern PathPatterns}. + *

This is delegated to + * {@link StandaloneMockMvcBuilder#setPatternParser(PathPatternParser)}. + */ + ControllerSpec patternParser(PathPatternParser parser); + + /** + * Whether to match trailing slashes. + *

This is delegated to + * {@link StandaloneMockMvcBuilder#setUseTrailingSlashPatternMatch(boolean)}. + */ + ControllerSpec useTrailingSlashPatternMatch(boolean useTrailingSlashPatternMatch); + + /** + * Configure placeholder values to use. + *

This is delegated to + * {@link StandaloneMockMvcBuilder#addPlaceholderValue(String, String)}. + */ + ControllerSpec placeholderValue(String name, String value); + + /** + * Configure factory for a custom {@link RequestMappingHandlerMapping}. + *

This is delegated to + * {@link StandaloneMockMvcBuilder#setCustomHandlerMapping(Supplier)}. + */ + ControllerSpec customHandlerMapping(Supplier factory); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/client/StandaloneMockMvcSpec.java b/spring-test/src/main/java/org/springframework/test/web/servlet/client/StandaloneMockMvcSpec.java new file mode 100644 index 00000000000..42264027681 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/client/StandaloneMockMvcSpec.java @@ -0,0 +1,176 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.servlet.client; + +import java.util.function.Supplier; + +import org.springframework.format.support.FormattingConversionService; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.lang.Nullable; +import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder; +import org.springframework.validation.Validator; +import org.springframework.web.accept.ContentNegotiationManager; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.HandlerMethodReturnValueHandler; +import org.springframework.web.servlet.FlashMapManager; +import org.springframework.web.servlet.HandlerExceptionResolver; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.servlet.View; +import org.springframework.web.servlet.ViewResolver; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.util.pattern.PathPatternParser; + +/** + * Simple wrapper around a {@link StandaloneMockMvcBuilder} that implements + * {@link MockMvcTestClient.ControllerSpec}. + * + * @author Rossen Stoyanchev + * @since 5.3 + */ +class StandaloneMockMvcSpec extends AbstractMockMvcServerSpec + implements MockMvcTestClient.ControllerSpec { + + private final StandaloneMockMvcBuilder mockMvcBuilder; + + + StandaloneMockMvcSpec(Object... controllers) { + this.mockMvcBuilder = MockMvcBuilders.standaloneSetup(controllers); + } + + @Override + public StandaloneMockMvcSpec controllerAdvice(Object... controllerAdvice) { + this.mockMvcBuilder.setControllerAdvice(controllerAdvice); + return this; + } + + @Override + public StandaloneMockMvcSpec messageConverters(HttpMessageConverter... messageConverters) { + this.mockMvcBuilder.setMessageConverters(messageConverters); + return this; + } + + @Override + public StandaloneMockMvcSpec validator(Validator validator) { + this.mockMvcBuilder.setValidator(validator); + return this; + } + + @Override + public StandaloneMockMvcSpec conversionService(FormattingConversionService conversionService) { + this.mockMvcBuilder.setConversionService(conversionService); + return this; + } + + @Override + public StandaloneMockMvcSpec interceptors(HandlerInterceptor... interceptors) { + mappedInterceptors(null, interceptors); + return this; + } + + @Override + public StandaloneMockMvcSpec mappedInterceptors( + @Nullable String[] pathPatterns, HandlerInterceptor... interceptors) { + + this.mockMvcBuilder.addMappedInterceptors(pathPatterns, interceptors); + return this; + } + + @Override + public StandaloneMockMvcSpec contentNegotiationManager(ContentNegotiationManager manager) { + this.mockMvcBuilder.setContentNegotiationManager(manager); + return this; + } + + @Override + public StandaloneMockMvcSpec asyncRequestTimeout(long timeout) { + this.mockMvcBuilder.setAsyncRequestTimeout(timeout); + return this; + } + + @Override + public StandaloneMockMvcSpec customArgumentResolvers(HandlerMethodArgumentResolver... argumentResolvers) { + this.mockMvcBuilder.setCustomArgumentResolvers(argumentResolvers); + return this; + } + + @Override + public StandaloneMockMvcSpec customReturnValueHandlers(HandlerMethodReturnValueHandler... handlers) { + this.mockMvcBuilder.setCustomReturnValueHandlers(handlers); + return this; + } + + @Override + public StandaloneMockMvcSpec handlerExceptionResolvers(HandlerExceptionResolver... exceptionResolvers) { + this.mockMvcBuilder.setHandlerExceptionResolvers(exceptionResolvers); + return this; + } + + @Override + public StandaloneMockMvcSpec viewResolvers(ViewResolver... resolvers) { + this.mockMvcBuilder.setViewResolvers(resolvers); + return this; + } + + @Override + public StandaloneMockMvcSpec singleView(View view) { + this.mockMvcBuilder.setSingleView(view); + return this; + } + + @Override + public StandaloneMockMvcSpec localeResolver(LocaleResolver localeResolver) { + this.mockMvcBuilder.setLocaleResolver(localeResolver); + return this; + } + + @Override + public StandaloneMockMvcSpec flashMapManager(FlashMapManager flashMapManager) { + this.mockMvcBuilder.setFlashMapManager(flashMapManager); + return this; + } + + @Override + public StandaloneMockMvcSpec patternParser(PathPatternParser parser) { + this.mockMvcBuilder.setPatternParser(parser); + return this; + } + + @Override + public StandaloneMockMvcSpec useTrailingSlashPatternMatch(boolean useTrailingSlashPatternMatch) { + this.mockMvcBuilder.setUseTrailingSlashPatternMatch(useTrailingSlashPatternMatch); + return this; + } + + @Override + public StandaloneMockMvcSpec placeholderValue(String name, String value) { + this.mockMvcBuilder.addPlaceholderValue(name, value); + return this; + } + + @Override + public StandaloneMockMvcSpec customHandlerMapping(Supplier factory) { + this.mockMvcBuilder.setCustomHandlerMapping(factory); + return this; + } + + @Override + public ConfigurableMockMvcBuilder getMockMvcBuilder() { + return this.mockMvcBuilder; + } +} diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/client/package-info.java b/spring-test/src/main/java/org/springframework/test/web/servlet/client/package-info.java new file mode 100644 index 00000000000..7323682c55d --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/client/package-info.java @@ -0,0 +1,13 @@ +/** + * Support for testing Spring MVC applications via + * {@link org.springframework.test.web.reactive.server.WebTestClient} + * with {@link org.springframework.test.web.servlet.MockMvc} for server request + * handling. + */ + +@NonNullApi +@NonNullFields +package org.springframework.test.web.servlet.client; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/context/AsyncControllerJavaConfigTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/context/AsyncControllerJavaConfigTests.java new file mode 100644 index 00000000000..1908958a63c --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/context/AsyncControllerJavaConfigTests.java @@ -0,0 +1,119 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.servlet.samples.client.context; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.Callable; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.client.MockMvcTestClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.request.async.CallableProcessingInterceptor; +import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import static org.mockito.ArgumentMatchers.any; + +/** + * MockMvcTestClient equivalent of the MockMvc + * {@link org.springframework.test.web.servlet.samples.context.AsyncControllerJavaConfigTests}. + * + * @author Rossen Stoyanchev + */ +@ExtendWith(SpringExtension.class) +@WebAppConfiguration +@ContextHierarchy(@ContextConfiguration(classes = AsyncControllerJavaConfigTests.WebConfig.class)) +public class AsyncControllerJavaConfigTests { + + @Autowired + private WebApplicationContext wac; + + @Autowired + private CallableProcessingInterceptor callableInterceptor; + + private WebTestClient testClient; + + + @BeforeEach + public void setup() { + this.testClient = MockMvcTestClient.bindToApplicationContext(this.wac).build(); + } + + @Test + public void callableInterceptor() throws Exception { + testClient.get().uri("/callable") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isOk() + .expectBody().json("{\"key\":\"value\"}"); + + Mockito.verify(this.callableInterceptor).beforeConcurrentHandling(any(), any()); + Mockito.verify(this.callableInterceptor).preProcess(any(), any()); + Mockito.verify(this.callableInterceptor).postProcess(any(), any(), any()); + Mockito.verify(this.callableInterceptor).afterCompletion(any(), any()); + Mockito.verifyNoMoreInteractions(this.callableInterceptor); + } + + + @Configuration + @EnableWebMvc + static class WebConfig implements WebMvcConfigurer { + + @Override + public void configureAsyncSupport(AsyncSupportConfigurer configurer) { + configurer.registerCallableInterceptors(callableInterceptor()); + } + + @Bean + public CallableProcessingInterceptor callableInterceptor() { + return Mockito.mock(CallableProcessingInterceptor.class); + } + + @Bean + public AsyncController asyncController() { + return new AsyncController(); + } + + } + + @RestController + static class AsyncController { + + @GetMapping("/callable") + public Callable> getCallable() { + return () -> Collections.singletonMap("key", "value"); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/context/JavaConfigTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/context/JavaConfigTests.java new file mode 100644 index 00000000000..5ddd1859ad7 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/context/JavaConfigTests.java @@ -0,0 +1,148 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.servlet.samples.client.context; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.Person; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.client.MockMvcTestClient; +import org.springframework.test.web.servlet.samples.context.PersonController; +import org.springframework.test.web.servlet.samples.context.PersonDao; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; +import org.springframework.web.servlet.config.annotation.ViewResolverRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.view.tiles3.TilesConfigurer; + +import static org.mockito.BDDMockito.given; + +/** + * MockMvcTestClient equivalent of the MockMvc + * {@link org.springframework.test.web.servlet.samples.context.JavaConfigTests}. + * + * @author Rossen Stoyanchev + */ +@ExtendWith(SpringExtension.class) +@WebAppConfiguration("classpath:META-INF/web-resources") +@ContextHierarchy({ + @ContextConfiguration(classes = JavaConfigTests.RootConfig.class), + @ContextConfiguration(classes = JavaConfigTests.WebConfig.class) +}) +public class JavaConfigTests { + + @Autowired + private WebApplicationContext wac; + + @Autowired + private PersonDao personDao; + + @Autowired + private PersonController personController; + + private WebTestClient testClient; + + + @BeforeEach + public void setup() { + this.testClient = MockMvcTestClient.bindToApplicationContext(this.wac).build(); + given(this.personDao.getPerson(5L)).willReturn(new Person("Joe")); + } + + + @Test + public void person() { + testClient.get().uri("/person/5") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isOk() + .expectBody().json("{\"name\":\"Joe\",\"someDouble\":0.0,\"someBoolean\":false}"); + } + + @Test + public void tilesDefinitions() { + testClient.get().uri("/") + .exchange() + .expectStatus().isOk() + .expectHeader().valueEquals("Forwarded-Url", "/WEB-INF/layouts/standardLayout.jsp"); + } + + + @Configuration + static class RootConfig { + + @Bean + public PersonDao personDao() { + return Mockito.mock(PersonDao.class); + } + } + + @Configuration + @EnableWebMvc + static class WebConfig implements WebMvcConfigurer { + + @Autowired + private RootConfig rootConfig; + + @Bean + public PersonController personController() { + return new PersonController(this.rootConfig.personDao()); + } + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/resources/**").addResourceLocations("/resources/"); + } + + @Override + public void addViewControllers(ViewControllerRegistry registry) { + registry.addViewController("/").setViewName("home"); + } + + @Override + public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { + configurer.enable(); + } + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.tiles(); + } + + @Bean + public TilesConfigurer tilesConfigurer() { + TilesConfigurer configurer = new TilesConfigurer(); + configurer.setDefinitions("/WEB-INF/**/tiles.xml"); + return configurer; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/context/WebAppResourceTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/context/WebAppResourceTests.java new file mode 100644 index 00000000000..93457f87fd1 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/context/WebAppResourceTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.servlet.samples.client.context; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.reactive.server.EntityExchangeResult; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.client.MockMvcTestClient; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler; + +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.handler; + +/** + * MockMvcTestClient equivalent of the MockMvc + * {@link org.springframework.test.web.servlet.samples.context.WebAppResourceTests}. + * + * @author Rossen Stoyanchev + */ +@ExtendWith(SpringExtension.class) +@WebAppConfiguration("src/test/resources/META-INF/web-resources") +@ContextHierarchy({ + @ContextConfiguration("../../context/root-context.xml"), + @ContextConfiguration("../../context/servlet-context.xml") +}) +public class WebAppResourceTests { + + @Autowired + private WebApplicationContext wac; + + private WebTestClient testClient; + + + @BeforeEach + public void setup() { + this.testClient = MockMvcTestClient.bindToApplicationContext(this.wac).build(); + } + + // TilesConfigurer: resources under "/WEB-INF/**/tiles.xml" + + @Test + public void tilesDefinitions() { + testClient.get().uri("/") + .exchange() + .expectStatus().isOk() + .expectHeader().valueEquals("Forwarded-Url", "/WEB-INF/layouts/standardLayout.jsp"); + } + + // Resources served via + + @Test + public void resourceRequest() { + testClient.get().uri("/resources/Spring.js") + .exchange() + .expectStatus().isOk() + .expectHeader().contentType("application/javascript") + .expectBody(String.class).value(containsString("Spring={};")); + } + + // Forwarded to the "default" servlet via + + @Test + public void resourcesViaDefaultServlet() throws Exception { + EntityExchangeResult result = testClient.get().uri("/unknown/resource") + .exchange() + .expectStatus().isOk() + .expectBody().isEmpty(); + + MockMvcTestClient.resultActionsFor(result) + .andExpect(handler().handlerType(DefaultServletHttpRequestHandler.class)) + .andExpect(forwardedUrl("default")); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/context/XmlConfigTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/context/XmlConfigTests.java new file mode 100644 index 00000000000..79ca42bf844 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/context/XmlConfigTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.servlet.samples.client.context; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextHierarchy; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.Person; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.client.MockMvcTestClient; +import org.springframework.test.web.servlet.samples.context.PersonDao; +import org.springframework.web.context.WebApplicationContext; + +import static org.mockito.BDDMockito.given; + +/** + * MockMvcTestClient equivalent of the MockMvc + * {@link org.springframework.test.web.servlet.samples.context.XmlConfigTests}. + * + * @author Rossen Stoyanchev + */ +@ExtendWith(SpringExtension.class) +@WebAppConfiguration("src/test/resources/META-INF/web-resources") +@ContextHierarchy({ + @ContextConfiguration("../../context/root-context.xml"), + @ContextConfiguration("../../context/servlet-context.xml") +}) +public class XmlConfigTests { + + @Autowired + private WebApplicationContext wac; + + @Autowired + private PersonDao personDao; + + private WebTestClient testClient; + + + @BeforeEach + public void setup() { + this.testClient = MockMvcTestClient.bindToApplicationContext(this.wac).build(); + given(this.personDao.getPerson(5L)).willReturn(new Person("Joe")); + } + + + @Test + public void person() { + testClient.get().uri("/person/5") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isOk() + .expectBody().json("{\"name\":\"Joe\",\"someDouble\":0.0,\"someBoolean\":false}"); + } + + @Test + public void tilesDefinitions() { + testClient.get().uri("/") + .exchange() + .expectStatus().isOk() + .expectHeader().valueEquals("Forwarded-Url", "/WEB-INF/layouts/standardLayout.jsp"); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/AsyncTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/AsyncTests.java new file mode 100644 index 00000000000..707602bfeb4 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/AsyncTests.java @@ -0,0 +1,221 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.servlet.samples.client.standalone; + +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.test.web.Person; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.client.MockMvcTestClient; +import org.springframework.util.concurrent.ListenableFuture; +import org.springframework.util.concurrent.ListenableFutureTask; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.async.DeferredResult; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; + +/** + * MockMvcTestClient equivalent of the MockMvc + * {@link org.springframework.test.web.servlet.samples.standalone.AsyncTests}. + * + * @author Rossen Stoyanchev + */ +public class AsyncTests { + + private final WebTestClient testClient = + MockMvcTestClient.bindToController(new AsyncController()).build(); + + + @Test + public void callable() { + this.testClient.get() + .uri("/1?callable=true") + .exchange() + .expectStatus().isOk() + .expectHeader().contentType(MediaType.APPLICATION_JSON) + .expectBody().json("{\"name\":\"Joe\",\"someDouble\":0.0,\"someBoolean\":false}"); + } + + @Test + public void streaming() { + this.testClient.get() + .uri("/1?streaming=true") + .exchange() + .expectStatus().isOk() + .expectBody(String.class).isEqualTo("name=Joe"); + } + + @Test + public void streamingSlow() { + this.testClient.get() + .uri("/1?streamingSlow=true") + .exchange() + .expectStatus().isOk() + .expectBody(String.class).isEqualTo("name=Joe&someBoolean=true"); + } + + @Test + public void streamingJson() { + this.testClient.get() + .uri("/1?streamingJson=true") + .exchange() + .expectStatus().isOk() + .expectHeader().contentType(MediaType.APPLICATION_JSON) + .expectBody().json("{\"name\":\"Joe\",\"someDouble\":0.5}"); + } + + @Test + public void deferredResult() { + this.testClient.get() + .uri("/1?deferredResult=true") + .exchange() + .expectStatus().isOk() + .expectHeader().contentType(MediaType.APPLICATION_JSON) + .expectBody().json("{\"name\":\"Joe\",\"someDouble\":0.0,\"someBoolean\":false}"); + } + + @Test + public void deferredResultWithImmediateValue() throws Exception { + this.testClient.get() + .uri("/1?deferredResultWithImmediateValue=true") + .exchange() + .expectStatus().isOk() + .expectHeader().contentType(MediaType.APPLICATION_JSON) + .expectBody().json("{\"name\":\"Joe\",\"someDouble\":0.0,\"someBoolean\":false}"); + } + + @Test + public void deferredResultWithDelayedError() throws Exception { + this.testClient.get() + .uri("/1?deferredResultWithDelayedError=true") + .exchange() + .expectStatus().is5xxServerError() + .expectBody(String.class).isEqualTo("Delayed Error"); + } + + @Test + public void listenableFuture() throws Exception { + this.testClient.get() + .uri("/1?listenableFuture=true") + .exchange() + .expectStatus().isOk() + .expectHeader().contentType(MediaType.APPLICATION_JSON) + .expectBody().json("{\"name\":\"Joe\",\"someDouble\":0.0,\"someBoolean\":false}"); + } + + @Test + public void completableFutureWithImmediateValue() throws Exception { + this.testClient.get() + .uri("/1?completableFutureWithImmediateValue=true") + .exchange() + .expectStatus().isOk() + .expectHeader().contentType(MediaType.APPLICATION_JSON) + .expectBody().json("{\"name\":\"Joe\",\"someDouble\":0.0,\"someBoolean\":false}"); + } + + + @RestController + @RequestMapping(path = "/{id}", produces = "application/json") + private static class AsyncController { + + @RequestMapping(params = "callable") + public Callable getCallable() { + return () -> new Person("Joe"); + } + + @RequestMapping(params = "streaming") + public StreamingResponseBody getStreaming() { + return os -> os.write("name=Joe".getBytes(StandardCharsets.UTF_8)); + } + + @RequestMapping(params = "streamingSlow") + public StreamingResponseBody getStreamingSlow() { + return os -> { + os.write("name=Joe".getBytes()); + try { + Thread.sleep(200); + os.write("&someBoolean=true".getBytes(StandardCharsets.UTF_8)); + } + catch (InterruptedException e) { + /* no-op */ + } + }; + } + + @RequestMapping(params = "streamingJson") + public ResponseEntity getStreamingJson() { + return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON) + .body(os -> os.write("{\"name\":\"Joe\",\"someDouble\":0.5}".getBytes(StandardCharsets.UTF_8))); + } + + @RequestMapping(params = "deferredResult") + public DeferredResult getDeferredResult() { + DeferredResult result = new DeferredResult<>(); + delay(100, () -> result.setResult(new Person("Joe"))); + return result; + } + + @RequestMapping(params = "deferredResultWithImmediateValue") + public DeferredResult getDeferredResultWithImmediateValue() { + DeferredResult result = new DeferredResult<>(); + result.setResult(new Person("Joe")); + return result; + } + + @RequestMapping(params = "deferredResultWithDelayedError") + public DeferredResult getDeferredResultWithDelayedError() { + DeferredResult result = new DeferredResult<>(); + delay(100, () -> result.setErrorResult(new RuntimeException("Delayed Error"))); + return result; + } + + @RequestMapping(params = "listenableFuture") + public ListenableFuture getListenableFuture() { + ListenableFutureTask futureTask = new ListenableFutureTask<>(() -> new Person("Joe")); + delay(100, futureTask); + return futureTask; + } + + @RequestMapping(params = "completableFutureWithImmediateValue") + public CompletableFuture getCompletableFutureWithImmediateValue() { + CompletableFuture future = new CompletableFuture<>(); + future.complete(new Person("Joe")); + return future; + } + + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public String errorHandler(Exception ex) { + return ex.getMessage(); + } + + private void delay(long millis, Runnable task) { + Mono.delay(Duration.ofMillis(millis)).doOnTerminate(task).subscribe(); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ExceptionHandlerTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ExceptionHandlerTests.java new file mode 100644 index 00000000000..6dce2699cce --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ExceptionHandlerTests.java @@ -0,0 +1,238 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.servlet.samples.client.standalone; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.client.MockMvcTestClient; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +/** + * MockMvcTestClient equivalent of the MockMvc + * {@link org.springframework.test.web.servlet.samples.standalone.ExceptionHandlerTests}. + * + * @author Rossen Stoyanchev + */ +class ExceptionHandlerTests { + + @Nested + class MvcTests { + + @Test + void localExceptionHandlerMethod() { + WebTestClient client = MockMvcTestClient.bindToController(new PersonController()).build(); + + client.get().uri("/person/Clyde") + .exchange() + .expectStatus().isOk() + .expectHeader().valueEquals("Forwarded-Url", "errorView"); + } + + @Test + void globalExceptionHandlerMethod() { + WebTestClient client = MockMvcTestClient.bindToController(new PersonController()) + .controllerAdvice(new GlobalExceptionHandler()) + .build(); + + client.get().uri("/person/Bonnie") + .exchange() + .expectStatus().isOk() + .expectHeader().valueEquals("Forwarded-Url", "globalErrorView"); + } + } + + + @Controller + private static class PersonController { + + @GetMapping("/person/{name}") + String show(@PathVariable String name) { + if (name.equals("Clyde")) { + throw new IllegalArgumentException("simulated exception"); + } + else if (name.equals("Bonnie")) { + throw new IllegalStateException("simulated exception"); + } + return "person/show"; + } + + @ExceptionHandler + String handleException(IllegalArgumentException exception) { + return "errorView"; + } + } + + @ControllerAdvice + private static class GlobalExceptionHandler { + + @ExceptionHandler + String handleException(IllegalStateException exception) { + return "globalErrorView"; + } + } + + + @Nested + class RestTests { + + @Test + void noException() { + WebTestClient client = MockMvcTestClient.bindToController(new RestPersonController()) + .controllerAdvice(new RestPersonControllerExceptionHandler()) + .build(); + + client.get().uri("/person/Yoda") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isOk() + .expectBody().jsonPath("$.name", "Yoda"); + } + + @Test + void localExceptionHandlerMethod() { + WebTestClient client = MockMvcTestClient.bindToController(new RestPersonController()) + .controllerAdvice(new RestPersonControllerExceptionHandler()) + .build(); + + client.get().uri("/person/Luke") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isOk() + .expectBody().jsonPath("$.error", "local - IllegalArgumentException"); + } + + @Test + void globalExceptionHandlerMethod() { + WebTestClient client = MockMvcTestClient.bindToController(new RestPersonController()) + .controllerAdvice(new RestGlobalExceptionHandler()) + .build(); + + client.get().uri("/person/Leia") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isOk() + .expectBody().jsonPath("$.error", "global - IllegalArgumentException"); + } + + @Test + void globalRestPersonControllerExceptionHandlerTakesPrecedenceOverGlobalExceptionHandler() { + WebTestClient client = MockMvcTestClient.bindToController(new RestPersonController()) + .controllerAdvice(RestGlobalExceptionHandler.class, RestPersonControllerExceptionHandler.class) + .build(); + + client.get().uri("/person/Leia") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isOk() + .expectBody().jsonPath("$.error", "globalPersonController - IllegalStateException"); + } + + @Test + void noHandlerFound() { + WebTestClient client = MockMvcTestClient.bindToController(new RestPersonController()) + .controllerAdvice(RestGlobalExceptionHandler.class, RestPersonControllerExceptionHandler.class) + .dispatcherServletCustomizer(servlet -> servlet.setThrowExceptionIfNoHandlerFound(true)) + .build(); + + client.get().uri("/bogus") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isOk() + .expectBody().jsonPath("$.error", "global - NoHandlerFoundException"); + } + } + + + @RestController + private static class RestPersonController { + + @GetMapping("/person/{name}") + Person get(@PathVariable String name) { + switch (name) { + case "Luke": + throw new IllegalArgumentException(); + case "Leia": + throw new IllegalStateException(); + default: + return new Person("Yoda"); + } + } + + @ExceptionHandler + Error handleException(IllegalArgumentException exception) { + return new Error("local - " + exception.getClass().getSimpleName()); + } + } + + @RestControllerAdvice(assignableTypes = RestPersonController.class) + @Order(Ordered.HIGHEST_PRECEDENCE) + private static class RestPersonControllerExceptionHandler { + + @ExceptionHandler + Error handleException(Throwable exception) { + return new Error("globalPersonController - " + exception.getClass().getSimpleName()); + } + } + + @RestControllerAdvice + @Order(Ordered.LOWEST_PRECEDENCE) + private static class RestGlobalExceptionHandler { + + @ExceptionHandler + Error handleException(Throwable exception) { + return new Error( "global - " + exception.getClass().getSimpleName()); + } + } + + static class Person { + + private final String name; + + Person(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + static class Error { + + private final String error; + + Error(String error) { + this.error = error; + } + + public String getError() { + return error; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/FilterTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/FilterTests.java new file mode 100644 index 00000000000..ea5b3a12058 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/FilterTests.java @@ -0,0 +1,323 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.servlet.samples.client.standalone; + +import java.io.IOException; +import java.security.Principal; +import java.util.concurrent.CompletableFuture; + +import javax.servlet.AsyncContext; +import javax.servlet.AsyncListener; +import javax.servlet.FilterChain; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; +import javax.validation.Valid; + +import org.junit.jupiter.api.Test; + +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.test.web.Person; +import org.springframework.test.web.reactive.server.EntityExchangeResult; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.client.MockMvcTestClient; +import org.springframework.validation.Errors; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.filter.ShallowEtagHeaderFilter; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.flash; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; + +/** + * MockMvcTestClient equivalent of the MockMvc + * {@link org.springframework.test.web.servlet.samples.standalone.FilterTests}. + * + * @author Rossen Stoyanchev + */ +public class FilterTests { + + @Test + public void whenFiltersCompleteMvcProcessesRequest() throws Exception { + WebTestClient client = MockMvcTestClient.bindToController(new PersonController()) + .filters(new ContinueFilter()) + .build(); + + EntityExchangeResult exchangeResult = client.post().uri("/persons?name=Andy") + .exchange() + .expectStatus().isFound() + .expectHeader().location("/person/1") + .expectBody().isEmpty(); + + // Further assertions on the server response + MockMvcTestClient.resultActionsFor(exchangeResult) + .andExpect(model().size(1)) + .andExpect(model().attributeExists("id")) + .andExpect(flash().attributeCount(1)) + .andExpect(flash().attribute("message", "success!")); + } + + @Test + public void filtersProcessRequest() { + WebTestClient client = MockMvcTestClient.bindToController(new PersonController()) + .filters(new ContinueFilter(), new RedirectFilter()) + .build(); + + client.post().uri("/persons?name=Andy") + .exchange() + .expectStatus().isFound() + .expectHeader().location("/login"); + } + + @Test + public void filterMappedBySuffix() { + WebTestClient client = MockMvcTestClient.bindToController(new PersonController()) + .filter(new RedirectFilter(), "*.html") + .build(); + + client.post().uri("/persons.html?name=Andy") + .exchange() + .expectStatus().isFound() + .expectHeader().location("/login"); + } + + @Test + public void filterWithExactMapping() { + WebTestClient client = MockMvcTestClient.bindToController(new PersonController()) + .filter(new RedirectFilter(), "/p", "/persons") + .build(); + + client.post().uri("/persons?name=Andy") + .exchange() + .expectStatus().isFound() + .expectHeader().location("/login"); + } + + @Test + public void filterSkipped() throws Exception { + WebTestClient client = MockMvcTestClient.bindToController(new PersonController()) + .filter(new RedirectFilter(), "/p", "/person") + .build(); + + EntityExchangeResult exchangeResult = + client.post().uri("/persons?name=Andy") + .exchange() + .expectStatus().isFound() + .expectHeader().location("/person/1") + .expectBody().isEmpty(); + + // Further assertions on the server response + MockMvcTestClient.resultActionsFor(exchangeResult) + .andExpect(model().size(1)) + .andExpect(model().attributeExists("id")) + .andExpect(flash().attributeCount(1)) + .andExpect(flash().attribute("message", "success!")); + } + + @Test + public void filterWrapsRequestResponse() throws Exception { + WebTestClient client = MockMvcTestClient.bindToController(new PersonController()) + .filter(new WrappingRequestResponseFilter()) + .build(); + + EntityExchangeResult exchangeResult = + client.post().uri("/user").exchange().expectBody().isEmpty(); + + // Further assertions on the server response + MockMvcTestClient.resultActionsFor(exchangeResult) + .andExpect(model().attribute("principal", WrappingRequestResponseFilter.PRINCIPAL_NAME)); + } + + @Test + public void filterWrapsRequestResponseAndPerformsAsyncDispatch() { + WebTestClient client = MockMvcTestClient.bindToController(new PersonController()) + .filters(new WrappingRequestResponseFilter(), new ShallowEtagHeaderFilter()) + .build(); + + client.get().uri("/persons/1") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isOk() + .expectHeader().contentLength(53) + .expectHeader().valueEquals("ETag", "\"0e37becb4f0c90709cb2e1efcc61eaa00\"") + .expectBody().json("{\"name\":\"Lukas\",\"someDouble\":0.0,\"someBoolean\":false}"); + } + + + @Controller + private static class PersonController { + + @PostMapping(path="/persons") + public String save(@Valid Person person, Errors errors, RedirectAttributes redirectAttrs) { + if (errors.hasErrors()) { + return "person/add"; + } + redirectAttrs.addAttribute("id", "1"); + redirectAttrs.addFlashAttribute("message", "success!"); + return "redirect:/person/{id}"; + } + + @PostMapping("/user") + public ModelAndView user(Principal principal) { + return new ModelAndView("user/view", "principal", principal.getName()); + } + + @GetMapping("/forward") + public String forward() { + return "forward:/persons"; + } + + @GetMapping("persons/{id}") + @ResponseBody + public CompletableFuture getPerson() { + return CompletableFuture.completedFuture(new Person("Lukas")); + } + } + + private class ContinueFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + + filterChain.doFilter(request, response); + } + } + + private static class WrappingRequestResponseFilter extends OncePerRequestFilter { + + public static final String PRINCIPAL_NAME = "WrapRequestResponseFilterPrincipal"; + + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + filterChain.doFilter(new HttpServletRequestWrapper(request) { + + @Override + public Principal getUserPrincipal() { + return () -> PRINCIPAL_NAME; + } + + // Like Spring Security does in HttpServlet3RequestFactory.. + + @Override + public AsyncContext getAsyncContext() { + return super.getAsyncContext() != null ? + new AsyncContextWrapper(super.getAsyncContext()) : null; + } + + }, new HttpServletResponseWrapper(response)); + } + } + + private class RedirectFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + response.sendRedirect("/login"); + } + } + + + private static class AsyncContextWrapper implements AsyncContext { + + private final AsyncContext delegate; + + public AsyncContextWrapper(AsyncContext delegate) { + this.delegate = delegate; + } + + @Override + public ServletRequest getRequest() { + return this.delegate.getRequest(); + } + + @Override + public ServletResponse getResponse() { + return this.delegate.getResponse(); + } + + @Override + public boolean hasOriginalRequestAndResponse() { + return this.delegate.hasOriginalRequestAndResponse(); + } + + @Override + public void dispatch() { + this.delegate.dispatch(); + } + + @Override + public void dispatch(String path) { + this.delegate.dispatch(path); + } + + @Override + public void dispatch(ServletContext context, String path) { + this.delegate.dispatch(context, path); + } + + @Override + public void complete() { + this.delegate.complete(); + } + + @Override + public void start(Runnable run) { + this.delegate.start(run); + } + + @Override + public void addListener(AsyncListener listener) { + this.delegate.addListener(listener); + } + + @Override + public void addListener(AsyncListener listener, ServletRequest req, ServletResponse res) { + this.delegate.addListener(listener, req, res); + } + + @Override + public T createListener(Class clazz) throws ServletException { + return this.delegate.createListener(clazz); + } + + @Override + public void setTimeout(long timeout) { + this.delegate.setTimeout(timeout); + } + + @Override + public long getTimeout() { + return this.delegate.getTimeout(); + } + } +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/FrameworkExtensionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/FrameworkExtensionTests.java new file mode 100644 index 00000000000..71dc5592376 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/FrameworkExtensionTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.servlet.samples.client.standalone; + +import java.security.Principal; + +import org.junit.jupiter.api.Test; + +import org.springframework.stereotype.Controller; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.client.MockMvcTestClient; +import org.springframework.test.web.servlet.request.RequestPostProcessor; +import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; +import org.springframework.test.web.servlet.setup.MockMvcConfigurerAdapter; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.context.WebApplicationContext; + +import static org.mockito.Mockito.mock; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * MockMvcTestClient equivalent of the MockMvc + * {@link org.springframework.test.web.servlet.samples.standalone.FrameworkExtensionTests}. + * + * @author Rossen Stoyanchev + */ +public class FrameworkExtensionTests { + + private final WebTestClient client = + MockMvcTestClient.bindToController(new SampleController()) + .apply(defaultSetup()) + .build(); + + + @Test + public void fooHeader() { + this.client.get().uri("/") + .header("Foo", "a=b") + .exchange() + .expectBody(String.class).isEqualTo("Foo"); + } + + @Test + public void barHeader() { + this.client.get().uri("/") + .header("Bar", "a=b") + .exchange() + .expectBody(String.class).isEqualTo("Bar"); + } + + private static TestMockMvcConfigurer defaultSetup() { + return new TestMockMvcConfigurer(); + } + + + /** + * Test {@code MockMvcConfigurer}. + */ + private static class TestMockMvcConfigurer extends MockMvcConfigurerAdapter { + + @Override + public void afterConfigurerAdded(ConfigurableMockMvcBuilder builder) { + builder.alwaysExpect(status().isOk()); + } + + @Override + public RequestPostProcessor beforeMockMvcCreated(ConfigurableMockMvcBuilder builder, + WebApplicationContext context) { + return request -> { + request.setUserPrincipal(mock(Principal.class)); + return request; + }; + } + } + + + @Controller + @RequestMapping("/") + private static class SampleController { + + @RequestMapping(headers = "Foo") + @ResponseBody + public String handleFoo(Principal principal) { + Assert.notNull(principal, "Principal must not be null"); + return "Foo"; + } + + @RequestMapping(headers = "Bar") + @ResponseBody + public String handleBar(Principal principal) { + Assert.notNull(principal, "Principal must not be null"); + return "Bar"; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/MultipartControllerTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/MultipartControllerTests.java new file mode 100644 index 00000000000..a2dbc15dfef --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/MultipartControllerTests.java @@ -0,0 +1,405 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.servlet.samples.client.standalone; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.Part; + +import org.junit.jupiter.api.Test; + +import org.springframework.http.MediaType; +import org.springframework.http.client.MultipartBodyBuilder; +import org.springframework.stereotype.Controller; +import org.springframework.test.web.reactive.server.EntityExchangeResult; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.client.MockMvcTestClient; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.multipart.MultipartFile; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; + +/** + * MockMvcTestClient equivalent of the MockMvc + * {@link org.springframework.test.web.servlet.samples.standalone.MultipartControllerTests}. + * + * @author Rossen Stoyanchev + */ +public class MultipartControllerTests { + + private final WebTestClient testClient = MockMvcTestClient.bindToController(new MultipartController()).build(); + + + @Test + public void multipartRequestWithSingleFile() throws Exception { + + byte[] fileContent = "bar".getBytes(StandardCharsets.UTF_8); + Map json = Collections.singletonMap("name", "yeeeah"); + + MultipartBodyBuilder bodyBuilder = new MultipartBodyBuilder(); + bodyBuilder.part("file", fileContent).filename("orig"); + bodyBuilder.part("json", json, MediaType.APPLICATION_JSON); + + EntityExchangeResult exchangeResult = testClient.post().uri("/multipartfile") + .bodyValue(bodyBuilder.build()) + .exchange() + .expectStatus().isFound() + .expectBody().isEmpty(); + + // Further assertions on the server response + MockMvcTestClient.resultActionsFor(exchangeResult) + .andExpect(model().attribute("fileContent", fileContent)) + .andExpect(model().attribute("jsonContent", json)); + } + + @Test + public void multipartRequestWithSingleFileNotPresent() { + testClient.post().uri("/multipartfile") + .exchange() + .expectStatus().isFound(); + } + + @Test + public void multipartRequestWithFileArray() throws Exception { + byte[] fileContent = "bar".getBytes(StandardCharsets.UTF_8); + Map json = Collections.singletonMap("name", "yeeeah"); + + MultipartBodyBuilder bodyBuilder = new MultipartBodyBuilder(); + bodyBuilder.part("file", fileContent).filename("orig"); + bodyBuilder.part("file", fileContent).filename("orig"); + bodyBuilder.part("json", json, MediaType.APPLICATION_JSON); + + EntityExchangeResult exchangeResult = testClient.post().uri("/multipartfilearray") + .bodyValue(bodyBuilder.build()) + .exchange() + .expectStatus().isFound() + .expectBody().isEmpty(); + + // Further assertions on the server response + MockMvcTestClient.resultActionsFor(exchangeResult) + .andExpect(model().attribute("fileContent", fileContent)) + .andExpect(model().attribute("jsonContent", json)); + } + + @Test + public void multipartRequestWithFileArrayNoMultipart() { + testClient.post().uri("/multipartfilearray") + .exchange() + .expectStatus().isFound(); + } + + @Test + public void multipartRequestWithOptionalFile() throws Exception { + byte[] fileContent = "bar".getBytes(StandardCharsets.UTF_8); + Map json = Collections.singletonMap("name", "yeeeah"); + + MultipartBodyBuilder bodyBuilder = new MultipartBodyBuilder(); + bodyBuilder.part("file", fileContent).filename("orig"); + bodyBuilder.part("json", json, MediaType.APPLICATION_JSON); + + EntityExchangeResult exchangeResult = testClient.post().uri("/optionalfile") + .bodyValue(bodyBuilder.build()) + .exchange() + .expectStatus().isFound() + .expectBody().isEmpty(); + + // Further assertions on the server response + MockMvcTestClient.resultActionsFor(exchangeResult) + .andExpect(model().attribute("fileContent", fileContent)) + .andExpect(model().attribute("jsonContent", json)); + } + + @Test + public void multipartRequestWithOptionalFileNotPresent() throws Exception { + Map json = Collections.singletonMap("name", "yeeeah"); + + MultipartBodyBuilder bodyBuilder = new MultipartBodyBuilder(); + bodyBuilder.part("json", json, MediaType.APPLICATION_JSON); + + EntityExchangeResult exchangeResult = testClient.post().uri("/optionalfile") + .bodyValue(bodyBuilder.build()) + .exchange() + .expectStatus().isFound() + .expectBody().isEmpty(); + + // Further assertions on the server response + MockMvcTestClient.resultActionsFor(exchangeResult) + .andExpect(model().attributeDoesNotExist("fileContent")) + .andExpect(model().attribute("jsonContent", json)); + } + + @Test + public void multipartRequestWithOptionalFileArray() throws Exception { + byte[] fileContent = "bar".getBytes(StandardCharsets.UTF_8); + Map json = Collections.singletonMap("name", "yeeeah"); + + MultipartBodyBuilder bodyBuilder = new MultipartBodyBuilder(); + bodyBuilder.part("file", fileContent).filename("orig"); + bodyBuilder.part("file", fileContent).filename("orig"); + bodyBuilder.part("json", json, MediaType.APPLICATION_JSON); + + EntityExchangeResult exchangeResult = testClient.post().uri("/optionalfilearray") + .bodyValue(bodyBuilder.build()) + .exchange() + .expectStatus().isFound() + .expectBody().isEmpty(); + + // Further assertions on the server response + MockMvcTestClient.resultActionsFor(exchangeResult) + .andExpect(model().attribute("fileContent", fileContent)) + .andExpect(model().attribute("jsonContent", json)); + } + + @Test + public void multipartRequestWithOptionalFileArrayNotPresent() throws Exception { + Map json = Collections.singletonMap("name", "yeeeah"); + + MultipartBodyBuilder bodyBuilder = new MultipartBodyBuilder(); + bodyBuilder.part("json", json, MediaType.APPLICATION_JSON); + + EntityExchangeResult exchangeResult = testClient.post().uri("/optionalfilearray") + .bodyValue(bodyBuilder.build()) + .exchange() + .expectStatus().isFound() + .expectBody().isEmpty(); + + // Further assertions on the server response + MockMvcTestClient.resultActionsFor(exchangeResult) + .andExpect(model().attributeDoesNotExist("fileContent")) + .andExpect(model().attribute("jsonContent", json)); + } + + @Test + public void multipartRequestWithOptionalFileList() throws Exception { + byte[] fileContent = "bar".getBytes(StandardCharsets.UTF_8); + Map json = Collections.singletonMap("name", "yeeeah"); + + MultipartBodyBuilder bodyBuilder = new MultipartBodyBuilder(); + bodyBuilder.part("file", fileContent).filename("orig"); + bodyBuilder.part("file", fileContent).filename("orig"); + bodyBuilder.part("json", json, MediaType.APPLICATION_JSON); + + EntityExchangeResult exchangeResult = testClient.post().uri("/optionalfilelist") + .bodyValue(bodyBuilder.build()) + .exchange() + .expectStatus().isFound() + .expectBody().isEmpty(); + + // Further assertions on the server response + MockMvcTestClient.resultActionsFor(exchangeResult) + .andExpect(model().attribute("fileContent", fileContent)) + .andExpect(model().attribute("jsonContent", json)); + } + + @Test + public void multipartRequestWithOptionalFileListNotPresent() throws Exception { + Map json = Collections.singletonMap("name", "yeeeah"); + + MultipartBodyBuilder bodyBuilder = new MultipartBodyBuilder(); + bodyBuilder.part("json", json, MediaType.APPLICATION_JSON); + + EntityExchangeResult exchangeResult = testClient.post().uri("/optionalfilelist") + .bodyValue(bodyBuilder.build()) + .exchange() + .expectStatus().isFound() + .expectBody().isEmpty(); + + // Further assertions on the server response + MockMvcTestClient.resultActionsFor(exchangeResult) + .andExpect(model().attributeDoesNotExist("fileContent")) + .andExpect(model().attribute("jsonContent", json)); + } + + @Test + public void multipartRequestWithServletParts() throws Exception { + byte[] fileContent = "bar".getBytes(StandardCharsets.UTF_8); + Map json = Collections.singletonMap("name", "yeeeah"); + + MultipartBodyBuilder bodyBuilder = new MultipartBodyBuilder(); + bodyBuilder.part("file", fileContent).filename("orig"); + bodyBuilder.part("json", json, MediaType.APPLICATION_JSON); + + EntityExchangeResult exchangeResult = testClient.post().uri("/multipartfile") + .bodyValue(bodyBuilder.build()) + .exchange() + .expectStatus().isFound() + .expectBody().isEmpty(); + + // Further assertions on the server response + MockMvcTestClient.resultActionsFor(exchangeResult) + .andExpect(model().attribute("fileContent", fileContent)) + .andExpect(model().attribute("jsonContent", json)); + } + + @Test + public void multipartRequestWrapped() throws Exception { + Map json = Collections.singletonMap("name", "yeeeah"); + + MultipartBodyBuilder bodyBuilder = new MultipartBodyBuilder(); + bodyBuilder.part("json", json, MediaType.APPLICATION_JSON); + + WebTestClient client = MockMvcTestClient.bindToController(new MultipartController()) + .filter(new RequestWrappingFilter()) + .build(); + + EntityExchangeResult exchangeResult = client.post().uri("/multipartfile") + .bodyValue(bodyBuilder.build()) + .exchange() + .expectStatus().isFound() + .expectBody().isEmpty(); + + // Further assertions on the server response + MockMvcTestClient.resultActionsFor(exchangeResult) + .andExpect(model().attribute("jsonContent", json)); + } + + + @Controller + private static class MultipartController { + + @RequestMapping(value = "/multipartfile", method = RequestMethod.POST) + public String processMultipartFile(@RequestParam(required = false) MultipartFile file, + @RequestPart(required = false) Map json, Model model) throws IOException { + + if (file != null) { + model.addAttribute("fileContent", file.getBytes()); + } + if (json != null) { + model.addAttribute("jsonContent", json); + } + + return "redirect:/index"; + } + + @RequestMapping(value = "/multipartfilearray", method = RequestMethod.POST) + public String processMultipartFileArray(@RequestParam(required = false) MultipartFile[] file, + @RequestPart(required = false) Map json, Model model) throws IOException { + + if (file != null && file.length > 0) { + byte[] content = file[0].getBytes(); + assertThat(file[1].getBytes()).isEqualTo(content); + model.addAttribute("fileContent", content); + } + if (json != null) { + model.addAttribute("jsonContent", json); + } + + return "redirect:/index"; + } + + @RequestMapping(value = "/multipartfilelist", method = RequestMethod.POST) + public String processMultipartFileList(@RequestParam(required = false) List file, + @RequestPart(required = false) Map json, Model model) throws IOException { + + if (file != null && !file.isEmpty()) { + byte[] content = file.get(0).getBytes(); + assertThat(file.get(1).getBytes()).isEqualTo(content); + model.addAttribute("fileContent", content); + } + if (json != null) { + model.addAttribute("jsonContent", json); + } + + return "redirect:/index"; + } + + @RequestMapping(value = "/optionalfile", method = RequestMethod.POST) + public String processOptionalFile(@RequestParam Optional file, + @RequestPart Map json, Model model) throws IOException { + + if (file.isPresent()) { + model.addAttribute("fileContent", file.get().getBytes()); + } + model.addAttribute("jsonContent", json); + + return "redirect:/index"; + } + + @RequestMapping(value = "/optionalfilearray", method = RequestMethod.POST) + public String processOptionalFileArray(@RequestParam Optional file, + @RequestPart Map json, Model model) throws IOException { + + if (file.isPresent()) { + byte[] content = file.get()[0].getBytes(); + assertThat(file.get()[1].getBytes()).isEqualTo(content); + model.addAttribute("fileContent", content); + } + model.addAttribute("jsonContent", json); + + return "redirect:/index"; + } + + @RequestMapping(value = "/optionalfilelist", method = RequestMethod.POST) + public String processOptionalFileList(@RequestParam Optional> file, + @RequestPart Map json, Model model) throws IOException { + + if (file.isPresent()) { + byte[] content = file.get().get(0).getBytes(); + assertThat(file.get().get(1).getBytes()).isEqualTo(content); + model.addAttribute("fileContent", content); + } + model.addAttribute("jsonContent", json); + + return "redirect:/index"; + } + + @RequestMapping(value = "/part", method = RequestMethod.POST) + public String processPart(@RequestParam Part part, + @RequestPart Map json, Model model) throws IOException { + + model.addAttribute("fileContent", part.getInputStream()); + model.addAttribute("jsonContent", json); + + return "redirect:/index"; + } + + @RequestMapping(value = "/json", method = RequestMethod.POST) + public String processMultipart(@RequestPart Map json, Model model) { + model.addAttribute("json", json); + return "redirect:/index"; + } + } + + + private static class RequestWrappingFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws IOException, ServletException { + + request = new HttpServletRequestWrapper(request); + filterChain.doFilter(request, response); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ReactiveReturnTypeTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ReactiveReturnTypeTests.java new file mode 100644 index 00000000000..bf6549fd269 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ReactiveReturnTypeTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.servlet.samples.client.standalone; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.client.MockMvcTestClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import static org.springframework.http.MediaType.TEXT_EVENT_STREAM; + +/** + * MockMvcTestClient equivalent of the MockMvc + * {@link org.springframework.test.web.servlet.samples.standalone.ReactiveReturnTypeTests}. + * + * @author Rossen Stoyanchev + */ +public class ReactiveReturnTypeTests { + + @Test + public void sseWithFlux() { + + WebTestClient testClient = + MockMvcTestClient.bindToController(new ReactiveController()).build(); + + Flux bodyFlux = testClient.get().uri("/spr16869") + .exchange() + .expectStatus().isOk() + .expectHeader().contentTypeCompatibleWith(TEXT_EVENT_STREAM) + .returnResult(String.class) + .getResponseBody(); + + StepVerifier.create(bodyFlux) + .expectNext("event0") + .expectNext("event1") + .expectNext("event2") + .verifyComplete(); + } + + + @RestController + static class ReactiveController { + + @GetMapping(path = "/spr16869", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + Flux sseFlux() { + return Flux.interval(Duration.ofSeconds(1)).take(3) + .map(aLong -> String.format("event%d", aLong)); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/RedirectTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/RedirectTests.java new file mode 100644 index 00000000000..2e8c1981a8d --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/RedirectTests.java @@ -0,0 +1,163 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.servlet.samples.client.standalone; + +import javax.validation.Valid; + +import org.junit.jupiter.api.Test; + +import org.springframework.stereotype.Controller; +import org.springframework.test.web.Person; +import org.springframework.test.web.reactive.server.EntityExchangeResult; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.client.MockMvcTestClient; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.ui.Model; +import org.springframework.validation.Errors; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.flash; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; + +/** + * MockMvcTestClient equivalent of the MockMvc + * {@link org.springframework.test.web.servlet.samples.standalone.RedirectTests}. + * + * @author Rossen Stoyanchev + */ +public class RedirectTests { + + private final WebTestClient testClient = + MockMvcTestClient.bindToController(new PersonController()).build(); + + + @Test + public void save() throws Exception { + EntityExchangeResult exchangeResult = + testClient.post().uri("/persons?name=Andy") + .exchange() + .expectStatus().isFound() + .expectHeader().location("/persons/Joe") + .expectBody().isEmpty(); + + // Further assertions on the server response + MockMvcTestClient.resultActionsFor(exchangeResult) + .andExpect(model().size(1)) + .andExpect(model().attributeExists("name")) + .andExpect(flash().attributeCount(1)) + .andExpect(flash().attribute("message", "success!")); + } + + @Test + public void saveSpecial() throws Exception { + EntityExchangeResult result = + testClient.post().uri("/people?name=Andy") + .exchange() + .expectStatus().isFound() + .expectHeader().location("/persons/Joe") + .expectBody().isEmpty(); + + // Further assertions on the server response + MockMvcTestClient.resultActionsFor(result) + .andExpect(model().size(1)) + .andExpect(model().attributeExists("name")) + .andExpect(flash().attributeCount(1)) + .andExpect(flash().attribute("message", "success!")); + } + + @Test + public void saveWithErrors() throws Exception { + EntityExchangeResult result = + testClient.post().uri("/persons").exchange().expectStatus().isOk().expectBody().isEmpty(); + + MockMvcTestClient.resultActionsFor(result) + .andExpect(forwardedUrl("persons/add")) + .andExpect(model().size(1)) + .andExpect(model().attributeExists("person")) + .andExpect(flash().attributeCount(0)); + } + + @Test + public void saveSpecialWithErrors() throws Exception { + EntityExchangeResult result = + testClient.post().uri("/people").exchange().expectStatus().isOk().expectBody().isEmpty(); + + MockMvcTestClient.resultActionsFor(result) + .andExpect(forwardedUrl("persons/add")) + .andExpect(model().size(1)) + .andExpect(model().attributeExists("person")) + .andExpect(flash().attributeCount(0)); + } + + @Test + public void getPerson() throws Exception { + EntityExchangeResult result = + MockMvcTestClient.bindToController(new PersonController()) + .defaultRequest(get("/").flashAttr("message", "success!")) + .build() + .get().uri("/persons/Joe") + .exchange() + .expectStatus().isOk() + .expectBody().isEmpty(); + + // Further assertions on the server response + MockMvcTestClient.resultActionsFor(result) + .andDo(MockMvcResultHandlers.print()) + .andExpect(forwardedUrl("persons/index")) + .andExpect(model().size(2)) + .andExpect(model().attribute("person", new Person("Joe"))) + .andExpect(model().attribute("message", "success!")) + .andExpect(flash().attributeCount(0)); + } + + + @Controller + private static class PersonController { + + @GetMapping("/persons/{name}") + public String getPerson(@PathVariable String name, Model model) { + model.addAttribute(new Person(name)); + return "persons/index"; + } + + @PostMapping("/persons") + public String save(@Valid Person person, Errors errors, RedirectAttributes redirectAttrs) { + if (errors.hasErrors()) { + return "persons/add"; + } + redirectAttrs.addAttribute("name", "Joe"); + redirectAttrs.addFlashAttribute("message", "success!"); + return "redirect:/persons/{name}"; + } + + @PostMapping("/people") + public Object saveSpecial(@Valid Person person, Errors errors, RedirectAttributes redirectAttrs) { + if (errors.hasErrors()) { + return "persons/add"; + } + redirectAttrs.addAttribute("name", "Joe"); + redirectAttrs.addFlashAttribute("message", "success!"); + return new StringBuilder("redirect:").append("/persons").append("/{name}"); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/RequestParameterTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/RequestParameterTests.java new file mode 100644 index 00000000000..fb706063ea0 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/RequestParameterTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.servlet.samples.client.standalone; + +import org.junit.jupiter.api.Test; + +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.test.web.Person; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.client.MockMvcTestClient; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * MockMvcTestClient equivalent of the MockMvc + * {@link org.springframework.test.web.servlet.samples.standalone.RequestParameterTests}. + * + * @author Rossen Stoyanchev + */ +public class RequestParameterTests { + + @Test + public void queryParameter() { + + WebTestClient client = MockMvcTestClient.bindToController(new PersonController()).build(); + + client.get().uri("/search?name=George") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isOk() + .expectHeader().contentType(MediaType.APPLICATION_JSON) + .expectBody().jsonPath("$.name", "George"); + } + + + @Controller + private class PersonController { + + @RequestMapping(value="/search") + @ResponseBody + public Person get(@RequestParam String name) { + Person person = new Person(name); + return person; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ResponseBodyTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ResponseBodyTests.java new file mode 100644 index 00000000000..2034d3c8de8 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ResponseBodyTests.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.servlet.samples.client.standalone; + +import javax.validation.constraints.NotNull; + +import org.junit.jupiter.api.Test; + +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.client.MockMvcTestClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +import static org.hamcrest.Matchers.equalTo; + +/** + * MockMvcTestClient equivalent of the MockMvc + * {@link org.springframework.test.web.servlet.samples.standalone.ResponseBodyTests}. + * + * @author Rossen Stoyanchev + */ +public class ResponseBodyTests { + + @Test + void json() { + MockMvcTestClient.bindToController(new PersonController()).build() + .get() + .uri("/person/Lee") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isOk() + .expectHeader().contentType(MediaType.APPLICATION_JSON) + .expectBody() + .jsonPath("$.name").isEqualTo("Lee") + .jsonPath("$.age").isEqualTo(42) + .jsonPath("$.age").value(equalTo(42)) + .jsonPath("$.age").value(equalTo(42.0f), Float.class); + } + + + @RestController + private static class PersonController { + + @GetMapping("/person/{name}") + public Person get(@PathVariable String name) { + Person person = new Person(name); + person.setAge(42); + return person; + } + } + + private static class Person { + + @NotNull + private final String name; + + private int age; + + public Person(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + + public int getAge() { + return this.age; + } + + public void setAge(int age) { + this.age = age; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ViewResolutionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ViewResolutionTests.java new file mode 100644 index 00000000000..91fa05eb5f4 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ViewResolutionTests.java @@ -0,0 +1,183 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.servlet.samples.client.standalone; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.http.MediaType; +import org.springframework.oxm.jaxb.Jaxb2Marshaller; +import org.springframework.stereotype.Controller; +import org.springframework.test.web.Person; +import org.springframework.test.web.reactive.server.EntityExchangeResult; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.client.MockMvcTestClient; +import org.springframework.ui.Model; +import org.springframework.web.accept.ContentNegotiationManager; +import org.springframework.web.accept.FixedContentNegotiationStrategy; +import org.springframework.web.accept.HeaderContentNegotiationStrategy; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.servlet.View; +import org.springframework.web.servlet.view.ContentNegotiatingViewResolver; +import org.springframework.web.servlet.view.InternalResourceViewResolver; +import org.springframework.web.servlet.view.json.MappingJackson2JsonView; +import org.springframework.web.servlet.view.xml.MarshallingView; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasProperty; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * MockMvcTestClient equivalent of the MockMvc + * {@link org.springframework.test.web.servlet.samples.standalone.RequestParameterTests}. + * + * @author Rossen Stoyanchev + */ +class ViewResolutionTests { + + @Test + void jspOnly() throws Exception { + WebTestClient testClient = + MockMvcTestClient.bindToController(new PersonController()) + .viewResolvers(new InternalResourceViewResolver("/WEB-INF/", ".jsp")) + .build(); + + EntityExchangeResult result = testClient.get().uri("/person/Corea") + .exchange() + .expectStatus().isOk() + .expectBody().isEmpty(); + + // Further assertions on the server response + MockMvcTestClient.resultActionsFor(result) + .andExpect(status().isOk()) + .andExpect(model().size(1)) + .andExpect(model().attributeExists("person")) + .andExpect(forwardedUrl("/WEB-INF/person/show.jsp")); + } + + @Test + void jsonOnly() { + WebTestClient testClient = + MockMvcTestClient.bindToController(new PersonController()) + .singleView(new MappingJackson2JsonView()) + .build(); + + testClient.get().uri("/person/Corea") + .exchange() + .expectStatus().isOk() + .expectHeader().contentTypeCompatibleWith(MediaType.APPLICATION_JSON) + .expectBody().jsonPath("$.person.name", "Corea"); + } + + @Test + void xmlOnly() { + Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); + marshaller.setClassesToBeBound(Person.class); + + WebTestClient testClient = + MockMvcTestClient.bindToController(new PersonController()) + .singleView(new MarshallingView(marshaller)) + .build(); + + testClient.get().uri("/person/Corea") + .exchange() + .expectStatus().isOk() + .expectHeader().contentType(MediaType.APPLICATION_XML) + .expectBody().xpath("/person/name/text()").isEqualTo("Corea"); + } + + @Test + void contentNegotiation() throws Exception { + Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); + marshaller.setClassesToBeBound(Person.class); + + List viewList = new ArrayList<>(); + viewList.add(new MappingJackson2JsonView()); + viewList.add(new MarshallingView(marshaller)); + + ContentNegotiationManager manager = new ContentNegotiationManager( + new HeaderContentNegotiationStrategy(), new FixedContentNegotiationStrategy(MediaType.TEXT_HTML)); + + ContentNegotiatingViewResolver cnViewResolver = new ContentNegotiatingViewResolver(); + cnViewResolver.setDefaultViews(viewList); + cnViewResolver.setContentNegotiationManager(manager); + cnViewResolver.afterPropertiesSet(); + + WebTestClient testClient = + MockMvcTestClient.bindToController(new PersonController()) + .viewResolvers(cnViewResolver, new InternalResourceViewResolver()) + .build(); + + EntityExchangeResult result = testClient.get().uri("/person/Corea") + .exchange() + .expectStatus().isOk() + .expectBody().isEmpty(); + + // Further assertions on the server response + MockMvcTestClient.resultActionsFor(result) + .andExpect(model().size(1)) + .andExpect(model().attributeExists("person")) + .andExpect(forwardedUrl("person/show")); + + testClient.get().uri("/person/Corea") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isOk() + .expectHeader().contentTypeCompatibleWith(MediaType.APPLICATION_JSON) + .expectBody().jsonPath("$.person.name", "Corea"); + + testClient.get().uri("/person/Corea") + .accept(MediaType.APPLICATION_XML) + .exchange() + .expectStatus().isOk() + .expectHeader().contentType(MediaType.APPLICATION_XML) + .expectBody().xpath("/person/name/text()").isEqualTo("Corea"); + } + + @Test + void defaultViewResolver() throws Exception { + WebTestClient client = MockMvcTestClient.bindToController(new PersonController()).build(); + + EntityExchangeResult result = client.get().uri("/person/Corea") + .exchange() + .expectStatus().isOk() + .expectBody().isEmpty(); + + // Further assertions on the server response + MockMvcTestClient.resultActionsFor(result) + .andExpect(model().attribute("person", hasProperty("name", equalTo("Corea")))) + .andExpect(forwardedUrl("person/show")); // InternalResourceViewResolver + } + + + @Controller + private static class PersonController { + + @GetMapping("/person/{name}") + String show(@PathVariable String name, Model model) { + Person person = new Person(name); + model.addAttribute(person); + return "person/show"; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resulthandlers/PrintingResultHandlerSmokeTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resulthandlers/PrintingResultHandlerSmokeTests.java new file mode 100644 index 00000000000..2fda9eac8a6 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resulthandlers/PrintingResultHandlerSmokeTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.servlet.samples.client.standalone.resulthandlers; + +import java.nio.charset.StandardCharsets; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletResponse; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.EntityExchangeResult; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.client.MockMvcTestClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * MockMvcTestClient equivalent of the MockMvc + * {@link org.springframework.test.web.servlet.samples.standalone.resulthandlers.PrintingResultHandlerSmokeTests}. + * + * @author Rossen Stoyanchev + */ +@Disabled +public class PrintingResultHandlerSmokeTests { + + private final WebTestClient testClient = + MockMvcTestClient.bindToController(new SimpleController()).build(); + + + // Not intended to be executed with the build. + // Comment out class-level @Disabled to see the output. + + @Test + public void printViaConsumer() { + testClient.post().uri("/") + .contentType(MediaType.TEXT_PLAIN) + .bodyValue("Hello Request".getBytes(StandardCharsets.UTF_8)) + .exchange() + .expectStatus().isOk() + .expectBody(String.class) + .consumeWith(System.out::println); + } + + @Test + public void returnResultAndPrint() { + EntityExchangeResult result = testClient.post().uri("/") + .contentType(MediaType.TEXT_PLAIN) + .bodyValue("Hello Request".getBytes(StandardCharsets.UTF_8)) + .exchange() + .expectStatus().isOk() + .expectBody(String.class) + .returnResult(); + + System.out.println(result); + } + + + @RestController + private static class SimpleController { + + @PostMapping("/") + public String hello(HttpServletResponse response) { + response.addCookie(new Cookie("enigma", "42")); + return "Hello Response"; + } + } +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/ContentAssertionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/ContentAssertionTests.java new file mode 100644 index 00000000000..0f1480d596b --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/ContentAssertionTests.java @@ -0,0 +1,145 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.servlet.samples.client.standalone.resultmatches; + +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.client.MockMvcTestClient; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; + +/** + * MockMvcTestClient equivalent of the MockMvc + * {@link org.springframework.test.web.servlet.samples.standalone.resultmatchers.ContentAssertionTests}. + * + * @author Rossen Stoyanchev + */ +public class ContentAssertionTests { + + private final WebTestClient testClient = + MockMvcTestClient.bindToController(new SimpleController()).build(); + + @Test + public void testContentType() { + testClient.get().uri("/handle").accept(MediaType.TEXT_PLAIN) + .exchange() + .expectStatus().isOk() + .expectHeader().contentType(MediaType.valueOf("text/plain;charset=ISO-8859-1")) + .expectHeader().contentType("text/plain;charset=ISO-8859-1") + .expectHeader().contentTypeCompatibleWith("text/plain") + .expectHeader().contentTypeCompatibleWith(MediaType.TEXT_PLAIN); + + testClient.get().uri("/handleUtf8") + .exchange() + .expectStatus().isOk() + .expectHeader().contentType(MediaType.valueOf("text/plain;charset=UTF-8")) + .expectHeader().contentType("text/plain;charset=UTF-8") + .expectHeader().contentTypeCompatibleWith("text/plain") + .expectHeader().contentTypeCompatibleWith(MediaType.TEXT_PLAIN); + } + + @Test + public void testContentAsString() { + + testClient.get().uri("/handle").accept(MediaType.TEXT_PLAIN) + .exchange() + .expectStatus().isOk() + .expectBody(String.class).isEqualTo("Hello world!"); + + testClient.get().uri("/handleUtf8").accept(MediaType.TEXT_PLAIN) + .exchange() + .expectStatus().isOk() + .expectBody(String.class).isEqualTo("\u3053\u3093\u306b\u3061\u306f\u4e16\u754c\uff01"); + + // Hamcrest matchers... + testClient.get().uri("/handle").accept(MediaType.TEXT_PLAIN) + .exchange() + .expectStatus().isOk() + .expectBody(String.class).value(equalTo("Hello world!")); + testClient.get().uri("/handleUtf8") + .exchange() + .expectStatus().isOk() + .expectBody(String.class).value(equalTo("\u3053\u3093\u306b\u3061\u306f\u4e16\u754c\uff01")); + } + + @Test + public void testContentAsBytes() { + + testClient.get().uri("/handle").accept(MediaType.TEXT_PLAIN) + .exchange() + .expectStatus().isOk() + .expectBody(byte[].class).isEqualTo( + "Hello world!".getBytes(StandardCharsets.ISO_8859_1)); + + testClient.get().uri("/handleUtf8") + .exchange() + .expectStatus().isOk() + .expectBody(byte[].class).isEqualTo( + "\u3053\u3093\u306b\u3061\u306f\u4e16\u754c\uff01".getBytes(StandardCharsets.UTF_8)); + } + + @Test + public void testContentStringMatcher() { + testClient.get().uri("/handle").accept(MediaType.TEXT_PLAIN) + .exchange() + .expectStatus().isOk() + .expectBody(String.class).value(containsString("world")); + } + + @Test + public void testCharacterEncoding() { + + testClient.get().uri("/handle").accept(MediaType.TEXT_PLAIN) + .exchange() + .expectStatus().isOk() + .expectHeader().contentType("text/plain;charset=ISO-8859-1") + .expectBody(String.class).value(containsString("world")); + + testClient.get().uri("/handleUtf8") + .exchange() + .expectStatus().isOk() + .expectHeader().contentType("text/plain;charset=UTF-8") + .expectBody(byte[].class) + .isEqualTo("\u3053\u3093\u306b\u3061\u306f\u4e16\u754c\uff01".getBytes(StandardCharsets.UTF_8)); + } + + + @Controller + private static class SimpleController { + + @RequestMapping(value="/handle", produces="text/plain") + @ResponseBody + public String handle() { + return "Hello world!"; + } + + @RequestMapping(value="/handleUtf8", produces="text/plain;charset=UTF-8") + @ResponseBody + public String handleWithCharset() { + return "\u3053\u3093\u306b\u3061\u306f\u4e16\u754c\uff01"; // "Hello world! (Japanese) + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/CookieAssertionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/CookieAssertionTests.java new file mode 100644 index 00000000000..5aa5cb27155 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/CookieAssertionTests.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.servlet.samples.client.standalone.resultmatches; + +import java.time.Duration; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.stereotype.Controller; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.client.MockMvcTestClient; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.i18n.CookieLocaleResolver; +import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.startsWith; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * MockMvcTestClient equivalent of the MockMvc + * {@link org.springframework.test.web.servlet.samples.standalone.resultmatchers.CookieAssertionTests}. + * + * @author Rossen Stoyanchev + */ +public class CookieAssertionTests { + + private static final String COOKIE_NAME = CookieLocaleResolver.DEFAULT_COOKIE_NAME; + + private WebTestClient client; + + + @BeforeEach + public void setup() { + CookieLocaleResolver localeResolver = new CookieLocaleResolver(); + localeResolver.setCookieDomain("domain"); + localeResolver.setCookieHttpOnly(true); + + client = MockMvcTestClient.bindToController(new SimpleController()) + .interceptors(new LocaleChangeInterceptor()) + .localeResolver(localeResolver) + .alwaysExpect(status().isOk()) + .configureClient() + .baseUrl("/?locale=en_US") + .build(); + } + + + @Test + public void testExists() { + client.get().uri("/").exchange().expectCookie().exists(COOKIE_NAME); + } + + @Test + public void testNotExists() { + client.get().uri("/").exchange().expectCookie().doesNotExist("unknownCookie"); + } + + @Test + public void testEqualTo() { + client.get().uri("/").exchange().expectCookie().valueEquals(COOKIE_NAME, "en-US"); + client.get().uri("/").exchange().expectCookie().value(COOKIE_NAME, equalTo("en-US")); + } + + @Test + public void testMatcher() { + client.get().uri("/").exchange().expectCookie().value(COOKIE_NAME, startsWith("en-US")); + } + + @Test + public void testMaxAge() { + client.get().uri("/").exchange().expectCookie().maxAge(COOKIE_NAME, Duration.ofSeconds(-1)); + } + + @Test + public void testDomain() { + client.get().uri("/").exchange().expectCookie().domain(COOKIE_NAME, "domain"); + } + + @Test + public void testPath() { + client.get().uri("/").exchange().expectCookie().path(COOKIE_NAME, "/"); + } + + @Test + public void testSecured() { + client.get().uri("/").exchange().expectCookie().secure(COOKIE_NAME, false); + } + + @Test + public void testHttpOnly() { + client.get().uri("/").exchange().expectCookie().httpOnly(COOKIE_NAME, true); + } + + + @Controller + private static class SimpleController { + + @RequestMapping("/") + public String home() { + return "home"; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/FlashAttributeAssertionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/FlashAttributeAssertionTests.java new file mode 100644 index 00000000000..640de972a5a --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/FlashAttributeAssertionTests.java @@ -0,0 +1,103 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.servlet.samples.client.standalone.resultmatches; + +import java.net.URL; + +import org.junit.jupiter.api.Test; + +import org.springframework.stereotype.Controller; +import org.springframework.test.web.reactive.server.EntityExchangeResult; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.client.MockMvcTestClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.hamcrest.Matchers.closeTo; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.flash; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * MockMvcTestClient equivalent of the MockMvc + * {@link org.springframework.test.web.servlet.samples.standalone.resultmatchers.FlashAttributeAssertionTests}. + * + * @author Rossen Stoyanchev + */ +public class FlashAttributeAssertionTests { + + private final WebTestClient client = + MockMvcTestClient.bindToController(new PersonController()) + .alwaysExpect(status().isFound()) + .alwaysExpect(flash().attributeCount(3)) + .build(); + + + @Test + void attributeCountWithWrongCount() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> performRequest().andExpect(flash().attributeCount(1))) + .withMessage("FlashMap size expected:<1> but was:<3>"); + } + + @Test + void attributeExists() throws Exception { + performRequest().andExpect(flash().attributeExists("one", "two", "three")); + } + + @Test + void attributeEqualTo() throws Exception { + performRequest() + .andExpect(flash().attribute("one", "1")) + .andExpect(flash().attribute("two", 2.222)) + .andExpect(flash().attribute("three", new URL("https://example.com"))); + } + + @Test + void attributeMatchers() throws Exception { + performRequest() + .andExpect(flash().attribute("one", containsString("1"))) + .andExpect(flash().attribute("two", closeTo(2, 0.5))) + .andExpect(flash().attribute("three", notNullValue())) + .andExpect(flash().attribute("one", equalTo("1"))) + .andExpect(flash().attribute("two", equalTo(2.222))) + .andExpect(flash().attribute("three", equalTo(new URL("https://example.com")))); + } + + private ResultActions performRequest() { + EntityExchangeResult result = client.post().uri("/persons").exchange().expectBody().isEmpty(); + return MockMvcTestClient.resultActionsFor(result); + } + + + @Controller + private static class PersonController { + + @PostMapping("/persons") + String save(RedirectAttributes redirectAttrs) throws Exception { + redirectAttrs.addFlashAttribute("one", "1"); + redirectAttrs.addFlashAttribute("two", 2.222); + redirectAttrs.addFlashAttribute("three", new URL("https://example.com")); + return "redirect:/person/1"; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/HandlerAssertionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/HandlerAssertionTests.java new file mode 100644 index 00000000000..61c0e41f55e --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/HandlerAssertionTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.servlet.samples.client.standalone.resultmatches; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; + +import org.springframework.http.ResponseEntity; +import org.springframework.test.web.reactive.server.EntityExchangeResult; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.client.MockMvcTestClient; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.handler; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder.on; + +/** + * MockMvcTestClient equivalent of the MockMvc + * {@link org.springframework.test.web.servlet.samples.standalone.resultmatchers.HandlerAssertionTests}. + * + * @author Rossen Stoyanchev + */ +public class HandlerAssertionTests { + + private final WebTestClient client = + MockMvcTestClient.bindToController(new SimpleController()) + .alwaysExpect(status().isOk()) + .build(); + + + @Test + public void handlerType() throws Exception { + performRequest().andExpect(handler().handlerType(SimpleController.class)); + } + + @Test + public void methodCallOnNonMock() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> performRequest().andExpect(handler().methodCall("bogus"))) + .withMessageContaining("The supplied object [bogus] is not an instance of") + .withMessageContaining(MvcUriComponentsBuilder.MethodInvocationInfo.class.getName()) + .withMessageContaining("Ensure that you invoke the handler method via MvcUriComponentsBuilder.on()"); + } + + @Test + public void methodCall() throws Exception { + performRequest().andExpect(handler().methodCall(on(SimpleController.class).handle())); + } + + @Test + public void methodName() throws Exception { + performRequest().andExpect(handler().methodName("handle")); + } + + @Test + public void methodNameMatchers() throws Exception { + performRequest() + .andExpect(handler().methodName(equalTo("handle"))) + .andExpect(handler().methodName(is(not("save")))); + } + + @Test + public void method() throws Exception { + Method method = SimpleController.class.getMethod("handle"); + performRequest().andExpect(handler().method(method)); + } + + private ResultActions performRequest() { + EntityExchangeResult result = client.get().uri("/").exchange().expectBody().isEmpty(); + return MockMvcTestClient.resultActionsFor(result); + } + + + @RestController + static class SimpleController { + + @RequestMapping("/") + public ResponseEntity handle() { + return ResponseEntity.ok().build(); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/HeaderAssertionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/HeaderAssertionTests.java new file mode 100644 index 00000000000..20c736b6f3d --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/HeaderAssertionTests.java @@ -0,0 +1,272 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.servlet.samples.client.standalone.resultmatches; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; +import java.util.function.Consumer; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.test.web.Person; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.client.MockMvcTestClient; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.context.request.WebRequest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.fail; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.startsWith; +import static org.springframework.http.HttpHeaders.IF_MODIFIED_SINCE; +import static org.springframework.http.HttpHeaders.LAST_MODIFIED; +import static org.springframework.http.HttpHeaders.VARY; + +/** + * MockMvcTestClient equivalent of the MockMvc + * {@link org.springframework.test.web.servlet.samples.standalone.resultmatchers.HeaderAssertionTests}. + * + * @author Rossen Stoyanchev + */ +public class HeaderAssertionTests { + + private static final String ERROR_MESSAGE = "Should have thrown an AssertionError"; + + + private String now; + + private String minuteAgo; + + private WebTestClient testClient; + + private final long currentTime = System.currentTimeMillis(); + + private SimpleDateFormat dateFormat; + + + @BeforeEach + public void setup() { + this.dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US); + this.dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + this.now = dateFormat.format(new Date(this.currentTime)); + this.minuteAgo = dateFormat.format(new Date(this.currentTime - (1000 * 60))); + + PersonController controller = new PersonController(); + controller.setStubTimestamp(this.currentTime); + this.testClient = MockMvcTestClient.bindToController(controller).build(); + } + + + @Test + public void stringWithCorrectResponseHeaderValue() { + testClient.get().uri("/persons/1").header(IF_MODIFIED_SINCE, minuteAgo) + .exchange() + .expectStatus().isOk() + .expectHeader().valueEquals(LAST_MODIFIED, now); + } + + @Test + public void stringWithMatcherAndCorrectResponseHeaderValue() { + testClient.get().uri("/persons/1").header(IF_MODIFIED_SINCE, minuteAgo) + .exchange() + .expectStatus().isOk() + .expectHeader().value(LAST_MODIFIED, equalTo(now)); + } + + @Test + public void multiStringHeaderValue() { + testClient.get().uri("/persons/1") + .exchange() + .expectStatus().isOk() + .expectHeader().valueEquals(VARY, "foo", "bar"); + } + + @Test + public void multiStringHeaderValueWithMatchers() { + testClient.get().uri("/persons/1") + .exchange() + .expectStatus().isOk() + .expectHeader().values(VARY, hasItems(containsString("foo"), startsWith("bar"))); + } + + @Test + public void dateValueWithCorrectResponseHeaderValue() { + testClient.get().uri("/persons/1") + .header(IF_MODIFIED_SINCE, minuteAgo) + .exchange() + .expectStatus().isOk() + .expectHeader().valueEqualsDate(LAST_MODIFIED, this.currentTime); + } + + @Test + public void longValueWithCorrectResponseHeaderValue() { + testClient.get().uri("/persons/1") + .exchange() + .expectStatus().isOk() + .expectHeader().valueEquals("X-Rate-Limiting", 42); + } + + @Test + public void stringWithMissingResponseHeader() { + testClient.get().uri("/persons/1") + .header(IF_MODIFIED_SINCE, now) + .exchange() + .expectStatus().isNotModified() + .expectHeader().valueEquals("X-Custom-Header"); + } + + @Test + public void stringWithMatcherAndMissingResponseHeader() { + testClient.get().uri("/persons/1").header(IF_MODIFIED_SINCE, now) + .exchange() + .expectStatus().isNotModified() + .expectHeader().value("X-Custom-Header", nullValue()); + } + + @Test + public void longValueWithMissingResponseHeader() { + try { + testClient.get().uri("/persons/1").header(IF_MODIFIED_SINCE, now) + .exchange() + .expectStatus().isNotModified() + .expectHeader().valueEquals("X-Custom-Header", 99L); + + fail(ERROR_MESSAGE); + } + catch (AssertionError err) { + if (ERROR_MESSAGE.equals(err.getMessage())) { + throw err; + } + assertThat(err.getMessage()).startsWith("Response does not contain header 'X-Custom-Header'"); + } + } + + @Test + public void exists() { + testClient.get().uri("/persons/1") + .exchange() + .expectStatus().isOk() + .expectHeader().exists(LAST_MODIFIED); + } + + @Test + public void existsFail() { + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> + testClient.get().uri("/persons/1") + .exchange() + .expectStatus().isOk() + .expectHeader().exists("X-Custom-Header")); + } + + @Test + public void doesNotExist() { + testClient.get().uri("/persons/1") + .exchange() + .expectStatus().isOk() + .expectHeader().doesNotExist("X-Custom-Header"); + } + + @Test + public void doesNotExistFail() { + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> + testClient.get().uri("/persons/1") + .exchange() + .expectStatus().isOk() + .expectHeader().doesNotExist(LAST_MODIFIED)); + } + + @Test + public void longValueWithIncorrectResponseHeaderValue() { + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> + testClient.get().uri("/persons/1") + .exchange() + .expectStatus().isOk() + .expectHeader().valueEquals("X-Rate-Limiting", 1)); + } + + @Test + public void stringWithMatcherAndIncorrectResponseHeaderValue() { + long secondLater = this.currentTime + 1000; + String expected = this.dateFormat.format(new Date(secondLater)); + assertIncorrectResponseHeader(spec -> spec.expectHeader().valueEquals(LAST_MODIFIED, expected), expected); + assertIncorrectResponseHeader(spec -> spec.expectHeader().value(LAST_MODIFIED, equalTo(expected)), expected); + // Comparison by date uses HttpHeaders to format the date in the error message. + HttpHeaders headers = new HttpHeaders(); + headers.setDate("expected", secondLater); + assertIncorrectResponseHeader(spec -> spec.expectHeader().valueEqualsDate(LAST_MODIFIED, secondLater), expected); + } + + private void assertIncorrectResponseHeader(Consumer assertions, String expected) { + try { + WebTestClient.ResponseSpec spec = testClient.get().uri("/persons/1") + .header(IF_MODIFIED_SINCE, minuteAgo) + .exchange() + .expectStatus().isOk(); + + assertions.accept(spec); + + fail(ERROR_MESSAGE); + } + catch (AssertionError err) { + if (ERROR_MESSAGE.equals(err.getMessage())) { + throw err; + } + assertMessageContains(err, "Response header '" + LAST_MODIFIED + "'"); + assertMessageContains(err, expected); + assertMessageContains(err, this.now); + } + } + + private void assertMessageContains(AssertionError error, String expected) { + assertThat(error.getMessage().contains(expected)) + .as("Failure message should contain [" + expected + "], actual is [" + error.getMessage() + "]") + .isTrue(); + } + + + @Controller + private static class PersonController { + + private long timestamp; + + public void setStubTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + @RequestMapping("/persons/{id}") + public ResponseEntity showEntity(@PathVariable long id, WebRequest request) { + return ResponseEntity + .ok() + .lastModified(this.timestamp) + .header("X-Rate-Limiting", "42") + .header("Vary", "foo", "bar") + .body(new Person("Jason")); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/JsonPathAssertionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/JsonPathAssertionTests.java new file mode 100644 index 00000000000..50216806c12 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/JsonPathAssertionTests.java @@ -0,0 +1,152 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.servlet.samples.client.standalone.resultmatches; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.web.Person; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.client.MockMvcTestClient; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.endsWith; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.in; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.startsWith; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * MockMvcTestClient equivalent of the MockMvc + * {@link org.springframework.test.web.servlet.samples.standalone.resultmatchers.JsonPathAssertionTests}. + * + * @author Rossen Stoyanchev + */ +public class JsonPathAssertionTests { + + private final WebTestClient client = + MockMvcTestClient.bindToController(new MusicController()) + .alwaysExpect(status().isOk()) + .alwaysExpect(content().contentType(MediaType.APPLICATION_JSON)) + .configureClient() + .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .build(); + + + @Test + public void exists() { + String composerByName = "$.composers[?(@.name == '%s')]"; + String performerByName = "$.performers[?(@.name == '%s')]"; + + client.get().uri("/music/people") + .exchange() + .expectBody() + .jsonPath(composerByName, "Johann Sebastian Bach").exists() + .jsonPath(composerByName, "Johannes Brahms").exists() + .jsonPath(composerByName, "Edvard Grieg").exists() + .jsonPath(composerByName, "Robert Schumann").exists() + .jsonPath(performerByName, "Vladimir Ashkenazy").exists() + .jsonPath(performerByName, "Yehudi Menuhin").exists() + .jsonPath("$.composers[0]").exists() + .jsonPath("$.composers[1]").exists() + .jsonPath("$.composers[2]").exists() + .jsonPath("$.composers[3]").exists(); + } + + @Test + public void doesNotExist() { + client.get().uri("/music/people") + .exchange() + .expectBody() + .jsonPath("$.composers[?(@.name == 'Edvard Grieeeeeeg')]").doesNotExist() + .jsonPath("$.composers[?(@.name == 'Robert Schuuuuuuman')]").doesNotExist() + .jsonPath("$.composers[4]").doesNotExist(); + } + + @Test + public void equality() { + client.get().uri("/music/people") + .exchange() + .expectBody() + .jsonPath("$.composers[0].name").isEqualTo("Johann Sebastian Bach") + .jsonPath("$.performers[1].name").isEqualTo("Yehudi Menuhin"); + + // Hamcrest matchers... + client.get().uri("/music/people") + .exchange() + .expectStatus().isOk() + .expectHeader().contentType(MediaType.APPLICATION_JSON) + .expectBody() + .jsonPath("$.composers[0].name").value(equalTo("Johann Sebastian Bach")) + .jsonPath("$.performers[1].name").value(equalTo("Yehudi Menuhin")); + } + + @Test + public void hamcrestMatcher() { + client.get().uri("/music/people") + .exchange() + .expectBody() + .jsonPath("$.composers[0].name").value(startsWith("Johann")) + .jsonPath("$.performers[0].name").value(endsWith("Ashkenazy")) + .jsonPath("$.performers[1].name").value(containsString("di Me")) + .jsonPath("$.composers[1].name").value(is(in(Arrays.asList("Johann Sebastian Bach", "Johannes Brahms")))); + } + + @Test + public void hamcrestMatcherWithParameterizedJsonPath() { + String composerName = "$.composers[%s].name"; + String performerName = "$.performers[%s].name"; + + client.get().uri("/music/people") + .exchange() + .expectBody() + .jsonPath(composerName, 0).value(startsWith("Johann")) + .jsonPath(performerName, 0).value(endsWith("Ashkenazy")) + .jsonPath(performerName, 1).value(containsString("di Me")) + .jsonPath(composerName, 1).value(is(in(Arrays.asList("Johann Sebastian Bach", "Johannes Brahms")))); + } + + + @RestController + private class MusicController { + + @RequestMapping("/music/people") + public MultiValueMap get() { + MultiValueMap map = new LinkedMultiValueMap<>(); + + map.add("composers", new Person("Johann Sebastian Bach")); + map.add("composers", new Person("Johannes Brahms")); + map.add("composers", new Person("Edvard Grieg")); + map.add("composers", new Person("Robert Schumann")); + + map.add("performers", new Person("Vladimir Ashkenazy")); + map.add("performers", new Person("Yehudi Menuhin")); + + return map; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/ModelAssertionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/ModelAssertionTests.java new file mode 100644 index 00000000000..274000430c3 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/ModelAssertionTests.java @@ -0,0 +1,135 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.servlet.samples.client.standalone.resultmatches; + +import javax.validation.Valid; + +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Controller; +import org.springframework.test.web.Person; +import org.springframework.test.web.reactive.server.EntityExchangeResult; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.client.MockMvcTestClient; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.endsWith; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasProperty; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.startsWith; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * MockMvcTestClient equivalent of the MockMvc + * {@link org.springframework.test.web.servlet.samples.standalone.resultmatchers.ModelAssertionTests}. + * + * @author Rossen Stoyanchev + */ +public class ModelAssertionTests { + + private final WebTestClient client = + MockMvcTestClient.bindToController(new SampleController("a string value", 3, new Person("a name"))) + .controllerAdvice(new ModelAttributeAdvice()) + .alwaysExpect(status().isOk()) + .build(); + + @Test + void attributeEqualTo() throws Exception { + performRequest(HttpMethod.GET, "/") + .andExpect(model().attribute("integer", 3)) + .andExpect(model().attribute("string", "a string value")) + .andExpect(model().attribute("integer", equalTo(3))) // Hamcrest... + .andExpect(model().attribute("string", equalTo("a string value"))) + .andExpect(model().attribute("globalAttrName", equalTo("Global Attribute Value"))); + } + + @Test + void attributeExists() throws Exception { + performRequest(HttpMethod.GET, "/") + .andExpect(model().attributeExists("integer", "string", "person")) + .andExpect(model().attribute("integer", notNullValue())) // Hamcrest... + .andExpect(model().attribute("INTEGER", nullValue())); + } + + @Test + void attributeHamcrestMatchers() throws Exception { + performRequest(HttpMethod.GET, "/") + .andExpect(model().attribute("integer", equalTo(3))) + .andExpect(model().attribute("string", allOf(startsWith("a string"), endsWith("value")))) + .andExpect(model().attribute("person", hasProperty("name", equalTo("a name")))); + } + + @Test + void hasErrors() throws Exception { + performRequest(HttpMethod.POST, "/persons").andExpect(model().attributeHasErrors("person")); + } + + @Test + void hasNoErrors() throws Exception { + performRequest(HttpMethod.GET, "/").andExpect(model().hasNoErrors()); + } + + private ResultActions performRequest(HttpMethod method, String uri) { + EntityExchangeResult result = client.method(method).uri(uri).exchange().expectBody().isEmpty(); + return MockMvcTestClient.resultActionsFor(result); + } + + + @Controller + private static class SampleController { + + private final Object[] values; + + SampleController(Object... values) { + this.values = values; + } + + @RequestMapping("/") + String handle(Model model) { + for (Object value : this.values) { + model.addAttribute(value); + } + return "view"; + } + + @PostMapping("/persons") + String create(@Valid Person person, BindingResult result, Model model) { + return "view"; + } + } + + @ControllerAdvice + private static class ModelAttributeAdvice { + + @ModelAttribute("globalAttrName") + String getAttribute() { + return "Global Attribute Value"; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/RequestAttributeAssertionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/RequestAttributeAssertionTests.java new file mode 100644 index 00000000000..bc48bccdf7e --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/RequestAttributeAssertionTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.servlet.samples.client.standalone.resultmatches; + +import org.junit.jupiter.api.Test; + +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.test.web.reactive.server.EntityExchangeResult; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.client.MockMvcTestClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.servlet.HandlerMapping; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.not; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; + +/** + * MockMvcTestClient equivalent of the MockMvc + * {@link org.springframework.test.web.servlet.samples.standalone.resultmatchers.RequestAttributeAssertionTests}. + * + * @author Rossen Stoyanchev + */ +public class RequestAttributeAssertionTests { + + private final WebTestClient mainServletClient = + MockMvcTestClient.bindToController(new SimpleController()) + .defaultRequest(get("/").servletPath("/main")) + .build(); + + private final WebTestClient client = + MockMvcTestClient.bindToController(new SimpleController()).build(); + + + @Test + void requestAttributeEqualTo() throws Exception { + performRequest(mainServletClient, "/main/1") + .andExpect(request().attribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, "/{id}")) + .andExpect(request().attribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "/1")); + } + + @Test + void requestAttributeMatcher() throws Exception { + String producibleMediaTypes = HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE; + + performRequest(client, "/1") + .andExpect(request().attribute(producibleMediaTypes, hasItem(MediaType.APPLICATION_JSON))) + .andExpect(request().attribute(producibleMediaTypes, not(hasItem(MediaType.APPLICATION_XML)))); + + performRequest(mainServletClient, "/main/1") + .andExpect(request().attribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, equalTo("/{id}"))) + .andExpect(request().attribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, equalTo("/1"))); + } + + private ResultActions performRequest(WebTestClient client, String uri) { + EntityExchangeResult result = client.get().uri(uri) + .exchange() + .expectStatus().isOk() + .expectBody().isEmpty(); + + return MockMvcTestClient.resultActionsFor(result); + } + + + @Controller + private static class SimpleController { + + @GetMapping(path="/{id}", produces="application/json") + String show() { + return "view"; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/SessionAttributeAssertionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/SessionAttributeAssertionTests.java new file mode 100644 index 00000000000..5b1add31ffe --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/SessionAttributeAssertionTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.servlet.samples.client.standalone.resultmatches; + +import java.util.Locale; + +import org.junit.jupiter.api.Test; + +import org.springframework.stereotype.Controller; +import org.springframework.test.web.reactive.server.EntityExchangeResult; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.client.MockMvcTestClient; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.SessionAttributes; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * MockMvcTestClient equivalent of the MockMvc + * {@link org.springframework.test.web.servlet.samples.standalone.resultmatchers.SessionAttributeAssertionTests}. + * + * @author Rossen Stoyanchev + */ +public class SessionAttributeAssertionTests { + + private final WebTestClient client = + MockMvcTestClient.bindToController(new SimpleController()) + .alwaysExpect(status().isOk()) + .build(); + + + @Test + void sessionAttributeEqualTo() throws Exception { + performRequest().andExpect(request().sessionAttribute("locale", Locale.UK)); + + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> performRequest().andExpect(request().sessionAttribute("locale", Locale.US))) + .withMessage("Session attribute 'locale' expected: but was:"); + } + + @Test + void sessionAttributeMatcher() throws Exception { + performRequest() + .andExpect(request().sessionAttribute("bogus", is(nullValue()))) + .andExpect(request().sessionAttribute("locale", is(notNullValue()))) + .andExpect(request().sessionAttribute("locale", equalTo(Locale.UK))); + + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> performRequest().andExpect(request().sessionAttribute("bogus", is(notNullValue())))) + .withMessageContaining("null"); + } + + @Test + void sessionAttributeDoesNotExist() throws Exception { + performRequest().andExpect(request().sessionAttributeDoesNotExist("bogus", "enigma")); + + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> performRequest().andExpect(request().sessionAttributeDoesNotExist("locale"))) + .withMessage("Session attribute 'locale' exists"); + } + + private ResultActions performRequest() { + EntityExchangeResult result = client.post().uri("/").exchange().expectBody().isEmpty(); + return MockMvcTestClient.resultActionsFor(result); + } + + + @Controller + @SessionAttributes("locale") + private static class SimpleController { + + @ModelAttribute + void populate(Model model) { + model.addAttribute("locale", Locale.UK); + } + + @RequestMapping("/") + String handle() { + return "view"; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/StatusAssertionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/StatusAssertionTests.java new file mode 100644 index 00000000000..80759c5fcb3 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/StatusAssertionTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.servlet.samples.client.standalone.resultmatches; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.annotation.AliasFor; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Controller; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.client.MockMvcTestClient; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; + +import static org.hamcrest.Matchers.equalTo; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.CREATED; +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; +import static org.springframework.http.HttpStatus.NOT_IMPLEMENTED; + +/** + * MockMvcTestClient equivalent of the MockMvc + * {@link org.springframework.test.web.servlet.samples.standalone.resultmatchers.StatusAssertionTests}. + * + * @author Rossen Stoyanchev + */ +public class StatusAssertionTests { + + private final WebTestClient testClient = + MockMvcTestClient.bindToController(new StatusController()).build(); + + + @Test + public void testStatusInt() { + testClient.get().uri("/created").exchange().expectStatus().isEqualTo(201); + testClient.get().uri("/createdWithComposedAnnotation").exchange().expectStatus().isEqualTo(201); + testClient.get().uri("/badRequest").exchange().expectStatus().isEqualTo(400); + } + + @Test + public void testHttpStatus() { + testClient.get().uri("/created").exchange().expectStatus().isCreated(); + testClient.get().uri("/createdWithComposedAnnotation").exchange().expectStatus().isCreated(); + testClient.get().uri("/badRequest").exchange().expectStatus().isBadRequest(); + } + + @Test + public void testMatcher() { + testClient.get().uri("/badRequest").exchange().expectStatus().value(equalTo(400)); + } + + + @RequestMapping + @ResponseStatus + @Retention(RetentionPolicy.RUNTIME) + @interface Get { + + @AliasFor(annotation = RequestMapping.class, attribute = "path") + String[] path() default {}; + + @AliasFor(annotation = ResponseStatus.class, attribute = "code") + HttpStatus status() default INTERNAL_SERVER_ERROR; + } + + @Controller + private static class StatusController { + + @RequestMapping("/created") + @ResponseStatus(CREATED) + public @ResponseBody void created(){ + } + + @Get(path = "/createdWithComposedAnnotation", status = CREATED) + public @ResponseBody void createdWithComposedAnnotation() { + } + + @RequestMapping("/badRequest") + @ResponseStatus(code = BAD_REQUEST, reason = "Expired token") + public @ResponseBody void badRequest(){ + } + + @RequestMapping("/notImplemented") + @ResponseStatus(NOT_IMPLEMENTED) + public @ResponseBody void notImplemented(){ + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/UrlAssertionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/UrlAssertionTests.java new file mode 100644 index 00000000000..15f8bb19bff --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/UrlAssertionTests.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.servlet.samples.client.standalone.resultmatches; + +import org.junit.jupiter.api.Test; + +import org.springframework.stereotype.Controller; +import org.springframework.test.web.reactive.server.EntityExchangeResult; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.client.MockMvcTestClient; +import org.springframework.web.bind.annotation.RequestMapping; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrlPattern; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrlPattern; + +/** + * MockMvcTestClient equivalent of the MockMvc + * {@link org.springframework.test.web.servlet.samples.standalone.resultmatchers.UrlAssertionTests}. + * + * @author Rossen Stoyanchev + */ +public class UrlAssertionTests { + + private final WebTestClient testClient = + MockMvcTestClient.bindToController(new SimpleController()).build(); + + + @Test + public void testRedirect() { + testClient.get().uri("/persons") + .exchange() + .expectStatus().isFound() + .expectHeader().location("/persons/1"); + } + + @Test + public void testRedirectPattern() throws Exception { + EntityExchangeResult result = + testClient.get().uri("/persons").exchange().expectBody().isEmpty(); + + MockMvcTestClient.resultActionsFor(result) + .andExpect(redirectedUrlPattern("/persons/*")); + } + + @Test + public void testForward() { + testClient.get().uri("/") + .exchange() + .expectStatus().isOk() + .expectHeader().valueEquals("Forwarded-Url", "/home"); + } + + @Test + public void testForwardPattern() throws Exception { + EntityExchangeResult result = + testClient.get().uri("/").exchange().expectBody().isEmpty(); + + MockMvcTestClient.resultActionsFor(result) + .andExpect(forwardedUrlPattern("/ho?e")); + } + + + @Controller + private static class SimpleController { + + @RequestMapping("/persons") + public String save() { + return "redirect:/persons/1"; + } + + @RequestMapping("/") + public String forward() { + return "forward:/home"; + } + } +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/ViewNameAssertionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/ViewNameAssertionTests.java new file mode 100644 index 00000000000..b60196e01dd --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/ViewNameAssertionTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.servlet.samples.client.standalone.resultmatches; + +import org.junit.jupiter.api.Test; + +import org.springframework.stereotype.Controller; +import org.springframework.test.web.reactive.server.EntityExchangeResult; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.client.MockMvcTestClient; +import org.springframework.web.bind.annotation.RequestMapping; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; + +/** + * MockMvcTestClient equivalent of the MockMvc + * {@link org.springframework.test.web.servlet.samples.standalone.resultmatchers.UrlAssertionTests}. + * + * @author Rossen Stoyanchev + */ +public class ViewNameAssertionTests { + + private final WebTestClient client = + MockMvcTestClient.bindToController(new SimpleController()) + .alwaysExpect(status().isOk()) + .build(); + + + @Test + public void testEqualTo() throws Exception { + MockMvcTestClient.resultActionsFor(performRequest()) + .andExpect(view().name("mySpecialView")) + .andExpect(view().name(equalTo("mySpecialView"))); + } + + @Test + public void testHamcrestMatcher() throws Exception { + MockMvcTestClient.resultActionsFor(performRequest()) + .andExpect(view().name(containsString("Special"))); + } + + private EntityExchangeResult performRequest() { + return client.get().uri("/").exchange().expectBody().isEmpty(); + } + + + @Controller + private static class SimpleController { + + @RequestMapping("/") + public String handle() { + return "mySpecialView"; + } + } +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/XmlContentAssertionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/XmlContentAssertionTests.java new file mode 100644 index 00000000000..26660211246 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/XmlContentAssertionTests.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.servlet.samples.client.standalone.resultmatches; + +import java.util.Arrays; +import java.util.List; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlRootElement; + +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.test.web.Person; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.client.MockMvcTestClient; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * MockMvcTestClient equivalent of the MockMvc + * {@link org.springframework.test.web.servlet.samples.standalone.resultmatchers.XmlContentAssertionTests}. + * + * @author Rossen Stoyanchev + */ +public class XmlContentAssertionTests { + + private static final String PEOPLE_XML = + "" + + "" + + "Johann Sebastian Bachfalse21.0" + + "Johannes Brahmsfalse0.0025" + + "Edvard Griegfalse1.6035" + + "Robert SchumannfalseNaN" + + ""; + + + private final WebTestClient testClient = + MockMvcTestClient.bindToController(new MusicController()) + .alwaysExpect(status().isOk()) + .alwaysExpect(content().contentType(MediaType.parseMediaType("application/xml;charset=UTF-8"))) + .configureClient() + .defaultHeader(HttpHeaders.ACCEPT, "application/xml;charset=UTF-8") + .build(); + + + @Test + public void testXmlEqualTo() { + testClient.get().uri("/music/people") + .exchange() + .expectBody().xml(PEOPLE_XML); + } + + @Test + public void testNodeHamcrestMatcher() { + testClient.get().uri("/music/people") + .exchange() + .expectBody().xpath("/people/composers/composer[1]").exists(); + } + + + @Controller + private static class MusicController { + + @RequestMapping(value="/music/people") + public @ResponseBody PeopleWrapper getPeople() { + + List composers = Arrays.asList( + new Person("Johann Sebastian Bach").setSomeDouble(21), + new Person("Johannes Brahms").setSomeDouble(.0025), + new Person("Edvard Grieg").setSomeDouble(1.6035), + new Person("Robert Schumann").setSomeDouble(Double.NaN)); + + return new PeopleWrapper(composers); + } + } + + @SuppressWarnings("unused") + @XmlRootElement(name="people") + @XmlAccessorType(XmlAccessType.FIELD) + private static class PeopleWrapper { + + @XmlElementWrapper(name="composers") + @XmlElement(name="composer") + private List composers; + + public PeopleWrapper() { + } + + public PeopleWrapper(List composers) { + this.composers = composers; + } + + public List getComposers() { + return this.composers; + } + } +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/XpathAssertionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/XpathAssertionTests.java new file mode 100644 index 00000000000..c7fd97f6a10 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/XpathAssertionTests.java @@ -0,0 +1,236 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.servlet.samples.client.standalone.resultmatches; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlRootElement; + +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.test.web.Person; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.client.MockMvcTestClient; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import static org.hamcrest.Matchers.closeTo; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.startsWith; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.web.bind.annotation.RequestMethod.GET; +import static org.springframework.web.bind.annotation.RequestMethod.HEAD; + +/** + * MockMvcTestClient equivalent of the MockMvc + * {@link org.springframework.test.web.servlet.samples.standalone.resultmatchers.XpathAssertionTests}. + * + * @author Rossen Stoyanchev + */ +public class XpathAssertionTests { + + private static final Map musicNamespace = + Collections.singletonMap("ns", "https://example.org/music/people"); + + private final WebTestClient testClient = + MockMvcTestClient.bindToController(new MusicController()) + .alwaysExpect(status().isOk()) + .alwaysExpect(content().contentType(MediaType.parseMediaType("application/xml;charset=UTF-8"))) + .configureClient() + .defaultHeader(HttpHeaders.ACCEPT, "application/xml;charset=UTF-8") + .build(); + + + @Test + public void testExists() { + String composer = "/ns:people/composers/composer[%s]"; + String performer = "/ns:people/performers/performer[%s]"; + + testClient.get().uri("/music/people") + .exchange() + .expectBody() + .xpath(composer, musicNamespace, 1).exists() + .xpath(composer, musicNamespace, 2).exists() + .xpath(composer, musicNamespace, 3).exists() + .xpath(composer, musicNamespace, 4).exists() + .xpath(performer, musicNamespace, 1).exists() + .xpath(performer, musicNamespace, 2).exists() + .xpath(composer, musicNamespace, 1).string(notNullValue()); + } + + @Test + public void testDoesNotExist() { + String composer = "/ns:people/composers/composer[%s]"; + String performer = "/ns:people/performers/performer[%s]"; + + testClient.get().uri("/music/people") + .exchange() + .expectBody() + .xpath(composer, musicNamespace, 0).doesNotExist() + .xpath(composer, musicNamespace, 5).doesNotExist() + .xpath(performer, musicNamespace, 0).doesNotExist() + .xpath(performer, musicNamespace, 3).doesNotExist(); + } + + @Test + public void testString() { + + String composerName = "/ns:people/composers/composer[%s]/name"; + String performerName = "/ns:people/performers/performer[%s]/name"; + + testClient.get().uri("/music/people") + .exchange() + .expectBody() + .xpath(composerName, musicNamespace, 1).isEqualTo("Johann Sebastian Bach") + .xpath(composerName, musicNamespace, 2).isEqualTo("Johannes Brahms") + .xpath(composerName, musicNamespace, 3).isEqualTo("Edvard Grieg") + .xpath(composerName, musicNamespace, 4).isEqualTo("Robert Schumann") + .xpath(performerName, musicNamespace, 1).isEqualTo("Vladimir Ashkenazy") + .xpath(performerName, musicNamespace, 2).isEqualTo("Yehudi Menuhin") + .xpath(composerName, musicNamespace, 1).string(equalTo("Johann Sebastian Bach")) // Hamcrest.. + .xpath(composerName, musicNamespace, 1).string(startsWith("Johann")) + .xpath(composerName, musicNamespace, 1).string(notNullValue()); + } + + @Test + public void testNumber() { + String expression = "/ns:people/composers/composer[%s]/someDouble"; + + testClient.get().uri("/music/people") + .exchange() + .expectBody() + .xpath(expression, musicNamespace, 1).isEqualTo(21d) + .xpath(expression, musicNamespace, 2).isEqualTo(.0025) + .xpath(expression, musicNamespace, 3).isEqualTo(1.6035) + .xpath(expression, musicNamespace, 4).isEqualTo(Double.NaN) + .xpath(expression, musicNamespace, 1).number(equalTo(21d)) // Hamcrest.. + .xpath(expression, musicNamespace, 3).number(closeTo(1.6, .01)); + } + + @Test + public void testBoolean() { + String expression = "/ns:people/performers/performer[%s]/someBoolean"; + + testClient.get().uri("/music/people") + .exchange() + .expectBody() + .xpath(expression, musicNamespace, 1).isEqualTo(false) + .xpath(expression, musicNamespace, 2).isEqualTo(true); + } + + @Test + public void testNodeCount() { + testClient.get().uri("/music/people") + .exchange() + .expectBody() + .xpath("/ns:people/composers/composer", musicNamespace).nodeCount(4) + .xpath("/ns:people/performers/performer", musicNamespace).nodeCount(2) + .xpath("/ns:people/composers/composer", musicNamespace).nodeCount(equalTo(4)) // Hamcrest.. + .xpath("/ns:people/performers/performer", musicNamespace).nodeCount(equalTo(2)); + } + + @Test + public void testFeedWithLinefeedChars() { + MockMvcTestClient.bindToController(new BlogFeedController()).build() + .get().uri("/blog.atom") + .accept(MediaType.APPLICATION_ATOM_XML) + .exchange() + .expectBody() + .xpath("//feed/title").isEqualTo("Test Feed") + .xpath("//feed/icon").isEqualTo("https://www.example.com/favicon.ico"); + } + + + @Controller + private static class MusicController { + + @RequestMapping(value = "/music/people") + public @ResponseBody + PeopleWrapper getPeople() { + + List composers = Arrays.asList( + new Person("Johann Sebastian Bach").setSomeDouble(21), + new Person("Johannes Brahms").setSomeDouble(.0025), + new Person("Edvard Grieg").setSomeDouble(1.6035), + new Person("Robert Schumann").setSomeDouble(Double.NaN)); + + List performers = Arrays.asList( + new Person("Vladimir Ashkenazy").setSomeBoolean(false), + new Person("Yehudi Menuhin").setSomeBoolean(true)); + + return new PeopleWrapper(composers, performers); + } + } + + @SuppressWarnings("unused") + @XmlRootElement(name = "people", namespace = "https://example.org/music/people") + @XmlAccessorType(XmlAccessType.FIELD) + private static class PeopleWrapper { + + @XmlElementWrapper(name = "composers") + @XmlElement(name = "composer") + private List composers; + + @XmlElementWrapper(name = "performers") + @XmlElement(name = "performer") + private List performers; + + public PeopleWrapper() { + } + + public PeopleWrapper(List composers, List performers) { + this.composers = composers; + this.performers = performers; + } + + public List getComposers() { + return this.composers; + } + + public List getPerformers() { + return this.performers; + } + } + + + @Controller + public class BlogFeedController { + + @RequestMapping(value = "/blog.atom", method = {GET, HEAD}) + @ResponseBody + public String listPublishedPosts() { + return "\r\n" + + "\r\n" + + " Test Feed\r\n" + + " https://www.example.com/favicon.ico\r\n" + + "\r\n\r\n"; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/context/PersonController.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/context/PersonController.java index fa3ad7fa263..31781062ba6 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/context/PersonController.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/context/PersonController.java @@ -29,7 +29,7 @@ public class PersonController { private final PersonDao personDao; - PersonController(PersonDao personDao) { + public PersonController(PersonDao personDao) { this.personDao = personDao; } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/AsyncTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/AsyncTests.java index 65d8be3be96..1f343fb17b7 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/AsyncTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/AsyncTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -18,12 +18,12 @@ package org.springframework.test.web.servlet.samples.standalone; import java.io.StringWriter; import java.nio.charset.StandardCharsets; -import java.util.Collection; +import java.time.Duration; import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CopyOnWriteArrayList; import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -113,8 +113,6 @@ public class AsyncTests { .andExpect(request().asyncStarted()) .andReturn(); - this.asyncController.onMessage("Joe"); - this.mockMvc.perform(asyncDispatch(mvcResult)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) @@ -151,8 +149,6 @@ public class AsyncTests { .andExpect(request().asyncStarted()) .andReturn(); - this.asyncController.onMessage("Joe"); - this.mockMvc.perform(asyncDispatch(mvcResult)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) @@ -183,8 +179,6 @@ public class AsyncTests { assertThat(writer.toString().contains("Async started = true")).isTrue(); writer = new StringWriter(); - this.asyncController.onMessage("Joe"); - this.mockMvc.perform(asyncDispatch(mvcResult)) .andDo(print(writer)) .andExpect(status().isOk()) @@ -199,10 +193,6 @@ public class AsyncTests { @RequestMapping(path = "/{id}", produces = "application/json") private static class AsyncController { - private final Collection> deferredResults = new CopyOnWriteArrayList<>(); - - private final Collection> futureTasks = new CopyOnWriteArrayList<>(); - @RequestMapping(params = "callable") public Callable getCallable() { return () -> new Person("Joe"); @@ -235,9 +225,9 @@ public class AsyncTests { @RequestMapping(params = "deferredResult") public DeferredResult getDeferredResult() { - DeferredResult deferredResult = new DeferredResult<>(); - this.deferredResults.add(deferredResult); - return deferredResult; + DeferredResult result = new DeferredResult<>(); + delay(100, () -> result.setResult(new Person("Joe"))); + return result; } @RequestMapping(params = "deferredResultWithImmediateValue") @@ -249,26 +239,15 @@ public class AsyncTests { @RequestMapping(params = "deferredResultWithDelayedError") public DeferredResult getDeferredResultWithDelayedError() { - final DeferredResult deferredResult = new DeferredResult<>(); - new Thread() { - @Override - public void run() { - try { - Thread.sleep(100); - deferredResult.setErrorResult(new RuntimeException("Delayed Error")); - } - catch (InterruptedException e) { - /* no-op */ - } - } - }.start(); - return deferredResult; + DeferredResult result = new DeferredResult<>(); + delay(100, () -> result.setErrorResult(new RuntimeException("Delayed Error"))); + return result; } @RequestMapping(params = "listenableFuture") public ListenableFuture getListenableFuture() { ListenableFutureTask futureTask = new ListenableFutureTask<>(() -> new Person("Joe")); - this.futureTasks.add(futureTask); + delay(100, futureTask); return futureTask; } @@ -281,19 +260,12 @@ public class AsyncTests { @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) - public String errorHandler(Exception e) { - return e.getMessage(); + public String errorHandler(Exception ex) { + return ex.getMessage(); } - void onMessage(String name) { - for (DeferredResult deferredResult : this.deferredResults) { - deferredResult.setResult(new Person(name)); - this.deferredResults.remove(deferredResult); - } - for (ListenableFutureTask futureTask : this.futureTasks) { - futureTask.run(); - this.futureTasks.remove(futureTask); - } + private void delay(long millis, Runnable task) { + Mono.delay(Duration.ofMillis(millis)).doOnTerminate(task).subscribe(); } } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/ExceptionHandlerTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/ExceptionHandlerTests.java index 1e21a37c708..d6b026bed5c 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/ExceptionHandlerTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/ExceptionHandlerTests.java @@ -42,7 +42,7 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standal * @author Rossen Stoyanchev * @author Sam Brannen */ -class ExceptionHandlerTests { +public class ExceptionHandlerTests { @Nested class MvcTests { @@ -62,14 +62,6 @@ class ExceptionHandlerTests { .andExpect(status().isOk()) .andExpect(forwardedUrl("globalErrorView")); } - - @Test - void globalExceptionHandlerMethodUsingClassArgument() throws Exception { - standaloneSetup(PersonController.class).setControllerAdvice(GlobalExceptionHandler.class).build() - .perform(get("/person/Bonnie")) - .andExpect(status().isOk()) - .andExpect(forwardedUrl("globalErrorView")); - } } @@ -146,7 +138,7 @@ class ExceptionHandlerTests { void noHandlerFound() throws Exception { standaloneSetup(RestPersonController.class) .setControllerAdvice(RestGlobalExceptionHandler.class, RestPersonControllerExceptionHandler.class) - .addDispatcherServletCustomizer(dispatcherServlet -> dispatcherServlet.setThrowExceptionIfNoHandlerFound(true)) + .addDispatcherServletCustomizer(servlet -> servlet.setThrowExceptionIfNoHandlerFound(true)) .build() .perform(get("/bogus").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/resulthandlers/PrintingResultHandlerSmokeTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/resulthandlers/PrintingResultHandlerSmokeTests.java index 72daced4b9d..7d23bebc0b5 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/resulthandlers/PrintingResultHandlerSmokeTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/resulthandlers/PrintingResultHandlerSmokeTests.java @@ -48,9 +48,12 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standal * @author Sam Brannen * @see org.springframework.test.web.servlet.result.PrintingResultHandlerTests */ -@Disabled("Not intended to be executed with the build. Comment out this line to inspect the output manually.") +@Disabled public class PrintingResultHandlerSmokeTests { + // Not intended to be executed with the build. + // Comment out class-level @Disabled to see the output. + @Test public void testPrint() throws Exception { StringWriter writer = new StringWriter(); diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/resultmatchers/FlashAttributeAssertionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/resultmatchers/FlashAttributeAssertionTests.java index 89466b75dfd..a6a5b7fc086 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/resultmatchers/FlashAttributeAssertionTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/resultmatchers/FlashAttributeAssertionTests.java @@ -41,7 +41,7 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standal * @author Rossen Stoyanchev * @author Sam Brannen */ -class FlashAttributeAssertionTests { +public class FlashAttributeAssertionTests { private final MockMvc mockMvc = standaloneSetup(new PersonController()) .alwaysExpect(status().isFound()) diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/resultmatchers/ModelAssertionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/resultmatchers/ModelAssertionTests.java index 6856253c3ab..11473a31ab1 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/resultmatchers/ModelAssertionTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/resultmatchers/ModelAssertionTests.java @@ -49,7 +49,7 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standal * * @author Rossen Stoyanchev */ -class ModelAssertionTests { +public class ModelAssertionTests { private MockMvc mockMvc; diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/resultmatchers/RequestAttributeAssertionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/resultmatchers/RequestAttributeAssertionTests.java index 37efd72414f..7f98ce33007 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/resultmatchers/RequestAttributeAssertionTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/resultmatchers/RequestAttributeAssertionTests.java @@ -36,7 +36,7 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standal * * @author Rossen Stoyanchev */ -class RequestAttributeAssertionTests { +public class RequestAttributeAssertionTests { private final MockMvc mockMvc = standaloneSetup(new SimpleController()).build(); diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/resultmatchers/SessionAttributeAssertionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/resultmatchers/SessionAttributeAssertionTests.java index 508c9f63720..d60a4c1428b 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/resultmatchers/SessionAttributeAssertionTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/resultmatchers/SessionAttributeAssertionTests.java @@ -43,7 +43,7 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standal * @author Rossen Stoyanchev * @author Sam Brannen */ -class SessionAttributeAssertionTests { +public class SessionAttributeAssertionTests { private final MockMvc mockMvc = standaloneSetup(new SimpleController()) .defaultRequest(get("/"))