diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockHttpServletRequestBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockHttpServletRequestBuilder.java index cb3fd23fd046..00a9586b1bb3 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockHttpServletRequestBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockHttpServletRequestBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * 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. @@ -17,8 +17,10 @@ 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; @@ -40,6 +42,7 @@ 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; @@ -121,6 +124,8 @@ public class MockHttpServletRequestBuilder private final MultiValueMap queryParams = new LinkedMultiValueMap<>(); + private final MultiValueMap formFields = new LinkedMultiValueMap<>(); + private final List cookies = new ArrayList<>(); private final List locales = new ArrayList<>(); @@ -422,6 +427,30 @@ public MockHttpServletRequestBuilder queryParams(MultiValueMap p return this; } + /** + * Appends the given value(s) to the given form field and also add to the + * {@link #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 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 @@ -629,6 +658,12 @@ public Object merge(@Nullable Object parent) { this.queryParams.put(paramName, entry.getValue()); } } + for (Map.Entry> 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); @@ -744,6 +779,24 @@ public final MockHttpServletRequest buildRequest(ServletContext servletContext) } }); + 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) { @@ -820,6 +873,32 @@ private void addRequestParams(MockHttpServletRequest request, MultiValueMap parseFormData(MediaType mediaType) { HttpInputMessage message = new HttpInputMessage() { @Override diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/request/MockHttpServletRequestBuilderTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/request/MockHttpServletRequestBuilderTests.java index eebbd9e9e588..35409a266422 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/request/MockHttpServletRequestBuilderTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/request/MockHttpServletRequestBuilderTests.java @@ -29,6 +29,7 @@ import jakarta.servlet.ServletContext; import jakarta.servlet.http.Cookie; +import org.assertj.core.api.ThrowingConsumer; import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; @@ -47,6 +48,8 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.entry; import static org.springframework.http.HttpMethod.GET; import static org.springframework.http.HttpMethod.POST; @@ -288,6 +291,70 @@ void queryParameterList() { assertThat(request.getParameter("foo[1]")).isEqualTo("baz"); } + @Test + void formField() { + this.builder = new MockHttpServletRequestBuilder(POST, "/"); + this.builder.formField("foo", "bar"); + this.builder.formField("foo", "baz"); + + MockHttpServletRequest request = this.builder.buildRequest(this.servletContext); + + assertThat(request.getParameterMap().get("foo")).containsExactly("bar", "baz"); + assertThat(request).satisfies(hasFormData("foo=bar&foo=baz")); + } + + @Test + void formFieldMap() { + this.builder = new MockHttpServletRequestBuilder(POST, "/"); + MultiValueMap formFields = new LinkedMultiValueMap<>(); + List values = new ArrayList<>(); + values.add("bar"); + values.add("baz"); + formFields.put("foo", values); + this.builder.formFields(formFields); + + MockHttpServletRequest request = this.builder.buildRequest(this.servletContext); + + assertThat(request.getParameterMap().get("foo")).containsExactly("bar", "baz"); + assertThat(request).satisfies(hasFormData("foo=bar&foo=baz")); + } + + @Test + void formFieldsAreEncoded() { + MockHttpServletRequest request = new MockHttpServletRequestBuilder(POST, "/") + .formField("name 1", "value 1").formField("name 2", "value A", "value B") + .buildRequest(new MockServletContext()); + assertThat(request.getParameterMap()).containsOnly( + entry("name 1", new String[] { "value 1" }), + entry("name 2", new String[] { "value A", "value B" })); + assertThat(request).satisfies(hasFormData("name+1=value+1&name+2=value+A&name+2=value+B")); + } + + @Test + void formFieldWithContent() { + this.builder = new MockHttpServletRequestBuilder(POST, "/"); + this.builder.content("Should not have content"); + this.builder.formField("foo", "bar"); + assertThatIllegalStateException().isThrownBy(() -> this.builder.buildRequest(this.servletContext)) + .withMessage("Could not write form data with an existing body"); + } + + @Test + void formFieldWithIncompatibleMediaType() { + this.builder = new MockHttpServletRequestBuilder(POST, "/"); + this.builder.contentType(MediaType.TEXT_PLAIN); + this.builder.formField("foo", "bar"); + assertThatIllegalStateException().isThrownBy(() -> this.builder.buildRequest(this.servletContext)) + .withMessage("Invalid content type: 'text/plain' is not compatible with 'application/x-www-form-urlencoded'"); + } + + private ThrowingConsumer hasFormData(String body) { + return request -> { + assertThat(request.getContentAsString()).isEqualTo(body); + assertThat(request.getContentType()).isEqualTo("application/x-www-form-urlencoded;charset=UTF-8"); + }; + } + @Test void requestParameterFromQueryWithEncoding() { this.builder = new MockHttpServletRequestBuilder(GET, "/?foo={value}", "bar=baz");