Add support for generating an HTTP request documentation snippet

Closes gh-13
This commit is contained in:
Andy Wilkinson
2015-03-25 13:49:44 +00:00
parent 86b431018d
commit e00734d693
16 changed files with 725 additions and 445 deletions

View File

@@ -279,9 +279,9 @@ command for the request and the resulting response to files in a directory named
`index` in the project's `build/generated-snippets/` directory. Three files will
be written:
- `index/request.asciidoc`
- `index/response.asciidoc`
- `index/request-response.asciidoc`
- `index/curl-request.asciidoc`
- `index/http-request.asciidoc`
- `index/http-response.asciidoc`
#### Parameterized output directories
@@ -340,8 +340,8 @@ which the snippets are written. For example, to include both the request and res
snippets described above:
```
include::{generated}/index/request.asciidoc[]
include::{generated}/index/response.asciidoc[]
include::{generated}/index/curl-request.asciidoc[]
include::{generated}/index/http-response.asciidoc[]
```
## Generating snippets in your IDE

View File

@@ -69,7 +69,7 @@ include::{generated}/error-example/response-fields.asciidoc[]
For example, a request that attempts to apply a non-existent tag to a note will produce a
`400 Bad Request` response:
include::{generated}/error-example/response.asciidoc[]
include::{generated}/error-example/http-response.asciidoc[]
[[overview-hypermedia]]
== Hypermedia
@@ -103,7 +103,7 @@ include::{generated}/index-example/response-fields.asciidoc[]
==== Example response
include::{generated}/index-example/response.asciidoc[]
include::{generated}/index-example/http-response.asciidoc[]
@@ -132,11 +132,11 @@ include::{generated}/notes-list-example/response-fields.asciidoc[]
==== Example request
include::{generated}/notes-list-example/request.asciidoc[]
include::{generated}/notes-list-example/curl-request.asciidoc[]
==== Example response
include::{generated}/notes-list-example/response.asciidoc[]
include::{generated}/notes-list-example/http-response.asciidoc[]
@@ -151,11 +151,11 @@ include::{generated}/notes-create-example/request-fields.asciidoc[]
==== Example request
include::{generated}/notes-create-example/request.asciidoc[]
include::{generated}/notes-create-example/curl-request.asciidoc[]
==== Example response
include::{generated}/notes-create-example/response.asciidoc[]
include::{generated}/notes-create-example/http-response.asciidoc[]
@@ -177,11 +177,11 @@ include::{generated}/tags-list-example/response-fields.asciidoc[]
==== Example request
include::{generated}/tags-list-example/request.asciidoc[]
include::{generated}/tags-list-example/curl-request.asciidoc[]
==== Example response
include::{generated}/tags-list-example/response.asciidoc[]
include::{generated}/tags-list-example/http-response.asciidoc[]
@@ -196,11 +196,11 @@ include::{generated}/tags-create-example/request-fields.asciidoc[]
==== Example request
include::{generated}/tags-create-example/request.asciidoc[]
include::{generated}/tags-create-example/curl-request.asciidoc[]
==== Example response
include::{generated}/tags-create-example/response.asciidoc[]
include::{generated}/tags-create-example/http-response.asciidoc[]
@@ -229,11 +229,11 @@ include::{generated}/note-get-example/response-fields.asciidoc[]
==== Example request
include::{generated}/note-get-example/request.asciidoc[]
include::{generated}/note-get-example/curl-request.asciidoc[]
==== Example response
include::{generated}/note-get-example/response.asciidoc[]
include::{generated}/note-get-example/http-response.asciidoc[]
@@ -250,11 +250,11 @@ To leave an attribute of a note unchanged, any of the above may be omitted from
==== Example request
include::{generated}/note-update-example/request.asciidoc[]
include::{generated}/note-update-example/curl-request.asciidoc[]
==== Example response
include::{generated}/note-update-example/response.asciidoc[]
include::{generated}/note-update-example/http-response.asciidoc[]
@@ -283,11 +283,11 @@ include::{generated}/tag-get-example/response-fields.asciidoc[]
==== Example request
include::{generated}/tag-get-example/request.asciidoc[]
include::{generated}/tag-get-example/curl-request.asciidoc[]
==== Example response
include::{generated}/tag-get-example/response.asciidoc[]
include::{generated}/tag-get-example/http-response.asciidoc[]
@@ -302,8 +302,8 @@ include::{generated}/tag-update-example/request-fields.asciidoc[]
==== Example request
include::{generated}/tag-update-example/request.asciidoc[]
include::{generated}/tag-update-example/curl-request.asciidoc[]
==== Example response
include::{generated}/tag-update-example/response.asciidoc[]
include::{generated}/tag-update-example/http-response.asciidoc[]

View File

@@ -42,12 +42,12 @@ $ java -jar build/libs/*.jar
You can check that the service is up and running by executing a simple request using
cURL:
include::{generated}/index/1/request.asciidoc[]
include::{generated}/index/1/curl-request.asciidoc[]
This request should yield the following response in the
http://stateless.co/hal_specification.html[Hypertext Application Language (HAL)] format:
include::{generated}/index/1/response.asciidoc[]
include::{generated}/index/1/http-response.asciidoc[]
Note the `_links` in the JSON response. They are key to navigating the API.
@@ -59,26 +59,26 @@ Now that you've started the service and verified that it works, the next step is
it to create a new note. As you saw above, the URI for working with notes is included as
a link when you perform a `GET` request against the root of the service:
include::{generated}/index/1/response.asciidoc[]
include::{generated}/index/1/http-response.asciidoc[]
To create a note, you need to execute a `POST` request to this URI including a JSON
payload containing the title and body of the note:
include::{generated}/creating-a-note/1/request.asciidoc[]
include::{generated}/creating-a-note/1/curl-request.asciidoc[]
The response from this request should have a status code of `201 Created` and contain a
`Location` header whose value is the URI of the newly created note:
include::{generated}/creating-a-note/1/response.asciidoc[]
include::{generated}/creating-a-note/1/http-response.asciidoc[]
To work with the newly created note you use the URI in the `Location` header. For example,
you can access the note's details by performing a `GET` request:
include::{generated}/creating-a-note/2/request.asciidoc[]
include::{generated}/creating-a-note/2/curl-request.asciidoc[]
This request will produce a response with the note's details in its body:
include::{generated}/creating-a-note/2/response.asciidoc[]
include::{generated}/creating-a-note/2/http-response.asciidoc[]
Note the `tags` link which we'll make use of later.
@@ -92,26 +92,26 @@ to tag a note, you must first create the tag.
Referring back to the response for the service's index, the URI for working with tags is
include as a link:
include::{generated}/index/1/response.asciidoc[]
include::{generated}/index/1/http-response.asciidoc[]
To create a tag you need to execute a `POST` request to this URI, including a JSON
payload containing the name of the tag:
include::{generated}/creating-a-note/3/request.asciidoc[]
include::{generated}/creating-a-note/3/curl-request.asciidoc[]
The response from this request should have a status code of `201 Created` and contain a
`Location` header whose value is the URI of the newly created tag:
include::{generated}/creating-a-note/3/response.asciidoc[]
include::{generated}/creating-a-note/3/http-response.asciidoc[]
To work with the newly created tag you use the URI in the `Location` header. For example
you can access the tag's details by performing a `GET` request:
include::{generated}/creating-a-note/4/request.asciidoc[]
include::{generated}/creating-a-note/4/curl-request.asciidoc[]
This request will produce a response with the tag's details in its body:
include::{generated}/creating-a-note/4/response.asciidoc[]
include::{generated}/creating-a-note/4/http-response.asciidoc[]
@@ -132,24 +132,25 @@ with it.
Once again we execute a `POST` request. However, this time, in an array named tags, we
include the URI of the tag we just created:
include::{generated}/creating-a-note/5/request.asciidoc[]
include::{generated}/creating-a-note/5/curl-request.asciidoc[]
Once again, the response's `Location` header tells us the URI of the newly created note:
include::{generated}/creating-a-note/5/response.asciidoc[]
include::{generated}/creating-a-note/5/http-response.asciidoc[]
As before, a `GET` request executed against this URI will retrieve the note's details:
include::{generated}/creating-a-note/6/request-response.asciidoc[]
include::{generated}/creating-a-note/6/curl-request.asciidoc[]
include::{generated}/creating-a-note/6/http-response.asciidoc[]
To verify that the tag has been associated with the note, we can perform a `GET` request
against the URI from the `tags` link:
include::{generated}/creating-a-note/7/request.asciidoc[]
include::{generated}/creating-a-note/7/curl-request.asciidoc[]
The response embeds information about the tag that we've just associated with the note:
include::{generated}/creating-a-note/7/response.asciidoc[]
include::{generated}/creating-a-note/7/http-response.asciidoc[]
@@ -159,17 +160,18 @@ An existing note can be tagged by executing a `PATCH` request against the note's
a body that contains the array of tags to be associated with the note. We'll used the
URI of the untagged note that we created earlier:
include::{generated}/creating-a-note/8/request.asciidoc[]
include::{generated}/creating-a-note/8/curl-request.asciidoc[]
This request should produce a `204 No Content` response:
include::{generated}/creating-a-note/8/response.asciidoc[]
include::{generated}/creating-a-note/8/http-response.asciidoc[]
When we first created this note, we noted the tags link included in its details:
include::{generated}/creating-a-note/2/response.asciidoc[]
include::{generated}/creating-a-note/2/http-response.asciidoc[]
We can use that link now and execute a `GET` request to see that the note now has a
single tag:
include::{generated}/creating-a-note/9/request-response.asciidoc[]
include::{generated}/creating-a-note/9/curl-request.asciidoc[]
include::{generated}/creating-a-note/9/http-response.asciidoc[]

View File

@@ -69,7 +69,7 @@ include::{generated}/error-example/response-fields.asciidoc
For example, a request that attempts to apply a non-existent tag to a note will produce a
`400 Bad Request` response:
include::{generated}/error-example/response.asciidoc[]
include::{generated}/error-example/http-response.asciidoc[]
[[overview-hypermedia]]
== Hypermedia
@@ -103,7 +103,7 @@ include::{generated}/index-example/response-fields.asciidoc[]
==== Example response
include::{generated}/index-example/response.asciidoc[]
include::{generated}/index-example/http-response.asciidoc[]
@@ -132,11 +132,11 @@ include::{generated}/notes-list-example/response-fields.asciidoc[]
==== Example request
include::{generated}/notes-list-example/request.asciidoc[]
include::{generated}/notes-list-example/curl-request.asciidoc[]
==== Example response
include::{generated}/notes-list-example/response.asciidoc[]
include::{generated}/notes-list-example/http-response.asciidoc[]
@@ -151,11 +151,11 @@ include::{generated}/notes-create-example/request-fields.asciidoc[]
==== Example request
include::{generated}/notes-create-example/request.asciidoc[]
include::{generated}/notes-create-example/curl-request.asciidoc[]
==== Example response
include::{generated}/notes-create-example/response.asciidoc[]
include::{generated}/notes-create-example/http-response.asciidoc[]
@@ -177,11 +177,11 @@ include::{generated}/tags-list-example/response-fields.asciidoc[]
==== Example request
include::{generated}/tags-list-example/request.asciidoc[]
include::{generated}/tags-list-example/curl-request.asciidoc[]
==== Example response
include::{generated}/tags-list-example/response.asciidoc[]
include::{generated}/tags-list-example/http-response.asciidoc[]
@@ -196,11 +196,11 @@ include::{generated}/tags-create-example/request-fields.asciidoc[]
==== Example request
include::{generated}/tags-create-example/request.asciidoc[]
include::{generated}/tags-create-example/curl-request.asciidoc[]
==== Example response
include::{generated}/tags-create-example/response.asciidoc[]
include::{generated}/tags-create-example/http-response.asciidoc[]
@@ -229,11 +229,11 @@ include::{generated}/note-get-example/response-fields.asciidoc[]
==== Example request
include::{generated}/note-get-example/request.asciidoc[]
include::{generated}/note-get-example/curl-request.asciidoc[]
==== Example response
include::{generated}/note-get-example/response.asciidoc[]
include::{generated}/note-get-example/http-response.asciidoc[]
@@ -250,11 +250,11 @@ To leave an attribute of a note unchanged, any of the above may be omitted from
==== Example request
include::{generated}/note-update-example/request.asciidoc[]
include::{generated}/note-update-example/curl-request.asciidoc[]
==== Example response
include::{generated}/note-update-example/response.asciidoc[]
include::{generated}/note-update-example/http-response.asciidoc[]
[[resources-tag]]
@@ -282,11 +282,11 @@ include::{generated}/tag-get-example/response-fields.asciidoc[]
==== Example request
include::{generated}/tag-get-example/request.asciidoc[]
include::{generated}/tag-get-example/curl-request.asciidoc[]
==== Example response
include::{generated}/tag-get-example/response.asciidoc[]
include::{generated}/tag-get-example/http-response.asciidoc[]
@@ -301,8 +301,8 @@ include::{generated}/tag-update-example/request-fields.asciidoc[]
==== Example request
include::{generated}/tag-update-example/request.asciidoc[]
include::{generated}/tag-update-example/curl-request.asciidoc[]
==== Example response
include::{generated}/tag-update-example/response.asciidoc[]
include::{generated}/tag-update-example/http-response.asciidoc[]

View File

@@ -42,11 +42,11 @@ $ java -jar build/libs/*.jar
You can check that the service is up and running by executing a simple request using
cURL:
include::{generated}/index/1/request.asciidoc[]
include::{generated}/index/1/curl-request.asciidoc[]
This request should yield the following response:
include::{generated}/index/1/response.asciidoc[]
include::{generated}/index/1/http-response.asciidoc[]
Note the `_links` in the JSON response. They are key to navigating the API.
@@ -58,26 +58,26 @@ Now that you've started the service and verified that it works, the next step is
it to create a new note. As you saw above, the URI for working with notes is included as
a link when you perform a `GET` request against the root of the service:
include::{generated}/index/1/response.asciidoc[]
include::{generated}/index/1/http-response.asciidoc[]
To create a note you need to execute a `POST` request to this URI, including a JSON
payload containing the title and body of the note:
include::{generated}/creating-a-note/1/request.asciidoc[]
include::{generated}/creating-a-note/1/curl-request.asciidoc[]
The response from this request should have a status code of `201 Created` and contain a
`Location` header whose value is the URI of the newly created note:
include::{generated}/creating-a-note/1/response.asciidoc[]
include::{generated}/creating-a-note/1/http-response.asciidoc[]
To work with the newly created note you use the URI in the `Location` header. For example
you can access the note's details by performing a `GET` request:
include::{generated}/creating-a-note/2/request.asciidoc[]
include::{generated}/creating-a-note/2/curl-request.asciidoc[]
This request will produce a response with the note's details in its body:
include::{generated}/creating-a-note/2/response.asciidoc[]
include::{generated}/creating-a-note/2/http-response.asciidoc[]
Note the `note-tags` link which we'll make use of later.
@@ -91,26 +91,26 @@ to tag a note, you must first create the tag.
Referring back to the response for the service's index, the URI for working with tags is
include as a link:
include::{generated}/index/1/response.asciidoc[]
include::{generated}/index/1/http-response.asciidoc[]
To create a tag you need to execute a `POST` request to this URI, including a JSON
payload containing the name of the tag:
include::{generated}/creating-a-note/3/request.asciidoc[]
include::{generated}/creating-a-note/3/curl-request.asciidoc[]
The response from this request should have a status code of `201 Created` and contain a
`Location` header whose value is the URI of the newly created tag:
include::{generated}/creating-a-note/3/response.asciidoc[]
include::{generated}/creating-a-note/3/http-response.asciidoc[]
To work with the newly created tag you use the URI in the `Location` header. For example
you can access the tag's details by performing a `GET` request:
include::{generated}/creating-a-note/4/request.asciidoc[]
include::{generated}/creating-a-note/4/curl-request.asciidoc[]
This request will produce a response with the tag's details in its body:
include::{generated}/creating-a-note/4/response.asciidoc[]
include::{generated}/creating-a-note/4/http-response.asciidoc[]
@@ -131,24 +131,25 @@ with it.
Once again we execute a `POST` request, but this time, in an array named tags, we include
the URI of the tag we just created:
include::{generated}/creating-a-note/5/request.asciidoc[]
include::{generated}/creating-a-note/5/curl-request.asciidoc[]
Once again, the response's `Location` header tells use the URI of the newly created note:
include::{generated}/creating-a-note/5/response.asciidoc[]
include::{generated}/creating-a-note/5/http-response.asciidoc[]
As before, a `GET` request executed against this URI will retrieve the note's details:
include::{generated}/creating-a-note/6/request-response.asciidoc[]
include::{generated}/creating-a-note/6/curl-request.asciidoc[]
include::{generated}/creating-a-note/6/http-response.asciidoc[]
To see the note's tags, execute a `GET` request against the URI of the note's
`note-tags` link:
include::{generated}/creating-a-note/7/request.asciidoc[]
include::{generated}/creating-a-note/7/curl-request.asciidoc[]
The response shows that, as expected, the note has a single tag:
include::{generated}/creating-a-note/7/response.asciidoc[]
include::{generated}/creating-a-note/7/http-response.asciidoc[]
@@ -158,17 +159,18 @@ An existing note can be tagged by executing a `PATCH` request against the note's
a body that contains the array of tags to be associated with the note. We'll use the
URI of the untagged note that we created earlier:
include::{generated}/creating-a-note/8/request.asciidoc[]
include::{generated}/creating-a-note/8/curl-request.asciidoc[]
This request should produce a `204 No Content` response:
include::{generated}/creating-a-note/8/response.asciidoc[]
include::{generated}/creating-a-note/8/http-response.asciidoc[]
When we first created this note, we noted the `note-tags` link included in its details:
include::{generated}/creating-a-note/2/response.asciidoc[]
include::{generated}/creating-a-note/2/http-response.asciidoc[]
We can use that link now and execute a `GET` request to see that the note now has a
single tag:
include::{generated}/creating-a-note/9/request-response.asciidoc[]
include::{generated}/creating-a-note/9/curl-request.asciidoc[]
include::{generated}/creating-a-note/9/http-response.asciidoc[]

View File

@@ -17,8 +17,8 @@
package org.springframework.restdocs;
import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRequest;
import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRequestAndResponse;
import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlResponse;
import static org.springframework.restdocs.http.HttpDocumentation.documentHttpRequest;
import static org.springframework.restdocs.http.HttpDocumentation.documentHttpResponse;
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.documentLinks;
import static org.springframework.restdocs.payload.PayloadDocumentation.documentRequestFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.documentResponseFields;
@@ -53,8 +53,8 @@ public class RestDocumentationResultHandler implements ResultHandler {
this.delegates = new ArrayList<ResultHandler>();
this.delegates.add(documentCurlRequest(this.outputDir));
this.delegates.add(documentCurlResponse(this.outputDir));
this.delegates.add(documentCurlRequestAndResponse(this.outputDir));
this.delegates.add(documentHttpRequest(this.outputDir));
this.delegates.add(documentHttpResponse(this.outputDir));
}
@Override

View File

@@ -21,6 +21,7 @@ import org.springframework.test.web.servlet.request.RequestPostProcessor;
import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder;
import org.springframework.test.web.servlet.setup.MockMvcConfigurer;
import org.springframework.test.web.servlet.setup.MockMvcConfigurerAdapter;
import org.springframework.util.StringUtils;
import org.springframework.web.context.WebApplicationContext;
/**
@@ -108,8 +109,19 @@ public class RestDocumentationConfigurer extends MockMvcConfigurerAdapter {
request.setScheme(RestDocumentationConfigurer.this.scheme);
request.setServerPort(RestDocumentationConfigurer.this.port);
request.setServerName(RestDocumentationConfigurer.this.host);
configureContentLengthHeaderIfAppropriate(request);
return request;
}
private void configureContentLengthHeaderIfAppropriate(
MockHttpServletRequest request) {
long contentLength = request.getContentLengthLong();
if (contentLength > 0
&& !StringUtils.hasText(request.getHeader("Content-Length"))) {
request.addHeader("Content-Length", request.getContentLengthLong());
}
}
};
}

View File

@@ -1,35 +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.curl;
/**
* Configuration for documenting Curl requests and responses
*
* @author Andy Wilkinson
*/
class CurlConfiguration {
private boolean includeResponseHeaders = true;
boolean isIncludeResponseHeaders() {
return this.includeResponseHeaders;
}
void setIncludeResponseHeaders(boolean includeResponseHeaders) {
this.includeResponseHeaders = includeResponseHeaders;
}
}

View File

@@ -16,29 +16,21 @@
package org.springframework.restdocs.curl;
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 java.util.List;
import java.util.Map.Entry;
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.restdocs.snippet.DocumentationWriter;
import org.springframework.restdocs.snippet.DocumentationWriter.DocumentationAction;
import org.springframework.restdocs.snippet.SnippetWritingResultHandler;
import org.springframework.restdocs.util.DocumentableHttpServletRequest;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMethod;
/**
* Static factory methods for documenting a RESTful API as if it were being driven using
* the cURL command-line utility.
*
*
* @author Andy Wilkinson
* @author Yann Le Guern
*/
@@ -50,58 +42,17 @@ public abstract class CurlDocumentation {
/**
* Produces a documentation snippet containing the request formatted as a cURL command
*
*
* @param outputDir The directory to which snippet should be written
* @return the handler that will produce the snippet
*/
public static CurlSnippetResultHandler documentCurlRequest(String outputDir) {
return new CurlSnippetResultHandler(outputDir, "request") {
public static SnippetWritingResultHandler documentCurlRequest(String outputDir) {
return new SnippetWritingResultHandler(outputDir, "curl-request") {
@Override
public void handle(MvcResult result, DocumentationWriter writer)
throws IOException {
writer.shellCommand(new CurlRequestDocumentationAction(writer, result,
getCurlConfiguration()));
}
};
}
/**
* Produces a documentation snippet containing the response formatted as the response
* to a cURL command
*
* @param outputDir The directory to which snippet should be written
* @return the handler that will produce the snippet
*/
public static CurlSnippetResultHandler documentCurlResponse(String outputDir) {
return new CurlSnippetResultHandler(outputDir, "response") {
@Override
public void handle(MvcResult result, DocumentationWriter writer)
throws IOException {
writer.codeBlock("http", new CurlResponseDocumentationAction(writer,
result, getCurlConfiguration()));
}
};
}
/**
* Produces a documentation snippet containing both the request formatted as a cURL
* command and the response formatted formatted s the response to a cURL command.
*
* @param outputDir The directory to which the snippet should be written
* @return the handler that will produce the snippet
*/
public static CurlSnippetResultHandler documentCurlRequestAndResponse(String outputDir) {
return new CurlSnippetResultHandler(outputDir, "request-response") {
@Override
public void handle(MvcResult result, DocumentationWriter writer)
throws IOException {
writer.shellCommand(new CurlRequestDocumentationAction(writer, result,
getCurlConfiguration()));
writer.codeBlock("http", new CurlResponseDocumentationAction(writer,
result, getCurlConfiguration()));
writer.shellCommand(new CurlRequestDocumentationAction(writer, result));
}
};
}
@@ -121,152 +72,55 @@ public abstract class CurlDocumentation {
private final MvcResult result;
private final CurlConfiguration curlConfiguration;
CurlRequestDocumentationAction(DocumentationWriter writer, MvcResult result,
CurlConfiguration curlConfiguration) {
CurlRequestDocumentationAction(DocumentationWriter writer, MvcResult result) {
this.writer = writer;
this.result = result;
this.curlConfiguration = curlConfiguration;
}
@Override
public void perform() throws IOException {
MockHttpServletRequest request = this.result.getRequest();
DocumentableHttpServletRequest request = new DocumentableHttpServletRequest(
this.result.getRequest());
this.writer.print(String.format("curl %s://%s", request.getScheme(),
request.getServerName()));
request.getHost()));
if (isNonStandardPort(request)) {
this.writer.print(String.format(":%d", request.getServerPort()));
this.writer.print(String.format(":%d", request.getPort()));
}
this.writer.print(getRequestUriWithQueryString(request));
this.writer.print(request.getRequestUriWithQueryString().replace("&", "\\&"));
if (this.curlConfiguration.isIncludeResponseHeaders()) {
this.writer.print(" -i");
}
this.writer.print(" -i");
if (!isGetRequest(request)) {
if (!request.isGetRequest()) {
this.writer.print(String.format(" -X %s", request.getMethod()));
}
for (String headerName : iterable(request.getHeaderNames())) {
for (String header : iterable(request.getHeaders(headerName))) {
this.writer
.print(String.format(" -H \"%s: %s\"", headerName, header));
for (Entry<String, List<String>> entry : request.getHeaders().entrySet()) {
for (String header : entry.getValue()) {
this.writer.print(String.format(" -H \"%s: %s\"", entry.getKey(),
header));
}
}
if (request.getContentLengthLong() > 0) {
this.writer.print(String.format(" -d '%s'", getContent(request)));
if (request.getContentLength() > 0) {
this.writer
.print(String.format(" -d '%s'", request.getContentAsString()));
}
else if (isPostRequest(request)) {
Map<String, String[]> parameters = request.getParameterMap();
if (!parameters.isEmpty()) {
this.writer.print(String
.format(" -d '%s'", toQueryString(parameters)));
else if (request.isPostRequest()) {
String queryString = request.getParameterMapAsQueryString();
if (StringUtils.hasText(queryString)) {
this.writer.print(String.format(" -d '%s'",
queryString.replace("&", "\\&")));
}
}
this.writer.println();
}
private boolean isGetRequest(HttpServletRequest request) {
return RequestMethod.GET == RequestMethod.valueOf(request.getMethod());
}
private boolean isPostRequest(HttpServletRequest request) {
return RequestMethod.POST == RequestMethod.valueOf(request.getMethod());
}
private boolean isNonStandardPort(HttpServletRequest request) {
return (SCHEME_HTTP.equals(request.getScheme()) && request.getServerPort() != STANDARD_PORT_HTTP)
|| (SCHEME_HTTPS.equals(request.getScheme()) && request
.getServerPort() != STANDARD_PORT_HTTPS);
}
private String getRequestUriWithQueryString(HttpServletRequest request) {
StringBuilder sb = new StringBuilder();
sb.append(request.getRequestURI());
String queryString = getQueryString(request);
if (StringUtils.hasText(queryString)) {
sb.append('?').append(queryString);
}
return sb.toString();
}
private String getQueryString(HttpServletRequest request) {
if (request.getQueryString() != null) {
return request.getQueryString().replace("&", "\\&");
}
if (isGetRequest(request)) {
return toQueryString(request.getParameterMap());
}
return null;
}
private static String toQueryString(Map<String, String[]> map) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String[]> entry : map.entrySet()) {
for (String value : entry.getValue()) {
if (sb.length() > 0) {
sb.append("\\&");
}
sb.append(urlEncodeUTF8(entry.getKey())).append('=')
.append(urlEncodeUTF8(value));
}
}
return sb.toString();
}
private static String urlEncodeUTF8(String s) {
try {
return URLEncoder.encode(s, "UTF-8");
}
catch (UnsupportedEncodingException ex) {
throw new IllegalStateException("Unable to URL encode " + s
+ " using UTF-8", ex);
}
}
private String getContent(MockHttpServletRequest request) throws IOException {
StringWriter bodyWriter = new StringWriter();
FileCopyUtils.copy(request.getReader(), bodyWriter);
return bodyWriter.toString();
}
}
private static final class CurlResponseDocumentationAction implements
DocumentationAction {
private final DocumentationWriter writer;
private final MvcResult result;
private final CurlConfiguration curlConfiguration;
CurlResponseDocumentationAction(DocumentationWriter writer, MvcResult result,
CurlConfiguration curlConfiguration) {
this.writer = writer;
this.result = result;
this.curlConfiguration = curlConfiguration;
}
@Override
public void perform() throws IOException {
if (this.curlConfiguration.isIncludeResponseHeaders()) {
HttpStatus status = HttpStatus.valueOf(this.result.getResponse()
.getStatus());
this.writer.println(String.format("HTTP/1.1 %d %s", status.value(),
status.getReasonPhrase()));
for (String headerName : this.result.getResponse().getHeaderNames()) {
for (String header : this.result.getResponse().getHeaders(headerName)) {
this.writer.println(String.format("%s: %s", headerName, header));
}
}
this.writer.println();
}
this.writer.println(this.result.getResponse().getContentAsString());
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);
}
}

View File

@@ -1,51 +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.curl;
import org.springframework.restdocs.snippet.SnippetWritingResultHandler;
import org.springframework.test.web.servlet.ResultHandler;
/**
* Abstract base class for Spring Mock MVC {@link ResultHandler ResultHandlers} that
* produce documentation snippets relating to cURL.
*
* @author Andy Wilkinson
*/
public abstract class CurlSnippetResultHandler extends SnippetWritingResultHandler {
private final CurlConfiguration curlConfiguration = new CurlConfiguration();
public CurlSnippetResultHandler(String outputDir, String fileName) {
super(outputDir, fileName);
}
CurlConfiguration getCurlConfiguration() {
return this.curlConfiguration;
}
/**
* Specify whether or not the generated cURL snippets should have contents as if cURL
* had been invoked with {@code -i, --include}.
*
* @param include {@code true} to use {@code -i, --include}, otherwise false
* @return {@code this}
*/
public CurlSnippetResultHandler includeResponseHeaders(boolean include) {
this.curlConfiguration.setIncludeResponseHeaders(include);
return this;
}
}

View File

@@ -0,0 +1,155 @@
/*
* 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.http;
import java.io.IOException;
import java.util.List;
import java.util.Map.Entry;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.restdocs.snippet.DocumentationWriter;
import org.springframework.restdocs.snippet.DocumentationWriter.DocumentationAction;
import org.springframework.restdocs.snippet.SnippetWritingResultHandler;
import org.springframework.restdocs.util.DocumentableHttpServletRequest;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.StringUtils;
/**
* Static factory methods for documenting a RESTful API's HTTP requests.
*
* @author Andy Wilkinson
*/
public abstract class HttpDocumentation {
private HttpDocumentation() {
}
/**
* Produces a documentation snippet containing the request formatted as an HTTP
* request
*
* @param outputDir The directory to which snippet should be written
* @return the handler that will produce the snippet
*/
public static SnippetWritingResultHandler documentHttpRequest(String outputDir) {
return new SnippetWritingResultHandler(outputDir, "http-request") {
@Override
public void handle(MvcResult result, DocumentationWriter writer)
throws IOException {
writer.codeBlock("http", new HttpRequestDocumentationAction(writer,
result));
}
};
}
/**
* Produces a documentation snippet containing the response formatted as the HTTP
* response sent by the server
*
* @param outputDir The directory to which snippet should be written
* @return the handler that will produce the snippet
*/
public static SnippetWritingResultHandler documentHttpResponse(String outputDir) {
return new SnippetWritingResultHandler(outputDir, "http-response") {
@Override
public void handle(MvcResult result, DocumentationWriter writer)
throws IOException {
writer.codeBlock("http", new HttpResponseDocumentationAction(writer,
result));
}
};
}
private static class HttpRequestDocumentationAction implements DocumentationAction {
private final DocumentationWriter writer;
private final MvcResult result;
HttpRequestDocumentationAction(DocumentationWriter writer, MvcResult result) {
this.writer = writer;
this.result = result;
}
@Override
public void perform() throws IOException {
DocumentableHttpServletRequest request = new DocumentableHttpServletRequest(
this.result.getRequest());
this.writer.printf("%s %s HTTP/1.1%n", request.getMethod(),
request.getRequestUriWithQueryString());
for (Entry<String, List<String>> header : request.getHeaders().entrySet()) {
for (String value : header.getValue()) {
this.writer.printf("%s: %s%n", header.getKey(), value);
}
}
if (requiresFormEncodingContentType(request)) {
this.writer.printf("%s: %s%n", HttpHeaders.CONTENT_TYPE,
MediaType.APPLICATION_FORM_URLENCODED_VALUE);
}
this.writer.println();
if (request.getContentLength() > 0) {
this.writer.println(request.getContentAsString());
}
else if (request.isPostRequest()) {
String queryString = request.getParameterMapAsQueryString();
if (StringUtils.hasText(queryString)) {
this.writer.println(queryString);
}
}
}
private boolean requiresFormEncodingContentType(
DocumentableHttpServletRequest request) {
return request.getHeaders().get(HttpHeaders.CONTENT_TYPE) == null
&& request.isPostRequest()
&& StringUtils.hasText(request.getParameterMapAsQueryString());
}
}
private static final class HttpResponseDocumentationAction implements
DocumentationAction {
private final DocumentationWriter writer;
private final MvcResult result;
HttpResponseDocumentationAction(DocumentationWriter writer, MvcResult result) {
this.writer = writer;
this.result = result;
}
@Override
public void perform() throws IOException {
HttpStatus status = HttpStatus.valueOf(this.result.getResponse().getStatus());
this.writer.println(String.format("HTTP/1.1 %d %s", status.value(),
status.getReasonPhrase()));
for (String headerName : this.result.getResponse().getHeaderNames()) {
for (String header : this.result.getResponse().getHeaders(headerName)) {
this.writer.println(String.format("%s: %s", headerName, header));
}
}
this.writer.println();
this.writer.println(this.result.getResponse().getContentAsString());
}
}
}

View File

@@ -0,0 +1,223 @@
/*
* 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.util;
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.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMethod;
/**
* An {@link HttpServletRequest} wrapper that provides a limited set of methods intended
* to help in the documentation of the request.
*
* @author Andy Wilkinson
*
*/
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());
}
/**
* Returns a Map of the request's headers. The entries are ordered based on the
* ordering of {@link HttpServletRequest#getHeaderNames()} and
* {@link HttpServletRequest#getHeaders(String)}.
*
* @return the request's headers, keyed by name
* @see HttpServletRequest#getHeaderNames()
* @see HttpServletRequest#getHeaders(String)
*/
public Map<String, List<String>> getHeaders() {
Map<String, List<String>> headersByName = new LinkedHashMap<String, List<String>>();
for (String headerName : iterable(this.delegate.getHeaderNames())) {
List<String> headers = new ArrayList<String>();
headersByName.put(headerName, headers);
for (String header : iterable(this.delegate.getHeaders(headerName))) {
headers.add(header);
}
}
return headersByName;
}
/**
* 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());
}
private String getQueryString() {
if (this.delegate.getQueryString() != null) {
return this.delegate.getQueryString();
}
if (isGetRequest()) {
return getParameterMapAsQueryString();
}
return null;
}
private static String toQueryString(Map<String, String[]> map) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String[]> entry : map.entrySet()) {
for (String value : entry.getValue()) {
if (sb.length() > 0) {
sb.append("&");
}
sb.append(urlEncodeUTF8(entry.getKey())).append('=')
.append(urlEncodeUTF8(value));
}
}
return sb.toString();
}
private static String urlEncodeUTF8(String s) {
try {
return URLEncoder.encode(s, "UTF-8");
}
catch (UnsupportedEncodingException ex) {
throw new IllegalStateException("Unable to URL encode " + s + " using UTF-8",
ex);
}
}
}

View File

@@ -84,7 +84,8 @@ public class RestDocumentationIntegrationTests {
mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()).andDo(document("basic"));
assertExpectedSnippetFilesExist(new File("build/generated-snippets/basic"),
"request.asciidoc", "response.asciidoc", "request-response.asciidoc");
"http-request.asciidoc", "http-response.asciidoc",
"curl-request.asciidoc");
}
@Test
@@ -96,7 +97,8 @@ public class RestDocumentationIntegrationTests {
.andExpect(status().isOk()).andDo(document("{method-name}"));
assertExpectedSnippetFilesExist(new File(
"build/generated-snippets/parameterized-output-directory"),
"request.asciidoc", "response.asciidoc", "request-response.asciidoc");
"http-request.asciidoc", "http-response.asciidoc",
"curl-request.asciidoc");
}
@Test
@@ -108,20 +110,23 @@ public class RestDocumentationIntegrationTests {
mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect(
status().isOk());
assertExpectedSnippetFilesExist(
new File("build/generated-snippets/multi-step-1/"), "request.asciidoc",
"response.asciidoc", "request-response.asciidoc");
new File("build/generated-snippets/multi-step-1/"),
"http-request.asciidoc", "http-response.asciidoc",
"curl-request.asciidoc");
mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect(
status().isOk());
assertExpectedSnippetFilesExist(
new File("build/generated-snippets/multi-step-2/"), "request.asciidoc",
"response.asciidoc", "request-response.asciidoc");
new File("build/generated-snippets/multi-step-2/"),
"http-request.asciidoc", "http-response.asciidoc",
"curl-request.asciidoc");
mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect(
status().isOk());
assertExpectedSnippetFilesExist(
new File("build/generated-snippets/multi-step-3/"), "request.asciidoc",
"response.asciidoc", "request-response.asciidoc");
new File("build/generated-snippets/multi-step-3/"),
"http-request.asciidoc", "http-response.asciidoc",
"curl-request.asciidoc");
}

View File

@@ -16,7 +16,11 @@
package org.springframework.restdocs.config;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import java.net.URI;
@@ -72,6 +76,25 @@ public class RestDocumentationConfigurerTests {
assertUriConfiguration("http", "localhost", 8081);
}
@Test
public void noContentLengthHeaderWhenRequestHasNotContent() {
RequestPostProcessor postProcessor = new RestDocumentationConfigurer().withPort(
8081).beforeMockMvcCreated(null, null);
postProcessor.postProcessRequest(this.request);
assertThat(this.request.getHeader("Content-Length"), is(nullValue()));
}
@Test
public void contentLengthHeaderIsSetWhenRequestHasContent() {
RequestPostProcessor postProcessor = new RestDocumentationConfigurer().withPort(
8081).beforeMockMvcCreated(null, null);
byte[] content = "Hello, world".getBytes();
this.request.setContent(content);
postProcessor.postProcessRequest(this.request);
assertThat(this.request.getHeader("Content-Length"),
is(equalTo(Integer.toString(content.length))));
}
private void assertUriConfiguration(String scheme, String host, int port) {
assertEquals(scheme, this.request.getScheme());
assertEquals(host, this.request.getServerName());

View File

@@ -17,12 +17,8 @@
package org.springframework.restdocs.curl;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.hasItems;
import static org.hamcrest.CoreMatchers.not;
import static org.junit.Assert.assertThat;
import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRequest;
import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRequestAndResponse;
import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlResponse;
import java.io.BufferedReader;
import java.io.File;
@@ -34,10 +30,8 @@ import java.util.List;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.restdocs.StubMvcResult;
/**
@@ -77,15 +71,6 @@ public class CurlDocumentationTests {
hasItem("$ curl http://localhost/foo -i -X POST"));
}
@Test
public void requestWithoutResponseHeaderInclusion() throws IOException {
documentCurlRequest("request-without-response-header-inclusion")
.includeResponseHeaders(false)
.handle(new StubMvcResult(new MockHttpServletRequest("GET", "/foo"), null));
assertThat(requestSnippetLines("request-without-response-header-inclusion"),
hasItem("$ curl http://localhost/foo"));
}
@Test
public void requestWithContent() throws IOException {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
@@ -192,67 +177,6 @@ public class CurlDocumentationTests {
hasItem("$ curl http://localhost/foo -i -H \"Content-Type: application/json\" -H \"a: alpha\""));
}
@Test
public void basicResponse() throws IOException {
documentCurlResponse("basic-response").handle(
new StubMvcResult(null, new MockHttpServletResponse()));
assertThat(responseSnippetLines("basic-response"), hasItem("HTTP/1.1 200 OK"));
}
@Test
public void nonOkResponse() throws IOException {
MockHttpServletResponse response = new MockHttpServletResponse();
response.setStatus(HttpStatus.BAD_REQUEST.value());
documentCurlResponse("non-ok-response").handle(new StubMvcResult(null, response));
assertThat(responseSnippetLines("non-ok-response"),
hasItem("HTTP/1.1 400 Bad Request"));
}
@Test
public void responseWithHeaders() throws IOException {
MockHttpServletResponse response = new MockHttpServletResponse();
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setHeader("a", "alpha");
documentCurlResponse("non-ok-response").handle(new StubMvcResult(null, response));
assertThat(responseSnippetLines("non-ok-response"),
hasItems("HTTP/1.1 200 OK", "Content-Type: application/json", "a: alpha"));
}
@Test
public void responseWithHeaderInclusionDisabled() throws IOException {
MockHttpServletResponse response = new MockHttpServletResponse();
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setHeader("a", "alpha");
response.getWriter().append("content");
documentCurlResponse("response-with-header-inclusion-disabled")
.includeResponseHeaders(false).handle(new StubMvcResult(null, response));
List<String> responseSnippetLines = responseSnippetLines("response-with-header-inclusion-disabled");
assertThat(
responseSnippetLines,
not(hasItems("HTTP/1.1 200 OK", "Content-Type: application/json",
"a: alpha")));
assertThat(responseSnippetLines, hasItem("content"));
}
@Test
public void responseWithContent() throws IOException {
MockHttpServletResponse response = new MockHttpServletResponse();
response.getWriter().append("content");
documentCurlResponse("response-with-content").handle(
new StubMvcResult(null, response));
assertThat(responseSnippetLines("response-with-content"),
hasItems("HTTP/1.1 200 OK", "content"));
}
@Test
public void requestAndResponse() throws IOException {
documentCurlRequestAndResponse("request-and-response").handle(
new StubMvcResult(new MockHttpServletRequest("GET", "/foo"),
new MockHttpServletResponse()));
assertThat(requestResponseSnippetLines("request-and-response"),
hasItems("$ curl http://localhost/foo -i", "HTTP/1.1 200 OK"));
}
@Test
public void httpWithNonStandardPort() throws IOException {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
@@ -296,16 +220,7 @@ public class CurlDocumentationTests {
}
private List<String> requestSnippetLines(String snippetName) throws IOException {
return snippetLines(snippetName, "request");
}
private List<String> responseSnippetLines(String snippetName) throws IOException {
return snippetLines(snippetName, "response");
}
private List<String> requestResponseSnippetLines(String snippetName)
throws IOException {
return snippetLines(snippetName, "request-response");
return snippetLines(snippetName, "curl-request");
}
private List<String> snippetLines(String snippetName, String snippetType)

View File

@@ -0,0 +1,175 @@
/*
* 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.http;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.hasItems;
import static org.junit.Assert.assertThat;
import static org.springframework.restdocs.http.HttpDocumentation.documentHttpRequest;
import static org.springframework.restdocs.http.HttpDocumentation.documentHttpResponse;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.restdocs.StubMvcResult;
/**
* Tests for {@link HttpDocumentation}
*
* @author Andy Wilkinson
*/
public class HttpDocumentationTests {
private final File outputDir = new File("build/http-documentation-tests");
@Before
public void setup() {
System.setProperty("org.springframework.restdocs.outputDir",
this.outputDir.getAbsolutePath());
}
@After
public void cleanup() {
System.clearProperty("org.springframework.restdocs.outputDir");
}
@Test
public void getRequest() throws IOException {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
request.addHeader("Alpha", "a");
documentHttpRequest("get-request").handle(new StubMvcResult(request, null));
assertThat(requestSnippetLines("get-request"),
hasItems("GET /foo HTTP/1.1", "Alpha: a"));
}
@Test
public void getRequestWithQueryString() throws IOException {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo?bar=baz");
documentHttpRequest("get-request-with-query-string").handle(
new StubMvcResult(request, null));
assertThat(requestSnippetLines("get-request-with-query-string"),
hasItems("GET /foo?bar=baz HTTP/1.1"));
}
@Test
public void getRequestWithParameter() throws IOException {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
request.addParameter("b&r", "baz");
documentHttpRequest("get-request-with-parameter").handle(
new StubMvcResult(request, null));
assertThat(requestSnippetLines("get-request-with-parameter"),
hasItems("GET /foo?b%26r=baz HTTP/1.1"));
}
@Test
public void postRequestWithContent() throws IOException {
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/foo");
byte[] content = "Hello, world".getBytes();
request.setContent(content);
documentHttpRequest("post-request-with-content").handle(
new StubMvcResult(request, null));
assertThat(requestSnippetLines("post-request-with-content"),
hasItems("POST /foo HTTP/1.1", "Hello, world"));
}
@Test
public void postRequestWithParameter() throws IOException {
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/foo");
request.addParameter("b&r", "baz");
request.addParameter("a", "alpha");
documentHttpRequest("post-request-with-parameter").handle(
new StubMvcResult(request, null));
assertThat(
requestSnippetLines("post-request-with-parameter"),
hasItems("POST /foo HTTP/1.1",
"Content-Type: application/x-www-form-urlencoded",
"b%26r=baz&a=alpha"));
}
@Test
public void basicResponse() throws IOException {
documentHttpResponse("basic-response").handle(
new StubMvcResult(null, new MockHttpServletResponse()));
assertThat(responseSnippetLines("basic-response"), hasItem("HTTP/1.1 200 OK"));
}
@Test
public void nonOkResponse() throws IOException {
MockHttpServletResponse response = new MockHttpServletResponse();
response.setStatus(HttpStatus.BAD_REQUEST.value());
documentHttpResponse("non-ok-response").handle(new StubMvcResult(null, response));
assertThat(responseSnippetLines("non-ok-response"),
hasItem("HTTP/1.1 400 Bad Request"));
}
@Test
public void responseWithHeaders() throws IOException {
MockHttpServletResponse response = new MockHttpServletResponse();
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setHeader("a", "alpha");
documentHttpResponse("non-ok-response").handle(new StubMvcResult(null, response));
assertThat(responseSnippetLines("non-ok-response"),
hasItems("HTTP/1.1 200 OK", "Content-Type: application/json", "a: alpha"));
}
@Test
public void responseWithContent() throws IOException {
MockHttpServletResponse response = new MockHttpServletResponse();
response.getWriter().append("content");
documentHttpResponse("response-with-content").handle(
new StubMvcResult(null, response));
assertThat(responseSnippetLines("response-with-content"),
hasItems("HTTP/1.1 200 OK", "content"));
}
private List<String> requestSnippetLines(String snippetName) throws IOException {
return snippetLines(snippetName, "http-request");
}
private List<String> responseSnippetLines(String snippetName) throws IOException {
return snippetLines(snippetName, "http-response");
}
private List<String> snippetLines(String snippetName, String snippetType)
throws IOException {
File snippetDir = new File(this.outputDir, snippetName);
File snippetFile = new File(snippetDir, snippetType + ".asciidoc");
String line = null;
List<String> lines = new ArrayList<String>();
BufferedReader reader = new BufferedReader(new FileReader(snippetFile));
try {
while ((line = reader.readLine()) != null) {
lines.add(line);
}
}
finally {
reader.close();
}
return lines;
}
}