mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-17 16:39:29 +00:00
Add integration tests and docs for multipart support
This commit adds integration tests and reference documentation for multipart support in `RestClient` and `RestTestClient`. Closes gh-35569 Closes gh-33263
This commit is contained in:
@@ -451,11 +451,59 @@ In most cases, you do not have to specify the `Content-Type` for each part.
|
||||
The content type is determined automatically based on the `HttpMessageConverter` chosen to serialize it or, in the case of a `Resource`, based on the file extension.
|
||||
If necessary, you can explicitly provide the `MediaType` with an `HttpEntity` wrapper.
|
||||
|
||||
The `Content-Type` is set to `multipart/form-data` by the `MultiPartHttpMessageConverter`.
|
||||
The `Content-Type` is set to `multipart/form-data` by the `MultipartHttpMessageConverter`.
|
||||
As seen in the previous section, `MultiValueMap` types can also be used for URL encoded forms.
|
||||
It is preferable to explicitly set the media type in the `Content-Type` or `Accept` HTTP request headers to ensure that the expected
|
||||
message converter is used.
|
||||
|
||||
`RestClient` can also receive multipart responses.
|
||||
To decode a multipart response body, use a `ParameterizedTypeReference<MultiValueMap<String, Part>>`.
|
||||
The decoded map contains `Part` instances where `FormFieldPart` represents form field values
|
||||
and `FilePart` represents file parts with a `filename()` and a `transferTo()` method.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim"]
|
||||
----
|
||||
MultiValueMap<String, Part> result = this.restClient.get()
|
||||
.uri("https://example.com/upload")
|
||||
.accept(MediaType.MULTIPART_FORM_DATA)
|
||||
.retrieve()
|
||||
.body(new ParameterizedTypeReference<>() {});
|
||||
|
||||
Part field = result.getFirst("fieldPart");
|
||||
if (field instanceof FormFieldPart formField) {
|
||||
String fieldValue = formField.value();
|
||||
}
|
||||
Part file = result.getFirst("filePart");
|
||||
if (file instanceof FilePart filePart) {
|
||||
filePart.transferTo(Path.of("/tmp/" + filePart.filename()));
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim"]
|
||||
----
|
||||
val result = this.restClient.get()
|
||||
.uri("https://example.com/upload")
|
||||
.accept(MediaType.MULTIPART_FORM_DATA)
|
||||
.retrieve()
|
||||
.body(object : ParameterizedTypeReference<MultiValueMap<String, Part>>() {})
|
||||
|
||||
val field = result?.getFirst("fieldPart")
|
||||
if (field is FormFieldPart) {
|
||||
val fieldValue = field.value()
|
||||
}
|
||||
val file = result?.getFirst("filePart")
|
||||
if (file is FilePart) {
|
||||
file.transferTo(Path.of("/tmp/" + file.filename()))
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
[[rest-request-factories]]
|
||||
=== Client Request Factories
|
||||
|
||||
@@ -142,6 +142,9 @@ provides two alternative ways to verify the response:
|
||||
1. xref:resttestclient-workflow[Built-in Assertions] extend the request workflow with a chain of expectations
|
||||
2. xref:resttestclient-assertj[AssertJ Integration] to verify the response via `assertThat()` statements
|
||||
|
||||
TIP: See the xref:integration/rest-clients.adoc#rest-message-conversion[HTTP Message Conversion]
|
||||
section for examples on how to prepare a request with any content, including form data and multipart data.
|
||||
|
||||
|
||||
|
||||
[[resttestclient.workflow]]
|
||||
@@ -213,6 +216,16 @@ To verify JSON content with https://github.com/jayway/JsonPath[JSONPath]:
|
||||
include-code::./JsonTests[tag=jsonPath,indent=0]
|
||||
|
||||
|
||||
[[resttestclient.multipart]]
|
||||
==== Multipart Content
|
||||
|
||||
When testing endpoints that return multipart responses, you can decode the body to a
|
||||
`MultiValueMap<String, Part>` and assert individual parts using the `FormFieldPart`
|
||||
and `FilePart` subtypes.
|
||||
|
||||
include-code::./MultipartTests[tag=multipart,indent=0]
|
||||
|
||||
|
||||
[[resttestclient.assertj]]
|
||||
=== AssertJ Integration
|
||||
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2025-present 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.docs.testing.resttestclient.multipart;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.multipart.FilePart;
|
||||
import org.springframework.http.converter.multipart.FormFieldPart;
|
||||
import org.springframework.http.converter.multipart.Part;
|
||||
import org.springframework.test.web.servlet.client.RestTestClient;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class MultipartTests {
|
||||
|
||||
RestTestClient client;
|
||||
|
||||
@Test
|
||||
void multipart() {
|
||||
// tag::multipart[]
|
||||
client.get().uri("/upload")
|
||||
.accept(MediaType.MULTIPART_FORM_DATA)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(new ParameterizedTypeReference<MultiValueMap<String, Part>>() {})
|
||||
.value(result -> {
|
||||
Part field = result.getFirst("fieldPart");
|
||||
assertThat(field).isInstanceOfSatisfying(FormFieldPart.class,
|
||||
formField -> assertThat(formField.value()).isEqualTo("fieldValue"));
|
||||
Part file = result.getFirst("filePart");
|
||||
assertThat(file).isInstanceOfSatisfying(FilePart.class,
|
||||
filePart -> assertThat(filePart.filename()).isEqualTo("logo.png"));
|
||||
});
|
||||
// end::multipart[]
|
||||
}
|
||||
|
||||
}
|
||||
+47
-1
@@ -22,19 +22,30 @@ import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.multipart.FilePart;
|
||||
import org.springframework.http.converter.multipart.FormFieldPart;
|
||||
import org.springframework.http.converter.multipart.Part;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.bind.annotation.CookieValue;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests that use a {@link RestTestClient} configured with a {@link MockMvc} instance
|
||||
* that uses a standalone controller.
|
||||
*
|
||||
* @author Rob Worsnop
|
||||
* @author Sam Brannen
|
||||
* @since 7.0
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
class MockMvcRestTestClientTests {
|
||||
|
||||
@@ -75,6 +86,26 @@ class MockMvcRestTestClientTests {
|
||||
.isEqualTo("some really bad request");
|
||||
}
|
||||
|
||||
@Test
|
||||
void retrieveMultipart() {
|
||||
client.get()
|
||||
.uri("/multipart")
|
||||
.accept(MediaType.MULTIPART_FORM_DATA)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(new ParameterizedTypeReference<MultiValueMap<String, Part>>() {})
|
||||
.value(result -> {
|
||||
assertThat(result).hasSize(3);
|
||||
assertThat(result).containsKeys("text1", "text2", "file1");
|
||||
assertThat(result.getFirst("text1")).isInstanceOfSatisfying(FormFieldPart.class,
|
||||
part -> assertThat(part.value()).isEqualTo("a"));
|
||||
assertThat(result.getFirst("text2")).isInstanceOfSatisfying(FormFieldPart.class,
|
||||
part -> assertThat(part.value()).isEqualTo("b"));
|
||||
assertThat(result.getFirst("file1")).isInstanceOfSatisfying(FilePart.class,
|
||||
part -> assertThat(part.filename()).isEqualTo("file1.txt"));
|
||||
});
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class TestController {
|
||||
|
||||
@@ -94,6 +125,21 @@ class MockMvcRestTestClientTests {
|
||||
response.sendError(400);
|
||||
response.getWriter().write("some really bad request");
|
||||
}
|
||||
|
||||
@GetMapping(value = "/multipart", produces = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
MultiValueMap<String, Object> multipart() {
|
||||
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||
parts.add("text1", "a");
|
||||
parts.add("text2", "b");
|
||||
Resource resource = new ByteArrayResource("Lorem ipsum dolor sit amet".getBytes()) {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return "file1.txt";
|
||||
}
|
||||
};
|
||||
parts.add("file1", resource);
|
||||
return parts;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+35
@@ -24,6 +24,7 @@ import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
@@ -40,6 +41,8 @@ import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
@@ -54,6 +57,9 @@ import org.springframework.http.client.JdkClientHttpRequestFactory;
|
||||
import org.springframework.http.client.JettyClientHttpRequestFactory;
|
||||
import org.springframework.http.client.ReactorClientHttpRequestFactory;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.http.converter.multipart.FilePart;
|
||||
import org.springframework.http.converter.multipart.FormFieldPart;
|
||||
import org.springframework.http.converter.multipart.Part;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.FastByteArrayOutputStream;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
@@ -354,6 +360,35 @@ class RestClientIntegrationTests {
|
||||
assertThat(result).isNull();
|
||||
}
|
||||
|
||||
@ParameterizedRestClientTest
|
||||
void retrieveMultipart(ClientHttpRequestFactory requestFactory) throws IOException {
|
||||
startServer(requestFactory);
|
||||
Resource resource = new ClassPathResource("simple.multipart", getClass());
|
||||
String multipartBody = Files.readString(resource.getFile().toPath());
|
||||
|
||||
prepareResponse(builder -> builder
|
||||
.setHeader("Content-Type", "multipart/form-data; boundary=---------------------------testboundary")
|
||||
.body(multipartBody));
|
||||
|
||||
MultiValueMap<String, Part> result = this.restClient.get()
|
||||
.uri("/multipart")
|
||||
.accept(MediaType.MULTIPART_FORM_DATA)
|
||||
.retrieve()
|
||||
.body(new ParameterizedTypeReference<>() {});
|
||||
|
||||
assertThat(result).hasSize(3);
|
||||
assertThat(result).containsKeys("text1", "text2", "file1");
|
||||
assertThat(result.get("text1").get(0)).isInstanceOfSatisfying(FormFieldPart.class, part -> assertThat(part.value()).isEqualTo("a"));
|
||||
assertThat(result.get("text2").get(0)).isInstanceOfSatisfying(FormFieldPart.class, part -> assertThat(part.value()).isEqualTo("b"));
|
||||
assertThat(result.get("file1").get(0)).isInstanceOfSatisfying(FilePart.class, part -> assertThat(part.filename()).isEqualTo("a.txt"));
|
||||
|
||||
expectRequestCount(1);
|
||||
expectRequest(request -> {
|
||||
assertThat(request.getTarget()).isEqualTo("/multipart");
|
||||
assertThat(request.getHeaders().get(HttpHeaders.ACCEPT)).isEqualTo("multipart/form-data");
|
||||
});
|
||||
}
|
||||
|
||||
@ParameterizedRestClientTest
|
||||
void retrieve404(ClientHttpRequestFactory requestFactory) throws IOException {
|
||||
startServer(requestFactory);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
-----------------------------testboundary
|
||||
Content-Disposition: form-data; name="text1"
|
||||
|
||||
a
|
||||
-----------------------------testboundary
|
||||
Content-Disposition: form-data; name="text2"
|
||||
|
||||
b
|
||||
-----------------------------testboundary
|
||||
Content-Disposition: form-data; name="file1"; filename="a.txt"
|
||||
Content-Type: text/plain
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer iaculis metus id vestibulum nullam.
|
||||
|
||||
-----------------------------testboundary--
|
||||
Reference in New Issue
Block a user