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:
Andy Wilkinson
2015-07-13 17:32:38 +01:00
parent 88e26ba88b
commit 5d0428b9d0
5 changed files with 182 additions and 1 deletions

View File

@@ -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) {

View File

@@ -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

View File

@@ -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

View File

@@ -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)));
}
}

View File

@@ -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)));
}
}