Skip to content

Commit 6250b64

Browse files
committed
Add support for form data in MockHttpServletRequestBuilder
Closes gh-32757
1 parent 5b1278d commit 6250b64

File tree

2 files changed

+147
-1
lines changed

2 files changed

+147
-1
lines changed

Diff for: spring-test/src/main/java/org/springframework/test/web/servlet/request/MockHttpServletRequestBuilder.java

+80-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,8 +17,10 @@
1717
package org.springframework.test.web.servlet.request;
1818

1919
import java.io.ByteArrayInputStream;
20+
import java.io.ByteArrayOutputStream;
2021
import java.io.IOException;
2122
import java.io.InputStream;
23+
import java.io.OutputStream;
2224
import java.net.URI;
2325
import java.nio.charset.Charset;
2426
import java.nio.charset.StandardCharsets;
@@ -40,6 +42,7 @@
4042
import org.springframework.http.HttpHeaders;
4143
import org.springframework.http.HttpInputMessage;
4244
import org.springframework.http.HttpMethod;
45+
import org.springframework.http.HttpOutputMessage;
4346
import org.springframework.http.MediaType;
4447
import org.springframework.http.converter.FormHttpMessageConverter;
4548
import org.springframework.lang.Nullable;
@@ -121,6 +124,8 @@ public class MockHttpServletRequestBuilder
121124

122125
private final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
123126

127+
private final MultiValueMap<String, String> formFields = new LinkedMultiValueMap<>();
128+
124129
private final List<Cookie> cookies = new ArrayList<>();
125130

126131
private final List<Locale> locales = new ArrayList<>();
@@ -422,6 +427,30 @@ public MockHttpServletRequestBuilder queryParams(MultiValueMap<String, String> p
422427
return this;
423428
}
424429

430+
/**
431+
* Appends the given value(s) to the given form field and also add to the
432+
* {@link #param(String, String...) request parameters} map.
433+
* @param name the field name
434+
* @param values one or more values
435+
* @since 6.1.7
436+
*/
437+
public MockHttpServletRequestBuilder formField(String name, String... values) {
438+
param(name, values);
439+
this.formFields.addAll(name, Arrays.asList(values));
440+
return this;
441+
}
442+
443+
/**
444+
* Variant of {@link #formField(String, String...)} with a {@link MultiValueMap}.
445+
* @param formFields the form fields to add
446+
* @since 6.1.7
447+
*/
448+
public MockHttpServletRequestBuilder formFields(MultiValueMap<String, String> formFields) {
449+
params(formFields);
450+
this.formFields.addAll(formFields);
451+
return this;
452+
}
453+
425454
/**
426455
* Add the given cookies to the request. Cookies are always added.
427456
* @param cookies the cookies to add
@@ -629,6 +658,12 @@ public Object merge(@Nullable Object parent) {
629658
this.queryParams.put(paramName, entry.getValue());
630659
}
631660
}
661+
for (Map.Entry<String, List<String>> entry : parentBuilder.formFields.entrySet()) {
662+
String paramName = entry.getKey();
663+
if (!this.formFields.containsKey(paramName)) {
664+
this.formFields.put(paramName, entry.getValue());
665+
}
666+
}
632667
for (Cookie cookie : parentBuilder.cookies) {
633668
if (!containsCookie(cookie)) {
634669
this.cookies.add(cookie);
@@ -744,6 +779,24 @@ public final MockHttpServletRequest buildRequest(ServletContext servletContext)
744779
}
745780
});
746781

782+
if (!this.formFields.isEmpty()) {
783+
if (this.content != null && this.content.length > 0) {
784+
throw new IllegalStateException("Could not write form data with an existing body");
785+
}
786+
Charset charset = (this.characterEncoding != null
787+
? Charset.forName(this.characterEncoding) : StandardCharsets.UTF_8);
788+
MediaType mediaType = (request.getContentType() != null
789+
? MediaType.parseMediaType(request.getContentType())
790+
: new MediaType(MediaType.APPLICATION_FORM_URLENCODED, charset));
791+
if (!mediaType.isCompatibleWith(MediaType.APPLICATION_FORM_URLENCODED)) {
792+
throw new IllegalStateException("Invalid content type: '" + mediaType
793+
+ "' is not compatible with '" + MediaType.APPLICATION_FORM_URLENCODED + "'");
794+
}
795+
request.setContent(writeFormData(mediaType, charset));
796+
if (request.getContentType() == null) {
797+
request.setContentType(mediaType.toString());
798+
}
799+
}
747800
if (this.content != null && this.content.length > 0) {
748801
String requestContentType = request.getContentType();
749802
if (requestContentType != null) {
@@ -820,6 +873,32 @@ private void addRequestParams(MockHttpServletRequest request, MultiValueMap<Stri
820873
}));
821874
}
822875

876+
private byte[] writeFormData(MediaType mediaType, Charset charset) {
877+
ByteArrayOutputStream out = new ByteArrayOutputStream();
878+
HttpOutputMessage message = new HttpOutputMessage() {
879+
@Override
880+
public OutputStream getBody() {
881+
return out;
882+
}
883+
884+
@Override
885+
public HttpHeaders getHeaders() {
886+
HttpHeaders headers = new HttpHeaders();
887+
headers.setContentType(mediaType);
888+
return headers;
889+
}
890+
};
891+
try {
892+
FormHttpMessageConverter messageConverter = new FormHttpMessageConverter();
893+
messageConverter.setCharset(charset);
894+
messageConverter.write(this.formFields, mediaType, message);
895+
return out.toByteArray();
896+
}
897+
catch (IOException ex) {
898+
throw new IllegalStateException("Failed to write form data to request body", ex);
899+
}
900+
}
901+
823902
private MultiValueMap<String, String> parseFormData(MediaType mediaType) {
824903
HttpInputMessage message = new HttpInputMessage() {
825904
@Override

Diff for: spring-test/src/test/java/org/springframework/test/web/servlet/request/MockHttpServletRequestBuilderTests.java

+67
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
import jakarta.servlet.ServletContext;
3131
import jakarta.servlet.http.Cookie;
32+
import org.assertj.core.api.ThrowingConsumer;
3233
import org.junit.jupiter.api.Test;
3334

3435
import org.springframework.http.HttpHeaders;
@@ -47,6 +48,8 @@
4748
import static java.nio.charset.StandardCharsets.UTF_8;
4849
import static org.assertj.core.api.Assertions.assertThat;
4950
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
51+
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
52+
import static org.assertj.core.api.Assertions.entry;
5053
import static org.springframework.http.HttpMethod.GET;
5154
import static org.springframework.http.HttpMethod.POST;
5255

@@ -288,6 +291,70 @@ void queryParameterList() {
288291
assertThat(request.getParameter("foo[1]")).isEqualTo("baz");
289292
}
290293

294+
@Test
295+
void formField() {
296+
this.builder = new MockHttpServletRequestBuilder(POST, "/");
297+
this.builder.formField("foo", "bar");
298+
this.builder.formField("foo", "baz");
299+
300+
MockHttpServletRequest request = this.builder.buildRequest(this.servletContext);
301+
302+
assertThat(request.getParameterMap().get("foo")).containsExactly("bar", "baz");
303+
assertThat(request).satisfies(hasFormData("foo=bar&foo=baz"));
304+
}
305+
306+
@Test
307+
void formFieldMap() {
308+
this.builder = new MockHttpServletRequestBuilder(POST, "/");
309+
MultiValueMap<String, String> formFields = new LinkedMultiValueMap<>();
310+
List<String> values = new ArrayList<>();
311+
values.add("bar");
312+
values.add("baz");
313+
formFields.put("foo", values);
314+
this.builder.formFields(formFields);
315+
316+
MockHttpServletRequest request = this.builder.buildRequest(this.servletContext);
317+
318+
assertThat(request.getParameterMap().get("foo")).containsExactly("bar", "baz");
319+
assertThat(request).satisfies(hasFormData("foo=bar&foo=baz"));
320+
}
321+
322+
@Test
323+
void formFieldsAreEncoded() {
324+
MockHttpServletRequest request = new MockHttpServletRequestBuilder(POST, "/")
325+
.formField("name 1", "value 1").formField("name 2", "value A", "value B")
326+
.buildRequest(new MockServletContext());
327+
assertThat(request.getParameterMap()).containsOnly(
328+
entry("name 1", new String[] { "value 1" }),
329+
entry("name 2", new String[] { "value A", "value B" }));
330+
assertThat(request).satisfies(hasFormData("name+1=value+1&name+2=value+A&name+2=value+B"));
331+
}
332+
333+
@Test
334+
void formFieldWithContent() {
335+
this.builder = new MockHttpServletRequestBuilder(POST, "/");
336+
this.builder.content("Should not have content");
337+
this.builder.formField("foo", "bar");
338+
assertThatIllegalStateException().isThrownBy(() -> this.builder.buildRequest(this.servletContext))
339+
.withMessage("Could not write form data with an existing body");
340+
}
341+
342+
@Test
343+
void formFieldWithIncompatibleMediaType() {
344+
this.builder = new MockHttpServletRequestBuilder(POST, "/");
345+
this.builder.contentType(MediaType.TEXT_PLAIN);
346+
this.builder.formField("foo", "bar");
347+
assertThatIllegalStateException().isThrownBy(() -> this.builder.buildRequest(this.servletContext))
348+
.withMessage("Invalid content type: 'text/plain' is not compatible with 'application/x-www-form-urlencoded'");
349+
}
350+
351+
private ThrowingConsumer<MockHttpServletRequest> hasFormData(String body) {
352+
return request -> {
353+
assertThat(request.getContentAsString()).isEqualTo(body);
354+
assertThat(request.getContentType()).isEqualTo("application/x-www-form-urlencoded;charset=UTF-8");
355+
};
356+
}
357+
291358
@Test
292359
void requestParameterFromQueryWithEncoding() {
293360
this.builder = new MockHttpServletRequestBuilder(GET, "/?foo={value}", "bar=baz");

0 commit comments

Comments
 (0)