Add support for documenting multipart requests
This commit adds support for documenting multipart requests in both curl and HTTP request snippets. Closes gh-48
This commit is contained in:
@@ -26,6 +26,7 @@ import org.springframework.restdocs.snippet.SnippetWritingResultHandler;
|
||||
import org.springframework.restdocs.util.DocumentableHttpServletRequest;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* Static factory methods for documenting a RESTful API as if it were being driven using
|
||||
@@ -94,6 +95,11 @@ public abstract class CurlDocumentation {
|
||||
writeOptionToIncludeHeadersInOutput();
|
||||
writeHttpMethodIfNecessary(request);
|
||||
writeHeaders(request);
|
||||
|
||||
if (request.isMultipartRequest()) {
|
||||
writeParts(request);
|
||||
}
|
||||
|
||||
writeContent(request);
|
||||
|
||||
this.writer.println();
|
||||
@@ -142,6 +148,28 @@ public abstract class CurlDocumentation {
|
||||
}
|
||||
}
|
||||
|
||||
private void writeParts(DocumentableHttpServletRequest request)
|
||||
throws IOException {
|
||||
for (Entry<String, List<MultipartFile>> entry : request.getMultipartFiles()
|
||||
.entrySet()) {
|
||||
for (MultipartFile file : entry.getValue()) {
|
||||
this.writer.printf(" -F '%s=", file.getName());
|
||||
if (!StringUtils.hasText(file.getOriginalFilename())) {
|
||||
this.writer.append(new String(file.getBytes()));
|
||||
}
|
||||
else {
|
||||
this.writer.printf("@%s", file.getOriginalFilename());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(file.getContentType())) {
|
||||
this.writer.append(";type=").append(file.getContentType());
|
||||
}
|
||||
this.writer.append("'");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void writeContent(DocumentableHttpServletRequest request)
|
||||
throws IOException {
|
||||
if (request.getContentLength() > 0) {
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.springframework.restdocs.snippet.SnippetWritingResultHandler;
|
||||
import org.springframework.restdocs.util.DocumentableHttpServletRequest;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* Static factory methods for documenting a RESTful API's HTTP requests.
|
||||
@@ -82,6 +83,8 @@ public abstract class HttpDocumentation {
|
||||
|
||||
private static class HttpRequestDocumentationAction implements DocumentationAction {
|
||||
|
||||
private static final String MULTIPART_BOUNDARY = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm";
|
||||
|
||||
private final DocumentationWriter writer;
|
||||
|
||||
private final MvcResult result;
|
||||
@@ -97,9 +100,18 @@ public abstract class HttpDocumentation {
|
||||
this.result.getRequest());
|
||||
this.writer.printf("%s %s HTTP/1.1%n", request.getMethod(),
|
||||
request.getRequestUriWithQueryString());
|
||||
|
||||
for (Entry<String, List<String>> header : request.getHeaders().entrySet()) {
|
||||
for (String value : header.getValue()) {
|
||||
this.writer.printf("%s: %s%n", header.getKey(), value);
|
||||
if (header.getKey() == HttpHeaders.CONTENT_TYPE
|
||||
&& request.isMultipartRequest()) {
|
||||
this.writer.printf("%s: %s; boundary=%s%n", header.getKey(),
|
||||
value, MULTIPART_BOUNDARY);
|
||||
}
|
||||
else {
|
||||
this.writer.printf("%s: %s%n", header.getKey(), value);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
if (requiresFormEncodingContentType(request)) {
|
||||
@@ -115,6 +127,9 @@ public abstract class HttpDocumentation {
|
||||
if (StringUtils.hasText(queryString)) {
|
||||
this.writer.println(queryString);
|
||||
}
|
||||
if (request.isMultipartRequest()) {
|
||||
writeParts(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +139,37 @@ public abstract class HttpDocumentation {
|
||||
&& (request.isPostRequest() || request.isPutRequest())
|
||||
&& StringUtils.hasText(request.getParameterMapAsQueryString());
|
||||
}
|
||||
|
||||
private void writeParts(DocumentableHttpServletRequest request)
|
||||
throws IOException {
|
||||
for (Entry<String, List<MultipartFile>> entry : request.getMultipartFiles()
|
||||
.entrySet()) {
|
||||
for (MultipartFile file : entry.getValue()) {
|
||||
writePartBoundary();
|
||||
writePart(file);
|
||||
this.writer.println();
|
||||
}
|
||||
}
|
||||
writeMultipartEnd();
|
||||
}
|
||||
|
||||
private void writePartBoundary() {
|
||||
this.writer.printf("--%s%n", MULTIPART_BOUNDARY);
|
||||
}
|
||||
|
||||
private void writePart(MultipartFile part) throws IOException {
|
||||
this.writer.printf("Content-Disposition: form-data; name=%s%n",
|
||||
part.getName());
|
||||
if (StringUtils.hasText(part.getContentType())) {
|
||||
this.writer.printf("Content-Type: %s%n", part.getContentType());
|
||||
}
|
||||
this.writer.println();
|
||||
this.writer.print(new String(part.getBytes()));
|
||||
}
|
||||
|
||||
private void writeMultipartEnd() {
|
||||
this.writer.printf("--%s--%n", MULTIPART_BOUNDARY);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class HttpResponseDocumentationAction implements
|
||||
|
||||
@@ -28,9 +28,12 @@ import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockMultipartHttpServletRequest;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* An {@link HttpServletRequest} wrapper that provides a limited set of methods intended
|
||||
@@ -83,6 +86,31 @@ public class DocumentableHttpServletRequest {
|
||||
return RequestMethod.PUT == RequestMethod.valueOf(this.delegate.getMethod());
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not this is a multipart request.
|
||||
*
|
||||
* @return {@code true} if it is a multipart request, otherwise {@code false}.
|
||||
* @see MockMultipartHttpServletRequest
|
||||
*/
|
||||
public boolean isMultipartRequest() {
|
||||
return this.delegate instanceof MockMultipartHttpServletRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code Map} of the request's multipart files, or {@code null} if this
|
||||
* request is not a multipart request.
|
||||
*
|
||||
* @return a {@code Map} of the multipart files contained in the request, or
|
||||
* {@code null}
|
||||
* @see #isMultipartRequest()
|
||||
*/
|
||||
public MultiValueMap<String, MultipartFile> getMultipartFiles() {
|
||||
if (!isMultipartRequest()) {
|
||||
return null;
|
||||
}
|
||||
return ((MockMultipartHttpServletRequest) this.delegate).getMultiFileMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request's headers. The headers are ordered based on the ordering of
|
||||
* {@link HttpServletRequest#getHeaderNames()} and
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.springframework.restdocs.curl;
|
||||
import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRequest;
|
||||
import static org.springframework.restdocs.test.SnippetMatchers.codeBlock;
|
||||
import static org.springframework.restdocs.test.StubMvcResult.result;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
@@ -27,8 +28,10 @@ import java.io.IOException;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.restdocs.test.ExpectedSnippet;
|
||||
|
||||
/**
|
||||
@@ -44,6 +47,9 @@ public class CurlDocumentationTests {
|
||||
@Rule
|
||||
public ExpectedSnippet snippet = new ExpectedSnippet();
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
@Test
|
||||
public void getRequest() throws IOException {
|
||||
this.snippet.expectCurlRequest("get-request").withContents(
|
||||
@@ -241,4 +247,44 @@ public class CurlDocumentationTests {
|
||||
result(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipartPostWithNoOriginalFilename() throws IOException {
|
||||
String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H "
|
||||
+ "'Content-Type: multipart/form-data' -F "
|
||||
+ "'metadata={\"description\": \"foo\"}'";
|
||||
this.snippet.expectCurlRequest("multipart-post-no-original-filename")
|
||||
.withContents(codeBlock("bash").content(expectedContent));
|
||||
MockMultipartFile multipartFile = new MockMultipartFile("metadata",
|
||||
"{\"description\": \"foo\"}".getBytes());
|
||||
documentCurlRequest("multipart-post-no-original-filename").handle(
|
||||
result(fileUpload("/upload").file(multipartFile)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipartPostWithContentType() throws IOException {
|
||||
String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H "
|
||||
+ "'Content-Type: multipart/form-data' -F "
|
||||
+ "'image=@documents/images/example.png;type=image/png'";
|
||||
this.snippet.expectCurlRequest("multipart-post-with-content-type").withContents(
|
||||
codeBlock("bash").content(expectedContent));
|
||||
MockMultipartFile multipartFile = new MockMultipartFile("image",
|
||||
"documents/images/example.png", MediaType.IMAGE_PNG_VALUE,
|
||||
"bytes".getBytes());
|
||||
documentCurlRequest("multipart-post-with-content-type").handle(
|
||||
result(fileUpload("/upload").file(multipartFile)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipartPost() throws IOException {
|
||||
String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H "
|
||||
+ "'Content-Type: multipart/form-data' -F "
|
||||
+ "'image=@documents/images/example.png'";
|
||||
this.snippet.expectCurlRequest("multipart-post").withContents(
|
||||
codeBlock("bash").content(expectedContent));
|
||||
MockMultipartFile multipartFile = new MockMultipartFile("image",
|
||||
"documents/images/example.png", null, "bytes".getBytes());
|
||||
documentCurlRequest("multipart-post").handle(
|
||||
result(fileUpload("/upload").file(multipartFile)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import static org.springframework.restdocs.http.HttpDocumentation.documentHttpRe
|
||||
import static org.springframework.restdocs.test.SnippetMatchers.httpRequest;
|
||||
import static org.springframework.restdocs.test.SnippetMatchers.httpResponse;
|
||||
import static org.springframework.restdocs.test.StubMvcResult.result;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
@@ -36,6 +37,7 @@ import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.restdocs.test.ExpectedSnippet;
|
||||
|
||||
/**
|
||||
@@ -156,4 +158,35 @@ public class HttpDocumentationTests {
|
||||
documentHttpResponse("response-with-content").handle(result(response));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipartPost() throws IOException {
|
||||
String boundary = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm";
|
||||
String expectedContent = String.format("--%s%nContent-Disposition: form-data; "
|
||||
+ "name=image%n%n<< data >>%n--%s--", boundary, boundary);
|
||||
this.snippet.expectHttpRequest("multipart-post").withContents(
|
||||
httpRequest(POST, "/upload").header("Content-Type",
|
||||
"multipart/form-data; boundary=" + boundary).content(
|
||||
expectedContent));
|
||||
MockMultipartFile multipartFile = new MockMultipartFile("image",
|
||||
"documents/images/example.png", null, "<< data >>".getBytes());
|
||||
documentHttpRequest("multipart-post").handle(
|
||||
result(fileUpload("/upload").file(multipartFile)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipartPostWithContentType() throws IOException {
|
||||
String boundary = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm";
|
||||
String expectedContent = String.format("--%s%nContent-Disposition: form-data; "
|
||||
+ "name=image%nContent-Type: image/png%n%n<< data >>%n--%s--", boundary,
|
||||
boundary);
|
||||
this.snippet.expectHttpRequest("multipart-post-with-content-type").withContents(
|
||||
httpRequest(POST, "/upload").header("Content-Type",
|
||||
"multipart/form-data; boundary=" + boundary).content(
|
||||
expectedContent));
|
||||
MockMultipartFile multipartFile = new MockMultipartFile("image",
|
||||
"documents/images/example.png", MediaType.IMAGE_PNG_VALUE,
|
||||
"<< data >>".getBytes());
|
||||
documentHttpRequest("multipart-post-with-content-type").handle(
|
||||
result(fileUpload("/upload").file(multipartFile)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user