8 changed files with 1270 additions and 967 deletions
@ -0,0 +1,948 @@
@@ -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); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,94 @@
@@ -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\"}"; |
||||
} |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue