Improve handling of Host header so a preprocessor can remove it

Previously, the Host header was treated specially in the HTTP request
snippet. If no Host header was specified, one would always be added
prior to producing the snippet. This ensured that the snippet was
valid (an HTTP 1.1 request must include a Host header), but came at
the cost of some confusion about why a preprocessor could not remove
it.

This commit updates the special treatment of the Host header so that
it's now performed in a central location so that all of the snippets
can benefit from a Host header being added if one isn't provided.
The handling of the Content-Length header has also been reworked so
that it's performed in the same location. The curl request snippet
has been updated so that it doesn't include setting the Host header
on the command line; it's unnecessary as curl will automatically
include a Host header in the request. The documentation of the
Host header's special treatment (made in 2fc0420) that noted that it
was unaffected by preprocessing has been reverted.

Closes gh-134
This commit is contained in:
Andy Wilkinson
2015-09-28 15:16:20 +01:00
parent 5da4bee3c6
commit 535bea24f9
23 changed files with 466 additions and 139 deletions

View File

@@ -77,10 +77,6 @@ different replacement can also be specified if you wish.
`removeHeaders` on `Preprocessors` removes any occurrences of the named headers
from the request or response.
NOTE: For an HTTP 1.1 request to be valid it must contain a `Host` header. Therefore,
irrespective of any preprocessing, the default HTTP request snippet will always contain a
`Host` header.
[[customizing-requests-and-responses-preprocessors-replace-patterns]]

View File

@@ -388,8 +388,7 @@ call that is being documented
| `http-request.adoc`
| Contains the HTTP request that is equivalent to the `MockMvc` call that is being
documented. HTTP 1.1 requires a `Host` header. If you do not provide one via the
`MockMvc` API the snippet will add one automatically.
documented
| `http-response.adoc`
| Contains the HTTP response that was returned

View File

@@ -18,10 +18,13 @@ package org.springframework.restdocs.curl;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
@@ -43,6 +46,15 @@ import org.springframework.util.StringUtils;
*/
public class CurlRequestSnippet extends TemplatedSnippet {
private static final Set<HeaderFilter> HEADER_FILTERS;
static {
Set<HeaderFilter> headerFilters = new HashSet<HeaderFilter>();
headerFilters.add(new NamedHeaderFilter(HttpHeaders.HOST));
headerFilters.add(new NamedHeaderFilter(HttpHeaders.CONTENT_LENGTH));
headerFilters.add(new BasicAuthHeaderFilter());
HEADER_FILTERS = Collections.unmodifiableSet(headerFilters);
}
/**
* Creates a new {@code CurlRequestSnippet} with no additional attributes.
*/
@@ -76,9 +88,9 @@ public class CurlRequestSnippet extends TemplatedSnippet {
StringWriter command = new StringWriter();
PrintWriter printer = new PrintWriter(command);
writeIncludeHeadersInOutputOption(printer);
HttpHeaders headers = writeUserOptionIfNecessary(operation.getRequest(), printer);
writeUserOptionIfNecessary(operation.getRequest(), printer);
writeHttpMethodIfNecessary(operation.getRequest(), printer);
writeHeaders(headers, printer);
writeHeaders(operation.getRequest().getHeaders(), printer);
writePartsIfNecessary(operation.getRequest(), printer);
writeContent(operation.getRequest(), printer);
@@ -89,22 +101,12 @@ public class CurlRequestSnippet extends TemplatedSnippet {
writer.print("-i");
}
private HttpHeaders writeUserOptionIfNecessary(OperationRequest request,
PrintWriter writer) {
HttpHeaders headers = new HttpHeaders();
headers.putAll(request.getHeaders());
String authorization = headers.getFirst(HttpHeaders.AUTHORIZATION);
if (isAuthorizationBasicHeader(authorization)) {
String credentials = new String(Base64Utils.decodeFromString(authorization
.substring(5).trim()));
private void writeUserOptionIfNecessary(OperationRequest request, PrintWriter writer) {
List<String> headerValue = request.getHeaders().get(HttpHeaders.AUTHORIZATION);
if (BasicAuthHeaderFilter.isBasicAuthHeader(headerValue)) {
String credentials = BasicAuthHeaderFilter.decodeBasicAuthHeader(headerValue);
writer.print(String.format(" -u '%s'", credentials));
headers.remove(HttpHeaders.AUTHORIZATION);
}
return headers;
}
private boolean isAuthorizationBasicHeader(String header) {
return header != null && header.startsWith("Basic");
}
private void writeHttpMethodIfNecessary(OperationRequest request, PrintWriter writer) {
@@ -115,7 +117,7 @@ public class CurlRequestSnippet extends TemplatedSnippet {
private void writeHeaders(HttpHeaders headers, PrintWriter writer) {
for (Entry<String, List<String>> entry : headers.entrySet()) {
if (!HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(entry.getKey())) {
if (allowedHeader(entry)) {
for (String header : entry.getValue()) {
writer.print(String.format(" -H '%s: %s'", entry.getKey(), header));
}
@@ -123,6 +125,15 @@ public class CurlRequestSnippet extends TemplatedSnippet {
}
}
private boolean allowedHeader(Entry<String, List<String>> header) {
for (HeaderFilter headerFilter : HEADER_FILTERS) {
if (!headerFilter.allow(header.getKey(), header.getValue())) {
return false;
}
}
return true;
}
private void writePartsIfNecessary(OperationRequest request, PrintWriter writer) {
for (OperationRequestPart part : request.getParts()) {
writer.printf(" -F '%s=", part.getName());
@@ -166,4 +177,45 @@ public class CurlRequestSnippet extends TemplatedSnippet {
|| HttpMethod.POST.equals(request.getMethod());
}
private interface HeaderFilter {
boolean allow(String name, List<String> value);
}
private static final class BasicAuthHeaderFilter implements HeaderFilter {
@Override
public boolean allow(String name, List<String> value) {
if (HttpHeaders.AUTHORIZATION.equals(name) && isBasicAuthHeader(value)) {
return false;
}
return true;
}
static boolean isBasicAuthHeader(List<String> value) {
return value != null && (!value.isEmpty())
&& value.get(0).startsWith("Basic ");
}
static String decodeBasicAuthHeader(List<String> value) {
return new String(Base64Utils.decodeFromString(value.get(0).substring(6)));
}
}
private static final class NamedHeaderFilter implements HeaderFilter {
private final String name;
private NamedHeaderFilter(String name) {
this.name = name;
}
@Override
public boolean allow(String name, List<String> value) {
return !this.name.equalsIgnoreCase(name);
}
}
}

View File

@@ -79,9 +79,6 @@ public class HttpRequestSnippet extends TemplatedSnippet {
private List<Map<String, String>> getHeaders(OperationRequest request) {
List<Map<String, String>> headers = new ArrayList<>();
if (requiresHostHeader(request)) {
headers.add(header(HttpHeaders.HOST, request.getUri().getHost()));
}
for (Entry<String, List<String>> header : request.getHeaders().entrySet()) {
for (String value : header.getValue()) {
@@ -170,10 +167,6 @@ public class HttpRequestSnippet extends TemplatedSnippet {
writer.printf("--%s--", MULTIPART_BOUNDARY);
}
private boolean requiresHostHeader(OperationRequest request) {
return request.getHeaders().get(HttpHeaders.HOST) == null;
}
private boolean requiresFormEncodingContentTypeHeader(OperationRequest request) {
return request.getHeaders().get(HttpHeaders.CONTENT_TYPE) == null
&& isPutOrPost(request) && !request.getParameters().isEmpty();

View File

@@ -35,18 +35,7 @@ abstract class AbstractOperationMessage {
AbstractOperationMessage(byte[] content, HttpHeaders headers) {
this.content = content == null ? new byte[0] : content;
this.headers = createHeaders(content, headers);
}
private static HttpHeaders createHeaders(byte[] content, HttpHeaders input) {
HttpHeaders headers = new HttpHeaders();
if (input != null) {
headers.putAll(input);
}
if (content != null && content.length > 0 && headers.getContentLength() == -1) {
headers.setContentLength(content.length);
}
return headers;
this.headers = headers;
}
public byte[] getContent() {

View File

@@ -0,0 +1,66 @@
/*
* 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;
/**
* Helper for working with {@link HttpHeaders}.
*
* @author Andy Wilkinson
*/
class HttpHeadersHelper {
private final HttpHeaders httpHeaders;
HttpHeadersHelper(HttpHeaders httpHeaders) {
HttpHeaders headers = new HttpHeaders();
if (httpHeaders != null) {
headers.putAll(httpHeaders);
}
this.httpHeaders = headers;
}
HttpHeadersHelper addIfAbsent(String name, String value) {
if (this.httpHeaders.get(name) == null) {
this.httpHeaders.add(name, value);
}
return this;
}
HttpHeadersHelper updateContentLengthHeaderIfPresent(byte[] content) {
if (this.httpHeaders.getContentLength() != -1) {
setContentLengthHeader(content);
}
return this;
}
HttpHeadersHelper setContentLengthHeader(byte[] content) {
if (content == null || content.length == 0) {
this.httpHeaders.remove(HttpHeaders.CONTENT_LENGTH);
}
else {
this.httpHeaders.setContentLength(content.length);
}
return this;
}
HttpHeaders getHeaders() {
return HttpHeaders.readOnlyHttpHeaders(this.httpHeaders);
}
}

View File

@@ -0,0 +1,96 @@
/*
* 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;
/**
* A factory for creating {@link OperationRequest OperationRequests}.
*
* @author Andy Wilkinson
*/
public class OperationRequestFactory {
/**
* Creates a new {@link OperationRequest}. The given {@code headers} will be augmented
* to ensure that they always include a {@code Content-Length} header if the request
* has any content and a {@code Host} header.
*
* @param uri the request's uri
* @param method the request method
* @param content the content of the request
* @param headers the request's headers
* @param parameters the request's parameters
* @param parts the request's parts
* @return the {@code OperationRequest}
*/
public OperationRequest create(URI uri, HttpMethod method, byte[] content,
HttpHeaders headers, Parameters parameters,
Collection<OperationRequestPart> parts) {
return new StandardOperationRequest(uri, method, content, augmentHeaders(headers,
uri, content), parameters, parts);
}
/**
* Creates a new {@code OperationRequest} based on the given {@code original} but with
* the given {@code newContent}. If the original request had a {@code Content-Length}
* header it will be modified to match the length of the new content.
*
* @param original The original request
* @param newContent The new content
*
* @return The new request with the new content
*/
public OperationRequest createFrom(OperationRequest original, byte[] newContent) {
return new StandardOperationRequest(original.getUri(), original.getMethod(),
newContent, getUpdatedHeaders(original.getHeaders(), newContent),
original.getParameters(), original.getParts());
}
/**
* Creates a new {@code OperationRequest} based on the given {@code original} but with
* the given {@code newHeaders}.
*
* @param original The original request
* @param newHeaders The new headers
*
* @return The new request with the new content
*/
public OperationRequest createFrom(OperationRequest original, HttpHeaders newHeaders) {
return new StandardOperationRequest(original.getUri(), original.getMethod(),
original.getContent(), newHeaders, original.getParameters(),
original.getParts());
}
private HttpHeaders augmentHeaders(HttpHeaders originalHeaders, URI uri,
byte[] content) {
return new HttpHeadersHelper(originalHeaders)
.addIfAbsent(HttpHeaders.HOST, uri.getHost())
.setContentLengthHeader(content).getHeaders();
}
private HttpHeaders getUpdatedHeaders(HttpHeaders originalHeaders,
byte[] updatedContent) {
return new HttpHeadersHelper(originalHeaders).updateContentLengthHeaderIfPresent(
updatedContent).getHeaders();
}
}

View File

@@ -0,0 +1,49 @@
/*
* 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 factory for creating {@link OperationRequestPart OperationRequestParts}.
*
* @author Andy Wilkinson
*/
public class OperationRequestPartFactory {
/**
* Creates a new {@link OperationRequestPart}. The given {@code headers} will be
* augmented to ensure that they always include a {@code Content-Length} header if the
* part has any content.
*
* @param name the name of the part
* @param submittedFileName the name of the file being submitted by the part
* @param content the content of the part
* @param headers the headers of the part
* @return the {@code OperationRequestPart}
*/
public OperationRequestPart create(String name, String submittedFileName,
byte[] content, HttpHeaders headers) {
return new StandardOperationRequestPart(name, submittedFileName, content,
augmentHeaders(headers, content));
}
private HttpHeaders augmentHeaders(HttpHeaders input, byte[] content) {
return new HttpHeadersHelper(input).setContentLengthHeader(content).getHeaders();
}
}

View File

@@ -0,0 +1,85 @@
/*
* 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;
/**
* A factory for creating {@link OperationResponse OperationResponses}.
*
* @author Andy Wilkinson
*/
public class OperationResponseFactory {
/**
* Creates a new {@link OperationResponse}. If the response has any content, the given
* {@code headers} will be augmented to ensure that they include a
* {@code Content-Length} header.
*
* @param status the status of the response
* @param headers the request's headers
* @param content the content of the request
* @return the {@code OperationResponse}
*/
public OperationResponse create(HttpStatus status, HttpHeaders headers, byte[] content) {
return new StandardOperationResponse(status, augmentHeaders(headers, content),
content);
}
/**
* Creates a new {@code OperationResponse} based on the given {@code original} but
* with the given {@code newContent}. If the original response had a
* {@code Content-Length} header it will be modified to match the length of the new
* content.
*
* @param original The original response
* @param newContent The new content
*
* @return The new response with the new content
*/
public OperationResponse createFrom(OperationResponse original, byte[] newContent) {
return new StandardOperationResponse(original.getStatus(), getUpdatedHeaders(
original.getHeaders(), newContent), newContent);
}
/**
* Creates a new {@code OperationResponse} based on the given {@code original} but
* with the given {@code newHeaders}.
*
* @param original The original response
* @param newHeaders The new headers
*
* @return The new response with the new headers
*/
public OperationResponse createFrom(OperationResponse original, HttpHeaders newHeaders) {
return new StandardOperationResponse(original.getStatus(), newHeaders,
original.getContent());
}
private HttpHeaders augmentHeaders(HttpHeaders originalHeaders, byte[] content) {
return new HttpHeadersHelper(originalHeaders).setContentLengthHeader(content)
.getHeaders();
}
private HttpHeaders getUpdatedHeaders(HttpHeaders originalHeaders,
byte[] updatedContent) {
return new HttpHeadersHelper(originalHeaders).updateContentLengthHeaderIfPresent(
updatedContent).getHeaders();
}
}

View File

@@ -28,7 +28,7 @@ import org.springframework.http.HttpMethod;
*
* @author Andy Wilkinson
*/
public class StandardOperationRequest extends AbstractOperationMessage implements
class StandardOperationRequest extends AbstractOperationMessage implements
OperationRequest {
private HttpMethod method;
@@ -50,7 +50,7 @@ public class StandardOperationRequest extends AbstractOperationMessage implement
* @param parameters the parameters
* @param parts the parts
*/
public StandardOperationRequest(URI uri, HttpMethod method, byte[] content,
StandardOperationRequest(URI uri, HttpMethod method, byte[] content,
HttpHeaders headers, Parameters parameters,
Collection<OperationRequestPart> parts) {
super(content, headers);

View File

@@ -23,7 +23,7 @@ import org.springframework.http.HttpHeaders;
*
* @author Andy Wilkinson
*/
public class StandardOperationRequestPart extends AbstractOperationMessage implements
class StandardOperationRequestPart extends AbstractOperationMessage implements
OperationRequestPart {
private final String name;
@@ -38,8 +38,8 @@ public class StandardOperationRequestPart extends AbstractOperationMessage imple
* @param content the contents of the part
* @param headers the headers of the part
*/
public StandardOperationRequestPart(String name, String submittedFileName,
byte[] content, HttpHeaders headers) {
StandardOperationRequestPart(String name, String submittedFileName, byte[] content,
HttpHeaders headers) {
super(content, headers);
this.name = name;
this.submittedFileName = submittedFileName;

View File

@@ -24,7 +24,7 @@ import org.springframework.http.HttpStatus;
*
* @author Andy Wilkinson
*/
public class StandardOperationResponse extends AbstractOperationMessage implements
class StandardOperationResponse extends AbstractOperationMessage implements
OperationResponse {
private final HttpStatus status;
@@ -37,8 +37,7 @@ public class StandardOperationResponse extends AbstractOperationMessage implemen
* @param headers the headers of the response
* @param content the content of the response
*/
public StandardOperationResponse(HttpStatus status, HttpHeaders headers,
byte[] content) {
StandardOperationResponse(HttpStatus status, HttpHeaders headers, byte[] content) {
super(content, headers);
this.status = status;
}

View File

@@ -16,11 +16,10 @@
package org.springframework.restdocs.operation.preprocess;
import org.springframework.http.HttpHeaders;
import org.springframework.restdocs.operation.OperationRequest;
import org.springframework.restdocs.operation.OperationRequestFactory;
import org.springframework.restdocs.operation.OperationResponse;
import org.springframework.restdocs.operation.StandardOperationRequest;
import org.springframework.restdocs.operation.StandardOperationResponse;
import org.springframework.restdocs.operation.OperationResponseFactory;
/**
* An {@link OperationPreprocessor} that applies a {@link ContentModifier} to the content
@@ -30,6 +29,10 @@ import org.springframework.restdocs.operation.StandardOperationResponse;
*/
public class ContentModifyingOperationPreprocessor implements OperationPreprocessor {
private final OperationRequestFactory requestFactory = new OperationRequestFactory();
private final OperationResponseFactory responseFactory = new OperationResponseFactory();
private final ContentModifier contentModifier;
/**
@@ -46,30 +49,14 @@ public class ContentModifyingOperationPreprocessor implements OperationPreproces
public OperationRequest preprocess(OperationRequest request) {
byte[] modifiedContent = this.contentModifier.modifyContent(request.getContent(),
request.getHeaders().getContentType());
return new StandardOperationRequest(request.getUri(), request.getMethod(),
modifiedContent,
getUpdatedHeaders(request.getHeaders(), modifiedContent),
request.getParameters(), request.getParts());
return this.requestFactory.createFrom(request, modifiedContent);
}
@Override
public OperationResponse preprocess(OperationResponse response) {
byte[] modifiedContent = this.contentModifier.modifyContent(
response.getContent(), response.getHeaders().getContentType());
return new StandardOperationResponse(response.getStatus(), getUpdatedHeaders(
response.getHeaders(), modifiedContent), modifiedContent);
}
private HttpHeaders getUpdatedHeaders(HttpHeaders headers, byte[] updatedContent) {
HttpHeaders updatedHeaders = new HttpHeaders();
updatedHeaders.putAll(headers);
if (updatedContent.length > 0) {
updatedHeaders.setContentLength(updatedContent.length);
}
else {
updatedHeaders.remove(HttpHeaders.CONTENT_LENGTH);
}
return updatedHeaders;
return this.responseFactory.createFrom(response, modifiedContent);
}
}

View File

@@ -22,9 +22,9 @@ import java.util.Set;
import org.springframework.http.HttpHeaders;
import org.springframework.restdocs.operation.OperationRequest;
import org.springframework.restdocs.operation.OperationRequestFactory;
import org.springframework.restdocs.operation.OperationResponse;
import org.springframework.restdocs.operation.StandardOperationRequest;
import org.springframework.restdocs.operation.StandardOperationResponse;
import org.springframework.restdocs.operation.OperationResponseFactory;
/**
* An {@link OperationPreprocessor} that removes headers.
@@ -33,6 +33,10 @@ import org.springframework.restdocs.operation.StandardOperationResponse;
*/
class HeaderRemovingOperationPreprocessor implements OperationPreprocessor {
private final OperationRequestFactory requestFactory = new OperationRequestFactory();
private final OperationResponseFactory responseFactory = new OperationResponseFactory();
private final Set<String> headersToRemove;
HeaderRemovingOperationPreprocessor(String... headersToRemove) {
@@ -41,15 +45,14 @@ class HeaderRemovingOperationPreprocessor implements OperationPreprocessor {
@Override
public OperationResponse preprocess(OperationResponse response) {
return new StandardOperationResponse(response.getStatus(),
removeHeaders(response.getHeaders()), response.getContent());
return this.responseFactory.createFrom(response,
removeHeaders(response.getHeaders()));
}
@Override
public OperationRequest preprocess(OperationRequest request) {
return new StandardOperationRequest(request.getUri(), request.getMethod(),
request.getContent(), removeHeaders(request.getHeaders()),
request.getParameters(), request.getParts());
return this.requestFactory.createFrom(request,
removeHeaders(request.getHeaders()));
}
private HttpHeaders removeHeaders(HttpHeaders originalHeaders) {

View File

@@ -54,8 +54,8 @@ public class HttpRequestSnippetTests {
@Test
public void getRequest() throws IOException {
this.snippet.expectHttpRequest("get-request").withContents(
httpRequest(RequestMethod.GET, "/foo").header(HttpHeaders.HOST,
"localhost").header("Alpha", "a"));
httpRequest(RequestMethod.GET, "/foo").header("Alpha", "a").header(
HttpHeaders.HOST, "localhost"));
new HttpRequestSnippet().document(new OperationBuilder("get-request",
this.snippet.getOutputDirectory()).request("http://localhost/foo")
@@ -92,8 +92,8 @@ public class HttpRequestSnippetTests {
byte[] contentBytes = japaneseContent.getBytes("UTF-8");
this.snippet.expectHttpRequest("post-request-with-charset").withContents(
httpRequest(RequestMethod.POST, "/foo")
.header(HttpHeaders.HOST, "localhost")
.header("Content-Type", "text/plain;charset=UTF-8")
.header(HttpHeaders.HOST, "localhost")
.header(HttpHeaders.CONTENT_LENGTH, contentBytes.length)
.content(japaneseContent));
@@ -151,10 +151,9 @@ public class HttpRequestSnippetTests {
+ "form-data; " + "name=image%n%n<< data >>"));
this.snippet.expectHttpRequest("multipart-post").withContents(
httpRequest(RequestMethod.POST, "/upload")
.header(HttpHeaders.HOST, "localhost")
.header("Content-Type",
"multipart/form-data; boundary=" + BOUNDARY)
.content(expectedContent));
.header(HttpHeaders.HOST, "localhost").content(expectedContent));
new HttpRequestSnippet().document(new OperationBuilder("multipart-post",
this.snippet.getOutputDirectory()).request("http://localhost/upload")
.method("POST")
@@ -175,10 +174,9 @@ public class HttpRequestSnippetTests {
String expectedContent = param1Part + param2Part + param3Part + filePart;
this.snippet.expectHttpRequest("multipart-post-with-parameters").withContents(
httpRequest(RequestMethod.POST, "/upload")
.header(HttpHeaders.HOST, "localhost")
.header("Content-Type",
"multipart/form-data; boundary=" + BOUNDARY)
.content(expectedContent));
.header(HttpHeaders.HOST, "localhost").content(expectedContent));
new HttpRequestSnippet().document(new OperationBuilder(
"multipart-post-with-parameters", this.snippet.getOutputDirectory())
.request("http://localhost/upload").method("POST")
@@ -194,10 +192,9 @@ public class HttpRequestSnippetTests {
+ "image/png%n%n<< data >>"));
this.snippet.expectHttpRequest("multipart-post-with-content-type").withContents(
httpRequest(RequestMethod.POST, "/upload")
.header(HttpHeaders.HOST, "localhost")
.header("Content-Type",
"multipart/form-data; boundary=" + BOUNDARY)
.content(expectedContent));
.header(HttpHeaders.HOST, "localhost").content(expectedContent));
new HttpRequestSnippet().document(new OperationBuilder(
"multipart-post-with-content-type", this.snippet.getOutputDirectory())
.request("http://localhost/upload").method("POST")

View File

@@ -26,7 +26,8 @@ import org.junit.rules.ExpectedException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.restdocs.operation.StandardOperationResponse;
import org.springframework.restdocs.operation.OperationResponse;
import org.springframework.restdocs.operation.OperationResponseFactory;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@@ -38,13 +39,15 @@ import static org.mockito.Mockito.verify;
*/
public class ContentTypeLinkExtractorTests {
private final OperationResponseFactory responseFactory = new OperationResponseFactory();
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void extractionFailsWithNullContentType() throws IOException {
this.thrown.expect(IllegalStateException.class);
new ContentTypeLinkExtractor().extractLinks(new StandardOperationResponse(
new ContentTypeLinkExtractor().extractLinks(this.responseFactory.create(
HttpStatus.OK, new HttpHeaders(), null));
}
@@ -55,7 +58,7 @@ public class ContentTypeLinkExtractorTests {
extractors.put(MediaType.APPLICATION_JSON, extractor);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
StandardOperationResponse response = new StandardOperationResponse(HttpStatus.OK,
OperationResponse response = this.responseFactory.create(HttpStatus.OK,
httpHeaders, null);
new ContentTypeLinkExtractor(extractors).extractLinks(response);
verify(extractor).extractLinks(response);
@@ -68,7 +71,7 @@ public class ContentTypeLinkExtractorTests {
extractors.put(MediaType.APPLICATION_JSON, extractor);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.parseMediaType("application/json;foo=bar"));
StandardOperationResponse response = new StandardOperationResponse(HttpStatus.OK,
OperationResponse response = this.responseFactory.create(HttpStatus.OK,
httpHeaders, null);
new ContentTypeLinkExtractor(extractors).extractLinks(response);
verify(extractor).extractLinks(response);

View File

@@ -30,7 +30,7 @@ import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import org.springframework.http.HttpStatus;
import org.springframework.restdocs.operation.OperationResponse;
import org.springframework.restdocs.operation.StandardOperationResponse;
import org.springframework.restdocs.operation.OperationResponseFactory;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@@ -46,6 +46,8 @@ import static org.junit.Assert.assertEquals;
@RunWith(Parameterized.class)
public class LinkExtractorsPayloadTests {
private final OperationResponseFactory responseFactory = new OperationResponseFactory();
private final LinkExtractor linkExtractor;
private final String linkType;
@@ -107,7 +109,7 @@ public class LinkExtractorsPayloadTests {
}
private OperationResponse createResponse(String contentName) throws IOException {
return new StandardOperationResponse(HttpStatus.OK, null,
return this.responseFactory.create(HttpStatus.OK, null,
FileCopyUtils.copyToByteArray(getPayloadFile(contentName)));
}

View File

@@ -25,11 +25,11 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.restdocs.operation.OperationRequest;
import org.springframework.restdocs.operation.OperationRequestFactory;
import org.springframework.restdocs.operation.OperationRequestPart;
import org.springframework.restdocs.operation.OperationResponse;
import org.springframework.restdocs.operation.OperationResponseFactory;
import org.springframework.restdocs.operation.Parameters;
import org.springframework.restdocs.operation.StandardOperationRequest;
import org.springframework.restdocs.operation.StandardOperationResponse;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
@@ -43,6 +43,10 @@ import static org.junit.Assert.assertThat;
*/
public class ContentModifyingOperationPreprocessorTests {
private final OperationRequestFactory requestFactory = new OperationRequestFactory();
private final OperationResponseFactory responseFactory = new OperationResponseFactory();
private final ContentModifyingOperationPreprocessor preprocessor = new ContentModifyingOperationPreprocessor(
new ContentModifier() {
@@ -55,7 +59,7 @@ public class ContentModifyingOperationPreprocessorTests {
@Test
public void modifyRequestContent() {
StandardOperationRequest request = new StandardOperationRequest(
OperationRequest request = this.requestFactory.create(
URI.create("http://localhost"), HttpMethod.GET, "content".getBytes(),
new HttpHeaders(), new Parameters(),
Collections.<OperationRequestPart>emptyList());
@@ -65,7 +69,7 @@ public class ContentModifyingOperationPreprocessorTests {
@Test
public void modifyResponseContent() {
StandardOperationResponse response = new StandardOperationResponse(HttpStatus.OK,
OperationResponse response = this.responseFactory.create(HttpStatus.OK,
new HttpHeaders(), "content".getBytes());
OperationResponse preprocessed = this.preprocessor.preprocess(response);
assertThat(preprocessed.getContent(), is(equalTo("modified".getBytes())));
@@ -75,7 +79,7 @@ public class ContentModifyingOperationPreprocessorTests {
public void contentLengthIsUpdated() {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentLength(7);
StandardOperationRequest request = new StandardOperationRequest(
OperationRequest request = this.requestFactory.create(
URI.create("http://localhost"), HttpMethod.GET, "content".getBytes(),
httpHeaders, new Parameters(),
Collections.<OperationRequestPart>emptyList());

View File

@@ -25,11 +25,11 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.restdocs.operation.OperationRequest;
import org.springframework.restdocs.operation.OperationRequestFactory;
import org.springframework.restdocs.operation.OperationRequestPart;
import org.springframework.restdocs.operation.OperationResponse;
import org.springframework.restdocs.operation.OperationResponseFactory;
import org.springframework.restdocs.operation.Parameters;
import org.springframework.restdocs.operation.StandardOperationRequest;
import org.springframework.restdocs.operation.StandardOperationResponse;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
@@ -44,23 +44,29 @@ import static org.junit.Assert.assertThat;
*/
public class HeaderRemovingOperationPreprocessorTests {
private final OperationRequestFactory requestFactory = new OperationRequestFactory();
private final OperationResponseFactory responseFactory = new OperationResponseFactory();
private final HeaderRemovingOperationPreprocessor preprocessor = new HeaderRemovingOperationPreprocessor(
"b");
@Test
public void modifyRequestHeaders() {
StandardOperationRequest request = new StandardOperationRequest(
OperationRequest request = this.requestFactory.create(
URI.create("http://localhost"), HttpMethod.GET, new byte[0],
getHttpHeaders(), new Parameters(),
Collections.<OperationRequestPart>emptyList());
OperationRequest preprocessed = this.preprocessor.preprocess(request);
assertThat(preprocessed.getHeaders().size(), is(equalTo(1)));
assertThat(preprocessed.getHeaders().size(), is(equalTo(2)));
assertThat(preprocessed.getHeaders(), hasEntry("a", Arrays.asList("alpha")));
assertThat(preprocessed.getHeaders(),
hasEntry("Host", Arrays.asList("localhost")));
}
@Test
public void modifyResponseHeaders() {
StandardOperationResponse response = new StandardOperationResponse(HttpStatus.OK,
OperationResponse response = this.responseFactory.create(HttpStatus.OK,
getHttpHeaders(), new byte[0]);
OperationResponse preprocessed = this.preprocessor.preprocess(response);
assertThat(preprocessed.getHeaders().size(), is(equalTo(1)));

View File

@@ -29,13 +29,13 @@ import org.springframework.http.HttpStatus;
import org.springframework.restdocs.RestDocumentationContext;
import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.operation.OperationRequest;
import org.springframework.restdocs.operation.OperationRequestFactory;
import org.springframework.restdocs.operation.OperationRequestPart;
import org.springframework.restdocs.operation.OperationRequestPartFactory;
import org.springframework.restdocs.operation.OperationResponse;
import org.springframework.restdocs.operation.OperationResponseFactory;
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.RestDocumentationContextPlaceholderResolver;
import org.springframework.restdocs.snippet.StandardWriterResolver;
import org.springframework.restdocs.snippet.WriterResolver;
@@ -122,7 +122,7 @@ public class OperationBuilder {
for (OperationRequestPartBuilder builder : this.partBuilders) {
parts.add(builder.buildPart());
}
return new StandardOperationRequest(this.requestUri, this.method,
return new OperationRequestFactory().create(this.requestUri, this.method,
this.content, this.headers, this.parameters, parts);
}
@@ -196,7 +196,7 @@ public class OperationBuilder {
}
private OperationRequestPart buildPart() {
return new StandardOperationRequestPart(this.name,
return new OperationRequestPartFactory().create(this.name,
this.submittedFileName, this.content, this.headers);
}
@@ -219,7 +219,8 @@ public class OperationBuilder {
private byte[] content = new byte[0];
private OperationResponse buildResponse() {
return new StandardOperationResponse(this.status, this.headers, this.content);
return new OperationResponseFactory().create(this.status, this.headers,
this.content);
}
public OperationResponseBuilder status(int status) {

View File

@@ -33,10 +33,10 @@ import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockMultipartHttpServletRequest;
import org.springframework.restdocs.operation.OperationRequest;
import org.springframework.restdocs.operation.OperationRequestFactory;
import org.springframework.restdocs.operation.OperationRequestPart;
import org.springframework.restdocs.operation.OperationRequestPartFactory;
import org.springframework.restdocs.operation.Parameters;
import org.springframework.restdocs.operation.StandardOperationRequest;
import org.springframework.restdocs.operation.StandardOperationRequestPart;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
@@ -67,7 +67,7 @@ class MockMvcOperationRequestFactory {
* @return the {@code OperationRequest}
* @throws Exception if the request could not be created
*/
public OperationRequest createOperationRequest(MockHttpServletRequest mockRequest)
OperationRequest createOperationRequest(MockHttpServletRequest mockRequest)
throws Exception {
HttpHeaders headers = extractHeaders(mockRequest);
Parameters parameters = extractParameters(mockRequest);
@@ -76,8 +76,9 @@ class MockMvcOperationRequestFactory {
if (!StringUtils.hasText(queryString) && "GET".equals(mockRequest.getMethod())) {
queryString = parameters.toQueryString();
}
return new StandardOperationRequest(URI.create(getRequestUri(mockRequest)
+ (StringUtils.hasText(queryString) ? "?" + queryString : "")),
return new OperationRequestFactory().create(
URI.create(getRequestUri(mockRequest)
+ (StringUtils.hasText(queryString) ? "?" + queryString : "")),
HttpMethod.valueOf(mockRequest.getMethod()),
FileCopyUtils.copyToByteArray(mockRequest.getInputStream()), headers,
parameters, parts);
@@ -102,16 +103,17 @@ class MockMvcOperationRequestFactory {
return parts;
}
private StandardOperationRequestPart createOperationRequestPart(Part part)
throws IOException {
private OperationRequestPart createOperationRequestPart(Part part) throws IOException {
HttpHeaders partHeaders = extractHeaders(part);
List<String> contentTypeHeader = partHeaders.get(HttpHeaders.CONTENT_TYPE);
if (part.getContentType() != null && contentTypeHeader == null) {
partHeaders.setContentType(MediaType.parseMediaType(part.getContentType()));
}
return new StandardOperationRequestPart(part.getName(), StringUtils.hasText(part
.getSubmittedFileName()) ? part.getSubmittedFileName() : null,
FileCopyUtils.copyToByteArray(part.getInputStream()), partHeaders);
return new OperationRequestPartFactory()
.create(part.getName(),
StringUtils.hasText(part.getSubmittedFileName()) ? part
.getSubmittedFileName() : null, FileCopyUtils
.copyToByteArray(part.getInputStream()), partHeaders);
}
private List<OperationRequestPart> extractMultipartRequestParts(
@@ -126,14 +128,14 @@ class MockMvcOperationRequestFactory {
return parts;
}
private StandardOperationRequestPart createOperationRequestPart(MultipartFile file)
private OperationRequestPart createOperationRequestPart(MultipartFile file)
throws IOException {
HttpHeaders partHeaders = new HttpHeaders();
if (StringUtils.hasText(file.getContentType())) {
partHeaders.setContentType(MediaType.parseMediaType(file.getContentType()));
}
return new StandardOperationRequestPart(file.getName(), StringUtils.hasText(file
.getOriginalFilename()) ? file.getOriginalFilename() : null,
return new OperationRequestPartFactory().create(file.getName(), StringUtils
.hasText(file.getOriginalFilename()) ? file.getOriginalFilename() : null,
file.getBytes(), partHeaders);
}

View File

@@ -20,7 +20,7 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.restdocs.operation.OperationResponse;
import org.springframework.restdocs.operation.StandardOperationResponse;
import org.springframework.restdocs.operation.OperationResponseFactory;
/**
* A factory for creating an {@link OperationResponse} derived from a
@@ -36,8 +36,8 @@ class MockMvcOperationResponseFactory {
* @param mockResponse the response
* @return the {@code OperationResponse}
*/
public OperationResponse createOperationResponse(MockHttpServletResponse mockResponse) {
return new StandardOperationResponse(
OperationResponse createOperationResponse(MockHttpServletResponse mockResponse) {
return new OperationResponseFactory().create(
HttpStatus.valueOf(mockResponse.getStatus()),
extractHeaders(mockResponse), mockResponse.getContentAsByteArray());
}

View File

@@ -269,33 +269,31 @@ public class MockMvcRestDocumentationIntegrationTests {
.andDo(document("original-request"))
.andDo(document(
"preprocessed-request",
preprocessRequest(prettyPrint(), removeHeaders("a"),
preprocessRequest(
prettyPrint(),
removeHeaders("a", HttpHeaders.HOST,
HttpHeaders.CONTENT_LENGTH),
replacePattern(pattern, "\"<<beta>>\""))));
assertThat(
new File("build/generated-snippets/original-request/http-request.adoc"),
is(snippet().withContents(
httpRequest(RequestMethod.GET, "/").header("Host", "localhost")
.header("a", "alpha").header("b", "bravo")
httpRequest(RequestMethod.GET, "/").header("a", "alpha")
.header("b", "bravo")
.header("Content-Type", "application/json")
.header("Accept", MediaType.APPLICATION_JSON_VALUE)
.header("Host", "localhost")
.header("Content-Length", "13")
.content("{\"a\":\"alpha\"}"))));
String prettyPrinted = String.format("{%n \"a\" : \"<<beta>>\"%n}");
assertThat(
new File(
"build/generated-snippets/preprocessed-request/http-request.adoc"),
is(snippet()
.withContents(
httpRequest(RequestMethod.GET, "/")
.header("Host", "localhost")
.header("b", "bravo")
.header("Content-Type", "application/json")
.header("Accept",
MediaType.APPLICATION_JSON_VALUE)
.header("Content-Length",
Integer.toString(prettyPrinted.getBytes().length))
.content(prettyPrinted))));
is(snippet().withContents(
httpRequest(RequestMethod.GET, "/").header("b", "bravo")
.header("Content-Type", "application/json")
.header("Accept", MediaType.APPLICATION_JSON_VALUE)
.content(prettyPrinted))));
}
@Test