Introduce a new operation abstraction to reduce coupling with MockMvc

Prior to this commit, MockMvc-classes were used throughout Spring REST
Docs. For example, each Snippet was called with an MvcResult. This
has proven problematic for a few reasons:

1. The MockMvc APIs aren't very amenable to modifying a request or
   response before it's documented. This caused the existing support
   for response modification to rely on CGLib proxies and method
   interceptors. A similary complex solution for request modifiction
   would also have been necessary.
2. Things are harder to reason about than they need to be as the
   MockHttpServletRequest and MockHttpServletResponse classes
   expose more than is required when generating API documentation.
3. Supporting other test frameworks, such as Rest Assured, is hard

This commit introduces a new Operation abstract that encapsulates
all of the information required to document the request that was sent
and the response that was received when performing an operation on a
RESTful service. The new abstraction uses types from Spring's Web
support, such as HttpHeaders, RequestMethod, and MediaType, but does
not rely on MockMvc.

Closes gh-108
This commit is contained in:
Andy Wilkinson
2015-08-27 22:17:44 +01:00
parent 65e37a6276
commit 3579b34a77
53 changed files with 1931 additions and 1100 deletions

View File

@@ -19,9 +19,6 @@ The default configuration for URIs documented by Spring REST Docs is:
|Port
|`8080`
|Context path
|Empty string
|===
This configuration is applied by `RestDocumentationConfigurer`. You can use its API to
@@ -32,6 +29,9 @@ change one or more of the defaults to suit your needs:
include::{examples-dir}/com/example/CustomUriConfiguration.java[tags=custom-uri-configuration]
----
TIP: To configure a request's context path, use the `contextPath` method on
`MockHttpServletRequestBuilder`.
[[configuration-snippet-encoding]]

View File

@@ -38,8 +38,7 @@ public class CustomUriConfiguration {
.apply(documentationConfiguration().uris()
.withScheme("https")
.withHost("example.com")
.withPort(443)
.withContextPath("/api"))
.withPort(443))
.build();
// end::custom-uri-configuration[]
}

View File

@@ -17,9 +17,9 @@
package com.example;
import static org.springframework.restdocs.RestDocumentation.document;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.get;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.pathParameters;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.springframework.test.web.servlet.MockMvc;

View File

@@ -17,13 +17,13 @@
package com.example;
import static org.springframework.restdocs.RestDocumentation.document;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.get;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.post;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
import static org.springframework.restdocs.snippet.Attributes.attributes;
import static org.springframework.restdocs.snippet.Attributes.key;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.get;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.springframework.http.MediaType;

View File

@@ -17,10 +17,10 @@
package com.example;
import static org.springframework.restdocs.RestDocumentation.document;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.requestParameters;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.get;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.post;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.requestParameters;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.springframework.test.web.servlet.MockMvc;

View File

@@ -16,10 +16,17 @@
package org.springframework.restdocs;
import static org.springframework.restdocs.util.IterableEnumeration.iterable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.restdocs.operation.MockMvcOperationRequestFactory;
import org.springframework.restdocs.operation.MockMvcOperationResponseFactory;
import org.springframework.restdocs.operation.StandardOperation;
import org.springframework.restdocs.snippet.Snippet;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultHandler;
@@ -44,14 +51,24 @@ public class RestDocumentationResultHandler implements ResultHandler {
@Override
public void handle(MvcResult result) throws Exception {
Map<String, Object> attributes = new HashMap<>();
for (String name : iterable(result.getRequest().getAttributeNames())) {
attributes.put(name, result.getRequest().getAttribute(name));
}
StandardOperation operation = new StandardOperation(this.identifier,
new MockMvcOperationRequestFactory().createOperationRequest(result
.getRequest()),
new MockMvcOperationResponseFactory().createOperationResponse(result
.getResponse()), attributes);
for (Snippet snippet : getSnippets(result)) {
snippet.document(this.identifier, result);
snippet.document(operation);
}
}
@SuppressWarnings("unchecked")
private List<Snippet> getSnippets(MvcResult result) {
List<Snippet> combinedSnippets = new ArrayList<>((List<Snippet>) result.getRequest()
List<Snippet> combinedSnippets = new ArrayList<>((List<Snippet>) result
.getRequest()
.getAttribute("org.springframework.restdocs.defaultSnippets"));
combinedSnippets.addAll(this.snippets);
return combinedSnippets;

View File

@@ -17,7 +17,6 @@
package org.springframework.restdocs.config;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.util.StringUtils;
/**
* A configurer that can be used to configure the documented URIs
@@ -44,20 +43,12 @@ public class UriConfigurer extends AbstractNestedConfigurer<RestDocumentationCon
*/
public static final int DEFAULT_PORT = 8080;
/**
* The default context path for documented URIs
* @see #withContextPath(String)
*/
public static final String DEFAULT_CONTEXT_PATH = "";
private String scheme = DEFAULT_SCHEME;
private String host = DEFAULT_HOST;
private int port = DEFAULT_PORT;
private String contextPath = DEFAULT_CONTEXT_PATH;
protected UriConfigurer(RestDocumentationConfigurer parent) {
super(parent);
}
@@ -98,25 +89,11 @@ public class UriConfigurer extends AbstractNestedConfigurer<RestDocumentationCon
return this;
}
/**
* Configures any documented URIs to use the given {@code contextPath}. The default is
* an empty string.
*
* @param contextPath The context path
* @return {@code this}
*/
public UriConfigurer withContextPath(String contextPath) {
this.contextPath = (StringUtils.hasText(contextPath) && !contextPath
.startsWith("/")) ? "/" + contextPath : contextPath;
return this;
}
@Override
void apply(MockHttpServletRequest request) {
request.setScheme(this.scheme);
request.setServerPort(this.port);
request.setServerName(this.host);
request.setContextPath(this.contextPath);
}
}

View File

@@ -24,12 +24,13 @@ import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.springframework.restdocs.snippet.DocumentableHttpServletRequest;
import org.springframework.http.HttpMethod;
import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.operation.OperationRequest;
import org.springframework.restdocs.operation.OperationRequestPart;
import org.springframework.restdocs.snippet.Snippet;
import org.springframework.restdocs.snippet.TemplatedSnippet;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
/**
* A {@link Snippet} that documents the curl command for a request.
@@ -38,14 +39,6 @@ import org.springframework.web.multipart.MultipartFile;
*/
class CurlRequestSnippet extends TemplatedSnippet {
private static final String SCHEME_HTTP = "http";
private static final String SCHEME_HTTPS = "https";
private static final int STANDARD_PORT_HTTP = 80;
private static final int STANDARD_PORT_HTTPS = 443;
CurlRequestSnippet() {
this(null);
}
@@ -55,71 +48,40 @@ class CurlRequestSnippet extends TemplatedSnippet {
}
@Override
public Map<String, Object> document(MvcResult result) throws IOException {
public Map<String, Object> createModel(Operation operation) throws IOException {
Map<String, Object> model = new HashMap<String, Object>();
model.put("arguments", getCurlCommandArguments(result));
model.put("arguments", getCurlCommandArguments(operation));
return model;
}
private String getCurlCommandArguments(MvcResult result) throws IOException {
private String getCurlCommandArguments(Operation operation) throws IOException {
StringWriter command = new StringWriter();
PrintWriter printer = new PrintWriter(command);
DocumentableHttpServletRequest request = new DocumentableHttpServletRequest(
result.getRequest());
printer.print("'");
writeAuthority(request, printer);
writePathAndQueryString(request, printer);
printer.print(operation.getRequest().getUri());
printer.print("'");
writeOptionToIncludeHeadersInOutput(printer);
writeHttpMethodIfNecessary(request, printer);
writeHeaders(request, printer);
writeHttpMethodIfNecessary(operation.getRequest(), printer);
writeHeaders(operation.getRequest(), printer);
writePartsIfNecessary(operation.getRequest(), printer);
if (request.isMultipartRequest()) {
writeParts(request, printer);
}
writeContent(request, printer);
writeContent(operation.getRequest(), printer);
return command.toString();
}
private void writeAuthority(DocumentableHttpServletRequest request, PrintWriter writer) {
writer.print(String.format("%s://%s", request.getScheme(), request.getHost()));
if (isNonStandardPort(request)) {
writer.print(String.format(":%d", request.getPort()));
}
}
private boolean isNonStandardPort(DocumentableHttpServletRequest request) {
return (SCHEME_HTTP.equals(request.getScheme()) && request.getPort() != STANDARD_PORT_HTTP)
|| (SCHEME_HTTPS.equals(request.getScheme()) && request.getPort() != STANDARD_PORT_HTTPS);
}
private void writePathAndQueryString(DocumentableHttpServletRequest request,
PrintWriter writer) {
if (StringUtils.hasText(request.getContextPath())) {
writer.print(String.format(request.getContextPath().startsWith("/") ? "%s"
: "/%s", request.getContextPath()));
}
writer.print(request.getRequestUriWithQueryString());
}
private void writeOptionToIncludeHeadersInOutput(PrintWriter writer) {
writer.print(" -i");
}
private void writeHttpMethodIfNecessary(DocumentableHttpServletRequest request,
PrintWriter writer) {
if (!request.isGetRequest()) {
private void writeHttpMethodIfNecessary(OperationRequest request, PrintWriter writer) {
if (!HttpMethod.GET.equals(request.getMethod())) {
writer.print(String.format(" -X %s", request.getMethod()));
}
}
private void writeHeaders(DocumentableHttpServletRequest request, PrintWriter writer) {
private void writeHeaders(OperationRequest request, PrintWriter writer) {
for (Entry<String, List<String>> entry : request.getHeaders().entrySet()) {
for (String header : entry.getValue()) {
writer.print(String.format(" -H '%s: %s'", entry.getKey(), header));
@@ -127,45 +89,48 @@ class CurlRequestSnippet extends TemplatedSnippet {
}
}
private void writeParts(DocumentableHttpServletRequest request, PrintWriter writer)
private void writePartsIfNecessary(OperationRequest request, PrintWriter writer)
throws IOException {
for (Entry<String, List<MultipartFile>> entry : request.getMultipartFiles()
.entrySet()) {
for (MultipartFile file : entry.getValue()) {
writer.printf(" -F '%s=", file.getName());
if (!StringUtils.hasText(file.getOriginalFilename())) {
writer.append(new String(file.getBytes()));
}
else {
writer.printf("@%s", file.getOriginalFilename());
}
if (StringUtils.hasText(file.getContentType())) {
writer.append(";type=").append(file.getContentType());
}
writer.append("'");
for (OperationRequestPart part : request.getParts()) {
writer.printf(" -F '%s=", part.getName());
if (!StringUtils.hasText(part.getSubmittedFileName())) {
writer.append(new String(part.getContent()));
}
else {
writer.printf("@%s", part.getSubmittedFileName());
}
if (part.getHeaders().getContentType() != null) {
writer.append(";type=").append(
part.getHeaders().getContentType().toString());
}
}
writer.append("'");
}
}
private void writeContent(DocumentableHttpServletRequest request, PrintWriter writer)
private void writeContent(OperationRequest request, PrintWriter writer)
throws IOException {
if (request.getContentLength() > 0) {
writer.print(String.format(" -d '%s'", request.getContentAsString()));
if (request.getContent().length > 0) {
writer.print(String.format(" -d '%s'", new String(request.getContent())));
}
else if (request.isMultipartRequest()) {
for (Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
else if (!request.getParts().isEmpty()) {
for (Entry<String, List<String>> entry : request.getParameters().entrySet()) {
for (String value : entry.getValue()) {
writer.print(String.format(" -F '%s=%s'", entry.getKey(), value));
}
}
}
else if (request.isPostRequest() || request.isPutRequest()) {
String queryString = request.getParameterMapAsQueryString();
else if (isPutOrPost(request)) {
String queryString = request.getParameters().toQueryString();
if (StringUtils.hasText(queryString)) {
writer.print(String.format(" -d '%s'", queryString));
}
}
}
private boolean isPutOrPost(OperationRequest request) {
return HttpMethod.PUT.equals(request.getMethod())
|| HttpMethod.POST.equals(request.getMethod());
}
}

View File

@@ -26,13 +26,14 @@ import java.util.Map;
import java.util.Map.Entry;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.restdocs.snippet.DocumentableHttpServletRequest;
import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.operation.OperationRequest;
import org.springframework.restdocs.operation.OperationRequestPart;
import org.springframework.restdocs.snippet.Snippet;
import org.springframework.restdocs.snippet.TemplatedSnippet;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
/**
* A {@link Snippet} that documents an HTTP request.
@@ -52,27 +53,31 @@ class HttpRequestSnippet extends TemplatedSnippet {
}
@Override
public Map<String, Object> document(MvcResult result) throws IOException {
DocumentableHttpServletRequest request = new DocumentableHttpServletRequest(
result.getRequest());
public Map<String, Object> createModel(Operation operation) throws IOException {
Map<String, Object> model = new HashMap<String, Object>();
model.put("method", result.getRequest().getMethod());
model.put("path", request.getRequestUriWithQueryString());
model.put("headers", getHeaders(request));
model.put("requestBody", getRequestBody(request));
model.put("method", operation.getRequest().getMethod());
model.put(
"path",
operation.getRequest().getUri().getRawPath()
+ (StringUtils.hasText(operation.getRequest().getUri()
.getRawQuery()) ? "?"
+ operation.getRequest().getUri().getRawQuery()
: ""));
model.put("headers", getHeaders(operation.getRequest()));
model.put("requestBody", getRequestBody(operation.getRequest()));
return model;
}
private List<Map<String, String>> getHeaders(DocumentableHttpServletRequest request) {
private List<Map<String, String>> getHeaders(OperationRequest request) {
List<Map<String, String>> headers = new ArrayList<>();
if (requiresHostHeader(request)) {
headers.add(header(HttpHeaders.HOST, request.getHost()));
headers.add(header(HttpHeaders.HOST, request.getUri().getHost()));
}
for (Entry<String, List<String>> header : request.getHeaders().entrySet()) {
for (String value : header.getValue()) {
if (header.getKey() == HttpHeaders.CONTENT_TYPE
&& request.isMultipartRequest()) {
&& !request.getParts().isEmpty()) {
headers.add(header(header.getKey(),
String.format("%s; boundary=%s", value, MULTIPART_BOUNDARY)));
}
@@ -82,53 +87,54 @@ class HttpRequestSnippet extends TemplatedSnippet {
}
}
if (requiresFormEncodingContentType(request)) {
if (requiresFormEncodingContentTypeHeader(request)) {
headers.add(header(HttpHeaders.CONTENT_TYPE,
MediaType.APPLICATION_FORM_URLENCODED_VALUE));
}
return headers;
}
private String getRequestBody(DocumentableHttpServletRequest request)
throws IOException {
private String getRequestBody(OperationRequest request) throws IOException {
StringWriter httpRequest = new StringWriter();
PrintWriter writer = new PrintWriter(httpRequest);
if (request.getContentLength() > 0) {
if (request.getContent().length > 0) {
writer.println();
writer.print(request.getContentAsString());
writer.print(new String(request.getContent()));
}
else if (request.isPostRequest() || request.isPutRequest()) {
if (request.isMultipartRequest()) {
writeParts(request, writer);
}
else {
String queryString = request.getParameterMapAsQueryString();
else if (isPutOrPost(request)) {
if (request.getParts().isEmpty()) {
String queryString = request.getParameters().toQueryString();
if (StringUtils.hasText(queryString)) {
writer.println();
writer.print(queryString);
}
}
else {
writeParts(request, writer);
}
}
return httpRequest.toString();
}
private void writeParts(DocumentableHttpServletRequest request, PrintWriter writer)
private boolean isPutOrPost(OperationRequest request) {
return HttpMethod.PUT.equals(request.getMethod())
|| HttpMethod.POST.equals(request.getMethod());
}
private void writeParts(OperationRequest request, PrintWriter writer)
throws IOException {
writer.println();
for (Entry<String, String[]> parameter : request.getParameterMap().entrySet()) {
for (Entry<String, List<String>> parameter : request.getParameters().entrySet()) {
for (String value : parameter.getValue()) {
writePartBoundary(writer);
writePart(parameter.getKey(), value, null, writer);
writer.println();
}
}
for (Entry<String, List<MultipartFile>> entry : request.getMultipartFiles()
.entrySet()) {
for (MultipartFile file : entry.getValue()) {
writePartBoundary(writer);
writePart(file, writer);
writer.println();
}
for (OperationRequestPart part : request.getParts()) {
writePartBoundary(writer);
writePart(part, writer);
writer.println();
}
writeMultipartEnd(writer);
}
@@ -137,33 +143,33 @@ class HttpRequestSnippet extends TemplatedSnippet {
writer.printf("--%s%n", MULTIPART_BOUNDARY);
}
private void writePart(String name, String value, String contentType,
private void writePart(OperationRequestPart part, PrintWriter writer)
throws IOException {
writePart(part.getName(), new String(part.getContent()), part.getHeaders()
.getContentType(), writer);
}
private void writePart(String name, String value, MediaType contentType,
PrintWriter writer) {
writer.printf("Content-Disposition: form-data; name=%s%n", name);
if (StringUtils.hasText(contentType)) {
if (contentType != null) {
writer.printf("Content-Type: %s%n", contentType);
}
writer.println();
writer.print(value);
}
private void writePart(MultipartFile part, PrintWriter writer) throws IOException {
writePart(part.getName(), new String(part.getBytes()), part.getContentType(),
writer);
}
private void writeMultipartEnd(PrintWriter writer) {
writer.printf("--%s--", MULTIPART_BOUNDARY);
}
private boolean requiresHostHeader(DocumentableHttpServletRequest request) {
private boolean requiresHostHeader(OperationRequest request) {
return request.getHeaders().get(HttpHeaders.HOST) == null;
}
private boolean requiresFormEncodingContentType(DocumentableHttpServletRequest request) {
return request.getHeaders().getContentType() == null
&& (request.isPostRequest() || request.isPutRequest())
&& StringUtils.hasText(request.getParameterMapAsQueryString());
private boolean requiresFormEncodingContentTypeHeader(OperationRequest request) {
return request.getHeaders().get(HttpHeaders.CONTENT_TYPE) == null
&& isPutOrPost(request) && !request.getParameters().isEmpty();
}
private Map<String, String> header(String name, String value) {
@@ -172,4 +178,5 @@ class HttpRequestSnippet extends TemplatedSnippet {
header.put("value", value);
return header;
}
}

View File

@@ -21,12 +21,13 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.springframework.http.HttpStatus;
import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.operation.OperationResponse;
import org.springframework.restdocs.snippet.Snippet;
import org.springframework.restdocs.snippet.TemplatedSnippet;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.StringUtils;
/**
* A {@link Snippet} that documents an HTTP response.
@@ -44,24 +45,26 @@ class HttpResponseSnippet extends TemplatedSnippet {
}
@Override
public Map<String, Object> document(MvcResult result) throws IOException {
HttpStatus status = HttpStatus.valueOf(result.getResponse().getStatus());
public Map<String, Object> createModel(Operation operation) throws IOException {
OperationResponse response = operation.getResponse();
HttpStatus status = response.getStatus();
Map<String, Object> model = new HashMap<String, Object>();
model.put(
"responseBody",
StringUtils.hasLength(result.getResponse().getContentAsString()) ? String
.format("%n%s", result.getResponse().getContentAsString()) : "");
response.getContent().length > 0 ? String.format("%n%s", new String(
response.getContent())) : "");
model.put("statusCode", status.value());
model.put("statusReason", status.getReasonPhrase());
model.put("headers", headers(result));
model.put("headers", headers(response));
return model;
}
private List<Map<String, String>> headers(MvcResult result) {
private List<Map<String, String>> headers(OperationResponse response) {
List<Map<String, String>> headers = new ArrayList<>();
for (String headerName : result.getResponse().getHeaderNames()) {
for (String header : result.getResponse().getHeaders(headerName)) {
headers.add(header(headerName, header));
for (Entry<String, List<String>> header : response.getHeaders().entrySet()) {
List<String> values = header.getValue();
for (String value : values) {
headers.add(header(header.getKey(), value));
}
}
return headers;

View File

@@ -20,7 +20,7 @@ import java.io.IOException;
import java.util.List;
import java.util.Map;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.restdocs.operation.OperationResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -35,10 +35,10 @@ abstract class AbstractJsonLinkExtractor implements LinkExtractor {
@Override
@SuppressWarnings("unchecked")
public Map<String, List<Link>> extractLinks(MockHttpServletResponse response)
public Map<String, List<Link>> extractLinks(OperationResponse response)
throws IOException {
Map<String, Object> jsonContent = this.objectMapper.readValue(
response.getContentAsString(), Map.class);
new String(response.getContent()), Map.class);
return extractLinks(jsonContent);
}

View File

@@ -7,8 +7,7 @@ import java.util.Map;
import java.util.Map.Entry;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.util.StringUtils;
import org.springframework.restdocs.operation.OperationResponse;
/**
* {@link LinkExtractor} that delegates to other link extractors based on the response's
@@ -31,9 +30,9 @@ class ContentTypeLinkExtractor implements LinkExtractor {
}
@Override
public Map<String, List<Link>> extractLinks(MockHttpServletResponse response)
public Map<String, List<Link>> extractLinks(OperationResponse response)
throws IOException {
String contentType = response.getContentType();
MediaType contentType = response.getHeaders().getContentType();
LinkExtractor extractorForContentType = getExtractorForContentType(contentType);
if (extractorForContentType != null) {
return extractorForContentType.extractLinks(response);
@@ -43,11 +42,10 @@ class ContentTypeLinkExtractor implements LinkExtractor {
+ "content type " + contentType);
}
private LinkExtractor getExtractorForContentType(String contentType) {
if (StringUtils.hasText(contentType)) {
MediaType mediaType = MediaType.parseMediaType(contentType);
private LinkExtractor getExtractorForContentType(MediaType contentType) {
if (contentType != null) {
for (Entry<MediaType, LinkExtractor> entry : this.linkExtractors.entrySet()) {
if (mediaType.isCompatibleWith(entry.getKey())) {
if (contentType.isCompatibleWith(entry.getKey())) {
return entry.getValue();
}
}

View File

@@ -20,7 +20,7 @@ import java.io.IOException;
import java.util.List;
import java.util.Map;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.restdocs.operation.OperationResponse;
/**
* A {@code LinkExtractor} is used to extract {@link Link links} from a JSON response. The
@@ -32,14 +32,13 @@ import org.springframework.mock.web.MockHttpServletResponse;
public interface LinkExtractor {
/**
* Extract the links from the given response, returning a {@code Map} of links where
* the keys are the link rels.
* Extract the links from the given {@code response}, returning a {@code Map} of links
* where the keys are the link rels.
*
* @param response The response from which the links are to be extracted
* @return The extracted links, keyed by rel
* @throws IOException if link extraction fails
*/
Map<String, List<Link>> extractLinks(MockHttpServletResponse response)
throws IOException;
Map<String, List<Link>> extractLinks(OperationResponse response) throws IOException;
}

View File

@@ -26,10 +26,11 @@ import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.operation.OperationResponse;
import org.springframework.restdocs.snippet.Snippet;
import org.springframework.restdocs.snippet.SnippetException;
import org.springframework.restdocs.snippet.TemplatedSnippet;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.Assert;
/**
@@ -64,8 +65,9 @@ class LinksSnippet extends TemplatedSnippet {
}
@Override
protected Map<String, Object> document(MvcResult result) throws IOException {
validate(this.linkExtractor.extractLinks(result.getResponse()));
protected Map<String, Object> createModel(Operation operation) throws IOException {
OperationResponse response = operation.getResponse();
validate(this.linkExtractor.extractLinks(response));
Map<String, Object> model = new HashMap<>();
model.put("links", createLinksModel());
return model;

View File

@@ -0,0 +1,162 @@
/*
* Copyright 2014-2015 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
*
* http://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.restdocs.operation;
import static org.springframework.restdocs.util.IterableEnumeration.iterable;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map.Entry;
import javax.servlet.ServletException;
import javax.servlet.http.Part;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockMultipartHttpServletRequest;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
/**
* A factory for creating an {@link OperationRequest} from a
* {@link MockHttpServletRequest}.
*
* @author Andy Wilkinson
*
*/
public class MockMvcOperationRequestFactory {
private static final String SCHEME_HTTP = "http";
private static final String SCHEME_HTTPS = "https";
private static final int STANDARD_PORT_HTTP = 80;
private static final int STANDARD_PORT_HTTPS = 443;
/**
* Creates a new {@code OperationRequest} derived from the given {@code mockRequest}.
*
* @param mockRequest the request
* @return the {@code OperationRequest}
* @throws Exception if the request could not be created
*/
public OperationRequest createOperationRequest(MockHttpServletRequest mockRequest)
throws Exception {
HttpHeaders headers = extractHeaders(mockRequest);
Parameters parameters = extractParameters(mockRequest);
List<OperationRequestPart> parts = extractParts(mockRequest);
String queryString = mockRequest.getQueryString();
if (!StringUtils.hasText(queryString) && "GET".equals(mockRequest.getMethod())) {
queryString = parameters.toQueryString();
}
return new StandardOperationRequest(URI.create(getRequestUri(mockRequest)
+ (StringUtils.hasText(queryString) ? "?" + queryString : "")),
HttpMethod.valueOf(mockRequest.getMethod()),
FileCopyUtils.copyToByteArray(mockRequest.getInputStream()), headers,
parameters, parts);
}
private List<OperationRequestPart> extractParts(MockHttpServletRequest servletRequest)
throws IOException, ServletException {
List<OperationRequestPart> parts = new ArrayList<>();
for (Part part : servletRequest.getParts()) {
HttpHeaders partHeaders = extractHeaders(part);
List<String> contentTypeHeader = partHeaders.get(HttpHeaders.CONTENT_TYPE);
if (part.getContentType() != null && contentTypeHeader == null) {
partHeaders
.setContentType(MediaType.parseMediaType(part.getContentType()));
}
parts.add(new StandardOperationRequestPart(part.getName(), StringUtils
.hasText(part.getSubmittedFileName()) ? part.getSubmittedFileName()
: null, FileCopyUtils.copyToByteArray(part.getInputStream()),
partHeaders));
}
if (servletRequest instanceof MockMultipartHttpServletRequest) {
for (Entry<String, List<MultipartFile>> entry : ((MockMultipartHttpServletRequest) servletRequest)
.getMultiFileMap().entrySet()) {
for (MultipartFile file : entry.getValue()) {
HttpHeaders partHeaders = new HttpHeaders();
if (StringUtils.hasText(file.getContentType())) {
partHeaders.setContentType(MediaType.parseMediaType(file
.getContentType()));
}
parts.add(new StandardOperationRequestPart(file.getName(),
StringUtils.hasText(file.getOriginalFilename()) ? file
.getOriginalFilename() : null, file.getBytes(),
partHeaders));
}
}
}
return parts;
}
private HttpHeaders extractHeaders(Part part) {
HttpHeaders partHeaders = new HttpHeaders();
for (String headerName : part.getHeaderNames()) {
for (String value : part.getHeaders(headerName)) {
partHeaders.add(headerName, value);
}
}
return partHeaders;
}
private Parameters extractParameters(MockHttpServletRequest servletRequest) {
Parameters parameters = new Parameters();
for (String name : iterable(servletRequest.getParameterNames())) {
for (String value : servletRequest.getParameterValues(name)) {
parameters.add(name, value);
}
}
return parameters;
}
private HttpHeaders extractHeaders(MockHttpServletRequest servletRequest) {
HttpHeaders headers = new HttpHeaders();
for (String headerName : iterable(servletRequest.getHeaderNames())) {
for (String value : iterable(servletRequest.getHeaders(headerName))) {
headers.add(headerName, value);
}
}
return headers;
}
private boolean isNonStandardPort(MockHttpServletRequest request) {
return (SCHEME_HTTP.equals(request.getScheme()) && request.getServerPort() != STANDARD_PORT_HTTP)
|| (SCHEME_HTTPS.equals(request.getScheme()) && request.getServerPort() != STANDARD_PORT_HTTPS);
}
private String getRequestUri(MockHttpServletRequest request) {
StringWriter uriWriter = new StringWriter();
PrintWriter printer = new PrintWriter(uriWriter);
printer.printf("%s://%s", request.getScheme(), request.getServerName());
if (isNonStandardPort(request)) {
printer.printf(":%d", request.getServerPort());
}
printer.print(request.getRequestURI());
return uriWriter.toString();
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2014-2015 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
*
* http://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.restdocs.operation;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockHttpServletResponse;
/**
* A factory for creating an {@link OperationResponse} derived from a
* {@link MockHttpServletResponse}.
*
* @author Andy Wilkinson
*/
public class MockMvcOperationResponseFactory {
/**
* Create a new {@code OperationResponse} derived from the given {@code mockResponse}.
*
* @param mockResponse the response
* @return the {@code OperationResponse}
*/
public OperationResponse createOperationResponse(MockHttpServletResponse mockResponse) {
return new StandardOperationResponse(
HttpStatus.valueOf(mockResponse.getStatus()),
extractHeaders(mockResponse), mockResponse.getContentAsByteArray());
}
private HttpHeaders extractHeaders(MockHttpServletResponse response) {
HttpHeaders headers = new HttpHeaders();
for (String headerName : response.getHeaderNames()) {
for (String value : response.getHeaders(headerName)) {
headers.add(headerName, value);
}
}
return headers;
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2014-2015 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
*
* http://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.restdocs.operation;
import java.util.Map;
/**
* Describes an operation performed on a RESTful service.
*
* @author Andy Wilkinson
*/
public interface Operation {
/**
* Returns a {@code Map} of attributes associated with the operation.
*
* @return the attributes
*/
Map<String, Object> getAttributes();
/**
* Returns the name of the operation.
*
* @return the name
*/
String getName();
/**
* Returns the request that was sent.
*
* @return the request
*/
OperationRequest getRequest();
/**
* Returns the response that was received.
*
* @return the response
*/
OperationResponse getResponse();
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2014-2015 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
*
* http://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.restdocs.operation;
import java.net.URI;
import java.util.Collection;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
/**
* The request that was sent as part of performing an operation on a RESTful service.
*
* @author Andy Wilkinson
* @see Operation#getRequest()
*/
public interface OperationRequest {
/**
* Returns the contents of the request. If the request has no content an empty array
* is returned
*
* @return the contents, never {@code null}
*/
byte[] getContent();
/**
* Returns the headers that were included in the request.
*
* @return the headers
*/
HttpHeaders getHeaders();
/**
* Returns the HTTP method of the request
*
* @return the HTTP method
*/
HttpMethod getMethod();
/**
* Returns the request's parameters. For a {@code GET} request, the parameters are
* derived from the query string. For a {@code POST} request, the parameters are
* derived form the request's body.
*
* @return the parameters
*/
Parameters getParameters();
/**
* Returns the request's parts, provided that it is a multipart request. If not, then
* an empty {@link Collection} is returned.
*
* @return the parts
*/
Collection<OperationRequestPart> getParts();
/**
* Returns the request's URI.
*
* @return the URI
*/
URI getUri();
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2014-2015 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
*
* http://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.restdocs.operation;
import org.springframework.http.HttpHeaders;
/**
* A part of a multipart request
*
* @author awilkinson
* @see OperationRequest#getParts()
*/
public interface OperationRequestPart {
/**
* Returns the name of the part.
*
* @return the name
*/
String getName();
/**
* Returns the name of the file that is being uploaded in this part.
*
* @return the name of the file
*/
String getSubmittedFileName();
/**
* Returns the contents of the part.
*
* @return the contents
*/
byte[] getContent();
/**
* Returns the part's headers.
*
* @return the headers
*/
HttpHeaders getHeaders();
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2014-2015 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
*
* http://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.restdocs.operation;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
/**
* The response that was received as part of performing an operation on a RESTful service.
*
* @author Andy Wilkinson
* @see Operation
* @see Operation#getRequest()
*/
public interface OperationResponse {
/**
* Returns the status of the response.
*
* @return the status
*/
HttpStatus getStatus();
/**
* Returns the headers in the response.
*
* @return the headers
*/
HttpHeaders getHeaders();
/**
* Returns the contents of the response. If the response has no content an empty array
* is returned.
*
* @return the contents, never {@code null}
*/
byte[] getContent();
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2014-2015 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
*
* http://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.restdocs.operation;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.List;
import java.util.Map;
import org.springframework.util.LinkedMultiValueMap;
/**
* The parameters received in a request
*
* @author Andy Wilkinson
*/
@SuppressWarnings("serial")
public class Parameters extends LinkedMultiValueMap<String, String> {
/**
* Converts the parameters to a query string suitable for use in a URI or the body of
* a form-encoded request
*
* @return the query string
*/
public String toQueryString() {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, List<String>> entry : entrySet()) {
for (String value : entry.getValue()) {
if (sb.length() > 0) {
sb.append("&");
}
sb.append(urlEncodeUTF8(entry.getKey())).append('=')
.append(urlEncodeUTF8(value));
}
}
return sb.toString();
}
private static String urlEncodeUTF8(String s) {
try {
return URLEncoder.encode(s, "UTF-8");
}
catch (UnsupportedEncodingException ex) {
throw new IllegalStateException("Unable to URL encode " + s + " using UTF-8",
ex);
}
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2014-2015 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
*
* http://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.restdocs.operation;
import java.util.Map;
/**
* Standard implementation of {@link Operation}
*
* @author Andy Wilkinson
*/
public class StandardOperation implements Operation {
private final String name;
private final OperationRequest request;
private final OperationResponse response;
private final Map<String, Object> attributes;
/**
* Creates a new {@code StandardOperation}
*
* @param name the name of the operation
* @param request the request that was sent
* @param response the response that was received
* @param attributes attributes to associate with the operation
*/
public StandardOperation(String name, OperationRequest request,
OperationResponse response, Map<String, Object> attributes) {
this.name = name;
this.request = request;
this.response = response;
this.attributes = attributes;
}
@Override
public Map<String, Object> getAttributes() {
return this.attributes;
}
@Override
public String getName() {
return this.name;
}
@Override
public OperationRequest getRequest() {
return this.request;
}
@Override
public OperationResponse getResponse() {
return this.response;
}
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright 2014-2015 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
*
* http://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.restdocs.operation;
import java.net.URI;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
/**
* Standard implementation of {@link OperationRequest}.
*
* @author Andy Wilkinson
*/
public class StandardOperationRequest implements OperationRequest {
private byte[] content;
private HttpHeaders headers;
private HttpMethod method;
private Parameters parameters;
private Collection<OperationRequestPart> parts;
private URI uri;
/**
* Creates a new request with the given {@code uri} and {@code method}. The request
* will have the given {@code headers}, {@code parameters}, and {@code parts}.
*
* @param uri the uri
* @param method the method
* @param content the content
* @param headers the headers
* @param parameters the parameters
* @param parts the parts
*/
public StandardOperationRequest(URI uri, HttpMethod method, byte[] content,
HttpHeaders headers, Parameters parameters,
Collection<OperationRequestPart> parts) {
this.uri = uri;
this.method = method;
this.content = content == null ? new byte[0] : content;
this.headers = headers;
this.parameters = parameters;
this.parts = parts;
}
@Override
public byte[] getContent() {
return Arrays.copyOf(this.content, this.content.length);
}
@Override
public HttpHeaders getHeaders() {
return this.headers;
}
@Override
public HttpMethod getMethod() {
return this.method;
}
@Override
public Parameters getParameters() {
return this.parameters;
}
@Override
public Collection<OperationRequestPart> getParts() {
return Collections.unmodifiableCollection(this.parts);
}
@Override
public URI getUri() {
return this.uri;
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2014-2015 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
*
* http://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.restdocs.operation;
import org.springframework.http.HttpHeaders;
/**
* Standard implementation of {@code OperationRequestPart}.
*
* @author Andy Wilkinson
*/
public class StandardOperationRequestPart implements OperationRequestPart {
private final String name;
private final String submittedFileName;
private final byte[] content;
private final HttpHeaders headers;
/**
* Creates a new {@code StandardOperationRequestPart} with the given {@code name}.
*
* @param name the name of the part
* @param submittedFileName the name of the file being uploaded by this part
* @param content the contents of the part
* @param headers the headers of the part
*/
public StandardOperationRequestPart(String name, String submittedFileName,
byte[] content, HttpHeaders headers) {
this.name = name;
this.submittedFileName = submittedFileName;
this.content = content;
this.headers = headers;
}
@Override
public String getName() {
return this.name;
}
@Override
public String getSubmittedFileName() {
return this.submittedFileName;
}
@Override
public byte[] getContent() {
return this.content;
}
@Override
public HttpHeaders getHeaders() {
return this.headers;
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2014-2015 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
*
* http://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.restdocs.operation;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
/**
* Standard implementation of {@link OperationResponse}.
*
* @author Andy Wilkinson
*/
public class StandardOperationResponse implements OperationResponse {
private final HttpStatus status;
private final HttpHeaders headers;
private final byte[] content;
/**
* Creates a new response with the given {@code status}, {@code headers}, and
* {@code content}.
*
* @param status the status of the response
* @param headers the headers of the response
* @param content the content of the response
*/
public StandardOperationResponse(HttpStatus status, HttpHeaders headers,
byte[] content) {
this.status = status;
this.headers = headers;
this.content = content == null ? new byte[0] : content;
}
@Override
public HttpStatus getStatus() {
return this.status;
}
@Override
public HttpHeaders getHeaders() {
return this.headers;
}
@Override
public byte[] getContent() {
return this.content;
}
}

View File

@@ -17,19 +17,16 @@
package org.springframework.restdocs.payload;
import java.io.IOException;
import java.io.Reader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.http.MediaType;
import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.snippet.SnippetException;
import org.springframework.restdocs.snippet.TemplatedSnippet;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;
/**
@@ -54,22 +51,22 @@ public abstract class AbstractFieldsSnippet extends TemplatedSnippet {
}
@Override
protected Map<String, Object> document(MvcResult result) throws IOException {
MediaType contentType = getContentType(result);
PayloadHandler payloadHandler;
protected Map<String, Object> createModel(Operation operation) throws IOException {
MediaType contentType = getContentType(operation);
ContentHandler contentHandler;
if (contentType != null
&& MediaType.APPLICATION_XML.isCompatibleWith(contentType)) {
payloadHandler = new XmlPayloadHandler(readPayload(result));
contentHandler = new XmlContentHandler(getContent(operation));
}
else {
payloadHandler = new JsonPayloadHandler(readPayload(result));
contentHandler = new JsonContentHandler(getContent(operation));
}
validateFieldDocumentation(payloadHandler);
validateFieldDocumentation(contentHandler);
for (FieldDescriptor descriptor : this.fieldDescriptors) {
if (descriptor.getType() == null) {
descriptor.type(payloadHandler.determineFieldType(descriptor.getPath()));
descriptor.type(contentHandler.determineFieldType(descriptor.getPath()));
}
}
@@ -82,17 +79,11 @@ public abstract class AbstractFieldsSnippet extends TemplatedSnippet {
return model;
}
private String readPayload(MvcResult result) throws IOException {
StringWriter writer = new StringWriter();
FileCopyUtils.copy(getPayloadReader(result), writer);
return writer.toString();
}
private void validateFieldDocumentation(PayloadHandler payloadHandler) {
private void validateFieldDocumentation(ContentHandler payloadHandler) {
List<FieldDescriptor> missingFields = payloadHandler
.findMissingFields(this.fieldDescriptors);
String undocumentedPayload = payloadHandler
.getUndocumentedPayload(this.fieldDescriptors);
.getUndocumentedContent(this.fieldDescriptors);
if (!missingFields.isEmpty() || StringUtils.hasText(undocumentedPayload)) {
String message = "";
@@ -115,8 +106,8 @@ public abstract class AbstractFieldsSnippet extends TemplatedSnippet {
}
}
protected abstract MediaType getContentType(MvcResult result);
protected abstract MediaType getContentType(Operation operation);
protected abstract Reader getPayloadReader(MvcResult result) throws IOException;
protected abstract byte[] getContent(Operation operation) throws IOException;
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2014-2015 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
*
* http://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.restdocs.payload;
import java.util.List;
/**
* A handler for the content of a request or response
*
* @author Andy Wilkinson
*/
interface ContentHandler {
/**
* Finds the fields that are missing from the handler's payload. A field is missing if
* it is described by one of the {@code fieldDescriptors} but is not present in the
* payload.
*
* @param fieldDescriptors the descriptors
* @return descriptors for the fields that are missing from the payload
* @throws PayloadHandlingException if a failure occurs
*/
List<FieldDescriptor> findMissingFields(List<FieldDescriptor> fieldDescriptors);
/**
* Returns modified content, formatted as a String, that only contains the fields that
* are undocumented. A field is undocumented if it is present in the handler's content
* but is not described by the given {@code fieldDescriptors}. If the content is
* completely documented, {@code null} is returned
*
* @param fieldDescriptors the descriptors
* @return the undocumented content, or {@code null} if all of the content is
* documented
* @throws PayloadHandlingException if a failure occurs
*/
String getUndocumentedContent(List<FieldDescriptor> fieldDescriptors);
/**
* Returns the type of the field with the given {@code path} based on the content of
* the payload.
*
* @param path the field path
* @return the type of the field
*/
Object determineFieldType(String path);
}

View File

@@ -10,27 +10,27 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
/**
* A {@link PayloadHandler} for JSON payloads
* A {@link ContentHandler} for JSON content
*
* @author Andy Wilkinson
*/
class JsonPayloadHandler implements PayloadHandler {
class JsonContentHandler implements ContentHandler {
private final JsonFieldProcessor fieldProcessor = new JsonFieldProcessor();
private final ObjectMapper objectMapper = new ObjectMapper()
.enable(SerializationFeature.INDENT_OUTPUT);
private final String rawPayload;
private final byte[] rawContent;
JsonPayloadHandler(String payload) throws IOException {
this.rawPayload = payload;
JsonContentHandler(byte[] content) throws IOException {
this.rawContent = content;
}
@Override
public List<FieldDescriptor> findMissingFields(List<FieldDescriptor> fieldDescriptors) {
List<FieldDescriptor> missingFields = new ArrayList<FieldDescriptor>();
Object payload = readPayload();
Object payload = readContent();
for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
if (!fieldDescriptor.isOptional()
&& !this.fieldProcessor.hasField(
@@ -43,15 +43,15 @@ class JsonPayloadHandler implements PayloadHandler {
}
@Override
public String getUndocumentedPayload(List<FieldDescriptor> fieldDescriptors) {
Object payload = readPayload();
public String getUndocumentedContent(List<FieldDescriptor> fieldDescriptors) {
Object content = readContent();
for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
JsonFieldPath path = JsonFieldPath.compile(fieldDescriptor.getPath());
this.fieldProcessor.remove(path, payload);
this.fieldProcessor.remove(path, content);
}
if (!isEmpty(payload)) {
if (!isEmpty(content)) {
try {
return this.objectMapper.writeValueAsString(payload);
return this.objectMapper.writeValueAsString(content);
}
catch (JsonProcessingException ex) {
throw new PayloadHandlingException(ex);
@@ -60,9 +60,9 @@ class JsonPayloadHandler implements PayloadHandler {
return null;
}
private Object readPayload() {
private Object readContent() {
try {
return new ObjectMapper().readValue(this.rawPayload, Object.class);
return new ObjectMapper().readValue(this.rawContent, Object.class);
}
catch (IOException ex) {
throw new PayloadHandlingException(ex);
@@ -79,7 +79,7 @@ class JsonPayloadHandler implements PayloadHandler {
@Override
public Object determineFieldType(String path) {
try {
return new JsonFieldTypeResolver().resolveFieldType(path, readPayload());
return new JsonFieldTypeResolver().resolveFieldType(path, readContent());
}
catch (FieldDoesNotExistException ex) {
String message = "Cannot determine the type of the field '" + path + "' as"

View File

@@ -1,45 +0,0 @@
package org.springframework.restdocs.payload;
import java.util.List;
/**
* A handler for a request or response payload
*
* @author Andy Wilkinson
*/
interface PayloadHandler {
/**
* Finds the fields that are missing from the handler's payload. A field is missing if
* it is described by one of the {@code fieldDescriptors} but is not present in the
* payload.
*
* @param fieldDescriptors the descriptors
* @return descriptors for the fields that are missing from the payload
* @throws PayloadHandlingException if a failure occurs
*/
List<FieldDescriptor> findMissingFields(List<FieldDescriptor> fieldDescriptors);
/**
* Returns a modified payload, formatted as a String, that only contains the fields
* that are undocumented. A field is undocumented if it is present in the handler's
* payload but is not described by the given {@code fieldDescriptors}. If the payload
* is completely documented, {@code null} is returned
*
* @param fieldDescriptors the descriptors
* @return the undocumented payload, or {@code null} if all of the payload is
* documented
* @throws PayloadHandlingException if a failure occurs
*/
String getUndocumentedPayload(List<FieldDescriptor> fieldDescriptors);
/**
* Returns the type of the field with the given {@code path} based on the content of
* the payload.
*
* @param path the field path
* @return the type of the field
*/
Object determineFieldType(String path);
}

View File

@@ -13,16 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.restdocs.payload;
import java.io.IOException;
import java.io.Reader;
import java.util.List;
import java.util.Map;
import org.springframework.http.MediaType;
import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.snippet.Snippet;
import org.springframework.test.web.servlet.MvcResult;
/**
* A {@link Snippet} that documents the fields in a request.
@@ -40,17 +40,13 @@ class RequestFieldsSnippet extends AbstractFieldsSnippet {
}
@Override
protected Reader getPayloadReader(MvcResult result) throws IOException {
return result.getRequest().getReader();
protected MediaType getContentType(Operation operation) {
return operation.getRequest().getHeaders().getContentType();
}
@Override
protected MediaType getContentType(MvcResult result) {
String contentType = result.getRequest().getContentType();
if (contentType != null) {
return MediaType.valueOf(contentType);
}
return null;
protected byte[] getContent(Operation operation) throws IOException {
return operation.getRequest().getContent();
}
}

View File

@@ -16,14 +16,12 @@
package org.springframework.restdocs.payload;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.List;
import java.util.Map;
import org.springframework.http.MediaType;
import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.snippet.Snippet;
import org.springframework.test.web.servlet.MvcResult;
/**
* A {@link Snippet} that documents the fields in a response.
@@ -42,17 +40,13 @@ class ResponseFieldsSnippet extends AbstractFieldsSnippet {
}
@Override
protected Reader getPayloadReader(MvcResult result) throws IOException {
return new StringReader(result.getResponse().getContentAsString());
protected MediaType getContentType(Operation operation) {
return operation.getResponse().getHeaders().getContentType();
}
@Override
protected MediaType getContentType(MvcResult result) {
String contentType = result.getResponse().getContentType();
if (contentType != null) {
return MediaType.valueOf(contentType);
}
return null;
protected byte[] getContent(Operation operation) throws IOException {
return operation.getResponse().getContent();
}
}

View File

@@ -1,6 +1,22 @@
/*
* Copyright 2014-2015 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
*
* http://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.restdocs.payload;
import java.io.StringReader;
import java.io.ByteArrayInputStream;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
@@ -24,17 +40,17 @@ import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
/**
* A {@link PayloadHandler} for XML payloads
* A {@link ContentHandler} for XML content
*
* @author Andy Wilkinson
*/
class XmlPayloadHandler implements PayloadHandler {
class XmlContentHandler implements ContentHandler {
private final DocumentBuilder documentBuilder;
private final String rawPayload;
private final byte[] rawContent;
XmlPayloadHandler(String rawPayload) {
XmlContentHandler(byte[] rawContent) {
try {
this.documentBuilder = DocumentBuilderFactory.newInstance()
.newDocumentBuilder();
@@ -42,7 +58,7 @@ class XmlPayloadHandler implements PayloadHandler {
catch (ParserConfigurationException ex) {
throw new IllegalStateException("Failed to create document builder", ex);
}
this.rawPayload = rawPayload;
this.rawContent = rawContent;
}
@Override
@@ -73,8 +89,8 @@ class XmlPayloadHandler implements PayloadHandler {
private Document readPayload() {
try {
return this.documentBuilder.parse(new InputSource(new StringReader(
this.rawPayload)));
return this.documentBuilder.parse(new InputSource(new ByteArrayInputStream(
this.rawContent)));
}
catch (Exception ex) {
throw new PayloadHandlingException(ex);
@@ -86,7 +102,7 @@ class XmlPayloadHandler implements PayloadHandler {
}
@Override
public String getUndocumentedPayload(List<FieldDescriptor> fieldDescriptors) {
public String getUndocumentedContent(List<FieldDescriptor> fieldDescriptors) {
Document payload = readPayload();
for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
NodeList matchingNodes;

View File

@@ -1,3 +1,19 @@
/*
* Copyright 2014-2015 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
*
* http://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.restdocs.request;
import java.io.IOException;
@@ -10,12 +26,11 @@ import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.snippet.TemplatedSnippet;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.Assert;
abstract class AbstractParametersSnippet extends
TemplatedSnippet {
abstract class AbstractParametersSnippet extends TemplatedSnippet {
private final Map<String, ParameterDescriptor> descriptorsByName = new LinkedHashMap<>();
@@ -30,8 +45,8 @@ abstract class AbstractParametersSnippet extends
}
@Override
protected Map<String, Object> document(MvcResult result) throws IOException {
verifyParameterDescriptors(result);
protected Map<String, Object> createModel(Operation operation) throws IOException {
verifyParameterDescriptors(operation);
Map<String, Object> model = new HashMap<>();
List<Map<String, Object>> parameters = new ArrayList<>();
@@ -42,8 +57,8 @@ abstract class AbstractParametersSnippet extends
return model;
}
protected void verifyParameterDescriptors(MvcResult result) {
Set<String> actualParameters = extractActualParameters(result);
protected void verifyParameterDescriptors(Operation operation) {
Set<String> actualParameters = extractActualParameters(operation);
Set<String> expectedParameters = this.descriptorsByName.keySet();
Set<String> undocumentedParameters = new HashSet<String>(actualParameters);
undocumentedParameters.removeAll(expectedParameters);
@@ -58,7 +73,7 @@ abstract class AbstractParametersSnippet extends
}
}
protected abstract Set<String> extractActualParameters(MvcResult result);
protected abstract Set<String> extractActualParameters(Operation operation);
protected abstract void verificationFailed(Set<String> undocumentedParameters,
Set<String> missingParameters);

View File

@@ -24,9 +24,9 @@ import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.snippet.Snippet;
import org.springframework.restdocs.snippet.SnippetException;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.Assert;
/**
@@ -48,9 +48,9 @@ class PathParametersSnippet extends AbstractParametersSnippet {
}
@Override
protected Map<String, Object> document(MvcResult result) throws IOException {
Map<String, Object> model = super.document(result);
model.put("path", remoteQueryStringIfPresent(extractUrlTemplate(result)));
protected Map<String, Object> createModel(Operation operation) throws IOException {
Map<String, Object> model = super.createModel(operation);
model.put("path", remoteQueryStringIfPresent(extractUrlTemplate(operation)));
return model;
}
@@ -63,8 +63,8 @@ class PathParametersSnippet extends AbstractParametersSnippet {
}
@Override
protected Set<String> extractActualParameters(MvcResult result) {
String urlTemplate = extractUrlTemplate(result);
protected Set<String> extractActualParameters(Operation operation) {
String urlTemplate = extractUrlTemplate(operation);
Matcher matcher = NAMES_PATTERN.matcher(urlTemplate);
Set<String> actualParameters = new HashSet<>();
while (matcher.find()) {
@@ -74,8 +74,8 @@ class PathParametersSnippet extends AbstractParametersSnippet {
return actualParameters;
}
private String extractUrlTemplate(MvcResult result) {
String urlTemplate = (String) result.getRequest().getAttribute(
private String extractUrlTemplate(Operation operation) {
String urlTemplate = (String) operation.getAttributes().get(
"org.springframework.restdocs.urlTemplate");
Assert.notNull(urlTemplate,
"urlTemplate not found. Did you use RestDocumentationRequestBuilders to "

View File

@@ -22,9 +22,9 @@ import java.util.Set;
import javax.servlet.ServletRequest;
import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.snippet.Snippet;
import org.springframework.restdocs.snippet.SnippetException;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.web.bind.annotation.RequestParam;
/**
@@ -67,8 +67,8 @@ class RequestParametersSnippet extends AbstractParametersSnippet {
}
@Override
protected Set<String> extractActualParameters(MvcResult result) {
return result.getRequest().getParameterMap().keySet();
protected Set<String> extractActualParameters(Operation operation) {
return operation.getRequest().getParameters().keySet();
}
}

View File

@@ -81,9 +81,7 @@ public abstract class ContentModifyingReponsePostProcessor implements
return this.modifiedContent;
}
if (this.getContentAsByteArray.equals(method)) {
throw new UnsupportedOperationException(
"Following modification, the response's content should be"
+ " accessed as a String");
return this.modifiedContent.getBytes();
}
return method.invoke(this.delegate, args);
}

View File

@@ -1,276 +0,0 @@
/*
* Copyright 2014-2015 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
*
* http://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.restdocs.snippet;
import static org.springframework.restdocs.util.IterableEnumeration.iterable;
import java.io.IOException;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Map;
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
* to help in the documentation of the request.
*
* @author Andy Wilkinson
* @author Jonathan Pearlin
*/
public class DocumentableHttpServletRequest {
private final MockHttpServletRequest delegate;
/**
* Creates a new {@link DocumentableHttpServletRequest} to document the given
* {@code request}.
*
* @param request the request that is to be documented
*/
public DocumentableHttpServletRequest(MockHttpServletRequest request) {
this.delegate = request;
}
/**
* Whether or not this request is a {@code GET} request.
*
* @return {@code true} if it is a {@code GET} request, otherwise {@code false}
* @see HttpServletRequest#getMethod()
*/
public boolean isGetRequest() {
return RequestMethod.GET == RequestMethod.valueOf(this.delegate.getMethod());
}
/**
* Whether or not this request is a {@code POST} request.
*
* @return {@code true} if it is a {@code POST} request, otherwise {@code false}
* @see HttpServletRequest#getMethod()
*/
public boolean isPostRequest() {
return RequestMethod.POST == RequestMethod.valueOf(this.delegate.getMethod());
}
/**
* Whether or not this request is a {@code PUT} request.
*
* @return {@code true} if it is a {@code PUT} request, otherwise {@code false}
* @see HttpServletRequest#getMethod()
*/
public boolean isPutRequest() {
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
* {@link HttpServletRequest#getHeaders(String)}.
*
* @return the request's headers
* @see HttpServletRequest#getHeaderNames()
* @see HttpServletRequest#getHeaders(String)
*/
public HttpHeaders getHeaders() {
HttpHeaders httpHeaders = new HttpHeaders();
for (String headerName : iterable(this.delegate.getHeaderNames())) {
for (String header : iterable(this.delegate.getHeaders(headerName))) {
httpHeaders.add(headerName, header);
}
}
return httpHeaders;
}
/**
* Returns the request's scheme.
*
* @return the request's scheme
* @see HttpServletRequest#getScheme()
*/
public String getScheme() {
return this.delegate.getScheme();
}
/**
* Returns the name of the host to which the request was sent.
*
* @return the host's name
* @see HttpServletRequest#getServerName()
*/
public String getHost() {
return this.delegate.getServerName();
}
/**
* Returns the port to which the request was sent.
*
* @return the port
* @see HttpServletRequest#getServerPort()
*/
public int getPort() {
return this.delegate.getServerPort();
}
/**
* Returns the request's method.
*
* @return the request's method
* @see HttpServletRequest#getMethod()
*/
public String getMethod() {
return this.delegate.getMethod();
}
/**
* Returns the length of the request's content
*
* @return the content length
* @see HttpServletRequest#getContentLength()
*/
public long getContentLength() {
return this.delegate.getContentLengthLong();
}
/**
* Returns a {@code String} of the request's content
*
* @return the request's content
* @throws IOException if the content cannot be read
*/
public String getContentAsString() throws IOException {
StringWriter bodyWriter = new StringWriter();
FileCopyUtils.copy(this.delegate.getReader(), bodyWriter);
return bodyWriter.toString();
}
/**
* Returns the request's URI including its query string. The query string is
* determined by calling {@link HttpServletRequest#getQueryString()}. If it's
* {@code null} and it is a {@code GET} request, the query string is then constructed
* from the request's {@link HttpServletRequest#getParameterMap()} parameter map.
*
* @return the URI of the request, including its query string
*/
public String getRequestUriWithQueryString() {
StringBuilder sb = new StringBuilder();
sb.append(this.delegate.getRequestURI());
String queryString = getQueryString();
if (StringUtils.hasText(queryString)) {
sb.append('?').append(queryString);
}
return sb.toString();
}
/**
* Returns the request's parameter map formatted as a query string
*
* @return The query string derived from the request's parameter map
* @see HttpServletRequest#getParameterMap()
*/
public String getParameterMapAsQueryString() {
return toQueryString(this.delegate.getParameterMap());
}
/**
* Returns the request's context path
*
* @return The context path of the request
* @see HttpServletRequest#getContextPath()
*/
public String getContextPath() {
return this.delegate.getContextPath();
}
/**
* Returns a map of the request's parameters
* @return The map of parameters
* @see HttpServletRequest#getParameterMap()
*/
public Map<String, String[]> getParameterMap() {
return this.delegate.getParameterMap();
}
private String getQueryString() {
if (this.delegate.getQueryString() != null) {
return this.delegate.getQueryString();
}
if (isGetRequest()) {
return getParameterMapAsQueryString();
}
return null;
}
private static String toQueryString(Map<String, String[]> map) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String[]> entry : map.entrySet()) {
for (String value : entry.getValue()) {
if (sb.length() > 0) {
sb.append("&");
}
sb.append(urlEncodeUTF8(entry.getKey())).append('=')
.append(urlEncodeUTF8(value));
}
}
return sb.toString();
}
private static String urlEncodeUTF8(String s) {
try {
return URLEncoder.encode(s, "UTF-8");
}
catch (UnsupportedEncodingException ex) {
throw new IllegalStateException("Unable to URL encode " + s + " using UTF-8",
ex);
}
}
}

View File

@@ -18,7 +18,7 @@ package org.springframework.restdocs.snippet;
import java.io.IOException;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.restdocs.operation.Operation;
/**
* A {@link Snippet} is used to document aspects of a call to a RESTful API.
@@ -28,13 +28,11 @@ import org.springframework.test.web.servlet.MvcResult;
public interface Snippet {
/**
* Documents the call to the RESTful API described by the given {@code result}. The
* call is identified by the given {@code operation}.
* Documents the call to the RESTful API described by the given {@code operation}.
*
* @param operation the API operation
* @param result the result of the operation
* @throws IOException if a failure occurs will documenting the result
* @throws IOException if a failure occurs will documenting the operation
*/
void document(String operation, MvcResult result) throws IOException;
void document(Operation operation) throws IOException;
}

View File

@@ -21,9 +21,9 @@ import java.io.Writer;
import java.util.HashMap;
import java.util.Map;
import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.templates.Template;
import org.springframework.restdocs.templates.TemplateEngine;
import org.springframework.test.web.servlet.MvcResult;
/**
* Base class for a {@link Snippet} that is produced using a {@link Template} and
@@ -45,18 +45,20 @@ public abstract class TemplatedSnippet implements Snippet {
}
@Override
public void document(String operation, MvcResult result) throws IOException {
WriterResolver writerResolver = (WriterResolver) result.getRequest()
.getAttribute(WriterResolver.class.getName());
try (Writer writer = writerResolver.resolve(operation, this.snippetName)) {
Map<String, Object> model = document(result);
public void document(Operation operation) throws IOException {
WriterResolver writerResolver = (WriterResolver) operation.getAttributes().get(
WriterResolver.class.getName());
try (Writer writer = writerResolver
.resolve(operation.getName(), this.snippetName)) {
Map<String, Object> model = createModel(operation);
model.putAll(this.attributes);
TemplateEngine templateEngine = (TemplateEngine) result.getRequest()
.getAttribute(TemplateEngine.class.getName());
TemplateEngine templateEngine = (TemplateEngine) operation.getAttributes()
.get(TemplateEngine.class.getName());
writer.append(templateEngine.compileTemplate(this.snippetName).render(model));
}
}
protected abstract Map<String, Object> document(MvcResult result) throws IOException;
protected abstract Map<String, Object> createModel(Operation operation)
throws IOException;
}

View File

@@ -91,28 +91,6 @@ public class RestDocumentationConfigurerTests {
assertUriConfiguration("http", "localhost", 8081);
}
@Test
public void customContextPathWithoutSlash() {
String contextPath = "context-path";
RequestPostProcessor postProcessor = new RestDocumentationConfigurer().uris()
.withContextPath(contextPath).beforeMockMvcCreated(null, null);
postProcessor.postProcessRequest(this.request);
assertUriConfiguration("http", "localhost", 8080);
assertThat(this.request.getContextPath(), equalTo("/" + contextPath));
}
@Test
public void customContextPathWithSlash() {
String contextPath = "/context-path";
RequestPostProcessor postProcessor = new RestDocumentationConfigurer().uris()
.withContextPath(contextPath).beforeMockMvcCreated(null, null);
postProcessor.postProcessRequest(this.request);
assertUriConfiguration("http", "localhost", 8080);
assertThat(this.request.getContextPath(), equalTo(contextPath));
}
@Test
public void noContentLengthHeaderWhenRequestHasNotContent() {
RequestPostProcessor postProcessor = new RestDocumentationConfigurer().uris()

View File

@@ -19,14 +19,9 @@ package org.springframework.restdocs.curl;
import static org.hamcrest.CoreMatchers.containsString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.fileUpload;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.get;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.post;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.put;
import static org.springframework.restdocs.snippet.Attributes.attributes;
import static org.springframework.restdocs.snippet.Attributes.key;
import static org.springframework.restdocs.test.SnippetMatchers.codeBlock;
import static org.springframework.restdocs.test.StubMvcResult.result;
import java.io.IOException;
@@ -34,13 +29,14 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.restdocs.templates.TemplateEngine;
import org.springframework.restdocs.templates.TemplateResourceResolver;
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
import org.springframework.restdocs.test.ExpectedSnippet;
import org.springframework.restdocs.test.OperationBuilder;
/**
* Tests for {@link CurlRequestSnippet}
@@ -62,16 +58,16 @@ public class CurlRequestSnippetTests {
public void getRequest() throws IOException {
this.snippet.expectCurlRequest("get-request").withContents(
codeBlock("bash").content("$ curl 'http://localhost/foo' -i"));
new CurlRequestSnippet()
.document("get-request", result(get("/foo")));
new CurlRequestSnippet().document(new OperationBuilder("get-request").request(
"http://localhost/foo").build());
}
@Test
public void nonGetRequest() throws IOException {
this.snippet.expectCurlRequest("non-get-request").withContents(
codeBlock("bash").content("$ curl 'http://localhost/foo' -i -X POST"));
new CurlRequestSnippet().document("non-get-request",
result(post("/foo")));
new CurlRequestSnippet().document(new OperationBuilder("non-get-request")
.request("http://localhost/foo").method("POST").build());
}
@Test
@@ -79,8 +75,8 @@ public class CurlRequestSnippetTests {
this.snippet.expectCurlRequest("request-with-content").withContents(
codeBlock("bash")
.content("$ curl 'http://localhost/foo' -i -d 'content'"));
new CurlRequestSnippet().document("request-with-content",
result(get("/foo").content("content")));
new CurlRequestSnippet().document(new OperationBuilder("request-with-content")
.request("http://localhost/foo").content("content").build());
}
@Test
@@ -89,37 +85,9 @@ public class CurlRequestSnippetTests {
.withContents(
codeBlock("bash").content(
"$ curl 'http://localhost/foo?param=value' -i"));
new CurlRequestSnippet().document("request-with-query-string",
result(get("/foo?param=value")));
}
@Test
public void requestWithOneParameter() throws IOException {
this.snippet.expectCurlRequest("request-with-one-parameter").withContents(
codeBlock("bash").content("$ curl 'http://localhost/foo?k1=v1' -i"));
new CurlRequestSnippet().document("request-with-one-parameter",
result(get("/foo").param("k1", "v1")));
}
@Test
public void requestWithMultipleParameters() throws IOException {
this.snippet.expectCurlRequest("request-with-multiple-parameters").withContents(
codeBlock("bash").content(
"$ curl 'http://localhost/foo?k1=v1&k1=v1-bis&k2=v2' -i"));
new CurlRequestSnippet().document(
"request-with-multiple-parameters", result(get("/foo").param("k1", "v1")
.param("k2", "v2").param("k1", "v1-bis")));
}
@Test
public void requestWithUrlEncodedParameter() throws IOException {
this.snippet.expectCurlRequest("request-with-url-encoded-parameter")
.withContents(
codeBlock("bash").content(
"$ curl 'http://localhost/foo?k1=foo+bar%26' -i"));
new CurlRequestSnippet().document(
"request-with-url-encoded-parameter",
result(get("/foo").param("k1", "foo bar&")));
new CurlRequestSnippet().document(new OperationBuilder(
"request-with-query-string").request("http://localhost/foo?param=value")
.build());
}
@Test
@@ -127,8 +95,9 @@ public class CurlRequestSnippetTests {
this.snippet.expectCurlRequest("post-request-with-one-parameter").withContents(
codeBlock("bash").content(
"$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1'"));
new CurlRequestSnippet().document("post-request-with-one-parameter",
result(post("/foo").param("k1", "v1")));
new CurlRequestSnippet().document(new OperationBuilder(
"post-request-with-one-parameter").request("http://localhost/foo")
.method("POST").param("k1", "v1").build());
}
@Test
@@ -138,9 +107,9 @@ public class CurlRequestSnippetTests {
codeBlock("bash").content(
"$ curl 'http://localhost/foo' -i -X POST"
+ " -d 'k1=v1&k1=v1-bis&k2=v2'"));
new CurlRequestSnippet().document(
"post-request-with-multiple-parameters",
result(post("/foo").param("k1", "v1", "v1-bis").param("k2", "v2")));
new CurlRequestSnippet().document(new OperationBuilder(
"post-request-with-multiple-parameters").request("http://localhost/foo")
.method("POST").param("k1", "v1", "v1-bis").param("k2", "v2").build());
}
@Test
@@ -150,9 +119,10 @@ public class CurlRequestSnippetTests {
.withContents(
codeBlock("bash").content(
"$ curl 'http://localhost/foo' -i -X POST -d 'k1=a%26b'"));
new CurlRequestSnippet().document(
"post-request-with-url-encoded-parameter",
result(post("/foo").param("k1", "a&b")));
new CurlRequestSnippet().document(new OperationBuilder(
"post-request-with-url-encoded-parameter")
.request("http://localhost/foo").method("POST").param("k1", "a&b")
.build());
}
@Test
@@ -160,8 +130,9 @@ public class CurlRequestSnippetTests {
this.snippet.expectCurlRequest("put-request-with-one-parameter").withContents(
codeBlock("bash").content(
"$ curl 'http://localhost/foo' -i -X PUT -d 'k1=v1'"));
new CurlRequestSnippet().document("put-request-with-one-parameter",
result(put("/foo").param("k1", "v1")));
new CurlRequestSnippet().document(new OperationBuilder(
"put-request-with-one-parameter").request("http://localhost/foo")
.method("PUT").param("k1", "v1").build());
}
@Test
@@ -171,9 +142,10 @@ public class CurlRequestSnippetTests {
codeBlock("bash").content(
"$ curl 'http://localhost/foo' -i -X PUT"
+ " -d 'k1=v1&k1=v1-bis&k2=v2'"));
new CurlRequestSnippet().document(
"put-request-with-multiple-parameters",
result(put("/foo").param("k1", "v1", "v1-bis").param("k2", "v2")));
new CurlRequestSnippet().document(new OperationBuilder(
"put-request-with-multiple-parameters").request("http://localhost/foo")
.method("PUT").param("k1", "v1").param("k1", "v1-bis").param("k2", "v2")
.build());
}
@Test
@@ -182,9 +154,9 @@ public class CurlRequestSnippetTests {
.withContents(
codeBlock("bash").content(
"$ curl 'http://localhost/foo' -i -X PUT -d 'k1=a%26b'"));
new CurlRequestSnippet().document(
"put-request-with-url-encoded-parameter",
result(put("/foo").param("k1", "a&b")));
new CurlRequestSnippet().document(new OperationBuilder(
"put-request-with-url-encoded-parameter").request("http://localhost/foo")
.method("PUT").param("k1", "a&b").build());
}
@Test
@@ -193,92 +165,24 @@ public class CurlRequestSnippetTests {
codeBlock("bash").content(
"$ curl 'http://localhost/foo' -i"
+ " -H 'Content-Type: application/json' -H 'a: alpha'"));
new CurlRequestSnippet().document(
"request-with-headers",
result(get("/foo").contentType(MediaType.APPLICATION_JSON).header("a",
"alpha")));
new CurlRequestSnippet().document(new OperationBuilder("request-with-headers")
.request("http://localhost/foo")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.header("a", "alpha").build());
}
@Test
public void httpWithNonStandardPort() throws IOException {
this.snippet.expectCurlRequest("http-with-non-standard-port").withContents(
codeBlock("bash").content("$ curl 'http://localhost:8080/foo' -i"));
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
request.setServerPort(8080);
new CurlRequestSnippet().document("http-with-non-standard-port",
result(request));
}
@Test
public void httpsWithStandardPort() throws IOException {
this.snippet.expectCurlRequest("https-with-standard-port").withContents(
codeBlock("bash").content("$ curl 'https://localhost/foo' -i"));
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
request.setServerPort(443);
request.setScheme("https");
new CurlRequestSnippet().document("https-with-standard-port",
result(request));
}
@Test
public void httpsWithNonStandardPort() throws IOException {
this.snippet.expectCurlRequest("https-with-non-standard-port").withContents(
codeBlock("bash").content("$ curl 'https://localhost:8443/foo' -i"));
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
request.setServerPort(8443);
request.setScheme("https");
new CurlRequestSnippet().document("https-with-non-standard-port",
result(request));
}
@Test
public void requestWithCustomHost() throws IOException {
this.snippet.expectCurlRequest("request-with-custom-host").withContents(
codeBlock("bash").content("$ curl 'http://api.example.com/foo' -i"));
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
request.setServerName("api.example.com");
new CurlRequestSnippet().document("request-with-custom-host",
result(request));
}
@Test
public void requestWithContextPathWithSlash() throws IOException {
this.snippet.expectCurlRequest("request-with-custom-context-with-slash")
.withContents(
codeBlock("bash").content(
"$ curl 'http://api.example.com/v3/foo' -i"));
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
request.setServerName("api.example.com");
request.setContextPath("/v3");
new CurlRequestSnippet().document(
"request-with-custom-context-with-slash", result(request));
}
@Test
public void requestWithContextPathWithoutSlash() throws IOException {
this.snippet.expectCurlRequest("request-with-custom-context-without-slash")
.withContents(
codeBlock("bash").content(
"$ curl 'http://api.example.com/v3/foo' -i"));
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
request.setServerName("api.example.com");
request.setContextPath("v3");
new CurlRequestSnippet().document(
"request-with-custom-context-without-slash", result(request));
}
@Test
public void multipartPostWithNoOriginalFilename() throws IOException {
public void multipartPostWithNoSubmittedFileName() 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());
new CurlRequestSnippet().document(
"multipart-post-no-original-filename",
result(fileUpload("/upload").file(multipartFile)));
new CurlRequestSnippet().document(new OperationBuilder(
"multipart-post-no-original-filename").request("http://localhost/upload")
.method("POST")
.header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE)
.part("metadata", "{\"description\": \"foo\"}".getBytes()).build());
}
@Test
@@ -288,12 +192,13 @@ public class CurlRequestSnippetTests {
+ "'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());
new CurlRequestSnippet().document(
"multipart-post-with-content-type",
result(fileUpload("/upload").file(multipartFile)));
new CurlRequestSnippet().document(new OperationBuilder(
"multipart-post-with-content-type").request("http://localhost/upload")
.method("POST")
.header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE)
.part("image", new byte[0])
.header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE)
.submittedFileName("documents/images/example.png").build());
}
@Test
@@ -303,10 +208,11 @@ public class CurlRequestSnippetTests {
+ "'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());
new CurlRequestSnippet().document("multipart-post",
result(fileUpload("/upload").file(multipartFile)));
new CurlRequestSnippet().document(new OperationBuilder("multipart-post")
.request("http://localhost/upload").method("POST")
.header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE)
.part("image", new byte[0])
.submittedFileName("documents/images/example.png").build());
}
@Test
@@ -315,14 +221,15 @@ public class CurlRequestSnippetTests {
+ "'Content-Type: multipart/form-data' -F "
+ "'image=@documents/images/example.png' -F 'a=apple' -F 'a=avocado' "
+ "-F 'b=banana'";
this.snippet.expectCurlRequest("multipart-post").withContents(
this.snippet.expectCurlRequest("multipart-post-with-parameters").withContents(
codeBlock("bash").content(expectedContent));
MockMultipartFile multipartFile = new MockMultipartFile("image",
"documents/images/example.png", null, "bytes".getBytes());
new CurlRequestSnippet().document(
"multipart-post",
result(fileUpload("/upload").file(multipartFile)
.param("a", "apple", "avocado").param("b", "banana")));
new CurlRequestSnippet().document(new OperationBuilder(
"multipart-post-with-parameters").request("http://localhost/upload")
.method("POST")
.header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE)
.part("image", new byte[0])
.submittedFileName("documents/images/example.png").and()
.param("a", "apple", "avocado").param("b", "banana").build());
}
@Test
@@ -337,8 +244,11 @@ public class CurlRequestSnippetTests {
"src/test/resources/custom-snippet-templates/curl-request-with-title.snippet"));
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
new CurlRequestSnippet(attributes(key("title").value(
"curl request title"))).document("custom-attributes", result(request));
new CurlRequestSnippet(attributes(key("title").value("curl request title")))
.document(new OperationBuilder("custom-attributes")
.attribute(TemplateEngine.class.getName(),
new MustacheTemplateEngine(resolver))
.request("http://localhost/foo").build());
}
}

View File

@@ -19,14 +19,9 @@ package org.springframework.restdocs.http;
import static org.hamcrest.CoreMatchers.containsString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.fileUpload;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.get;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.post;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.put;
import static org.springframework.restdocs.snippet.Attributes.attributes;
import static org.springframework.restdocs.snippet.Attributes.key;
import static org.springframework.restdocs.test.SnippetMatchers.httpRequest;
import static org.springframework.restdocs.test.StubMvcResult.result;
import static org.springframework.web.bind.annotation.RequestMethod.GET;
import static org.springframework.web.bind.annotation.RequestMethod.POST;
import static org.springframework.web.bind.annotation.RequestMethod.PUT;
@@ -38,12 +33,11 @@ import org.junit.Test;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.restdocs.templates.TemplateEngine;
import org.springframework.restdocs.templates.TemplateResourceResolver;
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
import org.springframework.restdocs.test.ExpectedSnippet;
import org.springframework.restdocs.test.OperationBuilder;
/**
* Tests for {@link HttpRequestSnippet}
@@ -65,8 +59,8 @@ public class HttpRequestSnippetTests {
httpRequest(GET, "/foo").header(HttpHeaders.HOST, "localhost").header(
"Alpha", "a"));
new HttpRequestSnippet().document("get-request", result(get("/foo")
.header("Alpha", "a")));
new HttpRequestSnippet().document(new OperationBuilder("get-request")
.request("http://localhost/foo").header("Alpha", "a").build());
}
@Test
@@ -74,17 +68,9 @@ public class HttpRequestSnippetTests {
this.snippet.expectHttpRequest("get-request-with-query-string").withContents(
httpRequest(GET, "/foo?bar=baz").header(HttpHeaders.HOST, "localhost"));
new HttpRequestSnippet().document("get-request-with-query-string",
result(get("/foo?bar=baz")));
}
@Test
public void getRequestWithParameter() throws IOException {
this.snippet.expectHttpRequest("get-request-with-parameter").withContents(
httpRequest(GET, "/foo?b%26r=baz").header(HttpHeaders.HOST, "localhost"));
new HttpRequestSnippet().document("get-request-with-parameter",
result(get("/foo").param("b&r", "baz")));
new HttpRequestSnippet().document(new OperationBuilder(
"get-request-with-query-string").request("http://localhost/foo?bar=baz")
.build());
}
@Test
@@ -93,8 +79,9 @@ public class HttpRequestSnippetTests {
httpRequest(POST, "/foo").header(HttpHeaders.HOST, "localhost").content(
"Hello, world"));
new HttpRequestSnippet().document("post-request-with-content",
result(post("/foo").content("Hello, world")));
new HttpRequestSnippet().document(new OperationBuilder(
"post-request-with-content").request("http://localhost/foo")
.method("POST").content("Hello, world").build());
}
@Test
@@ -104,8 +91,9 @@ public class HttpRequestSnippetTests {
.header("Content-Type", "application/x-www-form-urlencoded")
.content("b%26r=baz&a=alpha"));
new HttpRequestSnippet().document("post-request-with-parameter",
result(post("/foo").param("b&r", "baz").param("a", "alpha")));
new HttpRequestSnippet().document(new OperationBuilder(
"post-request-with-parameter").request("http://localhost/foo")
.method("POST").param("b&r", "baz").param("a", "alpha").build());
}
@Test
@@ -114,8 +102,10 @@ public class HttpRequestSnippetTests {
httpRequest(PUT, "/foo").header(HttpHeaders.HOST, "localhost").content(
"Hello, world"));
new HttpRequestSnippet().document("put-request-with-content",
result(put("/foo").content("Hello, world")));
new HttpRequestSnippet()
.document(new OperationBuilder("put-request-with-content")
.request("http://localhost/foo").method("PUT")
.content("Hello, world").build());
}
@Test
@@ -125,8 +115,9 @@ public class HttpRequestSnippetTests {
.header("Content-Type", "application/x-www-form-urlencoded")
.content("b%26r=baz&a=alpha"));
new HttpRequestSnippet().document("put-request-with-parameter",
result(put("/foo").param("b&r", "baz").param("a", "alpha")));
new HttpRequestSnippet().document(new OperationBuilder(
"put-request-with-parameter").request("http://localhost/foo")
.method("PUT").param("b&r", "baz").param("a", "alpha").build());
}
@Test
@@ -139,10 +130,10 @@ public class HttpRequestSnippetTests {
.header("Content-Type",
"multipart/form-data; boundary=" + BOUNDARY)
.content(expectedContent));
MockMultipartFile multipartFile = new MockMultipartFile("image",
"documents/images/example.png", null, "<< data >>".getBytes());
new HttpRequestSnippet().document("multipart-post",
result(fileUpload("/upload").file(multipartFile)));
new HttpRequestSnippet().document(new OperationBuilder("multipart-post")
.request("http://localhost/upload").method("POST")
.header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE)
.part("image", "<< data >>".getBytes()).build());
}
@Test
@@ -156,18 +147,18 @@ public class HttpRequestSnippetTests {
String filePart = createPart(String.format("Content-Disposition: form-data; "
+ "name=image%n%n<< data >>"));
String expectedContent = param1Part + param2Part + param3Part + filePart;
this.snippet.expectHttpRequest("multipart-post").withContents(
this.snippet.expectHttpRequest("multipart-post-with-parameters").withContents(
httpRequest(POST, "/upload")
.header(HttpHeaders.HOST, "localhost")
.header("Content-Type",
"multipart/form-data; boundary=" + BOUNDARY)
.content(expectedContent));
MockMultipartFile multipartFile = new MockMultipartFile("image",
"documents/images/example.png", null, "<< data >>".getBytes());
new HttpRequestSnippet().document(
"multipart-post",
result(fileUpload("/upload").file(multipartFile)
.param("a", "apple", "avocado").param("b", "banana")));
new HttpRequestSnippet().document(new OperationBuilder(
"multipart-post-with-parameters").request("http://localhost/upload")
.method("POST")
.header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE)
.param("a", "apple", "avocado").param("b", "banana")
.part("image", "<< data >>".getBytes()).build());
}
@Test
@@ -181,50 +172,37 @@ public class HttpRequestSnippetTests {
.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());
new HttpRequestSnippet().document(
"multipart-post-with-content-type",
result(fileUpload("/upload").file(multipartFile)));
}
@Test
public void getRequestWithCustomServerName() throws IOException {
this.snippet.expectHttpRequest("get-request-custom-server-name").withContents(
httpRequest(GET, "/foo").header(HttpHeaders.HOST, "api.example.com"));
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
request.setServerName("api.example.com");
new HttpRequestSnippet().document("get-request-custom-server-name",
result(request));
new HttpRequestSnippet().document(new OperationBuilder(
"multipart-post-with-content-type").request("http://localhost/upload")
.method("POST")
.header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE)
.part("image", "<< data >>".getBytes())
.header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE).build());
}
@Test
public void getRequestWithCustomHost() throws IOException {
this.snippet.expectHttpRequest("get-request-custom-host").withContents(
httpRequest(GET, "/foo").header(HttpHeaders.HOST, "api.example.com"));
new HttpRequestSnippet().document("get-request-custom-host",
result(get("/foo").header(HttpHeaders.HOST, "api.example.com")));
new HttpRequestSnippet().document(new OperationBuilder("get-request-custom-host")
.request("http://localhost/foo")
.header(HttpHeaders.HOST, "api.example.com").build());
}
@Test
public void requestWithCustomSnippetAttributes() throws IOException {
this.snippet.expectHttpRequest("request-with-snippet-attributes").withContents(
containsString("Title for the request"));
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("http-request"))
.thenReturn(
new FileSystemResource(
"src/test/resources/custom-snippet-templates/http-request-with-title.snippet"));
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
new HttpRequestSnippet(attributes(key("title").value(
"Title for the request"))).document("request-with-snippet-attributes",
result(request));
new HttpRequestSnippet(attributes(key("title").value("Title for the request")))
.document(new OperationBuilder("request-with-snippet-attributes")
.attribute(TemplateEngine.class.getName(),
new MustacheTemplateEngine(resolver))
.request("http://localhost/foo").build());
}
private String createPart(String content) {

View File

@@ -24,20 +24,19 @@ import static org.springframework.http.HttpStatus.OK;
import static org.springframework.restdocs.snippet.Attributes.attributes;
import static org.springframework.restdocs.snippet.Attributes.key;
import static org.springframework.restdocs.test.SnippetMatchers.httpResponse;
import static org.springframework.restdocs.test.StubMvcResult.result;
import java.io.IOException;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.restdocs.templates.TemplateEngine;
import org.springframework.restdocs.templates.TemplateResourceResolver;
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
import org.springframework.restdocs.test.ExpectedSnippet;
import org.springframework.restdocs.test.OperationBuilder;
/**
* Tests for {@link HttpResponseSnippet}
@@ -53,18 +52,16 @@ public class HttpResponseSnippetTests {
@Test
public void basicResponse() throws IOException {
this.snippet.expectHttpResponse("basic-response").withContents(httpResponse(OK));
new HttpResponseSnippet().document("basic-response", result());
new HttpResponseSnippet()
.document(new OperationBuilder("basic-response").build());
}
@Test
public void nonOkResponse() throws IOException {
this.snippet.expectHttpResponse("non-ok-response").withContents(
httpResponse(BAD_REQUEST));
MockHttpServletResponse response = new MockHttpServletResponse();
response.setStatus(BAD_REQUEST.value());
new HttpResponseSnippet().document("non-ok-response",
result(response));
new HttpResponseSnippet().document(new OperationBuilder("non-ok-response")
.response().status(BAD_REQUEST.value()).build());
}
@Test
@@ -73,39 +70,33 @@ public class HttpResponseSnippetTests {
httpResponse(OK) //
.header("Content-Type", "application/json") //
.header("a", "alpha"));
MockHttpServletResponse response = new MockHttpServletResponse();
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setHeader("a", "alpha");
new HttpResponseSnippet().document("response-with-headers",
result(response));
new HttpResponseSnippet().document(new OperationBuilder("response-with-headers")
.response()
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.header("a", "alpha").build());
}
@Test
public void responseWithContent() throws IOException {
this.snippet.expectHttpResponse("response-with-content").withContents(
httpResponse(OK).content("content"));
MockHttpServletResponse response = new MockHttpServletResponse();
response.getWriter().append("content");
new HttpResponseSnippet().document("response-with-content",
result(response));
new HttpResponseSnippet().document(new OperationBuilder("response-with-content")
.response().content("content").build());
}
@Test
public void responseWithCustomSnippetAttributes() throws IOException {
this.snippet.expectHttpResponse("response-with-snippet-attributes").withContents(
containsString("Title for the response"));
MockHttpServletRequest request = new MockHttpServletRequest();
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("http-response"))
.thenReturn(
new FileSystemResource(
"src/test/resources/custom-snippet-templates/http-response-with-title.snippet"));
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
new HttpResponseSnippet(attributes(key("title").value(
"Title for the response"))).document("response-with-snippet-attributes",
result(request));
new HttpResponseSnippet(attributes(key("title").value("Title for the response")))
.document(new OperationBuilder("response-with-snippet-attributes")
.attribute(TemplateEngine.class.getName(),
new MustacheTemplateEngine(resolver)).build());
}
}

View File

@@ -26,8 +26,10 @@ import java.util.Map;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.restdocs.operation.StandardOperationResponse;
/**
* Tests for {@link ContentTypeLinkExtractor}.
@@ -39,12 +41,11 @@ public class ContentTypeLinkExtractorTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
private final MockHttpServletResponse response = new MockHttpServletResponse();
@Test
public void extractionFailsWithNullContentType() throws IOException {
this.thrown.expect(IllegalStateException.class);
new ContentTypeLinkExtractor().extractLinks(this.response);
new ContentTypeLinkExtractor().extractLinks(new StandardOperationResponse(
HttpStatus.OK, new HttpHeaders(), null));
}
@Test
@@ -52,9 +53,12 @@ public class ContentTypeLinkExtractorTests {
Map<MediaType, LinkExtractor> extractors = new HashMap<>();
LinkExtractor extractor = mock(LinkExtractor.class);
extractors.put(MediaType.APPLICATION_JSON, extractor);
this.response.setContentType("application/json");
new ContentTypeLinkExtractor(extractors).extractLinks(this.response);
verify(extractor).extractLinks(this.response);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
StandardOperationResponse response = new StandardOperationResponse(HttpStatus.OK,
httpHeaders, null);
new ContentTypeLinkExtractor(extractors).extractLinks(response);
verify(extractor).extractLinks(response);
}
@Test
@@ -62,9 +66,12 @@ public class ContentTypeLinkExtractorTests {
Map<MediaType, LinkExtractor> extractors = new HashMap<>();
LinkExtractor extractor = mock(LinkExtractor.class);
extractors.put(MediaType.APPLICATION_JSON, extractor);
this.response.setContentType("application/json;foo=bar");
new ContentTypeLinkExtractor(extractors).extractLinks(this.response);
verify(extractor).extractLinks(this.response);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.parseMediaType("application/json;foo=bar"));
StandardOperationResponse response = new StandardOperationResponse(HttpStatus.OK,
httpHeaders, null);
new ContentTypeLinkExtractor(extractors).extractLinks(response);
verify(extractor).extractLinks(response);
}
}

View File

@@ -19,7 +19,6 @@ package org.springframework.restdocs.hypermedia;
import static org.junit.Assert.assertEquals;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
@@ -31,7 +30,9 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.restdocs.operation.OperationResponse;
import org.springframework.restdocs.operation.StandardOperationResponse;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@@ -105,11 +106,9 @@ public class LinkExtractorsPayloadTests {
assertEquals(expectedLinksByRel, actualLinks);
}
private MockHttpServletResponse createResponse(String contentName) throws IOException {
MockHttpServletResponse response = new MockHttpServletResponse();
FileCopyUtils.copy(new FileReader(getPayloadFile(contentName)),
response.getWriter());
return response;
private OperationResponse createResponse(String contentName) throws IOException {
return new StandardOperationResponse(HttpStatus.OK, null,
FileCopyUtils.copyToByteArray(getPayloadFile(contentName)));
}
private File getPayloadFile(String name) {

View File

@@ -23,7 +23,6 @@ import static org.mockito.Mockito.when;
import static org.springframework.restdocs.snippet.Attributes.attributes;
import static org.springframework.restdocs.snippet.Attributes.key;
import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader;
import static org.springframework.restdocs.test.StubMvcResult.result;
import java.io.IOException;
import java.util.Arrays;
@@ -34,12 +33,13 @@ import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.core.io.FileSystemResource;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.restdocs.operation.OperationResponse;
import org.springframework.restdocs.snippet.SnippetException;
import org.springframework.restdocs.templates.TemplateEngine;
import org.springframework.restdocs.templates.TemplateResourceResolver;
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
import org.springframework.restdocs.test.ExpectedSnippet;
import org.springframework.restdocs.test.OperationBuilder;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@@ -61,9 +61,9 @@ public class LinksSnippetTests {
this.thrown.expect(SnippetException.class);
this.thrown.expectMessage(equalTo("Links with the following relations were not"
+ " documented: [foo]"));
new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo",
"bar")), Collections.<LinkDescriptor> emptyList()).document(
"undocumented-link", result());
new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "bar")),
Collections.<LinkDescriptor> emptyList()).document(new OperationBuilder(
"undocumented-link").build());
}
@Test
@@ -71,9 +71,9 @@ public class LinksSnippetTests {
this.thrown.expect(SnippetException.class);
this.thrown.expectMessage(equalTo("Links with the following relations were not"
+ " found in the response: [foo]"));
new LinksSnippet(new StubLinkExtractor(),
Arrays.asList(new LinkDescriptor("foo").description("bar"))).document(
"missing-link", result());
new LinksSnippet(new StubLinkExtractor(), Arrays.asList(new LinkDescriptor("foo")
.description("bar"))).document(new OperationBuilder("missing-link")
.build());
}
@Test
@@ -81,9 +81,9 @@ public class LinksSnippetTests {
this.snippet.expectLinks("documented-optional-link").withContents( //
tableWithHeader("Relation", "Description") //
.row("foo", "bar"));
new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo",
"blah")), Arrays.asList(new LinkDescriptor("foo").description("bar")
.optional())).document("documented-optional-link", result());
new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "blah")),
Arrays.asList(new LinkDescriptor("foo").description("bar").optional()))
.document(new OperationBuilder("documented-optional-link").build());
}
@Test
@@ -91,9 +91,9 @@ public class LinksSnippetTests {
this.snippet.expectLinks("missing-optional-link").withContents( //
tableWithHeader("Relation", "Description") //
.row("foo", "bar"));
new LinksSnippet(new StubLinkExtractor(),
Arrays.asList(new LinkDescriptor("foo").description("bar").optional()))
.document("missing-optional-link", result());
new LinksSnippet(new StubLinkExtractor(), Arrays.asList(new LinkDescriptor("foo")
.description("bar").optional())).document(new OperationBuilder(
"missing-optional-link").build());
}
@Test
@@ -102,9 +102,10 @@ public class LinksSnippetTests {
this.thrown.expectMessage(equalTo("Links with the following relations were not"
+ " documented: [a]. Links with the following relations were not"
+ " found in the response: [foo]"));
new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a",
"alpha")), Arrays.asList(new LinkDescriptor("foo").description("bar")))
.document("undocumented-link-and-missing-link", result());
new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha")),
Arrays.asList(new LinkDescriptor("foo").description("bar")))
.document(new OperationBuilder("undocumented-link-and-missing-link")
.build());
}
@Test
@@ -113,11 +114,11 @@ public class LinksSnippetTests {
tableWithHeader("Relation", "Description") //
.row("a", "one") //
.row("b", "two"));
new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a",
"alpha"), new Link("b", "bravo")), Arrays.asList(
new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"),
new Link("b", "bravo")), Arrays.asList(
new LinkDescriptor("a").description("one"),
new LinkDescriptor("b").description("two"))).document("documented-links",
result());
new LinkDescriptor("b").description("two")))
.document(new OperationBuilder("documented-links").build());
}
@Test
@@ -134,33 +135,34 @@ public class LinksSnippetTests {
"src/test/resources/custom-snippet-templates/links-with-extra-column.snippet"));
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a",
"alpha"), new Link("b", "bravo")), Arrays.asList(
new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"),
new Link("b", "bravo")), Arrays.asList(
new LinkDescriptor("a").description("one").attributes(
key("foo").value("alpha")),
new LinkDescriptor("b").description("two").attributes(
key("foo").value("bravo")))).document(
"links-with-custom-descriptor-attributes", result(request));
key("foo").value("bravo")))).document(new OperationBuilder(
"links-with-custom-descriptor-attributes").attribute(
TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver))
.build());
}
@Test
public void linksWithCustomAttributes() throws IOException {
this.snippet.expectLinks("links-with-custom-attributes").withContents(
startsWith(".Title for the links"));
MockHttpServletRequest request = new MockHttpServletRequest();
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("links"))
.thenReturn(
new FileSystemResource(
"src/test/resources/custom-snippet-templates/links-with-title.snippet"));
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a",
"alpha"), new Link("b", "bravo")), attributes(key("title").value(
new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"),
new Link("b", "bravo")), attributes(key("title").value(
"Title for the links")), Arrays.asList(
new LinkDescriptor("a").description("one"),
new LinkDescriptor("b").description("two"))).document(
"links-with-custom-attributes", result(request));
new LinkDescriptor("b").description("two")))
.document(new OperationBuilder("links-with-custom-attributes").attribute(
TemplateEngine.class.getName(),
new MustacheTemplateEngine(resolver)).build());
}
private static class StubLinkExtractor implements LinkExtractor {
@@ -168,7 +170,7 @@ public class LinksSnippetTests {
private MultiValueMap<String, Link> linksByRel = new LinkedMultiValueMap<String, Link>();
@Override
public MultiValueMap<String, Link> extractLinks(MockHttpServletResponse response)
public MultiValueMap<String, Link> extractLinks(OperationResponse response)
throws IOException {
return this.linksByRel;
}

View File

@@ -0,0 +1,220 @@
package org.springframework.restdocs.hypermedia;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.hasEntry;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.ByteArrayInputStream;
import java.net.URI;
import java.util.Arrays;
import javax.servlet.http.Part;
import org.junit.Test;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.mock.web.MockServletContext;
import org.springframework.restdocs.operation.MockMvcOperationRequestFactory;
import org.springframework.restdocs.operation.OperationRequest;
import org.springframework.restdocs.operation.OperationRequestPart;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
/**
* Tests for {@link MockMvcOperationRequestFactory}
*
* @author Andy Wilkinson
*/
public class MockMvcOperationRequestFactoryTests {
private final MockMvcOperationRequestFactory factory = new MockMvcOperationRequestFactory();
@Test
public void httpRequest() throws Exception {
OperationRequest request = createOperationRequest(MockMvcRequestBuilders
.get("/foo"));
assertThat(request.getUri(), is(URI.create("http://localhost/foo")));
assertThat(request.getMethod(), is(HttpMethod.GET));
}
@Test
public void httpRequestWithCustomPort() throws Exception {
MockHttpServletRequest mockRequest = MockMvcRequestBuilders.get("/foo")
.buildRequest(new MockServletContext());
mockRequest.setServerPort(8080);
OperationRequest request = this.factory.createOperationRequest(mockRequest);
assertThat(request.getUri(), is(URI.create("http://localhost:8080/foo")));
assertThat(request.getMethod(), is(HttpMethod.GET));
}
@Test
public void requestWithContextPath() throws Exception {
OperationRequest request = createOperationRequest(MockMvcRequestBuilders.get(
"/foo/bar").contextPath("/foo"));
assertThat(request.getUri(), is(URI.create("http://localhost/foo/bar")));
assertThat(request.getMethod(), is(HttpMethod.GET));
}
@Test
public void requestWithHeaders() throws Exception {
OperationRequest request = createOperationRequest(MockMvcRequestBuilders
.get("/foo").header("a", "alpha", "apple").header("b", "bravo"));
assertThat(request.getUri(), is(URI.create("http://localhost/foo")));
assertThat(request.getMethod(), is(HttpMethod.GET));
assertThat(request.getHeaders(), hasEntry("a", Arrays.asList("alpha", "apple")));
assertThat(request.getHeaders(), hasEntry("b", Arrays.asList("bravo")));
}
@Test
public void httpsRequest() throws Exception {
MockHttpServletRequest mockRequest = MockMvcRequestBuilders.get("/foo")
.buildRequest(new MockServletContext());
mockRequest.setScheme("https");
mockRequest.setServerPort(443);
OperationRequest request = this.factory.createOperationRequest(mockRequest);
assertThat(request.getUri(), is(URI.create("https://localhost/foo")));
assertThat(request.getMethod(), is(HttpMethod.GET));
}
@Test
public void httpsRequestWithCustomPort() throws Exception {
MockHttpServletRequest mockRequest = MockMvcRequestBuilders.get("/foo")
.buildRequest(new MockServletContext());
mockRequest.setScheme("https");
mockRequest.setServerPort(8443);
OperationRequest request = this.factory.createOperationRequest(mockRequest);
assertThat(request.getUri(), is(URI.create("https://localhost:8443/foo")));
assertThat(request.getMethod(), is(HttpMethod.GET));
}
@Test
public void getRequestWithParametersProducesUriWithQueryString() throws Exception {
OperationRequest request = createOperationRequest(MockMvcRequestBuilders
.get("/foo").param("a", "alpha", "apple").param("b", "br&vo"));
assertThat(request.getUri(),
is(URI.create("http://localhost/foo?a=alpha&a=apple&b=br%26vo")));
assertThat(request.getParameters().size(), is(2));
assertThat(request.getParameters(),
hasEntry("a", Arrays.asList("alpha", "apple")));
assertThat(request.getParameters(), hasEntry("b", Arrays.asList("br&vo")));
assertThat(request.getMethod(), is(HttpMethod.GET));
}
@Test
public void getRequestWithQueryStringPopulatesParameters() throws Exception {
OperationRequest request = createOperationRequest(MockMvcRequestBuilders
.get("/foo?a=alpha&b=bravo"));
assertThat(request.getUri(),
is(URI.create("http://localhost/foo?a=alpha&b=bravo")));
assertThat(request.getParameters().size(), is(2));
assertThat(request.getParameters(), hasEntry("a", Arrays.asList("alpha")));
assertThat(request.getParameters(), hasEntry("b", Arrays.asList("bravo")));
assertThat(request.getMethod(), is(HttpMethod.GET));
}
@Test
public void postRequestWithParameters() throws Exception {
OperationRequest request = createOperationRequest(MockMvcRequestBuilders
.post("/foo").param("a", "alpha", "apple").param("b", "br&vo"));
assertThat(request.getUri(), is(URI.create("http://localhost/foo")));
assertThat(request.getMethod(), is(HttpMethod.POST));
assertThat(request.getParameters().size(), is(2));
assertThat(request.getParameters(),
hasEntry("a", Arrays.asList("alpha", "apple")));
assertThat(request.getParameters(), hasEntry("b", Arrays.asList("br&vo")));
}
@Test
public void mockMultipartFileUpload() throws Exception {
OperationRequest request = createOperationRequest(MockMvcRequestBuilders
.fileUpload("/foo").file(
new MockMultipartFile("file", new byte[] { 1, 2, 3, 4 })));
assertThat(request.getUri(), is(URI.create("http://localhost/foo")));
assertThat(request.getMethod(), is(HttpMethod.POST));
assertThat(request.getParts().size(), is(1));
OperationRequestPart part = request.getParts().iterator().next();
assertThat(part.getName(), is(equalTo("file")));
assertThat(part.getSubmittedFileName(), is(nullValue()));
assertThat(part.getHeaders().isEmpty(), is(true));
assertThat(part.getContent(), is(equalTo(new byte[] { 1, 2, 3, 4 })));
}
@Test
public void mockMultipartFileUploadWithContentType() throws Exception {
OperationRequest request = createOperationRequest(MockMvcRequestBuilders
.fileUpload("/foo").file(
new MockMultipartFile("file", "original", "image/png",
new byte[] { 1, 2, 3, 4 })));
assertThat(request.getUri(), is(URI.create("http://localhost/foo")));
assertThat(request.getMethod(), is(HttpMethod.POST));
assertThat(request.getParts().size(), is(1));
OperationRequestPart part = request.getParts().iterator().next();
assertThat(part.getName(), is(equalTo("file")));
assertThat(part.getSubmittedFileName(), is(equalTo("original")));
assertThat(part.getHeaders().getContentType(), is(MediaType.IMAGE_PNG));
assertThat(part.getContent(), is(equalTo(new byte[] { 1, 2, 3, 4 })));
}
@Test
public void requestWithPart() throws Exception {
MockHttpServletRequest mockRequest = MockMvcRequestBuilders.get("/foo")
.buildRequest(new MockServletContext());
Part mockPart = mock(Part.class);
when(mockPart.getHeaderNames()).thenReturn(Arrays.asList("a", "b"));
when(mockPart.getHeaders("a")).thenReturn(Arrays.asList("alpha"));
when(mockPart.getHeaders("b")).thenReturn(Arrays.asList("bravo", "banana"));
when(mockPart.getInputStream()).thenReturn(
new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 }));
when(mockPart.getName()).thenReturn("part-name");
when(mockPart.getSubmittedFileName()).thenReturn("submitted.txt");
mockRequest.addPart(mockPart);
OperationRequest request = this.factory.createOperationRequest(mockRequest);
assertThat(request.getParts().size(), is(1));
OperationRequestPart part = request.getParts().iterator().next();
assertThat(part.getName(), is(equalTo("part-name")));
assertThat(part.getSubmittedFileName(), is(equalTo("submitted.txt")));
assertThat(part.getHeaders().getContentType(), is(nullValue()));
assertThat(part.getHeaders().get("a"), contains("alpha"));
assertThat(part.getHeaders().get("b"), contains("bravo", "banana"));
assertThat(part.getContent(), is(equalTo(new byte[] { 1, 2, 3, 4 })));
}
@Test
public void requestWithPartWithContentType() throws Exception {
MockHttpServletRequest mockRequest = MockMvcRequestBuilders.get("/foo")
.buildRequest(new MockServletContext());
Part mockPart = mock(Part.class);
when(mockPart.getHeaderNames()).thenReturn(Arrays.asList("a", "b"));
when(mockPart.getHeaders("a")).thenReturn(Arrays.asList("alpha"));
when(mockPart.getHeaders("b")).thenReturn(Arrays.asList("bravo", "banana"));
when(mockPart.getInputStream()).thenReturn(
new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 }));
when(mockPart.getName()).thenReturn("part-name");
when(mockPart.getSubmittedFileName()).thenReturn("submitted.png");
when(mockPart.getContentType()).thenReturn("image/png");
mockRequest.addPart(mockPart);
OperationRequest request = this.factory.createOperationRequest(mockRequest);
assertThat(request.getParts().size(), is(1));
OperationRequestPart part = request.getParts().iterator().next();
assertThat(part.getName(), is(equalTo("part-name")));
assertThat(part.getSubmittedFileName(), is(equalTo("submitted.png")));
assertThat(part.getHeaders().getContentType(), is(MediaType.IMAGE_PNG));
assertThat(part.getHeaders().get("a"), contains("alpha"));
assertThat(part.getHeaders().get("b"), contains("bravo", "banana"));
assertThat(part.getContent(), is(equalTo(new byte[] { 1, 2, 3, 4 })));
}
private OperationRequest createOperationRequest(MockHttpServletRequestBuilder builder)
throws Exception {
return this.factory.createOperationRequest(builder
.buildRequest(new MockServletContext()));
}
}

View File

@@ -21,12 +21,10 @@ import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.get;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.snippet.Attributes.attributes;
import static org.springframework.restdocs.snippet.Attributes.key;
import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader;
import static org.springframework.restdocs.test.StubMvcResult.result;
import java.io.IOException;
import java.util.Arrays;
@@ -36,14 +34,14 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.restdocs.snippet.SnippetException;
import org.springframework.restdocs.templates.TemplateEngine;
import org.springframework.restdocs.templates.TemplateResourceResolver;
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
import org.springframework.restdocs.test.ExpectedSnippet;
import org.springframework.restdocs.test.OperationBuilder;
/**
* Tests for {@link RequestFieldsSnippet}
@@ -68,9 +66,9 @@ public class RequestFieldsSnippetTests {
new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"),
fieldWithPath("a.c").description("two"),
fieldWithPath("a").description("three"))).document(
"map-request-with-fields",
result(get("/foo").content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}")));
fieldWithPath("a").description("three"))).document(new OperationBuilder(
"map-request-with-fields").request("http://localhost")
.content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build());
}
@Test
@@ -83,10 +81,9 @@ public class RequestFieldsSnippetTests {
new RequestFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b").description("one"),
fieldWithPath("[]a.c").description("two"), fieldWithPath("[]a")
.description("three"))).document(
"array-request-with-fields",
result(get("/foo").content(
"[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]")));
.description("three"))).document(new OperationBuilder(
"array-request-with-fields").request("http://localhost")
.content("[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]").build());
}
@Test
@@ -95,8 +92,9 @@ public class RequestFieldsSnippetTests {
this.thrown
.expectMessage(startsWith("The following parts of the payload were not"
+ " documented:"));
new RequestFieldsSnippet(Collections.<FieldDescriptor> emptyList()).document(
"undocumented-request-field", result(get("/foo").content("{\"a\": 5}")));
new RequestFieldsSnippet(Collections.<FieldDescriptor> emptyList())
.document(new OperationBuilder("undocumented-request-field")
.request("http://localhost").content("{\"a\": 5}").build());
}
@Test
@@ -106,15 +104,17 @@ public class RequestFieldsSnippetTests {
.expectMessage(equalTo("Fields with the following paths were not found"
+ " in the payload: [a.b]"));
new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one")))
.document("missing-request-fields", result(get("/foo").content("{}")));
.document(new OperationBuilder("missing-request-fields")
.request("http://localhost").content("{}").build());
}
@Test
public void missingOptionalRequestFieldWithNoTypeProvided() throws IOException {
this.thrown.expect(FieldTypeRequiredException.class);
new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one")
.optional())).document("missing-optional-request-field-with-no-type",
result(get("/foo").content("{ }")));
.optional())).document(new OperationBuilder(
"missing-optional-request-field-with-no-type")
.request("http://localhost").content("{ }").build());
}
@Test
@@ -127,8 +127,10 @@ public class RequestFieldsSnippetTests {
.expectMessage(endsWith("Fields with the following paths were not found"
+ " in the payload: [a.b]"));
new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one")))
.document("undocumented-request-field-and-missing-request-field",
result(get("/foo").content("{ \"a\": { \"c\": 5 }}")));
.document(new OperationBuilder(
"undocumented-request-field-and-missing-request-field")
.request("http://localhost").content("{ \"a\": { \"c\": 5 }}")
.build());
}
@Test
@@ -142,18 +144,17 @@ public class RequestFieldsSnippetTests {
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("request-fields")).thenReturn(
snippetResource("request-fields-with-extra-column"));
MockHttpServletRequest request = new MockHttpServletRequest();
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
request.setContent("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}".getBytes());
new RequestFieldsSnippet(Arrays.asList(
fieldWithPath("a.b").description("one").attributes(
key("foo").value("alpha")),
fieldWithPath("a.c").description("two").attributes(
key("foo").value("bravo")),
fieldWithPath("a").description("three").attributes(
key("foo").value("charlie")))).document(
"request-fields-with-custom-descriptor-attributes", result(request));
key("foo").value("charlie")))).document(new OperationBuilder(
"request-fields-with-custom-descriptor-attributes")
.attribute(TemplateEngine.class.getName(),
new MustacheTemplateEngine(resolver)).request("http://localhost")
.content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build());
}
@Test
@@ -163,14 +164,12 @@ public class RequestFieldsSnippetTests {
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("request-fields")).thenReturn(
snippetResource("request-fields-with-title"));
MockHttpServletRequest request = new MockHttpServletRequest();
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
request.setContent("{\"a\": \"foo\"}".getBytes());
MockHttpServletResponse response = new MockHttpServletResponse();
new RequestFieldsSnippet(attributes(key("title").value("Custom title")),
Arrays.asList(fieldWithPath("a").description("one"))).document(
"request-fields-with-custom-attributes", result(request, response));
Arrays.asList(fieldWithPath("a").description("one")))
.document(new OperationBuilder("request-fields-with-custom-attributes")
.attribute(TemplateEngine.class.getName(),
new MustacheTemplateEngine(resolver))
.request("http://localhost").content("{\"a\": \"foo\"}").build());
}
@Test
@@ -183,10 +182,12 @@ public class RequestFieldsSnippetTests {
new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one")
.type("b"), fieldWithPath("a/c").description("two").type("c"),
fieldWithPath("a").description("three").type("a"))).document(
"xml-request",
result(get("/foo").content("<a><b>5</b><c>charlie</c></a>").contentType(
MediaType.APPLICATION_XML)));
fieldWithPath("a").description("three").type("a")))
.document(new OperationBuilder("xml-request")
.request("http://localhost")
.content("<a><b>5</b><c>charlie</c></a>")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE)
.build());
}
@Test
@@ -195,19 +196,23 @@ public class RequestFieldsSnippetTests {
this.thrown
.expectMessage(startsWith("The following parts of the payload were not"
+ " documented:"));
new RequestFieldsSnippet(Collections.<FieldDescriptor> emptyList()).document(
"undocumented-xml-request-field",
result(get("/foo").content("<a><b>5</b></a>").contentType(
MediaType.APPLICATION_XML)));
new RequestFieldsSnippet(Collections.<FieldDescriptor> emptyList())
.document(new OperationBuilder("undocumented-xml-request-field")
.request("http://localhost")
.content("<a><b>5</b></a>")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE)
.build());
}
@Test
public void xmlRequestFieldWithNoType() throws IOException {
this.thrown.expect(FieldTypeRequiredException.class);
new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")))
.document("missing-xml-request",
result(get("/foo").contentType(MediaType.APPLICATION_XML)
.content("<a>5</a>")));
.document(new OperationBuilder("missing-xml-request")
.request("http://localhost")
.content("<a>5</a>")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE)
.build());
}
@Test
@@ -217,10 +222,11 @@ public class RequestFieldsSnippetTests {
.expectMessage(equalTo("Fields with the following paths were not found"
+ " in the payload: [a/b]"));
new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"),
fieldWithPath("a").description("one"))).document(
"missing-xml-request-fields",
result(get("/foo").contentType(MediaType.APPLICATION_XML).content(
"<a></a>")));
fieldWithPath("a").description("one"))).document(new OperationBuilder(
"missing-xml-request-fields").request("http://localhost")
.content("<a></a>")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE)
.build());
}
@Test
@@ -233,9 +239,12 @@ public class RequestFieldsSnippetTests {
.expectMessage(endsWith("Fields with the following paths were not found"
+ " in the payload: [a/b]"));
new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one")))
.document("undocumented-xml-request-field-and-missing-xml-request-field",
result(get("/foo").contentType(MediaType.APPLICATION_XML)
.content("<a><c>5</c></a>")));
.document(new OperationBuilder(
"undocumented-xml-request-field-and-missing-xml-request-field")
.request("http://localhost")
.content("<a><c>5</c></a>")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE)
.build());
}
private FileSystemResource snippetResource(String name) {

View File

@@ -24,7 +24,6 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWit
import static org.springframework.restdocs.snippet.Attributes.attributes;
import static org.springframework.restdocs.snippet.Attributes.key;
import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader;
import static org.springframework.restdocs.test.StubMvcResult.result;
import java.io.IOException;
import java.util.Arrays;
@@ -34,14 +33,15 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.restdocs.snippet.SnippetException;
import org.springframework.restdocs.templates.TemplateEngine;
import org.springframework.restdocs.templates.TemplateResourceResolver;
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
import org.springframework.restdocs.test.ExpectedSnippet;
import org.springframework.restdocs.test.OperationBuilder;
/**
* Tests for {@link PayloadDocumentation}
@@ -66,18 +66,18 @@ public class ResponseFieldsSnippetTests {
.row("assets[]", "Object", "four") //
.row("assets[].id", "Number", "five") //
.row("assets[].name", "String", "six"));
MockHttpServletResponse response = new MockHttpServletResponse();
response.getWriter().append(
"{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":"
+ " [{\"id\":356,\"name\": \"sample\"}]}");
new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("id").description("one"),
fieldWithPath("date").description("two"), fieldWithPath("assets")
.description("three"),
fieldWithPath("assets[]").description("four"),
fieldWithPath("assets[].id").description("five"),
fieldWithPath("assets[].name").description("six"))).document(
"map-response-with-fields", result(response));
fieldWithPath("assets[].name").description("six")))
.document(new OperationBuilder("map-response-with-fields")
.response()
.content(
"{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":"
+ " [{\"id\":356,\"name\": \"sample\"}]}")
.build());
}
@Test
@@ -87,14 +87,12 @@ public class ResponseFieldsSnippetTests {
.row("[]a.b", "Number", "one") //
.row("[]a.c", "String", "two") //
.row("[]a", "Object", "three"));
MockHttpServletResponse response = new MockHttpServletResponse();
response.getWriter()
.append("[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]");
new ResponseFieldsSnippet(Arrays.asList(
fieldWithPath("[]a.b").description("one"), fieldWithPath("[]a.c")
.description("two"), fieldWithPath("[]a").description("three")))
.document("array-response-with-fields", result(response));
.document(new OperationBuilder("array-response-with-fields").response()
.content("[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]")
.build());
}
@Test
@@ -102,11 +100,9 @@ public class ResponseFieldsSnippetTests {
this.snippet.expectResponseFields("array-response").withContents( //
tableWithHeader("Path", "Type", "Description") //
.row("[]", "String", "one"));
MockHttpServletResponse response = new MockHttpServletResponse();
response.getWriter().append("[\"a\", \"b\", \"c\"]");
new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]").description("one")))
.document("array-response", result(response));
.document(new OperationBuilder("array-response").response()
.content("[\"a\", \"b\", \"c\"]").build());
}
@Test
@@ -120,19 +116,17 @@ public class ResponseFieldsSnippetTests {
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("response-fields")).thenReturn(
snippetResource("response-fields-with-extra-column"));
MockHttpServletRequest request = new MockHttpServletRequest();
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
MockHttpServletResponse response = new MockHttpServletResponse();
response.getOutputStream().print("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}");
new ResponseFieldsSnippet(Arrays.asList(
fieldWithPath("a.b").description("one").attributes(
key("foo").value("alpha")),
fieldWithPath("a.c").description("two").attributes(
key("foo").value("bravo")),
fieldWithPath("a").description("three").attributes(
key("foo").value("charlie")))).document(
"response-fields-with-custom-attributes", result(request, response));
key("foo").value("charlie")))).document(new OperationBuilder(
"response-fields-with-custom-attributes")
.attribute(TemplateEngine.class.getName(),
new MustacheTemplateEngine(resolver)).response()
.content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build());
}
@Test
@@ -142,14 +136,12 @@ public class ResponseFieldsSnippetTests {
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("response-fields")).thenReturn(
snippetResource("response-fields-with-title"));
MockHttpServletRequest request = new MockHttpServletRequest();
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
MockHttpServletResponse response = new MockHttpServletResponse();
response.getOutputStream().print("{\"a\": \"foo\"}");
new ResponseFieldsSnippet(attributes(key("title").value("Custom title")),
Arrays.asList(fieldWithPath("a").description("one"))).document(
"response-fields-with-custom-attributes", result(request, response));
Arrays.asList(fieldWithPath("a").description("one")))
.document(new OperationBuilder("response-fields-with-custom-attributes")
.attribute(TemplateEngine.class.getName(),
new MustacheTemplateEngine(resolver)).response()
.content("{\"a\": \"foo\"}").build());
}
@Test
@@ -159,13 +151,14 @@ public class ResponseFieldsSnippetTests {
.row("a/b", "b", "one") //
.row("a/c", "c", "two") //
.row("a", "a", "three"));
MockHttpServletResponse response = new MockHttpServletResponse();
response.setContentType(MediaType.APPLICATION_XML_VALUE);
response.getOutputStream().print("<a><b>5</b><c>charlie</c></a>");
new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one")
.type("b"), fieldWithPath("a/c").description("two").type("c"),
fieldWithPath("a").description("three").type("a"))).document(
"xml-response", result(response));
fieldWithPath("a").description("three").type("a")))
.document(new OperationBuilder("xml-response")
.response()
.content("<a><b>5</b><c>charlie</c></a>")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE)
.build());
}
@Test
@@ -177,8 +170,12 @@ public class ResponseFieldsSnippetTests {
MockHttpServletResponse response = new MockHttpServletResponse();
response.setContentType(MediaType.APPLICATION_XML_VALUE);
response.getOutputStream().print("<a><b>5</b></a>");
new ResponseFieldsSnippet(Collections.<FieldDescriptor> emptyList()).document(
"undocumented-xml-response-field", result(response));
new ResponseFieldsSnippet(Collections.<FieldDescriptor> emptyList())
.document(new OperationBuilder("undocumented-xml-response-field")
.response()
.content("<a><b>5</b></a>")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE)
.build());
}
@Test
@@ -188,7 +185,11 @@ public class ResponseFieldsSnippetTests {
response.setContentType(MediaType.APPLICATION_XML_VALUE);
response.getOutputStream().print("<a>5</a>");
new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")))
.document("xml-response-no-field-type", result(response));
.document(new OperationBuilder("xml-response-no-field-type")
.response()
.content("<a>5</a>")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE)
.build());
}
@Test
@@ -201,8 +202,10 @@ public class ResponseFieldsSnippetTests {
response.setContentType(MediaType.APPLICATION_XML_VALUE);
response.getOutputStream().print("<a></a>");
new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"),
fieldWithPath("a").description("one"))).document(
"missing-xml-response-field", result(response));
fieldWithPath("a").description("one"))).document(new OperationBuilder(
"missing-xml-response-field").response().content("<a></a>")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE)
.build());
}
@Test
@@ -219,8 +222,12 @@ public class ResponseFieldsSnippetTests {
response.setContentType(MediaType.APPLICATION_XML_VALUE);
response.getOutputStream().print("<a><c>5</c></a>");
new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one")))
.document("undocumented-xml-request-field-and-missing-xml-request-field",
result(response));
.document(new OperationBuilder(
"undocumented-xml-request-field-and-missing-xml-request-field")
.response()
.content("<a><c>5</c></a>")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE)
.build());
}
private FileSystemResource snippetResource(String name) {

View File

@@ -25,8 +25,6 @@ import static org.springframework.restdocs.snippet.Attributes.attributes;
import static org.springframework.restdocs.snippet.Attributes.key;
import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader;
import static org.springframework.restdocs.test.SnippetMatchers.tableWithTitleAndHeader;
import static org.springframework.restdocs.test.StubMvcResult.result;
import static org.springframework.restdocs.test.TestRequestBuilders.get;
import java.io.IOException;
import java.util.Arrays;
@@ -36,12 +34,12 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.core.io.FileSystemResource;
import org.springframework.restdocs.config.RestDocumentationContext;
import org.springframework.restdocs.snippet.SnippetException;
import org.springframework.restdocs.templates.TemplateEngine;
import org.springframework.restdocs.templates.TemplateResourceResolver;
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
import org.springframework.restdocs.test.ExpectedSnippet;
import org.springframework.restdocs.test.OperationBuilder;
/**
* Tests for {@link PathParametersSnippet}
@@ -63,7 +61,8 @@ public class PathParametersSnippetTests {
this.thrown.expectMessage(equalTo("Path parameters with the following names were"
+ " not documented: [a]"));
new PathParametersSnippet(Collections.<ParameterDescriptor> emptyList())
.document("undocumented-path-parameter", result(get("/{a}/", "alpha")));
.document(new OperationBuilder("undocumented-path-parameter").attribute(
"org.springframework.restdocs.urlTemplate", "/{a}/").build());
}
@Test
@@ -72,8 +71,9 @@ public class PathParametersSnippetTests {
this.thrown.expectMessage(equalTo("Path parameters with the following names were"
+ " not found in the request: [a]"));
new PathParametersSnippet(
Arrays.asList(parameterWithName("a").description("one"))).document(
"missing-path-parameter", result(get("/")));
Arrays.asList(parameterWithName("a").description("one")))
.document(new OperationBuilder("missing-path-parameter").attribute(
"org.springframework.restdocs.urlTemplate", "/").build());
}
@Test
@@ -83,8 +83,10 @@ public class PathParametersSnippetTests {
+ " not documented: [b]. Path parameters with the following"
+ " names were not found in the request: [a]"));
new PathParametersSnippet(
Arrays.asList(parameterWithName("a").description("one"))).document(
"undocumented-and-missing-path-parameters", result(get("/{b}", "bravo")));
Arrays.asList(parameterWithName("a").description("one")))
.document(new OperationBuilder("undocumented-and-missing-path-parameters")
.attribute("org.springframework.restdocs.urlTemplate", "/{b}")
.build());
}
@Test
@@ -94,11 +96,9 @@ public class PathParametersSnippetTests {
"one").row("b", "two"));
new PathParametersSnippet(Arrays.asList(
parameterWithName("a").description("one"), parameterWithName("b")
.description("two"))).document(
"path-parameters",
result(get("/{a}/{b}", "alpha", "banana").requestAttr(
RestDocumentationContext.class.getName(),
new RestDocumentationContext(null))));
.description("two"))).document(new OperationBuilder(
"path-parameters").attribute("org.springframework.restdocs.urlTemplate",
"/{a}/{b}").build());
}
@Test
@@ -109,11 +109,9 @@ public class PathParametersSnippetTests {
.row("a", "one").row("b", "two"));
new PathParametersSnippet(Arrays.asList(
parameterWithName("a").description("one"), parameterWithName("b")
.description("two"))).document(
"path-parameters-with-query-string",
result(get("/{a}/{b}?foo=bar", "alpha", "banana").requestAttr(
RestDocumentationContext.class.getName(),
new RestDocumentationContext(null))));
.description("two"))).document(new OperationBuilder(
"path-parameters-with-query-string").attribute(
"org.springframework.restdocs.urlTemplate", "/{a}/{b}?foo=bar").build());
}
@Test
@@ -127,11 +125,12 @@ public class PathParametersSnippetTests {
snippetResource("path-parameters-with-extra-column"));
new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one")
.attributes(key("foo").value("alpha")), parameterWithName("b")
.description("two").attributes(key("foo").value("bravo")))).document(
"path-parameters-with-custom-descriptor-attributes",
result(get("{a}/{b}", "alpha", "bravo").requestAttr(
TemplateEngine.class.getName(),
new MustacheTemplateEngine(resolver))));
.description("two").attributes(key("foo").value("bravo"))))
.document(new OperationBuilder(
"path-parameters-with-custom-descriptor-attributes")
.attribute("org.springframework.restdocs.urlTemplate", "/{a}/{b}")
.attribute(TemplateEngine.class.getName(),
new MustacheTemplateEngine(resolver)).build());
}
@Test
@@ -147,11 +146,10 @@ public class PathParametersSnippetTests {
parameterWithName("a").description("one").attributes(
key("foo").value("alpha")), parameterWithName("b")
.description("two").attributes(key("foo").value("bravo"))))
.document(
"path-parameters-with-custom-attributes",
result(get("{a}/{b}", "alpha", "bravo").requestAttr(
TemplateEngine.class.getName(),
new MustacheTemplateEngine(resolver))));
.document(new OperationBuilder("path-parameters-with-custom-attributes")
.attribute("org.springframework.restdocs.urlTemplate", "/{a}/{b}")
.attribute(TemplateEngine.class.getName(),
new MustacheTemplateEngine(resolver)).build());
}

View File

@@ -24,8 +24,6 @@ import static org.springframework.restdocs.request.RequestDocumentation.paramete
import static org.springframework.restdocs.snippet.Attributes.attributes;
import static org.springframework.restdocs.snippet.Attributes.key;
import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader;
import static org.springframework.restdocs.test.StubMvcResult.result;
import static org.springframework.restdocs.test.TestRequestBuilders.get;
import java.io.IOException;
import java.util.Arrays;
@@ -35,13 +33,12 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.core.io.FileSystemResource;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.restdocs.config.RestDocumentationContext;
import org.springframework.restdocs.snippet.SnippetException;
import org.springframework.restdocs.templates.TemplateEngine;
import org.springframework.restdocs.templates.TemplateResourceResolver;
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
import org.springframework.restdocs.test.ExpectedSnippet;
import org.springframework.restdocs.test.OperationBuilder;
/**
* Tests for {@link RequestParametersSnippet}
@@ -63,7 +60,8 @@ public class RequestParametersSnippetTests {
.expectMessage(equalTo("Request parameters with the following names were"
+ " not documented: [a]"));
new RequestParametersSnippet(Collections.<ParameterDescriptor> emptyList())
.document("undocumented-parameter", result(get("/").param("a", "alpha")));
.document(new OperationBuilder("undocumented-parameter")
.request("http://localhost").param("a", "alpha").build());
}
@Test
@@ -73,7 +71,8 @@ public class RequestParametersSnippetTests {
.expectMessage(equalTo("Request parameters with the following names were"
+ " not found in the request: [a]"));
new RequestParametersSnippet(Arrays.asList(parameterWithName("a").description(
"one"))).document("missing-parameter", result(get("/")));
"one"))).document(new OperationBuilder("missing-parameter").request(
"http://localhost").build());
}
@Test
@@ -84,36 +83,21 @@ public class RequestParametersSnippetTests {
+ " not documented: [b]. Request parameters with the following"
+ " names were not found in the request: [a]"));
new RequestParametersSnippet(Arrays.asList(parameterWithName("a").description(
"one"))).document("undocumented-and-missing-parameters", result(get("/")
.param("b", "bravo")));
"one"))).document(new OperationBuilder(
"undocumented-and-missing-parameters").request("http://localhost")
.param("b", "bravo").build());
}
@Test
public void requestParameterSnippetFromRequestParameters() throws IOException {
this.snippet.expectRequestParameters(
"request-parameter-snippet-request-parameters").withContents(
public void requestParameters() throws IOException {
this.snippet.expectRequestParameters("request-parameters").withContents(
tableWithHeader("Parameter", "Description").row("a", "one").row("b",
"two"));
new RequestParametersSnippet(Arrays.asList(
parameterWithName("a").description("one"), parameterWithName("b")
.description("two"))).document(
"request-parameter-snippet-request-parameters",
result(get("/").param("a", "bravo").param("b", "bravo")));
}
@Test
public void requestParameterSnippetFromRequestUriQueryString() throws IOException {
this.snippet.expectRequestParameters(
"request-parameter-snippet-request-uri-query-string").withContents(
tableWithHeader("Parameter", "Description").row("a", "one").row("b",
"two"));
new RequestParametersSnippet(Arrays.asList(
parameterWithName("a").description("one"), parameterWithName("b")
.description("two"))).document(
"request-parameter-snippet-request-uri-query-string",
result(get("/?a=alpha&b=bravo").requestAttr(
RestDocumentationContext.class.getName(),
new RestDocumentationContext(null))));
.description("two"))).document(new OperationBuilder(
"request-parameters").request("http://localhost").param("a", "bravo")
.param("b", "bravo").build());
}
@Test
@@ -125,17 +109,15 @@ public class RequestParametersSnippetTests {
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("request-parameters")).thenReturn(
snippetResource("request-parameters-with-extra-column"));
MockHttpServletRequest request = new MockHttpServletRequest();
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
request.addParameter("a", "bravo");
request.addParameter("b", "bravo");
new RequestParametersSnippet(Arrays.asList(
parameterWithName("a").description("one").attributes(
key("foo").value("alpha")),
parameterWithName("b").description("two").attributes(
key("foo").value("bravo")))).document(
"request-parameters-with-custom-descriptor-attributes", result(request));
key("foo").value("bravo")))).document(new OperationBuilder(
"request-parameters-with-custom-descriptor-attributes")
.attribute(TemplateEngine.class.getName(),
new MustacheTemplateEngine(resolver)).request("http://localhost")
.param("a", "alpha").param("b", "bravo").build());
}
@Test
@@ -145,18 +127,18 @@ public class RequestParametersSnippetTests {
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("request-parameters")).thenReturn(
snippetResource("request-parameters-with-title"));
MockHttpServletRequest request = new MockHttpServletRequest();
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
request.addParameter("a", "bravo");
request.addParameter("b", "bravo");
new RequestParametersSnippet(
attributes(key("title").value("The title")),
Arrays.asList(
parameterWithName("a").description("one").attributes(
key("foo").value("alpha")), parameterWithName("b")
.description("two").attributes(key("foo").value("bravo"))))
.document("request-parameters-with-custom-attributes", result(request));
.document(new OperationBuilder(
"request-parameters-with-custom-attributes")
.attribute(TemplateEngine.class.getName(),
new MustacheTemplateEngine(resolver))
.request("http://localhost").param("a", "alpha")
.param("b", "bravo").build());
}
private FileSystemResource snippetResource(String name) {

View File

@@ -38,6 +38,7 @@ public class ContentModifyingResponsePostProcessorTests {
public void contentCanBeModified() throws Exception {
MockHttpServletResponse modified = this.postProcessor.postProcess(this.original);
assertThat(modified.getContentAsString(), is(equalTo("modified")));
assertThat(modified.getContentAsByteArray(), is(equalTo("modified".getBytes())));
}
@Test
@@ -47,14 +48,6 @@ public class ContentModifyingResponsePostProcessorTests {
assertThat(modified.getHeader("a"), is(equalTo("alpha")));
}
@Test
public void getContentAsByteArrayIsUnsupported() throws Exception {
this.thrown.expect(UnsupportedOperationException.class);
this.thrown.expectMessage(equalTo("Following modification, the response's"
+ " content should be accessed as a String"));
this.postProcessor.postProcess(this.original).getContentAsByteArray();
}
private static final class TestContentModifyingResponsePostProcessor extends
ContentModifyingReponsePostProcessor {

View File

@@ -0,0 +1,211 @@
package org.springframework.restdocs.test;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.restdocs.config.RestDocumentationContext;
import org.springframework.restdocs.config.RestDocumentationContextPlaceholderResolver;
import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.operation.OperationRequest;
import org.springframework.restdocs.operation.OperationRequestPart;
import org.springframework.restdocs.operation.OperationResponse;
import org.springframework.restdocs.operation.Parameters;
import org.springframework.restdocs.operation.StandardOperation;
import org.springframework.restdocs.operation.StandardOperationRequest;
import org.springframework.restdocs.operation.StandardOperationRequestPart;
import org.springframework.restdocs.operation.StandardOperationResponse;
import org.springframework.restdocs.snippet.StandardWriterResolver;
import org.springframework.restdocs.snippet.WriterResolver;
import org.springframework.restdocs.templates.StandardTemplateResourceResolver;
import org.springframework.restdocs.templates.TemplateEngine;
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
public class OperationBuilder {
private final Map<String, Object> attributes = new HashMap<>();
private final OperationResponseBuilder responseBuilder = new OperationResponseBuilder();
private final String name;
private OperationRequestBuilder requestBuilder;
public OperationBuilder(String name) {
this.name = name;
}
public OperationRequestBuilder request(String uri) {
this.requestBuilder = new OperationRequestBuilder(uri);
return this.requestBuilder;
}
public OperationResponseBuilder response() {
return this.responseBuilder;
}
public OperationBuilder attribute(String name, Object value) {
this.attributes.put(name, value);
return this;
}
public Operation build() {
if (this.attributes.get(TemplateEngine.class.getName()) == null) {
this.attributes.put(TemplateEngine.class.getName(),
new MustacheTemplateEngine(new StandardTemplateResourceResolver()));
}
RestDocumentationContext context = new RestDocumentationContext(null);
this.attributes.put(RestDocumentationContext.class.getName(), context);
this.attributes.put(WriterResolver.class.getName(), new StandardWriterResolver(
new RestDocumentationContextPlaceholderResolver(context)));
return new StandardOperation(this.name,
(this.requestBuilder == null ? new OperationRequestBuilder(
"http://localhost/").buildRequest() : this.requestBuilder
.buildRequest()),
this.responseBuilder.buildResponse(), this.attributes);
}
public class OperationRequestBuilder {
private URI requestUri = URI.create("http://localhost/");
private HttpMethod method = HttpMethod.GET;
private byte[] content = new byte[0];
private HttpHeaders headers = new HttpHeaders();
private Parameters parameters = new Parameters();
private List<OperationRequestPartBuilder> partBuilders = new ArrayList<>();
public OperationRequestBuilder(String uri) {
this.requestUri = URI.create(uri);
}
private OperationRequest buildRequest() {
List<OperationRequestPart> parts = new ArrayList<>();
for (OperationRequestPartBuilder builder : this.partBuilders) {
parts.add(builder.buildPart());
}
return new StandardOperationRequest(this.requestUri, this.method,
this.content, this.headers, this.parameters, parts);
}
public Operation build() {
return OperationBuilder.this.build();
}
public OperationRequestBuilder method(String method) {
this.method = HttpMethod.valueOf(method);
return this;
}
public OperationRequestBuilder content(String content) {
this.content = content.getBytes();
return this;
}
public OperationRequestBuilder param(String name, String... values) {
for (String value : values) {
this.parameters.add(name, value);
}
return this;
}
public OperationRequestBuilder header(String name, String value) {
this.headers.add(name, value);
return this;
}
public OperationRequestPartBuilder part(String name, byte[] content) {
OperationRequestPartBuilder partBuilder = new OperationRequestPartBuilder(
name, content);
this.partBuilders.add(partBuilder);
return partBuilder;
}
public class OperationRequestPartBuilder {
private final String name;
private final byte[] content;
private String submittedFileName;
private HttpHeaders headers = new HttpHeaders();
private OperationRequestPartBuilder(String name, byte[] content) {
this.name = name;
this.content = content;
}
public OperationRequestPartBuilder submittedFileName(String submittedFileName) {
this.submittedFileName = submittedFileName;
return this;
}
public OperationRequestBuilder and() {
return OperationRequestBuilder.this;
}
public Operation build() {
return OperationBuilder.this.build();
}
private OperationRequestPart buildPart() {
return new StandardOperationRequestPart(this.name,
this.submittedFileName, this.content, this.headers);
}
public OperationRequestPartBuilder header(String name, String value) {
this.headers.add(name, value);
return this;
}
}
}
public class OperationResponseBuilder {
private HttpStatus status = HttpStatus.OK;
private HttpHeaders headers = new HttpHeaders();
private byte[] content = new byte[0];
private OperationResponse buildResponse() {
return new StandardOperationResponse(this.status, this.headers, this.content);
}
public OperationResponseBuilder status(int status) {
this.status = HttpStatus.valueOf(status);
return this;
}
public OperationResponseBuilder header(String name, String value) {
this.headers.add(name, value);
return this;
}
public OperationResponseBuilder content(byte[] content) {
this.content = content;
return this;
}
public OperationResponseBuilder content(String content) {
this.content = content.getBytes();
return this;
}
public Operation build() {
return OperationBuilder.this.build();
}
}
}