Skip to content

Commit d76f37c

Browse files
committed
Add multipart support for MockMvcTester
File uploads with MockMvc require a separate MockHttpServletRequestBuilder implementation. This commit applies the same change to support AssertJ on this builder, but for the multipart version. Any request builder can now use `multipart()` to "down cast" to a dedicated multipart request builder that contains the settings configured thus far. Closes gh-33027
1 parent f2137c9 commit d76f37c

File tree

4 files changed

+260
-121
lines changed

4 files changed

+260
-121
lines changed

spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MockMvcTester.java

+36
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,12 @@
3131
import org.springframework.http.converter.HttpMessageConverter;
3232
import org.springframework.lang.Nullable;
3333
import org.springframework.mock.web.MockHttpServletRequest;
34+
import org.springframework.mock.web.MockMultipartHttpServletRequest;
3435
import org.springframework.test.web.servlet.MockMvc;
3536
import org.springframework.test.web.servlet.MvcResult;
3637
import org.springframework.test.web.servlet.RequestBuilder;
3738
import org.springframework.test.web.servlet.request.AbstractMockHttpServletRequestBuilder;
39+
import org.springframework.test.web.servlet.request.AbstractMockMultipartHttpServletRequestBuilder;
3840
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
3941
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder;
4042
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
@@ -389,8 +391,42 @@ private GenericHttpMessageConverter<Object> findJsonMessageConverter(
389391
public final class MockMvcRequestBuilder extends AbstractMockHttpServletRequestBuilder<MockMvcRequestBuilder>
390392
implements AssertProvider<MvcTestResultAssert> {
391393

394+
private final HttpMethod httpMethod;
395+
392396
private MockMvcRequestBuilder(HttpMethod httpMethod) {
393397
super(httpMethod);
398+
this.httpMethod = httpMethod;
399+
}
400+
401+
/**
402+
* Enable file upload support using multipart.
403+
* @return a {@link MockMultipartMvcRequestBuilder} with the settings
404+
* configured thus far
405+
*/
406+
public MockMultipartMvcRequestBuilder multipart() {
407+
return new MockMultipartMvcRequestBuilder(this);
408+
}
409+
410+
public MvcTestResult exchange() {
411+
return perform(this);
412+
}
413+
414+
@Override
415+
public MvcTestResultAssert assertThat() {
416+
return new MvcTestResultAssert(exchange(), MockMvcTester.this.jsonMessageConverter);
417+
}
418+
}
419+
420+
/**
421+
* A builder for {@link MockMultipartHttpServletRequest} that supports AssertJ.
422+
*/
423+
public final class MockMultipartMvcRequestBuilder
424+
extends AbstractMockMultipartHttpServletRequestBuilder<MockMultipartMvcRequestBuilder>
425+
implements AssertProvider<MvcTestResultAssert> {
426+
427+
private MockMultipartMvcRequestBuilder(MockMvcRequestBuilder currentBuilder) {
428+
super(currentBuilder.httpMethod);
429+
merge(currentBuilder);
394430
}
395431

396432
public MvcTestResult exchange() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.web.servlet.request;
18+
19+
import java.io.IOException;
20+
import java.io.InputStream;
21+
import java.io.InputStreamReader;
22+
import java.nio.charset.Charset;
23+
import java.nio.charset.StandardCharsets;
24+
import java.util.ArrayList;
25+
import java.util.Collection;
26+
import java.util.List;
27+
28+
import jakarta.servlet.ServletContext;
29+
import jakarta.servlet.http.Part;
30+
31+
import org.springframework.http.HttpMethod;
32+
import org.springframework.http.MediaType;
33+
import org.springframework.lang.Nullable;
34+
import org.springframework.mock.web.MockHttpServletRequest;
35+
import org.springframework.mock.web.MockMultipartFile;
36+
import org.springframework.mock.web.MockMultipartHttpServletRequest;
37+
import org.springframework.util.Assert;
38+
import org.springframework.util.FileCopyUtils;
39+
import org.springframework.util.LinkedMultiValueMap;
40+
import org.springframework.util.MultiValueMap;
41+
42+
/**
43+
* Base builder for {@link MockMultipartHttpServletRequest}.
44+
*
45+
* @author Rossen Stoyanchev
46+
* @author Arjen Poutsma
47+
* @author Stephane Nicoll
48+
* @since 6.2
49+
* @param <B> a self reference to the builder type
50+
*/
51+
public abstract class AbstractMockMultipartHttpServletRequestBuilder<B extends AbstractMockMultipartHttpServletRequestBuilder<B>>
52+
extends AbstractMockHttpServletRequestBuilder<B> {
53+
54+
private final List<MockMultipartFile> files = new ArrayList<>();
55+
56+
private final MultiValueMap<String, Part> parts = new LinkedMultiValueMap<>();
57+
58+
59+
protected AbstractMockMultipartHttpServletRequestBuilder(HttpMethod httpMethod) {
60+
super(httpMethod);
61+
}
62+
63+
/**
64+
* Add a new {@link MockMultipartFile} with the given content.
65+
* @param name the name of the file
66+
* @param content the content of the file
67+
*/
68+
public B file(String name, byte[] content) {
69+
this.files.add(new MockMultipartFile(name, content));
70+
return self();
71+
}
72+
73+
/**
74+
* Add the given {@link MockMultipartFile}.
75+
* @param file the multipart file
76+
*/
77+
public B file(MockMultipartFile file) {
78+
this.files.add(file);
79+
return self();
80+
}
81+
82+
/**
83+
* Add {@link Part} components to the request.
84+
* @param parts one or more parts to add
85+
* @since 5.0
86+
*/
87+
public B part(Part... parts) {
88+
Assert.notEmpty(parts, "'parts' must not be empty");
89+
for (Part part : parts) {
90+
this.parts.add(part.getName(), part);
91+
}
92+
return self();
93+
}
94+
95+
@Override
96+
public Object merge(@Nullable Object parent) {
97+
if (parent == null) {
98+
return this;
99+
}
100+
if (parent instanceof AbstractMockHttpServletRequestBuilder<?>) {
101+
super.merge(parent);
102+
if (parent instanceof AbstractMockMultipartHttpServletRequestBuilder<?> parentBuilder) {
103+
this.files.addAll(parentBuilder.files);
104+
parentBuilder.parts.keySet().forEach(name ->
105+
this.parts.putIfAbsent(name, parentBuilder.parts.get(name)));
106+
}
107+
}
108+
else {
109+
throw new IllegalArgumentException("Cannot merge with [" + parent.getClass().getName() + "]");
110+
}
111+
return this;
112+
}
113+
114+
/**
115+
* Create a new {@link MockMultipartHttpServletRequest} based on the
116+
* supplied {@code ServletContext} and the {@code MockMultipartFiles}
117+
* added to this builder.
118+
*/
119+
@Override
120+
protected final MockHttpServletRequest createServletRequest(ServletContext servletContext) {
121+
MockMultipartHttpServletRequest request = new MockMultipartHttpServletRequest(servletContext);
122+
Charset defaultCharset = (request.getCharacterEncoding() != null ?
123+
Charset.forName(request.getCharacterEncoding()) : StandardCharsets.UTF_8);
124+
125+
this.files.forEach(request::addFile);
126+
this.parts.values().stream().flatMap(Collection::stream).forEach(part -> {
127+
request.addPart(part);
128+
try {
129+
String name = part.getName();
130+
String filename = part.getSubmittedFileName();
131+
InputStream is = part.getInputStream();
132+
if (filename != null) {
133+
request.addFile(new MockMultipartFile(name, filename, part.getContentType(), is));
134+
}
135+
else {
136+
InputStreamReader reader = new InputStreamReader(is, getCharsetOrDefault(part, defaultCharset));
137+
String value = FileCopyUtils.copyToString(reader);
138+
request.addParameter(part.getName(), value);
139+
}
140+
}
141+
catch (IOException ex) {
142+
throw new IllegalStateException("Failed to read content for part " + part.getName(), ex);
143+
}
144+
});
145+
146+
return request;
147+
}
148+
149+
private Charset getCharsetOrDefault(Part part, Charset defaultCharset) {
150+
if (part.getContentType() != null) {
151+
MediaType mediaType = MediaType.parseMediaType(part.getContentType());
152+
if (mediaType.getCharset() != null) {
153+
return mediaType.getCharset();
154+
}
155+
}
156+
return defaultCharset;
157+
}
158+
159+
}

spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMultipartHttpServletRequestBuilder.java

+3-120
Original file line numberDiff line numberDiff line change
@@ -16,42 +16,22 @@
1616

1717
package org.springframework.test.web.servlet.request;
1818

19-
import java.io.IOException;
20-
import java.io.InputStream;
21-
import java.io.InputStreamReader;
2219
import java.net.URI;
23-
import java.nio.charset.Charset;
24-
import java.nio.charset.StandardCharsets;
25-
import java.util.ArrayList;
26-
import java.util.Collection;
27-
import java.util.List;
28-
29-
import jakarta.servlet.ServletContext;
30-
import jakarta.servlet.http.Part;
3120

3221
import org.springframework.http.HttpMethod;
3322
import org.springframework.http.MediaType;
34-
import org.springframework.lang.Nullable;
35-
import org.springframework.mock.web.MockHttpServletRequest;
36-
import org.springframework.mock.web.MockMultipartFile;
3723
import org.springframework.mock.web.MockMultipartHttpServletRequest;
38-
import org.springframework.util.Assert;
39-
import org.springframework.util.FileCopyUtils;
40-
import org.springframework.util.LinkedMultiValueMap;
41-
import org.springframework.util.MultiValueMap;
4224

4325
/**
4426
* Default builder for {@link MockMultipartHttpServletRequest}.
4527
*
4628
* @author Rossen Stoyanchev
4729
* @author Arjen Poutsma
30+
* @author Stephane Nicoll
4831
* @since 3.2
4932
*/
50-
public class MockMultipartHttpServletRequestBuilder extends AbstractMockHttpServletRequestBuilder<MockMultipartHttpServletRequestBuilder> {
51-
52-
private final List<MockMultipartFile> files = new ArrayList<>();
53-
54-
private final MultiValueMap<String, Part> parts = new LinkedMultiValueMap<>();
33+
public class MockMultipartHttpServletRequestBuilder
34+
extends AbstractMockMultipartHttpServletRequestBuilder<MockMultipartHttpServletRequestBuilder> {
5535

5636

5737
/**
@@ -98,101 +78,4 @@ public class MockMultipartHttpServletRequestBuilder extends AbstractMockHttpServ
9878
super.contentType(MediaType.MULTIPART_FORM_DATA);
9979
}
10080

101-
102-
/**
103-
* Add a new {@link MockMultipartFile} with the given content.
104-
* @param name the name of the file
105-
* @param content the content of the file
106-
*/
107-
public MockMultipartHttpServletRequestBuilder file(String name, byte[] content) {
108-
this.files.add(new MockMultipartFile(name, content));
109-
return this;
110-
}
111-
112-
/**
113-
* Add the given {@link MockMultipartFile}.
114-
* @param file the multipart file
115-
*/
116-
public MockMultipartHttpServletRequestBuilder file(MockMultipartFile file) {
117-
this.files.add(file);
118-
return this;
119-
}
120-
121-
/**
122-
* Add {@link Part} components to the request.
123-
* @param parts one or more parts to add
124-
* @since 5.0
125-
*/
126-
public MockMultipartHttpServletRequestBuilder part(Part... parts) {
127-
Assert.notEmpty(parts, "'parts' must not be empty");
128-
for (Part part : parts) {
129-
this.parts.add(part.getName(), part);
130-
}
131-
return this;
132-
}
133-
134-
@Override
135-
public Object merge(@Nullable Object parent) {
136-
if (parent == null) {
137-
return this;
138-
}
139-
if (parent instanceof AbstractMockHttpServletRequestBuilder) {
140-
super.merge(parent);
141-
if (parent instanceof MockMultipartHttpServletRequestBuilder parentBuilder) {
142-
this.files.addAll(parentBuilder.files);
143-
parentBuilder.parts.keySet().forEach(name ->
144-
this.parts.putIfAbsent(name, parentBuilder.parts.get(name)));
145-
}
146-
}
147-
else {
148-
throw new IllegalArgumentException("Cannot merge with [" + parent.getClass().getName() + "]");
149-
}
150-
return this;
151-
}
152-
153-
/**
154-
* Create a new {@link MockMultipartHttpServletRequest} based on the
155-
* supplied {@code ServletContext} and the {@code MockMultipartFiles}
156-
* added to this builder.
157-
*/
158-
@Override
159-
protected final MockHttpServletRequest createServletRequest(ServletContext servletContext) {
160-
MockMultipartHttpServletRequest request = new MockMultipartHttpServletRequest(servletContext);
161-
Charset defaultCharset = (request.getCharacterEncoding() != null ?
162-
Charset.forName(request.getCharacterEncoding()) : StandardCharsets.UTF_8);
163-
164-
this.files.forEach(request::addFile);
165-
this.parts.values().stream().flatMap(Collection::stream).forEach(part -> {
166-
request.addPart(part);
167-
try {
168-
String name = part.getName();
169-
String filename = part.getSubmittedFileName();
170-
InputStream is = part.getInputStream();
171-
if (filename != null) {
172-
request.addFile(new MockMultipartFile(name, filename, part.getContentType(), is));
173-
}
174-
else {
175-
InputStreamReader reader = new InputStreamReader(is, getCharsetOrDefault(part, defaultCharset));
176-
String value = FileCopyUtils.copyToString(reader);
177-
request.addParameter(part.getName(), value);
178-
}
179-
}
180-
catch (IOException ex) {
181-
throw new IllegalStateException("Failed to read content for part " + part.getName(), ex);
182-
}
183-
});
184-
185-
return request;
186-
}
187-
188-
private Charset getCharsetOrDefault(Part part, Charset defaultCharset) {
189-
if (part.getContentType() != null) {
190-
MediaType mediaType = MediaType.parseMediaType(part.getContentType());
191-
if (mediaType.getCharset() != null) {
192-
return mediaType.getCharset();
193-
}
194-
}
195-
return defaultCharset;
196-
}
197-
19881
}

0 commit comments

Comments
 (0)