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 0000000000..0e8dd4c2aa --- /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 0000000000..1c6c357779 --- /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 0000000000..705e89432b --- /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 0000000000..8e481389e5 --- /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 0000000000..4226402768 --- /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 0000000000..7323682c55 --- /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 0000000000..1908958a63 --- /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 0000000000..5ddd1859ad --- /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 0000000000..93457f87fd --- /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 0000000000..79ca42bf84 --- /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 0000000000..707602bfeb --- /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 0000000000..6dce2699cc --- /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 0000000000..ea5b3a1205 --- /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 0000000000..71dc559237 --- /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 0000000000..a2dbc15dfe --- /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 0000000000..bf6549fd26 --- /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 0000000000..2e8c1981a8 --- /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 0000000000..fb706063ea --- /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 0000000000..2034d3c8de --- /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 0000000000..91fa05eb5f --- /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 0000000000..2fda9eac8a --- /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 0000000000..0f1480d596 --- /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 0000000000..5aa5cb2715 --- /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 0000000000..640de972a5 --- /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 0000000000..61c0e41f55 --- /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 0000000000..20c736b6f3 --- /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 0000000000..50216806c1 --- /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 0000000000..274000430c --- /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 0000000000..bc48bccdf7 --- /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 0000000000..5b1add31ff --- /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 0000000000..80759c5fcb --- /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 0000000000..15f8bb19bf --- /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 0000000000..b60196e01d --- /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 0000000000..2666021124 --- /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 0000000000..c7fd97f6a1 --- /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 fa3ad7fa26..31781062ba 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 65d8be3be9..1f343fb17b 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 1e21a37c70..d6b026bed5 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 72daced4b9..7d23bebc0b 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 89466b75df..a6a5b7fc08 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 6856253c3a..11473a31ab 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 37efd72414..7f98ce3300 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 508c9f6372..d60a4c1428 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("/"))