From 3579b34a7793f9017aad06fc9c1183e9b98ec686 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 27 Aug 2015 22:17:44 +0100 Subject: [PATCH] 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 --- docs/src/docs/asciidoc/configuration.adoc | 6 +- .../com/example/CustomUriConfiguration.java | 3 +- .../test/java/com/example/PathParameters.java | 2 +- docs/src/test/java/com/example/Payload.java | 4 +- .../java/com/example/RequestParameters.java | 4 +- .../RestDocumentationResultHandler.java | 21 +- .../restdocs/config/UriConfigurer.java | 23 -- .../restdocs/curl/CurlRequestSnippet.java | 119 +++----- .../restdocs/http/HttpRequestSnippet.java | 97 +++--- .../restdocs/http/HttpResponseSnippet.java | 25 +- .../hypermedia/AbstractJsonLinkExtractor.java | 6 +- .../hypermedia/ContentTypeLinkExtractor.java | 14 +- .../restdocs/hypermedia/LinkExtractor.java | 9 +- .../restdocs/hypermedia/LinksSnippet.java | 8 +- .../MockMvcOperationRequestFactory.java | 162 ++++++++++ .../MockMvcOperationResponseFactory.java | 52 ++++ .../restdocs/operation/Operation.java | 56 ++++ .../restdocs/operation/OperationRequest.java | 79 +++++ .../operation/OperationRequestPart.java | 57 ++++ .../restdocs/operation/OperationResponse.java | 52 ++++ .../restdocs/operation/Parameters.java | 64 ++++ .../restdocs/operation/StandardOperation.java | 72 +++++ .../operation/StandardOperationRequest.java | 98 +++++++ .../StandardOperationRequestPart.java | 72 +++++ .../operation/StandardOperationResponse.java | 65 +++++ .../payload/AbstractFieldsSnippet.java | 33 +-- .../restdocs/payload/ContentHandler.java | 61 ++++ ...adHandler.java => JsonContentHandler.java} | 28 +- .../restdocs/payload/PayloadHandler.java | 45 --- .../payload/RequestFieldsSnippet.java | 16 +- .../payload/ResponseFieldsSnippet.java | 16 +- ...oadHandler.java => XmlContentHandler.java} | 34 ++- .../request/AbstractParametersSnippet.java | 31 +- .../request/PathParametersSnippet.java | 16 +- .../request/RequestParametersSnippet.java | 6 +- .../ContentModifyingReponsePostProcessor.java | 4 +- .../DocumentableHttpServletRequest.java | 276 ------------------ .../restdocs/snippet/Snippet.java | 10 +- .../restdocs/snippet/TemplatedSnippet.java | 20 +- .../RestDocumentationConfigurerTests.java | 22 -- .../curl/CurlRequestSnippetTests.java | 222 +++++--------- .../http/HttpRequestSnippetTests.java | 110 +++---- .../http/HttpResponseSnippetTests.java | 41 +-- .../ContentTypeLinkExtractorTests.java | 27 +- .../LinkExtractorsPayloadTests.java | 13 +- .../hypermedia/LinksSnippetTests.java | 68 ++--- .../MockMvcOperationRequestFactoryTests.java | 220 ++++++++++++++ .../payload/RequestFieldsSnippetTests.java | 107 +++---- .../payload/ResponseFieldsSnippetTests.java | 95 +++--- .../request/PathParametersSnippetTests.java | 54 ++-- .../RequestParametersSnippetTests.java | 66 ++--- ...ntModifyingResponsePostProcessorTests.java | 9 +- .../restdocs/test/OperationBuilder.java | 211 +++++++++++++ 53 files changed, 1931 insertions(+), 1100 deletions(-) create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/operation/MockMvcOperationRequestFactory.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/operation/MockMvcOperationResponseFactory.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/operation/Operation.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/operation/OperationRequest.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/operation/OperationRequestPart.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/operation/OperationResponse.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/operation/Parameters.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperation.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/payload/ContentHandler.java rename spring-restdocs/src/main/java/org/springframework/restdocs/payload/{JsonPayloadHandler.java => JsonContentHandler.java} (77%) delete mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadHandler.java rename spring-restdocs/src/main/java/org/springframework/restdocs/payload/{XmlPayloadHandler.java => XmlContentHandler.java} (79%) delete mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentableHttpServletRequest.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/MockMvcOperationRequestFactoryTests.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/test/OperationBuilder.java diff --git a/docs/src/docs/asciidoc/configuration.adoc b/docs/src/docs/asciidoc/configuration.adoc index 9654df1a..e7a0a375 100644 --- a/docs/src/docs/asciidoc/configuration.adoc +++ b/docs/src/docs/asciidoc/configuration.adoc @@ -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]] diff --git a/docs/src/test/java/com/example/CustomUriConfiguration.java b/docs/src/test/java/com/example/CustomUriConfiguration.java index 265e414b..038d5354 100644 --- a/docs/src/test/java/com/example/CustomUriConfiguration.java +++ b/docs/src/test/java/com/example/CustomUriConfiguration.java @@ -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[] } diff --git a/docs/src/test/java/com/example/PathParameters.java b/docs/src/test/java/com/example/PathParameters.java index 8bff0a44..1754947e 100644 --- a/docs/src/test/java/com/example/PathParameters.java +++ b/docs/src/test/java/com/example/PathParameters.java @@ -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; diff --git a/docs/src/test/java/com/example/Payload.java b/docs/src/test/java/com/example/Payload.java index 1ca668b8..132568a2 100644 --- a/docs/src/test/java/com/example/Payload.java +++ b/docs/src/test/java/com/example/Payload.java @@ -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; diff --git a/docs/src/test/java/com/example/RequestParameters.java b/docs/src/test/java/com/example/RequestParameters.java index 62e88291..9155a5b9 100644 --- a/docs/src/test/java/com/example/RequestParameters.java +++ b/docs/src/test/java/com/example/RequestParameters.java @@ -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; diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java index 68d59555..71bca2b5 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java @@ -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 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 getSnippets(MvcResult result) { - List combinedSnippets = new ArrayList<>((List) result.getRequest() + List combinedSnippets = new ArrayList<>((List) result + .getRequest() .getAttribute("org.springframework.restdocs.defaultSnippets")); combinedSnippets.addAll(this.snippets); return combinedSnippets; diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/UriConfigurer.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/UriConfigurer.java index c0fbc03e..30baa03c 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/config/UriConfigurer.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/UriConfigurer.java @@ -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 document(MvcResult result) throws IOException { + public Map createModel(Operation operation) throws IOException { Map model = new HashMap(); - 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> 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> 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 entry : request.getParameterMap().entrySet()) { + else if (!request.getParts().isEmpty()) { + for (Entry> 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()); + } + } \ No newline at end of file diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java index 7ff39756..fd30849a 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java @@ -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 document(MvcResult result) throws IOException { - DocumentableHttpServletRequest request = new DocumentableHttpServletRequest( - result.getRequest()); + public Map createModel(Operation operation) throws IOException { Map model = new HashMap(); - 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> getHeaders(DocumentableHttpServletRequest request) { + private List> getHeaders(OperationRequest request) { List> headers = new ArrayList<>(); if (requiresHostHeader(request)) { - headers.add(header(HttpHeaders.HOST, request.getHost())); + headers.add(header(HttpHeaders.HOST, request.getUri().getHost())); } for (Entry> 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 parameter : request.getParameterMap().entrySet()) { + for (Entry> parameter : request.getParameters().entrySet()) { for (String value : parameter.getValue()) { writePartBoundary(writer); writePart(parameter.getKey(), value, null, writer); writer.println(); } } - for (Entry> 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 header(String name, String value) { @@ -172,4 +178,5 @@ class HttpRequestSnippet extends TemplatedSnippet { header.put("value", value); return header; } + } \ No newline at end of file diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java index b3dfb3c2..7f4f6b65 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java @@ -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 document(MvcResult result) throws IOException { - HttpStatus status = HttpStatus.valueOf(result.getResponse().getStatus()); + public Map createModel(Operation operation) throws IOException { + OperationResponse response = operation.getResponse(); + HttpStatus status = response.getStatus(); Map model = new HashMap(); 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> headers(MvcResult result) { + private List> headers(OperationResponse response) { List> headers = new ArrayList<>(); - for (String headerName : result.getResponse().getHeaderNames()) { - for (String header : result.getResponse().getHeaders(headerName)) { - headers.add(header(headerName, header)); + for (Entry> header : response.getHeaders().entrySet()) { + List values = header.getValue(); + for (String value : values) { + headers.add(header(header.getKey(), value)); } } return headers; diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java index 45c73c23..ed877ab7 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java @@ -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> extractLinks(MockHttpServletResponse response) + public Map> extractLinks(OperationResponse response) throws IOException { Map jsonContent = this.objectMapper.readValue( - response.getContentAsString(), Map.class); + new String(response.getContent()), Map.class); return extractLinks(jsonContent); } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractor.java index c114d138..1c31ad3a 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractor.java @@ -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> extractLinks(MockHttpServletResponse response) + public Map> 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 entry : this.linkExtractors.entrySet()) { - if (mediaType.isCompatibleWith(entry.getKey())) { + if (contentType.isCompatibleWith(entry.getKey())) { return entry.getValue(); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractor.java index 0627a306..747731d6 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkExtractor.java @@ -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> extractLinks(MockHttpServletResponse response) - throws IOException; + Map> extractLinks(OperationResponse response) throws IOException; } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java index 3557581b..1e01716c 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java @@ -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 document(MvcResult result) throws IOException { - validate(this.linkExtractor.extractLinks(result.getResponse())); + protected Map createModel(Operation operation) throws IOException { + OperationResponse response = operation.getResponse(); + validate(this.linkExtractor.extractLinks(response)); Map model = new HashMap<>(); model.put("links", createLinksModel()); return model; diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/MockMvcOperationRequestFactory.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/MockMvcOperationRequestFactory.java new file mode 100644 index 00000000..60c82404 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/MockMvcOperationRequestFactory.java @@ -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 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 extractParts(MockHttpServletRequest servletRequest) + throws IOException, ServletException { + List parts = new ArrayList<>(); + for (Part part : servletRequest.getParts()) { + HttpHeaders partHeaders = extractHeaders(part); + List 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> 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(); + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/MockMvcOperationResponseFactory.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/MockMvcOperationResponseFactory.java new file mode 100644 index 00000000..5ace0f06 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/MockMvcOperationResponseFactory.java @@ -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; + } +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/Operation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/Operation.java new file mode 100644 index 00000000..fdd5fe11 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/Operation.java @@ -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 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(); + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/OperationRequest.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/OperationRequest.java new file mode 100644 index 00000000..919e84ad --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/OperationRequest.java @@ -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 getParts(); + + /** + * Returns the request's URI. + * + * @return the URI + */ + URI getUri(); + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/OperationRequestPart.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/OperationRequestPart.java new file mode 100644 index 00000000..09e9bb80 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/OperationRequestPart.java @@ -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(); + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/OperationResponse.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/OperationResponse.java new file mode 100644 index 00000000..d7254477 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/OperationResponse.java @@ -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(); +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/Parameters.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/Parameters.java new file mode 100644 index 00000000..d53392d1 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/Parameters.java @@ -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 { + + /** + * 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> 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); + } + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperation.java new file mode 100644 index 00000000..a748df45 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperation.java @@ -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 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 attributes) { + this.name = name; + this.request = request; + this.response = response; + this.attributes = attributes; + } + + @Override + public Map 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; + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java new file mode 100644 index 00000000..37ed936f --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java @@ -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 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 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 getParts() { + return Collections.unmodifiableCollection(this.parts); + } + + @Override + public URI getUri() { + return this.uri; + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java new file mode 100644 index 00000000..cfa93d11 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java @@ -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; + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java new file mode 100644 index 00000000..f7a1c594 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java @@ -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; + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index b8d4aa2d..ec93b8d1 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -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 document(MvcResult result) throws IOException { - MediaType contentType = getContentType(result); - PayloadHandler payloadHandler; + protected Map 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 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; } \ No newline at end of file diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ContentHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ContentHandler.java new file mode 100644 index 00000000..0104f143 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ContentHandler.java @@ -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 findMissingFields(List 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 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); + +} \ No newline at end of file diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonPayloadHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java similarity index 77% rename from spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonPayloadHandler.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java index 93c824cb..2fe9e897 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonPayloadHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java @@ -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 findMissingFields(List fieldDescriptors) { List missingFields = new ArrayList(); - 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 fieldDescriptors) { - Object payload = readPayload(); + public String getUndocumentedContent(List 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" diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadHandler.java deleted file mode 100644 index 5d6c2d47..00000000 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadHandler.java +++ /dev/null @@ -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 findMissingFields(List 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 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); - -} \ No newline at end of file diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java index c7e25c9b..1f72d4a3 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java @@ -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(); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java index 440e5b71..4cd92cea 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java @@ -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(); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/XmlPayloadHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java similarity index 79% rename from spring-restdocs/src/main/java/org/springframework/restdocs/payload/XmlPayloadHandler.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java index c50aaba0..53eb22e6 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/XmlPayloadHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java @@ -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 fieldDescriptors) { + public String getUndocumentedContent(List fieldDescriptors) { Document payload = readPayload(); for (FieldDescriptor fieldDescriptor : fieldDescriptors) { NodeList matchingNodes; diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java b/spring-restdocs/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java index 35a3ba2a..df0411dc 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java @@ -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 descriptorsByName = new LinkedHashMap<>(); @@ -30,8 +45,8 @@ abstract class AbstractParametersSnippet extends } @Override - protected Map document(MvcResult result) throws IOException { - verifyParameterDescriptors(result); + protected Map createModel(Operation operation) throws IOException { + verifyParameterDescriptors(operation); Map model = new HashMap<>(); List> parameters = new ArrayList<>(); @@ -42,8 +57,8 @@ abstract class AbstractParametersSnippet extends return model; } - protected void verifyParameterDescriptors(MvcResult result) { - Set actualParameters = extractActualParameters(result); + protected void verifyParameterDescriptors(Operation operation) { + Set actualParameters = extractActualParameters(operation); Set expectedParameters = this.descriptorsByName.keySet(); Set undocumentedParameters = new HashSet(actualParameters); undocumentedParameters.removeAll(expectedParameters); @@ -58,7 +73,7 @@ abstract class AbstractParametersSnippet extends } } - protected abstract Set extractActualParameters(MvcResult result); + protected abstract Set extractActualParameters(Operation operation); protected abstract void verificationFailed(Set undocumentedParameters, Set missingParameters); diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java b/spring-restdocs/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java index a8ae9d2a..99c3c765 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java @@ -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 document(MvcResult result) throws IOException { - Map model = super.document(result); - model.put("path", remoteQueryStringIfPresent(extractUrlTemplate(result))); + protected Map createModel(Operation operation) throws IOException { + Map model = super.createModel(operation); + model.put("path", remoteQueryStringIfPresent(extractUrlTemplate(operation))); return model; } @@ -63,8 +63,8 @@ class PathParametersSnippet extends AbstractParametersSnippet { } @Override - protected Set extractActualParameters(MvcResult result) { - String urlTemplate = extractUrlTemplate(result); + protected Set extractActualParameters(Operation operation) { + String urlTemplate = extractUrlTemplate(operation); Matcher matcher = NAMES_PATTERN.matcher(urlTemplate); Set 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 " diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java b/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java index 1916c9b6..386fcd1a 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java @@ -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 extractActualParameters(MvcResult result) { - return result.getRequest().getParameterMap().keySet(); + protected Set extractActualParameters(Operation operation) { + return operation.getRequest().getParameters().keySet(); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/response/ContentModifyingReponsePostProcessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/response/ContentModifyingReponsePostProcessor.java index 44d78999..52640b3f 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/response/ContentModifyingReponsePostProcessor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/response/ContentModifyingReponsePostProcessor.java @@ -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); } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentableHttpServletRequest.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentableHttpServletRequest.java deleted file mode 100644 index 7c243399..00000000 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentableHttpServletRequest.java +++ /dev/null @@ -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 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 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 map) { - StringBuilder sb = new StringBuilder(); - for (Map.Entry 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); - } - } - -} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/Snippet.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/Snippet.java index 323af9e2..63438924 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/Snippet.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/Snippet.java @@ -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; } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java index de9f966a..7fc188bc 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java @@ -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 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 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 document(MvcResult result) throws IOException; + protected abstract Map createModel(Operation operation) + throws IOException; } \ No newline at end of file diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index b8344b2f..8af90689 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -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() diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java index b1cec398..48a8a566 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java @@ -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()); } } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index 4d569339..3eacda6e 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -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) { diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java index f9f7bac5..068d6b88 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java @@ -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()); } } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java index 1c258a26..161fefbf 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java @@ -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 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 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); } } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java index 9e6256d7..6711ae19 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java @@ -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) { diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index 99ad2bc6..97979bdc 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -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. emptyList()).document( - "undocumented-link", result()); + new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "bar")), + Collections. 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 linksByRel = new LinkedMultiValueMap(); @Override - public MultiValueMap extractLinks(MockHttpServletResponse response) + public MultiValueMap extractLinks(OperationResponse response) throws IOException { return this.linksByRel; } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/MockMvcOperationRequestFactoryTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/MockMvcOperationRequestFactoryTests.java new file mode 100644 index 00000000..9d286610 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/MockMvcOperationRequestFactoryTests.java @@ -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())); + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index fb257e54..bec7ee7e 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -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. emptyList()).document( - "undocumented-request-field", result(get("/foo").content("{\"a\": 5}"))); + new RequestFieldsSnippet(Collections. 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("5charlie").contentType( - MediaType.APPLICATION_XML))); + fieldWithPath("a").description("three").type("a"))) + .document(new OperationBuilder("xml-request") + .request("http://localhost") + .content("5charlie") + .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. emptyList()).document( - "undocumented-xml-request-field", - result(get("/foo").content("5").contentType( - MediaType.APPLICATION_XML))); + new RequestFieldsSnippet(Collections. emptyList()) + .document(new OperationBuilder("undocumented-xml-request-field") + .request("http://localhost") + .content("5") + .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("5"))); + .document(new OperationBuilder("missing-xml-request") + .request("http://localhost") + .content("5") + .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( - ""))); + fieldWithPath("a").description("one"))).document(new OperationBuilder( + "missing-xml-request-fields").request("http://localhost") + .content("") + .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("5"))); + .document(new OperationBuilder( + "undocumented-xml-request-field-and-missing-xml-request-field") + .request("http://localhost") + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); } private FileSystemResource snippetResource(String name) { diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index a768f05b..9018f96d 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -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("5charlie"); 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("5charlie") + .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("5"); - new ResponseFieldsSnippet(Collections. emptyList()).document( - "undocumented-xml-response-field", result(response)); + new ResponseFieldsSnippet(Collections. emptyList()) + .document(new OperationBuilder("undocumented-xml-response-field") + .response() + .content("5") + .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("5"); 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("5") + .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(""); 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("") + .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("5"); 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("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); } private FileSystemResource snippetResource(String name) { diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index 43ed107b..c4204d8c 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -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. 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()); } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index fd74544d..0ec85773 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -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. 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) { diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/response/ContentModifyingResponsePostProcessorTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/response/ContentModifyingResponsePostProcessorTests.java index dd07830a..09487a3d 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/response/ContentModifyingResponsePostProcessorTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/response/ContentModifyingResponsePostProcessorTests.java @@ -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 { diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs/src/test/java/org/springframework/restdocs/test/OperationBuilder.java new file mode 100644 index 00000000..fbb19337 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -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 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 partBuilders = new ArrayList<>(); + + public OperationRequestBuilder(String uri) { + this.requestUri = URI.create(uri); + } + + private OperationRequest buildRequest() { + List 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(); + } + + } + +}