Add support for building a request directly from MockMvcTester

Closes gh-32913'
This commit is contained in:
Stéphane Nicoll 2024-05-28 15:33:21 +02:00
commit bad4e18b4d
8 changed files with 1270 additions and 967 deletions

View File

@ -23,13 +23,18 @@ import java.util.Map;
import java.util.function.Function;
import java.util.stream.StreamSupport;
import org.assertj.core.api.AssertProvider;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.mock.web.MockHttpServletRequest;
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.AbstractMockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
@ -58,11 +63,25 @@ import org.springframework.web.context.WebApplicationContext;
* MockMvcTester mvc = MockMvcTester.of(new PersonController());
* </code></pre>
*
* <p>Once a tester instance is available, you can perform requests in a similar
* fashion as with {@link MockMvc}, and wrapping the result in
* {@code assertThat()} provides access to assertions. For instance:
* <p>Simple, single-statement assertions can be done wrapping the request
* builder in {@code assertThat()} provides access to assertions. For instance:
* <pre><code class="java">
* // perform a GET on /hi and assert the response body is equal to Hello
* assertThat(mvc.get().uri("/hi")).hasStatusOk().hasBodyTextEqualTo("Hello");
* </code></pre>
*
*<p>For more complex scenarios the {@linkplain MvcTestResult result} of the
* exchange can be assigned in a variable to run multiple assertions:
* <pre><code class="java">
* // perform a POST on /save and assert the response body is empty
* MvcTestResult result = mvc.post().uri("/save").exchange();
* assertThat(result).hasStatus(HttpStatus.CREATED);
* assertThat(result).body().isEmpty();
* </code></pre>
*
* <p>You can also perform requests using the static builders approach that
* {@link MockMvc} uses. For instance:<pre><code class="java">
* // perform a GET on /hi and assert the response body is equal to Hello
* assertThat(mvc.perform(get("/hi")))
* .hasStatusOk().hasBodyTextEqualTo("Hello");
* </code></pre>
@ -74,12 +93,11 @@ import org.springframework.web.context.WebApplicationContext;
* which allows you to assert that a request failed unexpectedly:
* <pre><code class="java">
* // perform a GET on /boom and assert the message for the the unresolved exception
* assertThat(mvc.perform(get("/boom")))
* .hasUnresolvedException())
* assertThat(mvc.get().uri("/boom")).hasUnresolvedException())
* .withMessage("Test exception");
* </code></pre>
*
* <p>{@link MockMvcTester} can be configured with a list of
* <p>{@code MockMvcTester} can be configured with a list of
* {@linkplain HttpMessageConverter message converters} to allow the response
* body to be deserialized, rather than asserting on the raw values.
*
@ -104,8 +122,7 @@ public final class MockMvcTester {
}
/**
* Create a {@link MockMvcTester} instance that delegates to the given
* {@link MockMvc} instance.
* Create an instance that delegates to the given {@link MockMvc} instance.
* @param mockMvc the MockMvc instance to delegate calls to
*/
public static MockMvcTester create(MockMvc mockMvc) {
@ -113,9 +130,9 @@ public final class MockMvcTester {
}
/**
* Create an {@link MockMvcTester} instance using the given, fully
* initialized (i.e., <em>refreshed</em>) {@link WebApplicationContext}. The
* given {@code customizations} are applied to the {@link DefaultMockMvcBuilder}
* Create an instance using the given, fully initialized (i.e.,
* <em>refreshed</em>) {@link WebApplicationContext}. The given
* {@code customizations} are applied to the {@link DefaultMockMvcBuilder}
* that ultimately creates the underlying {@link MockMvc} instance.
* <p>If no further customization of the underlying {@link MockMvc} instance
* is required, use {@link #from(WebApplicationContext)}.
@ -134,8 +151,8 @@ public final class MockMvcTester {
}
/**
* Shortcut to create an {@link MockMvcTester} instance using the given,
* fully initialized (i.e., <em>refreshed</em>) {@link WebApplicationContext}.
* Shortcut to create an instance using the given fully initialized (i.e.,
* <em>refreshed</em>) {@link WebApplicationContext}.
* <p>Consider using {@link #from(WebApplicationContext, Function)} if
* further customization of the underlying {@link MockMvc} instance is
* required.
@ -148,9 +165,8 @@ public final class MockMvcTester {
}
/**
* Create an {@link MockMvcTester} instance by registering one or more
* {@code @Controller} instances and configuring Spring MVC infrastructure
* programmatically.
* Create an instance by registering one or more {@code @Controller} instances
* and configuring Spring MVC infrastructure programmatically.
* <p>This allows full control over the instantiation and initialization of
* controllers and their dependencies, similar to plain unit tests while
* also making it possible to test one controller at a time.
@ -170,8 +186,8 @@ public final class MockMvcTester {
}
/**
* Shortcut to create an {@link MockMvcTester} instance by registering one
* or more {@code @Controller} instances.
* Shortcut to create an instance by registering one or more {@code @Controller}
* instances.
* <p>The minimum infrastructure required by the
* {@link org.springframework.web.servlet.DispatcherServlet DispatcherServlet}
* to serve requests with annotated controllers is created. Consider using
@ -187,8 +203,8 @@ public final class MockMvcTester {
}
/**
* Return a new {@link MockMvcTester} instance using the specified
* {@linkplain HttpMessageConverter message converters}.
* Return a new instance using the specified {@linkplain HttpMessageConverter
* message converters}.
* <p>If none are specified, only basic assertions on the response body can
* be performed. Consider registering a suitable JSON converter for asserting
* against JSON data structures.
@ -200,8 +216,105 @@ public final class MockMvcTester {
}
/**
* Perform a request and return a {@link MvcTestResult result} that can be
* used with standard {@link org.assertj.core.api.Assertions AssertJ} assertions.
* Prepare an HTTP GET request.
* <p>The returned builder can be wrapped in {@code assertThat} to enable
* assertions on the result. For multi-statements assertions, use
* {@linkplain MockMvcRequestBuilder#exchange() exchange} to assign the
* result.
* @return a request builder for specifying the target URI
*/
public MockMvcRequestBuilder get() {
return method(HttpMethod.GET);
}
/**
* Prepare an HTTP HEAD request.
* <p>The returned builder can be wrapped in {@code assertThat} to enable
* assertions on the result. For multi-statements assertions, use
* {@linkplain MockMvcRequestBuilder#exchange() exchange} to assign the
* result.
* @return a request builder for specifying the target URI
*/
public MockMvcRequestBuilder head() {
return method(HttpMethod.HEAD);
}
/**
* Prepare an HTTP POST request.
* <p>The returned builder can be wrapped in {@code assertThat} to enable
* assertions on the result. For multi-statements assertions, use
* {@linkplain MockMvcRequestBuilder#exchange() exchange} to assign the
* result.
* @return a request builder for specifying the target URI
*/
public MockMvcRequestBuilder post() {
return method(HttpMethod.POST);
}
/**
* Prepare an HTTP PUT request.
* <p>The returned builder can be wrapped in {@code assertThat} to enable
* assertions on the result. For multi-statements assertions, use
* {@linkplain MockMvcRequestBuilder#exchange() exchange} to assign the
* result.
* @return a request builder for specifying the target URI
*/
public MockMvcRequestBuilder put() {
return method(HttpMethod.PUT);
}
/**
* Prepare an HTTP PATCH request.
* <p>The returned builder can be wrapped in {@code assertThat} to enable
* assertions on the result. For multi-statements assertions, use
* {@linkplain MockMvcRequestBuilder#exchange() exchange} to assign the
* result.
* @return a request builder for specifying the target URI
*/
public MockMvcRequestBuilder patch() {
return method(HttpMethod.PATCH);
}
/**
* Prepare an HTTP DELETE request.
* <p>The returned builder can be wrapped in {@code assertThat} to enable
* assertions on the result. For multi-statements assertions, use
* {@linkplain MockMvcRequestBuilder#exchange() exchange} to assign the
* result.
* @return a request builder for specifying the target URI
*/
public MockMvcRequestBuilder delete() {
return method(HttpMethod.DELETE);
}
/**
* Prepare an HTTP OPTIONS request.
* <p>The returned builder can be wrapped in {@code assertThat} to enable
* assertions on the result. For multi-statements assertions, use
* {@linkplain MockMvcRequestBuilder#exchange() exchange} to assign the
* result.
* @return a request builder for specifying the target URI
*/
public MockMvcRequestBuilder options() {
return method(HttpMethod.OPTIONS);
}
/**
* Prepare a request for the specified {@code HttpMethod}.
* <p>The returned builder can be wrapped in {@code assertThat} to enable
* assertions on the result. For multi-statements assertions, use
* {@linkplain MockMvcRequestBuilder#exchange() exchange} to assign the
* result.
* @return a request builder for specifying the target URI
*/
public MockMvcRequestBuilder method(HttpMethod method) {
return new MockMvcRequestBuilder(method);
}
/**
* Perform a request using {@link MockMvcRequestBuilders} and return a
* {@link MvcTestResult result} that can be used with standard
* {@link org.assertj.core.api.Assertions AssertJ} assertions.
* <p>Use static methods of {@link MockMvcRequestBuilders} to prepare the
* request, wrapping the invocation in {@code assertThat}. The following
* asserts that a {@linkplain MockMvcRequestBuilders#get(URI) GET} request
@ -226,6 +339,8 @@ public final class MockMvcTester {
* {@link org.springframework.test.web.servlet.request.MockMvcRequestBuilders}
* @return an {@link MvcTestResult} to be wrapped in {@code assertThat}
* @see MockMvc#perform(RequestBuilder)
* @see #get()
* @see #post()
*/
public MvcTestResult perform(RequestBuilder requestBuilder) {
Object result = getMvcResultOrFailure(requestBuilder);
@ -259,4 +374,25 @@ public final class MockMvcTester {
.findFirst().orElse(null);
}
/**
* A builder for {@link MockHttpServletRequest} that supports AssertJ.
*/
public final class MockMvcRequestBuilder extends AbstractMockHttpServletRequestBuilder<MockMvcRequestBuilder>
implements AssertProvider<MvcTestResultAssert> {
private MockMvcRequestBuilder(HttpMethod httpMethod) {
super(httpMethod);
}
public MvcTestResult exchange() {
return perform(this);
}
@Override
public MvcTestResultAssert assertThat() {
return new MvcTestResultAssert(exchange(), MockMvcTester.this.jsonMessageConverter);
}
}
}

View File

@ -54,6 +54,7 @@ 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.AbstractMockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
@ -134,7 +135,7 @@ public class MockMvcHttpConnector implements ClientHttpConnector {
// Initialize the client request
requestCallback.apply(httpRequest).block(TIMEOUT);
MockHttpServletRequestBuilder requestBuilder =
AbstractMockHttpServletRequestBuilder<?> requestBuilder =
initRequestBuilder(httpMethod, uri, httpRequest, contentRef.get());
requestBuilder.headers(httpRequest.getHeaders());
@ -149,7 +150,7 @@ public class MockMvcHttpConnector implements ClientHttpConnector {
return requestBuilder;
}
private MockHttpServletRequestBuilder initRequestBuilder(
private AbstractMockHttpServletRequestBuilder<?> initRequestBuilder(
HttpMethod httpMethod, URI uri, MockClientHttpRequest httpRequest, @Nullable byte[] bytes) {
String contentType = httpRequest.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);

View File

@ -0,0 +1,948 @@
/*
* Copyright 2002-2024 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.request;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpSession;
import org.springframework.beans.Mergeable;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.FlashMap;
import org.springframework.web.servlet.FlashMapManager;
import org.springframework.web.servlet.support.SessionFlashMapManager;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
import org.springframework.web.util.UrlPathHelper;
/**
* Base builder for {@link MockHttpServletRequest} required as input to
* perform requests in {@link MockMvc}.
*
* @author Rossen Stoyanchev
* @author Juergen Hoeller
* @author Arjen Poutsma
* @author Sam Brannen
* @author Kamill Sokol
* @since 6.2
* @param <B> a self reference to the builder type
*/
public abstract class AbstractMockHttpServletRequestBuilder<B extends AbstractMockHttpServletRequestBuilder<B>>
implements ConfigurableSmartRequestBuilder<B>, Mergeable {
private final HttpMethod method;
@Nullable
private URI uri;
private String contextPath = "";
private String servletPath = "";
@Nullable
private String pathInfo = "";
@Nullable
private Boolean secure;
@Nullable
private Principal principal;
@Nullable
private MockHttpSession session;
@Nullable
private String remoteAddress;
@Nullable
private String characterEncoding;
@Nullable
private byte[] content;
@Nullable
private String contentType;
private final MultiValueMap<String, Object> headers = new LinkedMultiValueMap<>();
private final MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
private final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
private final MultiValueMap<String, String> formFields = new LinkedMultiValueMap<>();
private final List<Cookie> cookies = new ArrayList<>();
private final List<Locale> locales = new ArrayList<>();
private final Map<String, Object> requestAttributes = new LinkedHashMap<>();
private final Map<String, Object> sessionAttributes = new LinkedHashMap<>();
private final Map<String, Object> flashAttributes = new LinkedHashMap<>();
private final List<RequestPostProcessor> postProcessors = new ArrayList<>();
/**
* Create a new instance using the specified {@link HttpMethod}.
* @param httpMethod the HTTP method (GET, POST, etc.)
*/
protected AbstractMockHttpServletRequestBuilder(HttpMethod httpMethod) {
Assert.notNull(httpMethod, "'httpMethod' is required");
this.method = httpMethod;
}
@SuppressWarnings("unchecked")
protected B self() {
return (B) this;
}
/**
* Specify the URI using an absolute, fully constructed {@link java.net.URI}.
*/
public B uri(URI uri) {
this.uri = uri;
return self();
}
/**
* Specify the URI for the request using a URI template and URI variables.
*/
public B uri(String uriTemplate, Object... uriVariables) {
return uri(initUri(uriTemplate, uriVariables));
}
private static URI initUri(String uri, Object[] vars) {
Assert.notNull(uri, "'uri' must not be null");
Assert.isTrue(uri.isEmpty() || uri.startsWith("/") || uri.startsWith("http://") || uri.startsWith("https://"),
() -> "'uri' should start with a path or be a complete HTTP URI: " + uri);
String uriString = (uri.isEmpty() ? "/" : uri);
return UriComponentsBuilder.fromUriString(uriString).buildAndExpand(vars).encode().toUri();
}
/**
* Specify the portion of the requestURI that represents the context path.
* The context path, if specified, must match to the start of the request URI.
* <p>In most cases, tests can be written by omitting the context path from
* the requestURI. This is because most applications don't actually depend
* on the name under which they're deployed. If specified here, the context
* path must start with a "/" and must not end with a "/".
* @see jakarta.servlet.http.HttpServletRequest#getContextPath()
*/
public B contextPath(String contextPath) {
if (StringUtils.hasText(contextPath)) {
Assert.isTrue(contextPath.startsWith("/"), "Context path must start with a '/'");
Assert.isTrue(!contextPath.endsWith("/"), "Context path must not end with a '/'");
}
this.contextPath = contextPath;
return self();
}
/**
* Specify the portion of the requestURI that represents the path to which
* the Servlet is mapped. This is typically a portion of the requestURI
* after the context path.
* <p>In most cases, tests can be written by omitting the servlet path from
* the requestURI. This is because most applications don't actually depend
* on the prefix to which a servlet is mapped. For example if a Servlet is
* mapped to {@code "/main/*"}, tests can be written with the requestURI
* {@code "/accounts/1"} as opposed to {@code "/main/accounts/1"}.
* If specified here, the servletPath must start with a "/" and must not
* end with a "/".
* @see jakarta.servlet.http.HttpServletRequest#getServletPath()
*/
public B servletPath(String servletPath) {
if (StringUtils.hasText(servletPath)) {
Assert.isTrue(servletPath.startsWith("/"), "Servlet path must start with a '/'");
Assert.isTrue(!servletPath.endsWith("/"), "Servlet path must not end with a '/'");
}
this.servletPath = servletPath;
return self();
}
/**
* Specify the portion of the requestURI that represents the pathInfo.
* <p>If left unspecified (recommended), the pathInfo will be automatically derived
* by removing the contextPath and the servletPath from the requestURI and using any
* remaining part. If specified here, the pathInfo must start with a "/".
* <p>If specified, the pathInfo will be used as-is.
* @see jakarta.servlet.http.HttpServletRequest#getPathInfo()
*/
public B pathInfo(@Nullable String pathInfo) {
if (StringUtils.hasText(pathInfo)) {
Assert.isTrue(pathInfo.startsWith("/"), "Path info must start with a '/'");
}
this.pathInfo = pathInfo;
return self();
}
/**
* Set the secure property of the {@link ServletRequest} indicating use of a
* secure channel, such as HTTPS.
* @param secure whether the request is using a secure channel
*/
public B secure(boolean secure){
this.secure = secure;
return self();
}
/**
* Set the character encoding of the request.
* @param encoding the character encoding
* @since 5.3.10
* @see StandardCharsets
* @see #characterEncoding(String)
*/
public B characterEncoding(Charset encoding) {
return characterEncoding(encoding.name());
}
/**
* Set the character encoding of the request.
* @param encoding the character encoding
*/
public B characterEncoding(String encoding) {
this.characterEncoding = encoding;
return self();
}
/**
* Set the request body.
* <p>If content is provided and {@link #contentType(MediaType)} is set to
* {@code application/x-www-form-urlencoded}, the content will be parsed
* and used to populate the {@link #param(String, String...) request
* parameters} map.
* @param content the body content
*/
public B content(byte[] content) {
this.content = content;
return self();
}
/**
* Set the request body as a UTF-8 String.
* <p>If content is provided and {@link #contentType(MediaType)} is set to
* {@code application/x-www-form-urlencoded}, the content will be parsed
* and used to populate the {@link #param(String, String...) request
* parameters} map.
* @param content the body content
*/
public B content(String content) {
this.content = content.getBytes(StandardCharsets.UTF_8);
return self();
}
/**
* Set the 'Content-Type' header of the request.
* <p>If content is provided and {@code contentType} is set to
* {@code application/x-www-form-urlencoded}, the content will be parsed
* and used to populate the {@link #param(String, String...) request
* parameters} map.
* @param contentType the content type
*/
public B contentType(MediaType contentType) {
Assert.notNull(contentType, "'contentType' must not be null");
this.contentType = contentType.toString();
return self();
}
/**
* Set the 'Content-Type' header of the request as a raw String value,
* possibly not even well-formed (for testing purposes).
* @param contentType the content type
* @since 4.1.2
*/
public B contentType(String contentType) {
Assert.notNull(contentType, "'contentType' must not be null");
this.contentType = contentType;
return self();
}
/**
* Set the 'Accept' header to the given media type(s).
* @param mediaTypes one or more media types
*/
public B accept(MediaType... mediaTypes) {
Assert.notEmpty(mediaTypes, "'mediaTypes' must not be empty");
this.headers.set("Accept", MediaType.toString(Arrays.asList(mediaTypes)));
return self();
}
/**
* Set the {@code Accept} header using raw String values, possibly not even
* well-formed (for testing purposes).
* @param mediaTypes one or more media types; internally joined as
* comma-separated String
*/
public B accept(String... mediaTypes) {
Assert.notEmpty(mediaTypes, "'mediaTypes' must not be empty");
this.headers.set("Accept", String.join(", ", mediaTypes));
return self();
}
/**
* Add a header to the request. Values are always added.
* @param name the header name
* @param values one or more header values
*/
public B header(String name, Object... values) {
addToMultiValueMap(this.headers, name, values);
return self();
}
/**
* Add all headers to the request. Values are always added.
* @param httpHeaders the headers and values to add
*/
public B headers(HttpHeaders httpHeaders) {
httpHeaders.forEach(this.headers::addAll);
return self();
}
/**
* Add a request parameter to {@link MockHttpServletRequest#getParameterMap()}.
* <p>In the Servlet API, a request parameter may be parsed from the query
* string and/or from the body of an {@code application/x-www-form-urlencoded}
* request. This method simply adds to the request parameter map. You may
* also use add Servlet request parameters by specifying the query or form
* data through one of the following:
* <ul>
* <li>Supply a URL with a query to {@link MockMvcRequestBuilders}.
* <li>Add query params via {@link #queryParam} or {@link #queryParams}.
* <li>Provide {@link #content} with {@link #contentType}
* {@code application/x-www-form-urlencoded}.
* </ul>
* @param name the parameter name
* @param values one or more values
*/
public B param(String name, String... values) {
addToMultiValueMap(this.parameters, name, values);
return self();
}
/**
* Variant of {@link #param(String, String...)} with a {@link MultiValueMap}.
* @param params the parameters to add
* @since 4.2.4
*/
public B params(MultiValueMap<String, String> params) {
params.forEach((name, values) -> {
for (String value : values) {
this.parameters.add(name, value);
}
});
return self();
}
/**
* Append to the query string and also add to the
* {@link #param(String, String...) request parameters} map. The parameter
* name and value are encoded when they are added to the query string.
* @param name the parameter name
* @param values one or more values
* @since 5.2.2
*/
public B queryParam(String name, String... values) {
param(name, values);
this.queryParams.addAll(name, Arrays.asList(values));
return self();
}
/**
* Append to the query string and also add to the
* {@link #params(MultiValueMap) request parameters} map. The parameter
* name and value are encoded when they are added to the query string.
* @param params the parameters to add
* @since 5.2.2
*/
public B queryParams(MultiValueMap<String, String> params) {
params(params);
this.queryParams.addAll(params);
return self();
}
/**
* Append the given value(s) to the given form field and also add them to the
* {@linkplain #param(String, String...) request parameters} map.
* @param name the field name
* @param values one or more values
* @since 6.1.7
*/
public B formField(String name, String... values) {
param(name, values);
this.formFields.addAll(name, Arrays.asList(values));
return self();
}
/**
* Variant of {@link #formField(String, String...)} with a {@link MultiValueMap}.
* @param formFields the form fields to add
* @since 6.1.7
*/
public B formFields(MultiValueMap<String, String> formFields) {
params(formFields);
this.formFields.addAll(formFields);
return self();
}
/**
* Add the given cookies to the request. Cookies are always added.
* @param cookies the cookies to add
*/
public B cookie(Cookie... cookies) {
Assert.notEmpty(cookies, "'cookies' must not be empty");
this.cookies.addAll(Arrays.asList(cookies));
return self();
}
/**
* Add the specified locales as preferred request locales.
* @param locales the locales to add
* @since 4.3.6
* @see #locale(Locale)
*/
public B locale(Locale... locales) {
Assert.notEmpty(locales, "'locales' must not be empty");
this.locales.addAll(Arrays.asList(locales));
return self();
}
/**
* Set the locale of the request, overriding any previous locales.
* @param locale the locale, or {@code null} to reset it
* @see #locale(Locale...)
*/
public B locale(@Nullable Locale locale) {
this.locales.clear();
if (locale != null) {
this.locales.add(locale);
}
return self();
}
/**
* Set a request attribute.
* @param name the attribute name
* @param value the attribute value
*/
public B requestAttr(String name, Object value) {
addToMap(this.requestAttributes, name, value);
return self();
}
/**
* Set a session attribute.
* @param name the session attribute name
* @param value the session attribute value
*/
public B sessionAttr(String name, Object value) {
addToMap(this.sessionAttributes, name, value);
return self();
}
/**
* Set session attributes.
* @param sessionAttributes the session attributes
*/
public B sessionAttrs(Map<String, Object> sessionAttributes) {
Assert.notEmpty(sessionAttributes, "'sessionAttributes' must not be empty");
sessionAttributes.forEach(this::sessionAttr);
return self();
}
/**
* Set an "input" flash attribute.
* @param name the flash attribute name
* @param value the flash attribute value
*/
public B flashAttr(String name, Object value) {
addToMap(this.flashAttributes, name, value);
return self();
}
/**
* Set flash attributes.
* @param flashAttributes the flash attributes
*/
public B flashAttrs(Map<String, Object> flashAttributes) {
Assert.notEmpty(flashAttributes, "'flashAttributes' must not be empty");
flashAttributes.forEach(this::flashAttr);
return self();
}
/**
* Set the HTTP session to use, possibly re-used across requests.
* <p>Individual attributes provided via {@link #sessionAttr(String, Object)}
* override the content of the session provided here.
* @param session the HTTP session
*/
public B session(MockHttpSession session) {
Assert.notNull(session, "'session' must not be null");
this.session = session;
return self();
}
/**
* Set the principal of the request.
* @param principal the principal
*/
public B principal(Principal principal) {
Assert.notNull(principal, "'principal' must not be null");
this.principal = principal;
return self();
}
/**
* Set the remote address of the request.
* @param remoteAddress the remote address (IP)
* @since 6.0.10
*/
public B remoteAddress(String remoteAddress) {
Assert.hasText(remoteAddress, "'remoteAddress' must not be null or blank");
this.remoteAddress = remoteAddress;
return self();
}
/**
* An extension point for further initialization of {@link MockHttpServletRequest}
* in ways not built directly into the {@code MockHttpServletRequestBuilder}.
* Implementation of this interface can have builder-style methods themselves
* and be made accessible through static factory methods.
* @param postProcessor a post-processor to add
*/
@Override
public B with(RequestPostProcessor postProcessor) {
Assert.notNull(postProcessor, "postProcessor is required");
this.postProcessors.add(postProcessor);
return self();
}
/**
* {@inheritDoc}
* @return always returns {@code true}.
*/
@Override
public boolean isMergeEnabled() {
return true;
}
/**
* Merges the properties of the "parent" RequestBuilder accepting values
* only if not already set in "this" instance.
* @param parent the parent {@code RequestBuilder} to inherit properties from
* @return the result of the merge
*/
@Override
public Object merge(@Nullable Object parent) {
if (parent == null) {
return this;
}
if (!(parent instanceof AbstractMockHttpServletRequestBuilder<?> parentBuilder)) {
throw new IllegalArgumentException("Cannot merge with [" + parent.getClass().getName() + "]");
}
if (!StringUtils.hasText(this.contextPath)) {
this.contextPath = parentBuilder.contextPath;
}
if (!StringUtils.hasText(this.servletPath)) {
this.servletPath = parentBuilder.servletPath;
}
if ("".equals(this.pathInfo)) {
this.pathInfo = parentBuilder.pathInfo;
}
if (this.secure == null) {
this.secure = parentBuilder.secure;
}
if (this.principal == null) {
this.principal = parentBuilder.principal;
}
if (this.session == null) {
this.session = parentBuilder.session;
}
if (this.remoteAddress == null) {
this.remoteAddress = parentBuilder.remoteAddress;
}
if (this.characterEncoding == null) {
this.characterEncoding = parentBuilder.characterEncoding;
}
if (this.content == null) {
this.content = parentBuilder.content;
}
if (this.contentType == null) {
this.contentType = parentBuilder.contentType;
}
for (Map.Entry<String, List<Object>> entry : parentBuilder.headers.entrySet()) {
String headerName = entry.getKey();
if (!this.headers.containsKey(headerName)) {
this.headers.put(headerName, entry.getValue());
}
}
for (Map.Entry<String, List<String>> entry : parentBuilder.parameters.entrySet()) {
String paramName = entry.getKey();
if (!this.parameters.containsKey(paramName)) {
this.parameters.put(paramName, entry.getValue());
}
}
for (Map.Entry<String, List<String>> entry : parentBuilder.queryParams.entrySet()) {
String paramName = entry.getKey();
if (!this.queryParams.containsKey(paramName)) {
this.queryParams.put(paramName, entry.getValue());
}
}
for (Map.Entry<String, List<String>> entry : parentBuilder.formFields.entrySet()) {
String paramName = entry.getKey();
if (!this.formFields.containsKey(paramName)) {
this.formFields.put(paramName, entry.getValue());
}
}
for (Cookie cookie : parentBuilder.cookies) {
if (!containsCookie(cookie)) {
this.cookies.add(cookie);
}
}
for (Locale locale : parentBuilder.locales) {
if (!this.locales.contains(locale)) {
this.locales.add(locale);
}
}
for (Map.Entry<String, Object> entry : parentBuilder.requestAttributes.entrySet()) {
String attributeName = entry.getKey();
if (!this.requestAttributes.containsKey(attributeName)) {
this.requestAttributes.put(attributeName, entry.getValue());
}
}
for (Map.Entry<String, Object> entry : parentBuilder.sessionAttributes.entrySet()) {
String attributeName = entry.getKey();
if (!this.sessionAttributes.containsKey(attributeName)) {
this.sessionAttributes.put(attributeName, entry.getValue());
}
}
for (Map.Entry<String, Object> entry : parentBuilder.flashAttributes.entrySet()) {
String attributeName = entry.getKey();
if (!this.flashAttributes.containsKey(attributeName)) {
this.flashAttributes.put(attributeName, entry.getValue());
}
}
this.postProcessors.addAll(0, parentBuilder.postProcessors);
return this;
}
private boolean containsCookie(Cookie cookie) {
for (Cookie cookieToCheck : this.cookies) {
if (ObjectUtils.nullSafeEquals(cookieToCheck.getName(), cookie.getName())) {
return true;
}
}
return false;
}
/**
* Build a {@link MockHttpServletRequest}.
*/
@Override
public final MockHttpServletRequest buildRequest(ServletContext servletContext) {
Assert.notNull(this.uri, "'uri' is required");
MockHttpServletRequest request = createServletRequest(servletContext);
request.setAsyncSupported(true);
request.setMethod(this.method.name());
String requestUri = this.uri.getRawPath();
request.setRequestURI(requestUri);
if (this.uri.getScheme() != null) {
request.setScheme(this.uri.getScheme());
}
if (this.uri.getHost() != null) {
request.setServerName(this.uri.getHost());
}
if (this.uri.getPort() != -1) {
request.setServerPort(this.uri.getPort());
}
updatePathRequestProperties(request, requestUri);
if (this.secure != null) {
request.setSecure(this.secure);
}
if (this.principal != null) {
request.setUserPrincipal(this.principal);
}
if (this.remoteAddress != null) {
request.setRemoteAddr(this.remoteAddress);
}
if (this.session != null) {
request.setSession(this.session);
}
request.setCharacterEncoding(this.characterEncoding);
request.setContent(this.content);
request.setContentType(this.contentType);
this.headers.forEach((name, values) -> {
for (Object value : values) {
request.addHeader(name, value);
}
});
if (!ObjectUtils.isEmpty(this.content) &&
!this.headers.containsKey(HttpHeaders.CONTENT_LENGTH) &&
!this.headers.containsKey(HttpHeaders.TRANSFER_ENCODING)) {
request.addHeader(HttpHeaders.CONTENT_LENGTH, this.content.length);
}
String query = this.uri.getRawQuery();
if (!this.queryParams.isEmpty()) {
String str = UriComponentsBuilder.newInstance().queryParams(this.queryParams).build().encode().getQuery();
query = StringUtils.hasLength(query) ? (query + "&" + str) : str;
}
if (query != null) {
request.setQueryString(query);
}
addRequestParams(request, UriComponentsBuilder.fromUri(this.uri).build().getQueryParams());
this.parameters.forEach((name, values) -> {
for (String value : values) {
request.addParameter(name, value);
}
});
if (!this.formFields.isEmpty()) {
if (this.content != null && this.content.length > 0) {
throw new IllegalStateException("Could not write form data with an existing body");
}
Charset charset = (this.characterEncoding != null ?
Charset.forName(this.characterEncoding) : StandardCharsets.UTF_8);
MediaType mediaType = (request.getContentType() != null ?
MediaType.parseMediaType(request.getContentType()) :
new MediaType(MediaType.APPLICATION_FORM_URLENCODED, charset));
if (!mediaType.isCompatibleWith(MediaType.APPLICATION_FORM_URLENCODED)) {
throw new IllegalStateException("Invalid content type: '" + mediaType +
"' is not compatible with '" + MediaType.APPLICATION_FORM_URLENCODED + "'");
}
request.setContent(writeFormData(mediaType, charset));
if (request.getContentType() == null) {
request.setContentType(mediaType.toString());
}
}
if (this.content != null && this.content.length > 0) {
String requestContentType = request.getContentType();
if (requestContentType != null) {
try {
MediaType mediaType = MediaType.parseMediaType(requestContentType);
if (MediaType.APPLICATION_FORM_URLENCODED.includes(mediaType)) {
addRequestParams(request, parseFormData(mediaType));
}
}
catch (Exception ex) {
// Must be invalid, ignore
}
}
}
if (!ObjectUtils.isEmpty(this.cookies)) {
request.setCookies(this.cookies.toArray(new Cookie[0]));
}
if (!ObjectUtils.isEmpty(this.locales)) {
request.setPreferredLocales(this.locales);
}
this.requestAttributes.forEach(request::setAttribute);
this.sessionAttributes.forEach((name, attribute) -> {
HttpSession session = request.getSession();
Assert.state(session != null, "No HttpSession");
session.setAttribute(name, attribute);
});
FlashMap flashMap = new FlashMap();
flashMap.putAll(this.flashAttributes);
FlashMapManager flashMapManager = getFlashMapManager(request);
flashMapManager.saveOutputFlashMap(flashMap, request, new MockHttpServletResponse());
return request;
}
/**
* Create a new {@link MockHttpServletRequest} based on the supplied
* {@code ServletContext}.
* <p>Can be overridden in subclasses.
*/
protected MockHttpServletRequest createServletRequest(ServletContext servletContext) {
return new MockHttpServletRequest(servletContext);
}
/**
* Update the contextPath, servletPath, and pathInfo of the request.
*/
private void updatePathRequestProperties(MockHttpServletRequest request, String requestUri) {
if (!requestUri.startsWith(this.contextPath)) {
throw new IllegalArgumentException(
"Request URI [" + requestUri + "] does not start with context path [" + this.contextPath + "]");
}
request.setContextPath(this.contextPath);
request.setServletPath(this.servletPath);
if ("".equals(this.pathInfo)) {
if (!requestUri.startsWith(this.contextPath + this.servletPath)) {
throw new IllegalArgumentException(
"Invalid servlet path [" + this.servletPath + "] for request URI [" + requestUri + "]");
}
String extraPath = requestUri.substring(this.contextPath.length() + this.servletPath.length());
this.pathInfo = (StringUtils.hasText(extraPath) ?
UrlPathHelper.defaultInstance.decodeRequestString(request, extraPath) : null);
}
request.setPathInfo(this.pathInfo);
}
private void addRequestParams(MockHttpServletRequest request, MultiValueMap<String, String> map) {
map.forEach((key, values) -> values.forEach(value -> {
value = (value != null ? UriUtils.decode(value, StandardCharsets.UTF_8) : null);
request.addParameter(UriUtils.decode(key, StandardCharsets.UTF_8), value);
}));
}
private byte[] writeFormData(MediaType mediaType, Charset charset) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
HttpOutputMessage message = new HttpOutputMessage() {
@Override
public OutputStream getBody() {
return out;
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(mediaType);
return headers;
}
};
try {
FormHttpMessageConverter messageConverter = new FormHttpMessageConverter();
messageConverter.setCharset(charset);
messageConverter.write(this.formFields, mediaType, message);
return out.toByteArray();
}
catch (IOException ex) {
throw new IllegalStateException("Failed to write form data to request body", ex);
}
}
@SuppressWarnings("unchecked")
private MultiValueMap<String, String> parseFormData(MediaType mediaType) {
HttpInputMessage message = new HttpInputMessage() {
@Override
public InputStream getBody() {
byte[] bodyContent = AbstractMockHttpServletRequestBuilder.this.content;
return (bodyContent != null ? new ByteArrayInputStream(bodyContent) : InputStream.nullInputStream());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(mediaType);
return headers;
}
};
try {
return (MultiValueMap<String, String>) new FormHttpMessageConverter().read(null, message);
}
catch (IOException ex) {
throw new IllegalStateException("Failed to parse form data in request body", ex);
}
}
private FlashMapManager getFlashMapManager(MockHttpServletRequest request) {
FlashMapManager flashMapManager = null;
try {
ServletContext servletContext = request.getServletContext();
WebApplicationContext wac = WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
flashMapManager = wac.getBean(DispatcherServlet.FLASH_MAP_MANAGER_BEAN_NAME, FlashMapManager.class);
}
catch (IllegalStateException | NoSuchBeanDefinitionException ex) {
// ignore
}
return (flashMapManager != null ? flashMapManager : new SessionFlashMapManager());
}
@Override
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
for (RequestPostProcessor postProcessor : this.postProcessors) {
request = postProcessor.postProcessRequest(request);
}
return request;
}
private static void addToMap(Map<String, Object> map, String name, Object value) {
Assert.hasLength(name, "'name' must not be empty");
Assert.notNull(value, "'value' must not be null");
map.put(name, value);
}
private static <T> void addToMultiValueMap(MultiValueMap<String, T> map, String name, T[] values) {
Assert.hasLength(name, "'name' must not be empty");
Assert.notEmpty(values, "'values' must not be empty");
for (T value : values) {
map.add(name, value);
}
}
}

View File

@ -16,54 +16,12 @@
package org.springframework.test.web.servlet.request;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpSession;
import org.springframework.beans.Mergeable;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.FlashMap;
import org.springframework.web.servlet.FlashMapManager;
import org.springframework.web.servlet.support.SessionFlashMapManager;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
import org.springframework.web.util.UrlPathHelper;
/**
* Default builder for {@link MockHttpServletRequest} required as input to
@ -84,60 +42,7 @@ import org.springframework.web.util.UrlPathHelper;
* @since 3.2
*/
public class MockHttpServletRequestBuilder
implements ConfigurableSmartRequestBuilder<MockHttpServletRequestBuilder>, Mergeable {
private final HttpMethod method;
private final URI uri;
private String contextPath = "";
private String servletPath = "";
@Nullable
private String pathInfo = "";
@Nullable
private Boolean secure;
@Nullable
private Principal principal;
@Nullable
private MockHttpSession session;
@Nullable
private String remoteAddress;
@Nullable
private String characterEncoding;
@Nullable
private byte[] content;
@Nullable
private String contentType;
private final MultiValueMap<String, Object> headers = new LinkedMultiValueMap<>();
private final MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
private final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
private final MultiValueMap<String, String> formFields = new LinkedMultiValueMap<>();
private final List<Cookie> cookies = new ArrayList<>();
private final List<Locale> locales = new ArrayList<>();
private final Map<String, Object> requestAttributes = new LinkedHashMap<>();
private final Map<String, Object> sessionAttributes = new LinkedHashMap<>();
private final Map<String, Object> flashAttributes = new LinkedHashMap<>();
private final List<RequestPostProcessor> postProcessors = new ArrayList<>();
extends AbstractMockHttpServletRequestBuilder<MockHttpServletRequestBuilder> {
/**
* Package private constructor. To get an instance, use static factory
@ -150,15 +55,8 @@ public class MockHttpServletRequestBuilder
* @param uriVariables zero or more URI variables
*/
MockHttpServletRequestBuilder(HttpMethod httpMethod, String uriTemplate, Object... uriVariables) {
this(httpMethod, initUri(uriTemplate, uriVariables));
}
private static URI initUri(String uri, Object[] vars) {
Assert.notNull(uri, "'uri' must not be null");
Assert.isTrue(uri.isEmpty() || uri.startsWith("/") || uri.startsWith("http://") || uri.startsWith("https://"),
() -> "'uri' should start with a path or be a complete HTTP URI: " + uri);
String uriString = (uri.isEmpty() ? "/" : uri);
return UriComponentsBuilder.fromUriString(uriString).buildAndExpand(vars).encode().toUri();
super(httpMethod);
super.uri(uriTemplate, uriVariables);
}
/**
@ -169,784 +67,9 @@ public class MockHttpServletRequestBuilder
* @since 4.0.3
*/
MockHttpServletRequestBuilder(HttpMethod httpMethod, URI uri) {
Assert.notNull(httpMethod, "'httpMethod' is required");
super(httpMethod);
Assert.notNull(uri, "'uri' is required");
this.method = httpMethod;
this.uri = uri;
}
/**
* Specify the portion of the requestURI that represents the context path.
* The context path, if specified, must match to the start of the request URI.
* <p>In most cases, tests can be written by omitting the context path from
* the requestURI. This is because most applications don't actually depend
* on the name under which they're deployed. If specified here, the context
* path must start with a "/" and must not end with a "/".
* @see jakarta.servlet.http.HttpServletRequest#getContextPath()
*/
public MockHttpServletRequestBuilder contextPath(String contextPath) {
if (StringUtils.hasText(contextPath)) {
Assert.isTrue(contextPath.startsWith("/"), "Context path must start with a '/'");
Assert.isTrue(!contextPath.endsWith("/"), "Context path must not end with a '/'");
}
this.contextPath = contextPath;
return this;
}
/**
* Specify the portion of the requestURI that represents the path to which
* the Servlet is mapped. This is typically a portion of the requestURI
* after the context path.
* <p>In most cases, tests can be written by omitting the servlet path from
* the requestURI. This is because most applications don't actually depend
* on the prefix to which a servlet is mapped. For example if a Servlet is
* mapped to {@code "/main/*"}, tests can be written with the requestURI
* {@code "/accounts/1"} as opposed to {@code "/main/accounts/1"}.
* If specified here, the servletPath must start with a "/" and must not
* end with a "/".
* @see jakarta.servlet.http.HttpServletRequest#getServletPath()
*/
public MockHttpServletRequestBuilder servletPath(String servletPath) {
if (StringUtils.hasText(servletPath)) {
Assert.isTrue(servletPath.startsWith("/"), "Servlet path must start with a '/'");
Assert.isTrue(!servletPath.endsWith("/"), "Servlet path must not end with a '/'");
}
this.servletPath = servletPath;
return this;
}
/**
* Specify the portion of the requestURI that represents the pathInfo.
* <p>If left unspecified (recommended), the pathInfo will be automatically derived
* by removing the contextPath and the servletPath from the requestURI and using any
* remaining part. If specified here, the pathInfo must start with a "/".
* <p>If specified, the pathInfo will be used as-is.
* @see jakarta.servlet.http.HttpServletRequest#getPathInfo()
*/
public MockHttpServletRequestBuilder pathInfo(@Nullable String pathInfo) {
if (StringUtils.hasText(pathInfo)) {
Assert.isTrue(pathInfo.startsWith("/"), "Path info must start with a '/'");
}
this.pathInfo = pathInfo;
return this;
}
/**
* Set the secure property of the {@link ServletRequest} indicating use of a
* secure channel, such as HTTPS.
* @param secure whether the request is using a secure channel
*/
public MockHttpServletRequestBuilder secure(boolean secure){
this.secure = secure;
return this;
}
/**
* Set the character encoding of the request.
* @param encoding the character encoding
* @since 5.3.10
* @see StandardCharsets
* @see #characterEncoding(String)
*/
public MockHttpServletRequestBuilder characterEncoding(Charset encoding) {
return this.characterEncoding(encoding.name());
}
/**
* Set the character encoding of the request.
* @param encoding the character encoding
*/
public MockHttpServletRequestBuilder characterEncoding(String encoding) {
this.characterEncoding = encoding;
return this;
}
/**
* Set the request body.
* <p>If content is provided and {@link #contentType(MediaType)} is set to
* {@code application/x-www-form-urlencoded}, the content will be parsed
* and used to populate the {@link #param(String, String...) request
* parameters} map.
* @param content the body content
*/
public MockHttpServletRequestBuilder content(byte[] content) {
this.content = content;
return this;
}
/**
* Set the request body as a UTF-8 String.
* <p>If content is provided and {@link #contentType(MediaType)} is set to
* {@code application/x-www-form-urlencoded}, the content will be parsed
* and used to populate the {@link #param(String, String...) request
* parameters} map.
* @param content the body content
*/
public MockHttpServletRequestBuilder content(String content) {
this.content = content.getBytes(StandardCharsets.UTF_8);
return this;
}
/**
* Set the 'Content-Type' header of the request.
* <p>If content is provided and {@code contentType} is set to
* {@code application/x-www-form-urlencoded}, the content will be parsed
* and used to populate the {@link #param(String, String...) request
* parameters} map.
* @param contentType the content type
*/
public MockHttpServletRequestBuilder contentType(MediaType contentType) {
Assert.notNull(contentType, "'contentType' must not be null");
this.contentType = contentType.toString();
return this;
}
/**
* Set the 'Content-Type' header of the request as a raw String value,
* possibly not even well-formed (for testing purposes).
* @param contentType the content type
* @since 4.1.2
*/
public MockHttpServletRequestBuilder contentType(String contentType) {
Assert.notNull(contentType, "'contentType' must not be null");
this.contentType = contentType;
return this;
}
/**
* Set the 'Accept' header to the given media type(s).
* @param mediaTypes one or more media types
*/
public MockHttpServletRequestBuilder accept(MediaType... mediaTypes) {
Assert.notEmpty(mediaTypes, "'mediaTypes' must not be empty");
this.headers.set("Accept", MediaType.toString(Arrays.asList(mediaTypes)));
return this;
}
/**
* Set the {@code Accept} header using raw String values, possibly not even
* well-formed (for testing purposes).
* @param mediaTypes one or more media types; internally joined as
* comma-separated String
*/
public MockHttpServletRequestBuilder accept(String... mediaTypes) {
Assert.notEmpty(mediaTypes, "'mediaTypes' must not be empty");
this.headers.set("Accept", String.join(", ", mediaTypes));
return this;
}
/**
* Add a header to the request. Values are always added.
* @param name the header name
* @param values one or more header values
*/
public MockHttpServletRequestBuilder header(String name, Object... values) {
addToMultiValueMap(this.headers, name, values);
return this;
}
/**
* Add all headers to the request. Values are always added.
* @param httpHeaders the headers and values to add
*/
public MockHttpServletRequestBuilder headers(HttpHeaders httpHeaders) {
httpHeaders.forEach(this.headers::addAll);
return this;
}
/**
* Add a request parameter to {@link MockHttpServletRequest#getParameterMap()}.
* <p>In the Servlet API, a request parameter may be parsed from the query
* string and/or from the body of an {@code application/x-www-form-urlencoded}
* request. This method simply adds to the request parameter map. You may
* also use add Servlet request parameters by specifying the query or form
* data through one of the following:
* <ul>
* <li>Supply a URI with a query to {@link MockMvcRequestBuilders}.
* <li>Add query params via {@link #queryParam} or {@link #queryParams}.
* <li>Provide {@link #content} with {@link #contentType}
* {@code application/x-www-form-urlencoded}.
* </ul>
* @param name the parameter name
* @param values one or more values
*/
public MockHttpServletRequestBuilder param(String name, String... values) {
addToMultiValueMap(this.parameters, name, values);
return this;
}
/**
* Variant of {@link #param(String, String...)} with a {@link MultiValueMap}.
* @param params the parameters to add
* @since 4.2.4
*/
public MockHttpServletRequestBuilder params(MultiValueMap<String, String> params) {
params.forEach((name, values) -> {
for (String value : values) {
this.parameters.add(name, value);
}
});
return this;
}
/**
* Append to the query string and also add to the
* {@link #param(String, String...) request parameters} map. The parameter
* name and value are encoded when they are added to the query string.
* @param name the parameter name
* @param values one or more values
* @since 5.2.2
*/
public MockHttpServletRequestBuilder queryParam(String name, String... values) {
param(name, values);
this.queryParams.addAll(name, Arrays.asList(values));
return this;
}
/**
* Append to the query string and also add to the
* {@link #params(MultiValueMap) request parameters} map. The parameter
* name and value are encoded when they are added to the query string.
* @param params the parameters to add
* @since 5.2.2
*/
public MockHttpServletRequestBuilder queryParams(MultiValueMap<String, String> params) {
params(params);
this.queryParams.addAll(params);
return this;
}
/**
* Append the given value(s) to the given form field and also add them to the
* {@linkplain #param(String, String...) request parameters} map.
* @param name the field name
* @param values one or more values
* @since 6.1.7
*/
public MockHttpServletRequestBuilder formField(String name, String... values) {
param(name, values);
this.formFields.addAll(name, Arrays.asList(values));
return this;
}
/**
* Variant of {@link #formField(String, String...)} with a {@link MultiValueMap}.
* @param formFields the form fields to add
* @since 6.1.7
*/
public MockHttpServletRequestBuilder formFields(MultiValueMap<String, String> formFields) {
params(formFields);
this.formFields.addAll(formFields);
return this;
}
/**
* Add the given cookies to the request. Cookies are always added.
* @param cookies the cookies to add
*/
public MockHttpServletRequestBuilder cookie(Cookie... cookies) {
Assert.notEmpty(cookies, "'cookies' must not be empty");
this.cookies.addAll(Arrays.asList(cookies));
return this;
}
/**
* Add the specified locales as preferred request locales.
* @param locales the locales to add
* @since 4.3.6
* @see #locale(Locale)
*/
public MockHttpServletRequestBuilder locale(Locale... locales) {
Assert.notEmpty(locales, "'locales' must not be empty");
this.locales.addAll(Arrays.asList(locales));
return this;
}
/**
* Set the locale of the request, overriding any previous locales.
* @param locale the locale, or {@code null} to reset it
* @see #locale(Locale...)
*/
public MockHttpServletRequestBuilder locale(@Nullable Locale locale) {
this.locales.clear();
if (locale != null) {
this.locales.add(locale);
}
return this;
}
/**
* Set a request attribute.
* @param name the attribute name
* @param value the attribute value
*/
public MockHttpServletRequestBuilder requestAttr(String name, Object value) {
addToMap(this.requestAttributes, name, value);
return this;
}
/**
* Set a session attribute.
* @param name the session attribute name
* @param value the session attribute value
*/
public MockHttpServletRequestBuilder sessionAttr(String name, Object value) {
addToMap(this.sessionAttributes, name, value);
return this;
}
/**
* Set session attributes.
* @param sessionAttributes the session attributes
*/
public MockHttpServletRequestBuilder sessionAttrs(Map<String, Object> sessionAttributes) {
Assert.notEmpty(sessionAttributes, "'sessionAttributes' must not be empty");
sessionAttributes.forEach(this::sessionAttr);
return this;
}
/**
* Set an "input" flash attribute.
* @param name the flash attribute name
* @param value the flash attribute value
*/
public MockHttpServletRequestBuilder flashAttr(String name, Object value) {
addToMap(this.flashAttributes, name, value);
return this;
}
/**
* Set flash attributes.
* @param flashAttributes the flash attributes
*/
public MockHttpServletRequestBuilder flashAttrs(Map<String, Object> flashAttributes) {
Assert.notEmpty(flashAttributes, "'flashAttributes' must not be empty");
flashAttributes.forEach(this::flashAttr);
return this;
}
/**
* Set the HTTP session to use, possibly re-used across requests.
* <p>Individual attributes provided via {@link #sessionAttr(String, Object)}
* override the content of the session provided here.
* @param session the HTTP session
*/
public MockHttpServletRequestBuilder session(MockHttpSession session) {
Assert.notNull(session, "'session' must not be null");
this.session = session;
return this;
}
/**
* Set the principal of the request.
* @param principal the principal
*/
public MockHttpServletRequestBuilder principal(Principal principal) {
Assert.notNull(principal, "'principal' must not be null");
this.principal = principal;
return this;
}
/**
* Set the remote address of the request.
* @param remoteAddress the remote address (IP)
* @since 6.0.10
*/
public MockHttpServletRequestBuilder remoteAddress(String remoteAddress) {
Assert.hasText(remoteAddress, "'remoteAddress' must not be null or blank");
this.remoteAddress = remoteAddress;
return this;
}
/**
* An extension point for further initialization of {@link MockHttpServletRequest}
* in ways not built directly into the {@code MockHttpServletRequestBuilder}.
* Implementation of this interface can have builder-style methods themselves
* and be made accessible through static factory methods.
* @param postProcessor a post-processor to add
*/
@Override
public MockHttpServletRequestBuilder with(RequestPostProcessor postProcessor) {
Assert.notNull(postProcessor, "postProcessor is required");
this.postProcessors.add(postProcessor);
return this;
}
/**
* {@inheritDoc}
* @return always returns {@code true}.
*/
@Override
public boolean isMergeEnabled() {
return true;
}
/**
* Merges the properties of the "parent" RequestBuilder accepting values
* only if not already set in "this" instance.
* @param parent the parent {@code RequestBuilder} to inherit properties from
* @return the result of the merge
*/
@Override
public Object merge(@Nullable Object parent) {
if (parent == null) {
return this;
}
if (!(parent instanceof MockHttpServletRequestBuilder parentBuilder)) {
throw new IllegalArgumentException("Cannot merge with [" + parent.getClass().getName() + "]");
}
if (!StringUtils.hasText(this.contextPath)) {
this.contextPath = parentBuilder.contextPath;
}
if (!StringUtils.hasText(this.servletPath)) {
this.servletPath = parentBuilder.servletPath;
}
if ("".equals(this.pathInfo)) {
this.pathInfo = parentBuilder.pathInfo;
}
if (this.secure == null) {
this.secure = parentBuilder.secure;
}
if (this.principal == null) {
this.principal = parentBuilder.principal;
}
if (this.session == null) {
this.session = parentBuilder.session;
}
if (this.remoteAddress == null) {
this.remoteAddress = parentBuilder.remoteAddress;
}
if (this.characterEncoding == null) {
this.characterEncoding = parentBuilder.characterEncoding;
}
if (this.content == null) {
this.content = parentBuilder.content;
}
if (this.contentType == null) {
this.contentType = parentBuilder.contentType;
}
for (Map.Entry<String, List<Object>> entry : parentBuilder.headers.entrySet()) {
String headerName = entry.getKey();
if (!this.headers.containsKey(headerName)) {
this.headers.put(headerName, entry.getValue());
}
}
for (Map.Entry<String, List<String>> entry : parentBuilder.parameters.entrySet()) {
String paramName = entry.getKey();
if (!this.parameters.containsKey(paramName)) {
this.parameters.put(paramName, entry.getValue());
}
}
for (Map.Entry<String, List<String>> entry : parentBuilder.queryParams.entrySet()) {
String paramName = entry.getKey();
if (!this.queryParams.containsKey(paramName)) {
this.queryParams.put(paramName, entry.getValue());
}
}
for (Map.Entry<String, List<String>> entry : parentBuilder.formFields.entrySet()) {
String paramName = entry.getKey();
if (!this.formFields.containsKey(paramName)) {
this.formFields.put(paramName, entry.getValue());
}
}
for (Cookie cookie : parentBuilder.cookies) {
if (!containsCookie(cookie)) {
this.cookies.add(cookie);
}
}
for (Locale locale : parentBuilder.locales) {
if (!this.locales.contains(locale)) {
this.locales.add(locale);
}
}
for (Map.Entry<String, Object> entry : parentBuilder.requestAttributes.entrySet()) {
String attributeName = entry.getKey();
if (!this.requestAttributes.containsKey(attributeName)) {
this.requestAttributes.put(attributeName, entry.getValue());
}
}
for (Map.Entry<String, Object> entry : parentBuilder.sessionAttributes.entrySet()) {
String attributeName = entry.getKey();
if (!this.sessionAttributes.containsKey(attributeName)) {
this.sessionAttributes.put(attributeName, entry.getValue());
}
}
for (Map.Entry<String, Object> entry : parentBuilder.flashAttributes.entrySet()) {
String attributeName = entry.getKey();
if (!this.flashAttributes.containsKey(attributeName)) {
this.flashAttributes.put(attributeName, entry.getValue());
}
}
this.postProcessors.addAll(0, parentBuilder.postProcessors);
return this;
}
private boolean containsCookie(Cookie cookie) {
for (Cookie cookieToCheck : this.cookies) {
if (ObjectUtils.nullSafeEquals(cookieToCheck.getName(), cookie.getName())) {
return true;
}
}
return false;
}
/**
* Build a {@link MockHttpServletRequest}.
*/
@Override
public final MockHttpServletRequest buildRequest(ServletContext servletContext) {
MockHttpServletRequest request = createServletRequest(servletContext);
request.setAsyncSupported(true);
request.setMethod(this.method.name());
String requestUri = this.uri.getRawPath();
request.setRequestURI(requestUri);
if (this.uri.getScheme() != null) {
request.setScheme(this.uri.getScheme());
}
if (this.uri.getHost() != null) {
request.setServerName(this.uri.getHost());
}
if (this.uri.getPort() != -1) {
request.setServerPort(this.uri.getPort());
}
updatePathRequestProperties(request, requestUri);
if (this.secure != null) {
request.setSecure(this.secure);
}
if (this.principal != null) {
request.setUserPrincipal(this.principal);
}
if (this.remoteAddress != null) {
request.setRemoteAddr(this.remoteAddress);
}
if (this.session != null) {
request.setSession(this.session);
}
request.setCharacterEncoding(this.characterEncoding);
request.setContent(this.content);
request.setContentType(this.contentType);
this.headers.forEach((name, values) -> {
for (Object value : values) {
request.addHeader(name, value);
}
});
if (!ObjectUtils.isEmpty(this.content) &&
!this.headers.containsKey(HttpHeaders.CONTENT_LENGTH) &&
!this.headers.containsKey(HttpHeaders.TRANSFER_ENCODING)) {
request.addHeader(HttpHeaders.CONTENT_LENGTH, this.content.length);
}
String query = this.uri.getRawQuery();
if (!this.queryParams.isEmpty()) {
String str = UriComponentsBuilder.newInstance().queryParams(this.queryParams).build().encode().getQuery();
query = StringUtils.hasLength(query) ? (query + "&" + str) : str;
}
if (query != null) {
request.setQueryString(query);
}
addRequestParams(request, UriComponentsBuilder.fromUri(this.uri).build().getQueryParams());
this.parameters.forEach((name, values) -> {
for (String value : values) {
request.addParameter(name, value);
}
});
if (!this.formFields.isEmpty()) {
if (this.content != null && this.content.length > 0) {
throw new IllegalStateException("Could not write form data with an existing body");
}
Charset charset = (this.characterEncoding != null ?
Charset.forName(this.characterEncoding) : StandardCharsets.UTF_8);
MediaType mediaType = (request.getContentType() != null ?
MediaType.parseMediaType(request.getContentType()) :
new MediaType(MediaType.APPLICATION_FORM_URLENCODED, charset));
if (!mediaType.isCompatibleWith(MediaType.APPLICATION_FORM_URLENCODED)) {
throw new IllegalStateException("Invalid content type: '" + mediaType +
"' is not compatible with '" + MediaType.APPLICATION_FORM_URLENCODED + "'");
}
request.setContent(writeFormData(mediaType, charset));
if (request.getContentType() == null) {
request.setContentType(mediaType.toString());
}
}
if (this.content != null && this.content.length > 0) {
String requestContentType = request.getContentType();
if (requestContentType != null) {
try {
MediaType mediaType = MediaType.parseMediaType(requestContentType);
if (MediaType.APPLICATION_FORM_URLENCODED.includes(mediaType)) {
addRequestParams(request, parseFormData(mediaType));
}
}
catch (Exception ex) {
// Must be invalid, ignore
}
}
}
if (!ObjectUtils.isEmpty(this.cookies)) {
request.setCookies(this.cookies.toArray(new Cookie[0]));
}
if (!ObjectUtils.isEmpty(this.locales)) {
request.setPreferredLocales(this.locales);
}
this.requestAttributes.forEach(request::setAttribute);
this.sessionAttributes.forEach((name, attribute) -> {
HttpSession session = request.getSession();
Assert.state(session != null, "No HttpSession");
session.setAttribute(name, attribute);
});
FlashMap flashMap = new FlashMap();
flashMap.putAll(this.flashAttributes);
FlashMapManager flashMapManager = getFlashMapManager(request);
flashMapManager.saveOutputFlashMap(flashMap, request, new MockHttpServletResponse());
return request;
}
/**
* Create a new {@link MockHttpServletRequest} based on the supplied
* {@code ServletContext}.
* <p>Can be overridden in subclasses.
*/
protected MockHttpServletRequest createServletRequest(ServletContext servletContext) {
return new MockHttpServletRequest(servletContext);
}
/**
* Update the contextPath, servletPath, and pathInfo of the request.
*/
private void updatePathRequestProperties(MockHttpServletRequest request, String requestUri) {
if (!requestUri.startsWith(this.contextPath)) {
throw new IllegalArgumentException(
"Request URI [" + requestUri + "] does not start with context path [" + this.contextPath + "]");
}
request.setContextPath(this.contextPath);
request.setServletPath(this.servletPath);
if ("".equals(this.pathInfo)) {
if (!requestUri.startsWith(this.contextPath + this.servletPath)) {
throw new IllegalArgumentException(
"Invalid servlet path [" + this.servletPath + "] for request URI [" + requestUri + "]");
}
String extraPath = requestUri.substring(this.contextPath.length() + this.servletPath.length());
this.pathInfo = (StringUtils.hasText(extraPath) ?
UrlPathHelper.defaultInstance.decodeRequestString(request, extraPath) : null);
}
request.setPathInfo(this.pathInfo);
}
private void addRequestParams(MockHttpServletRequest request, MultiValueMap<String, String> map) {
map.forEach((key, values) -> values.forEach(value -> {
value = (value != null ? UriUtils.decode(value, StandardCharsets.UTF_8) : null);
request.addParameter(UriUtils.decode(key, StandardCharsets.UTF_8), value);
}));
}
private byte[] writeFormData(MediaType mediaType, Charset charset) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
HttpOutputMessage message = new HttpOutputMessage() {
@Override
public OutputStream getBody() {
return out;
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(mediaType);
return headers;
}
};
try {
FormHttpMessageConverter messageConverter = new FormHttpMessageConverter();
messageConverter.setCharset(charset);
messageConverter.write(this.formFields, mediaType, message);
return out.toByteArray();
}
catch (IOException ex) {
throw new IllegalStateException("Failed to write form data to request body", ex);
}
}
@SuppressWarnings("unchecked")
private MultiValueMap<String, String> parseFormData(MediaType mediaType) {
HttpInputMessage message = new HttpInputMessage() {
@Override
public InputStream getBody() {
byte[] bodyContent = MockHttpServletRequestBuilder.this.content;
return (bodyContent != null ? new ByteArrayInputStream(bodyContent) : InputStream.nullInputStream());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(mediaType);
return headers;
}
};
try {
return (MultiValueMap<String, String>) new FormHttpMessageConverter().read(null, message);
}
catch (IOException ex) {
throw new IllegalStateException("Failed to parse form data in request body", ex);
}
}
private FlashMapManager getFlashMapManager(MockHttpServletRequest request) {
FlashMapManager flashMapManager = null;
try {
ServletContext servletContext = request.getServletContext();
WebApplicationContext wac = WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
flashMapManager = wac.getBean(DispatcherServlet.FLASH_MAP_MANAGER_BEAN_NAME, FlashMapManager.class);
}
catch (IllegalStateException | NoSuchBeanDefinitionException ex) {
// ignore
}
return (flashMapManager != null ? flashMapManager : new SessionFlashMapManager());
}
@Override
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
for (RequestPostProcessor postProcessor : this.postProcessors) {
request = postProcessor.postProcessRequest(request);
}
return request;
}
private static void addToMap(Map<String, Object> map, String name, Object value) {
Assert.hasLength(name, "'name' must not be empty");
Assert.notNull(value, "'value' must not be null");
map.put(name, value);
}
private static <T> void addToMultiValueMap(MultiValueMap<String, T> map, String name, T[] values) {
Assert.hasLength(name, "'name' must not be empty");
Assert.notEmpty(values, "'values' must not be empty");
for (T value : values) {
map.add(name, value);
}
super.uri(uri);
}
}

View File

@ -47,7 +47,7 @@ import org.springframework.util.MultiValueMap;
* @author Arjen Poutsma
* @since 3.2
*/
public class MockMultipartHttpServletRequestBuilder extends MockHttpServletRequestBuilder {
public class MockMultipartHttpServletRequestBuilder extends AbstractMockHttpServletRequestBuilder<MockMultipartHttpServletRequestBuilder> {
private final List<MockMultipartFile> files = new ArrayList<>();
@ -73,7 +73,8 @@ public class MockMultipartHttpServletRequestBuilder extends MockHttpServletReque
* @since 5.3.22
*/
MockMultipartHttpServletRequestBuilder(HttpMethod httpMethod, String uriTemplate, Object... uriVariables) {
super(httpMethod, uriTemplate, uriVariables);
super(httpMethod);
super.uri(uriTemplate, uriVariables);
super.contentType(MediaType.MULTIPART_FORM_DATA);
}
@ -92,7 +93,8 @@ public class MockMultipartHttpServletRequestBuilder extends MockHttpServletReque
* @since 5.3.21
*/
MockMultipartHttpServletRequestBuilder(HttpMethod httpMethod, URI uri) {
super(httpMethod, uri);
super(httpMethod);
super.uri(uri);
super.contentType(MediaType.MULTIPART_FORM_DATA);
}
@ -134,7 +136,7 @@ public class MockMultipartHttpServletRequestBuilder extends MockHttpServletReque
if (parent == null) {
return this;
}
if (parent instanceof MockHttpServletRequestBuilder) {
if (parent instanceof AbstractMockHttpServletRequestBuilder) {
super.merge(parent);
if (parent instanceof MockMultipartHttpServletRequestBuilder parentBuilder) {
this.files.addAll(parentBuilder.files);

View File

@ -25,6 +25,7 @@ import org.springframework.util.MultiValueMap
import java.security.Principal
import java.util.*
import jakarta.servlet.http.Cookie
import org.springframework.test.web.servlet.request.AbstractMockHttpServletRequestBuilder
/**
* Provide a [MockHttpServletRequestBuilder] Kotlin DSL in order to be able to write idiomatic Kotlin code.
@ -40,7 +41,7 @@ import jakarta.servlet.http.Cookie
* @author Sebastien Deleuze
* @since 5.2
*/
open class MockHttpServletRequestDsl internal constructor (private val builder: MockHttpServletRequestBuilder) {
open class MockHttpServletRequestDsl internal constructor (private val builder: AbstractMockHttpServletRequestBuilder<*>) {
/**
* @see [MockHttpServletRequestBuilder.contextPath]

View File

@ -0,0 +1,94 @@
/*
* Copyright 2002-2024 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.assertj;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Integration tests for {@link MockMvcTester} that use the methods that
* integrate with {@link MockMvc} way of building the requests and
* asserting the responses.
*
* @author Stephane Nicoll
*/
@SpringJUnitConfig
@WebAppConfiguration
class MockMvcTesterCompatibilityIntegrationTests {
private final MockMvcTester mvc;
MockMvcTesterCompatibilityIntegrationTests(@Autowired WebApplicationContext wac) {
this.mvc = MockMvcTester.from(wac);
}
@Test
void performGet() {
assertThat(this.mvc.perform(get("/greet"))).hasStatusOk();
}
@Test
void performGetWithInvalidMediaTypeAssertion() {
MvcTestResult result = this.mvc.perform(get("/greet"));
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(result).hasContentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.withMessageContaining("is compatible with 'application/json'");
}
@Test
void assertHttpStatusCode() {
assertThat(this.mvc.get().uri("/greet")).matches(status().isOk());
}
@Configuration
@EnableWebMvc
@Import(TestController.class)
static class WebConfiguration {
}
@RestController
static class TestController {
@GetMapping(path = "/greet", produces = "text/plain")
String greet() {
return "hello";
}
@GetMapping(path = "/message", produces = MediaType.APPLICATION_JSON_VALUE)
String message() {
return "{\"message\": \"hello\"}";
}
}
}

View File

@ -43,7 +43,7 @@ import org.springframework.stereotype.Controller;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.Person;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.ResultMatcher;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.GetMapping;
@ -63,9 +63,8 @@ import static java.util.Map.entry;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.InstanceOfAssertFactories.map;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Integration tests for {@link MockMvcTester}.
@ -77,10 +76,10 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@WebAppConfiguration
public class MockMvcTesterIntegrationTests {
private final MockMvcTester mockMvc;
private final MockMvcTester mvc;
MockMvcTesterIntegrationTests(WebApplicationContext wac) {
this.mockMvc = MockMvcTester.from(wac);
this.mvc = MockMvcTester.from(wac);
}
@Nested
@ -88,24 +87,24 @@ public class MockMvcTesterIntegrationTests {
@Test
void hasAsyncStartedTrue() {
assertThat(perform(get("/callable").accept(MediaType.APPLICATION_JSON)))
assertThat(mvc.get().uri("/callable").accept(MediaType.APPLICATION_JSON))
.request().hasAsyncStarted(true);
}
@Test
void hasAsyncStartedFalse() {
assertThat(perform(get("/greet"))).request().hasAsyncStarted(false);
assertThat(mvc.get().uri("/greet")).request().hasAsyncStarted(false);
}
@Test
void attributes() {
assertThat(perform(get("/greet"))).request().attributes()
assertThat(mvc.get().uri("/greet")).request().attributes()
.containsKey(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE);
}
@Test
void sessionAttributes() {
assertThat(perform(get("/locale"))).request().sessionAttributes()
assertThat(mvc.get().uri("/locale")).request().sessionAttributes()
.containsOnly(entry("locale", Locale.UK));
}
}
@ -116,17 +115,17 @@ public class MockMvcTesterIntegrationTests {
@Test
void containsCookie() {
Cookie cookie = new Cookie("test", "value");
assertThat(performWithCookie(cookie, get("/greet"))).cookies().containsCookie("test");
assertThat(withCookie(cookie).get().uri("/greet")).cookies().containsCookie("test");
}
@Test
void hasValue() {
Cookie cookie = new Cookie("test", "value");
assertThat(performWithCookie(cookie, get("/greet"))).cookies().hasValue("test", "value");
assertThat(withCookie(cookie).get().uri("/greet")).cookies().hasValue("test", "value");
}
private MvcTestResult performWithCookie(Cookie cookie, MockHttpServletRequestBuilder request) {
MockMvcTester mockMvc = MockMvcTester.of(List.of(new TestController()), builder -> builder.addInterceptors(
private MockMvcTester withCookie(Cookie cookie) {
return MockMvcTester.of(List.of(new TestController()), builder -> builder.addInterceptors(
new HandlerInterceptor() {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
@ -134,7 +133,6 @@ public class MockMvcTesterIntegrationTests {
return true;
}
}).build());
return mockMvc.perform(request);
}
}
@ -143,12 +141,12 @@ public class MockMvcTesterIntegrationTests {
@Test
void statusOk() {
assertThat(perform(get("/greet"))).hasStatusOk();
assertThat(mvc.get().uri("/greet")).hasStatusOk();
}
@Test
void statusSeries() {
assertThat(perform(get("/greet"))).hasStatus2xxSuccessful();
assertThat(mvc.get().uri("/greet")).hasStatus2xxSuccessful();
}
}
@ -158,13 +156,13 @@ public class MockMvcTesterIntegrationTests {
@Test
void shouldAssertHeader() {
assertThat(perform(get("/greet")))
assertThat(mvc.get().uri("/greet"))
.hasHeader("Content-Type", "text/plain;charset=ISO-8859-1");
}
@Test
void shouldAssertHeaderWithCallback() {
assertThat(perform(get("/greet"))).headers().satisfies(textContent("ISO-8859-1"));
assertThat(mvc.get().uri("/greet")).headers().satisfies(textContent("ISO-8859-1"));
}
private Consumer<HttpHeaders> textContent(String charset) {
@ -179,33 +177,33 @@ public class MockMvcTesterIntegrationTests {
@Test
void hasViewName() {
assertThat(perform(get("/persons/{0}", "Andy"))).hasViewName("persons/index");
assertThat(mvc.get().uri("/persons/{0}", "Andy")).hasViewName("persons/index");
}
@Test
void viewNameWithCustomAssertion() {
assertThat(perform(get("/persons/{0}", "Andy"))).viewName().startsWith("persons");
assertThat(mvc.get().uri("/persons/{0}", "Andy")).viewName().startsWith("persons");
}
@Test
void containsAttributes() {
assertThat(perform(post("/persons").param("name", "Andy"))).model()
assertThat(mvc.post().uri("/persons").param("name", "Andy")).model()
.containsOnlyKeys("name").containsEntry("name", "Andy");
}
@Test
void hasErrors() {
assertThat(perform(post("/persons"))).model().hasErrors();
assertThat(mvc.post().uri("/persons")).model().hasErrors();
}
@Test
void hasAttributeErrors() {
assertThat(perform(post("/persons"))).model().hasAttributeErrors("person");
assertThat(mvc.post().uri("/persons")).model().hasAttributeErrors("person");
}
@Test
void hasAttributeErrorsCount() {
assertThat(perform(post("/persons"))).model().extractingBindingResult("person").hasErrorsCount(1);
assertThat(mvc.post().uri("/persons")).model().extractingBindingResult("person").hasErrorsCount(1);
}
}
@ -215,7 +213,7 @@ public class MockMvcTesterIntegrationTests {
@Test
void containsAttributes() {
assertThat(perform(post("/persons").param("name", "Andy"))).flash()
assertThat(mvc.post().uri("/persons").param("name", "Andy")).flash()
.containsOnlyKeys("message").hasEntrySatisfying("message",
value -> assertThat(value).isInstanceOfSatisfying(String.class,
stringValue -> assertThat(stringValue).startsWith("success")));
@ -227,31 +225,31 @@ public class MockMvcTesterIntegrationTests {
@Test
void asyncResult() {
assertThat(perform(get("/callable").accept(MediaType.APPLICATION_JSON)))
assertThat(mvc.get().uri("/callable").accept(MediaType.APPLICATION_JSON))
.asyncResult().asInstanceOf(map(String.class, Object.class))
.containsOnly(entry("key", "value"));
}
@Test
void stringContent() {
assertThat(perform(get("/greet"))).body().asString().isEqualTo("hello");
assertThat(mvc.get().uri("/greet")).body().asString().isEqualTo("hello");
}
@Test
void jsonPathContent() {
assertThat(perform(get("/message"))).bodyJson()
assertThat(mvc.get().uri("/message")).bodyJson()
.extractingPath("$.message").asString().isEqualTo("hello");
}
@Test
void jsonContentCanLoadResourceFromClasspath() {
assertThat(perform(get("/message"))).bodyJson().isLenientlyEqualTo(
assertThat(mvc.get().uri("/message")).bodyJson().isLenientlyEqualTo(
new ClassPathResource("message.json", MockMvcTesterIntegrationTests.class));
}
@Test
void jsonContentUsingResourceLoaderClass() {
assertThat(perform(get("/message"))).bodyJson().withResourceLoadClass(MockMvcTesterIntegrationTests.class)
assertThat(mvc.get().uri("/message")).bodyJson().withResourceLoadClass(MockMvcTesterIntegrationTests.class)
.isLenientlyEqualTo("message.json");
}
@ -262,22 +260,22 @@ public class MockMvcTesterIntegrationTests {
@Test
void handlerOn404() {
assertThat(perform(get("/unknown-resource"))).handler().isNull();
assertThat(mvc.get().uri("/unknown-resource")).handler().isNull();
}
@Test
void hasType() {
assertThat(perform(get("/greet"))).handler().hasType(TestController.class);
assertThat(mvc.get().uri("/greet")).handler().hasType(TestController.class);
}
@Test
void isMethodHandler() {
assertThat(perform(get("/greet"))).handler().isMethodHandler();
assertThat(mvc.get().uri("/greet")).handler().isMethodHandler();
}
@Test
void isInvokedOn() {
assertThat(perform(get("/callable"))).handler()
assertThat(mvc.get().uri("/callable")).handler()
.isInvokedOn(AsyncController.class, AsyncController::getCallable);
}
@ -288,31 +286,31 @@ public class MockMvcTesterIntegrationTests {
@Test
void doesNotHaveUnresolvedException() {
assertThat(perform(get("/greet"))).doesNotHaveUnresolvedException();
assertThat(mvc.get().uri("/greet")).doesNotHaveUnresolvedException();
}
@Test
void hasUnresolvedException() {
assertThat(perform(get("/error/1"))).hasUnresolvedException();
assertThat(mvc.get().uri("/error/1")).hasUnresolvedException();
}
@Test
void doesNotHaveUnresolvedExceptionWithUnresolvedException() {
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(perform(get("/error/1"))).doesNotHaveUnresolvedException())
.isThrownBy(() -> assertThat(mvc.get().uri("/error/1")).doesNotHaveUnresolvedException())
.withMessage("Expected request to succeed, but it failed");
}
@Test
void hasUnresolvedExceptionWithoutUnresolvedException() {
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(perform(get("/greet"))).hasUnresolvedException())
.isThrownBy(() -> assertThat(mvc.get().uri("/greet")).hasUnresolvedException())
.withMessage("Expected request to fail, but it succeeded");
}
@Test
void unresolvedExceptionWithFailedRequest() {
assertThat(perform(get("/error/1"))).unresolvedException()
assertThat(mvc.get().uri("/error/1")).unresolvedException()
.isInstanceOf(ServletException.class)
.cause().isInstanceOf(IllegalStateException.class).hasMessage("Expected");
}
@ -320,7 +318,7 @@ public class MockMvcTesterIntegrationTests {
@Test
void unresolvedExceptionWithSuccessfulRequest() {
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(perform(get("/greet"))).unresolvedException())
.isThrownBy(() -> assertThat(mvc.get().uri("/greet")).unresolvedException())
.withMessage("Expected request to fail, but it succeeded");
}
@ -406,7 +404,7 @@ public class MockMvcTesterIntegrationTests {
private void testAssertionFailureWithUnresolvableException(Consumer<MvcTestResult> assertions) {
MvcTestResult result = perform(get("/error/1"));
MvcTestResult result = mvc.get().uri("/error/1").exchange();
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertions.accept(result))
.withMessageContainingAll("Request failed unexpectedly:",
@ -418,49 +416,49 @@ public class MockMvcTesterIntegrationTests {
@Test
void hasForwardUrl() {
assertThat(perform(get("/persons/John"))).hasForwardedUrl("persons/index");
assertThat(mvc.get().uri("/persons/John")).hasForwardedUrl("persons/index");
}
@Test
void hasRedirectUrl() {
assertThat(perform(post("/persons").param("name", "Andy"))).hasStatus(HttpStatus.FOUND)
assertThat(mvc.post().uri("/persons").param("name", "Andy")).hasStatus(HttpStatus.FOUND)
.hasRedirectedUrl("/persons/Andy");
}
@Test
void satisfiesAllowsAdditionalAssertions() {
assertThat(this.mockMvc.perform(get("/greet"))).satisfies(result -> {
assertThat(mvc.get().uri("/greet")).satisfies(result -> {
assertThat(result).isInstanceOf(MvcTestResult.class);
assertThat(result).hasStatusOk();
});
}
@Test
void resultMatcherCanBeReused() {
assertThat(this.mockMvc.perform(get("/greet"))).matches(status().isOk());
void resultMatcherCanBeReused() throws Exception {
MvcTestResult result = mvc.get().uri("/greet").exchange();
ResultMatcher matcher = mock(ResultMatcher.class);
assertThat(result).matches(matcher);
verify(matcher).match(result.getMvcResult());
}
@Test
void resultMatcherFailsWithDedicatedException() {
ResultMatcher matcher = result -> assertThat(result.getResponse().getStatus())
.isEqualTo(HttpStatus.NOT_FOUND.value());
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(this.mockMvc.perform(get("/greet")))
.matches(status().isNotFound()))
.withMessageContaining("Status expected:<404> but was:<200>");
.isThrownBy(() -> assertThat(mvc.get().uri("/greet"))
.matches(matcher))
.withMessageContaining("expected: 404").withMessageContaining(" but was: 200");
}
@Test
void shouldApplyResultHandler() { // Spring RESTDocs example
AtomicBoolean applied = new AtomicBoolean();
assertThat(this.mockMvc.perform(get("/greet"))).apply(result -> applied.set(true));
assertThat(mvc.get().uri("/greet")).apply(result -> applied.set(true));
assertThat(applied).isTrue();
}
private MvcTestResult perform(MockHttpServletRequestBuilder builder) {
return this.mockMvc.perform(builder);
}
@Configuration
@EnableWebMvc
@Import({ TestController.class, PersonController.class, AsyncController.class,