Add support for post-processing a response before it is documented

This commit adds support for post-processing a response before it is
documented. Post-processors are provided to pretty-print JSON and XML
responses, remove headers from the response, and mask the hrefs of
Atom- and HAL-formatted links in JSON responses.

Response post-processing is configured prior to configuring the
documentation. For example,

mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON))
        .andExpect(status().isOk())
		.andDo(modifyResponseTo(prettyPrintContent(), removeHeaders("a"),
				maskLinks()).andDocument("post-processed"));

Closes gh-61
Closes gh-31
Closes gh-54
This commit is contained in:
Andy Wilkinson
2015-04-30 11:12:55 +01:00
parent b3eee08b5d
commit e5f35c98f3
19 changed files with 1196 additions and 72 deletions

View File

@@ -59,13 +59,13 @@ project(':spring-restdocs') {
cleanEclipseJdt.onlyIf { false }
dependencies {
compile "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion"
compile "junit:junit:$junitVersion"
compile "org.springframework:spring-test:$springVersion"
compile "org.springframework:spring-web:$springVersion"
compile "org.springframework:spring-webmvc:$springVersion"
compile "javax.servlet:javax.servlet-api:$servletApiVersion"
compile "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion"
jacoco "org.jacoco:org.jacoco.agent:$jacocoVersion:runtime"
testCompile "org.springframework:spring-webmvc:$springVersion"
testCompile "org.springframework.hateoas:spring-hateoas:$springHateoasVersion"
testCompile "org.mockito:mockito-core:$mockitoVersion"
testCompile "org.hamcrest:hamcrest-core:$hamcrestVersion"

View File

@@ -0,0 +1,117 @@
/*
* 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;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import org.springframework.core.BridgeMethodResolver;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.restdocs.response.ResponsePostProcessor;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.ReflectionUtils;
/**
* Modifies the response in an {@link MvcResult} by applying {@link ResponsePostProcessor
* ResponsePostProcessors} to it.
*
* @see RestDocumentation#modifyResponseTo(ResponsePostProcessor...)
* @author Andy Wilkinson
*/
public class ResponseModifier {
private final List<ResponsePostProcessor> postProcessors;
ResponseModifier(ResponsePostProcessor... postProcessors) {
this.postProcessors = Arrays.asList(postProcessors);
}
/**
* Provides a {@link RestDocumentationResultHandler} that can be used to document the
* request and modified result.
* @param outputDir The directory to which the documentation will be written
* @return the result handler that will produce the documentation
*/
public RestDocumentationResultHandler andDocument(String outputDir) {
return new ResponseModifyingRestDocumentationResultHandler(outputDir);
}
class ResponseModifyingRestDocumentationResultHandler extends
RestDocumentationResultHandler {
public ResponseModifyingRestDocumentationResultHandler(String outputDir) {
super(outputDir);
}
@Override
public void handle(MvcResult result) throws Exception {
super.handle(postProcessResponse(result));
}
MvcResult postProcessResponse(MvcResult result) throws Exception {
MockHttpServletResponse response = result.getResponse();
for (ResponsePostProcessor postProcessor : ResponseModifier.this.postProcessors) {
response = postProcessor.postProcess(response);
}
return decorateResult(result, response);
}
private MvcResult decorateResult(MvcResult result,
MockHttpServletResponse response) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MvcResult.class);
enhancer.setCallback(new GetResponseMethodInterceptor(response, result));
return (MvcResult) enhancer.create();
}
private class GetResponseMethodInterceptor implements MethodInterceptor {
private final MvcResult delegate;
private final MockHttpServletResponse response;
private final Method getResponseMethod = findMethod("getResponse");
private GetResponseMethodInterceptor(MockHttpServletResponse response,
MvcResult delegate) {
this.delegate = delegate;
this.response = response;
}
@Override
public Object intercept(Object proxy, Method method, Object[] args,
MethodProxy methodProxy) throws Throwable {
if (this.getResponseMethod.equals(method)) {
return this.response;
}
return method.invoke(this.delegate, args);
}
private Method findMethod(String methodName) {
return BridgeMethodResolver.findBridgedMethod(ReflectionUtils.findMethod(
MvcResult.class, methodName));
}
}
}
}

View File

@@ -16,7 +16,10 @@
package org.springframework.restdocs;
import org.springframework.restdocs.response.ResponsePostProcessor;
import org.springframework.restdocs.response.ResponsePostProcessors;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions;
/**
@@ -42,4 +45,18 @@ public abstract class RestDocumentation {
return new RestDocumentationResultHandler(outputDir);
}
/**
* Enables the modification of the response in a {@link MvcResult} prior to it being
* documented. The modification is performed using the given
* {@code responsePostProcessors}.
*
* @param responsePostProcessors the post-processors to use to modify the response
* @return the response modifier
* @see ResponsePostProcessors
*/
public static ResponseModifier modifyResponseTo(
ResponsePostProcessor... responsePostProcessors) {
return new ResponseModifier(responsePostProcessors);
}
}

View File

@@ -49,7 +49,7 @@ public class RestDocumentationResultHandler implements ResultHandler {
private final String outputDir;
private List<ResultHandler> delegates;
private List<ResultHandler> delegates = new ArrayList<>();
RestDocumentationResultHandler(String outputDir) {
this.outputDir = outputDir;
@@ -60,13 +60,6 @@ public class RestDocumentationResultHandler implements ResultHandler {
this.delegates.add(documentHttpResponse(this.outputDir));
}
@Override
public void handle(MvcResult result) throws Exception {
for (ResultHandler delegate : this.delegates) {
delegate.handle(result);
}
}
/**
* Document the links in the response using the given {@code descriptors}. The links
* are extracted from the response based on its content type.
@@ -159,4 +152,12 @@ public class RestDocumentationResultHandler implements ResultHandler {
this.delegates.add(documentQueryParameters(this.outputDir, descriptors));
return this;
}
@Override
public void handle(MvcResult result) throws Exception {
for (ResultHandler delegate : this.delegates) {
delegate.handle(result);
}
}
}

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.response;
import java.lang.reflect.Method;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import org.springframework.core.BridgeMethodResolver;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.util.ReflectionUtils;
/**
* A base class for {@link ResponsePostProcessor ResponsePostProcessors} that modify the
* content of the response.
*
* @author Andy Wilkinson
*/
public abstract class ContentModifyingReponsePostProcessor implements
ResponsePostProcessor {
@Override
public MockHttpServletResponse postProcess(MockHttpServletResponse response)
throws Exception {
String modifiedContent = modifyContent(response.getContentAsString());
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MockHttpServletResponse.class);
enhancer.setCallback(new ContentModifyingMethodInterceptor(modifiedContent,
response));
return (MockHttpServletResponse) enhancer.create();
}
/**
* Returns a modified version of the given {@code originalContent}
*
* @param originalContent the content to modify
* @return the modified content
* @throws Exception if a failure occurs while modifying the content
*/
protected abstract String modifyContent(String originalContent) throws Exception;
private static class ContentModifyingMethodInterceptor implements MethodInterceptor {
private final Method getContentAsStringMethod = findMethod("getContentAsString");
private final Method getContentAsByteArray = findMethod("getContentAsByteArray");
private final String modifiedContent;
private final MockHttpServletResponse delegate;
public ContentModifyingMethodInterceptor(String modifiedContent,
MockHttpServletResponse delegate) {
this.modifiedContent = modifiedContent;
this.delegate = delegate;
}
@Override
public Object intercept(Object proxy, Method method, Object[] args,
MethodProxy methodProxy) throws Throwable {
if (this.getContentAsStringMethod.equals(method)) {
return this.modifiedContent;
}
if (this.getContentAsByteArray.equals(method)) {
throw new UnsupportedOperationException(
"Following modification, the response's content should be"
+ " accessed as a String");
}
return method.invoke(this.delegate, args);
}
private static Method findMethod(String methodName) {
return BridgeMethodResolver.findBridgedMethod(ReflectionUtils.findMethod(
MockHttpServletResponse.class, methodName));
}
}
}

View File

@@ -0,0 +1,132 @@
/*
* 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.response;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import org.springframework.core.BridgeMethodResolver;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.util.ReflectionUtils;
/**
* A {@link ResponsePostProcessor} that removes headers from the response
*
* @author Andy Wilkinson
*/
class HeaderRemovingResponsePostProcessor implements ResponsePostProcessor {
private final Set<String> headersToRemove;
public HeaderRemovingResponsePostProcessor(String... headersToRemove) {
this.headersToRemove = new HashSet<>(Arrays.asList(headersToRemove));
}
@Override
public MockHttpServletResponse postProcess(final MockHttpServletResponse response) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MockHttpServletResponse.class);
enhancer.setCallback(new HeaderHidingMethodInterceptor(this.headersToRemove,
response));
return (MockHttpServletResponse) enhancer.create();
}
private static final class HeaderHidingMethodInterceptor implements MethodInterceptor {
private final MockHttpServletResponse response;
private final List<Method> interceptedMethods = Arrays.asList(
findHeaderMethod("containsHeader", String.class),
findHeaderMethod("getHeader", String.class),
findHeaderMethod("getHeaderValue", String.class),
findHeaderMethod("getHeaders", String.class),
findHeaderMethod("getHeaderValues", String.class));
private final Method getHeaderNamesMethod = findHeaderMethod("getHeaderNames");
private final Set<String> hiddenHeaders;
private HeaderHidingMethodInterceptor(Set<String> hiddenHeaders,
MockHttpServletResponse response) {
this.hiddenHeaders = hiddenHeaders;
this.response = response;
}
@Override
public Object intercept(Object proxy, Method method, Object[] args,
MethodProxy methodProxy) throws Throwable {
if (this.getHeaderNamesMethod.equals(method)) {
List<String> headerNames = new ArrayList<>();
for (String candidate : this.response.getHeaderNames()) {
if (!isHiddenHeader(candidate)) {
headerNames.add(candidate);
}
}
return headerNames;
}
if (this.interceptedMethods.contains(method) && isHiddenHeader(args)) {
if (method.getReturnType().equals(boolean.class)) {
return false;
}
else if (Collection.class.isAssignableFrom(method.getReturnType())) {
return Collections.emptyList();
}
else {
return null;
}
}
return method.invoke(this.response, args);
}
private boolean isHiddenHeader(Object[] args) {
if (args.length == 1 && args[0] instanceof String) {
return isHiddenHeader((String) args[0]);
}
return false;
}
private boolean isHiddenHeader(String headerName) {
for (String hiddenHeader : this.hiddenHeaders) {
if (hiddenHeader.equalsIgnoreCase(headerName)) {
return true;
}
}
return false;
}
private static Method findHeaderMethod(String methodName, Class<?>... args) {
Method candidate = ReflectionUtils.findMethod(MockHttpServletResponse.class,
methodName, args);
if (candidate.isBridge()) {
return BridgeMethodResolver.findBridgedMethod(candidate);
}
return candidate;
}
}
}

View File

@@ -0,0 +1,55 @@
/*
* 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.response;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
class LinkMaskingResponsePostProcessor extends ContentModifyingReponsePostProcessor {
private static final String DEFAULT_MASK = "...";
private static final Pattern LINK_HREF = Pattern.compile(
"\"href\"\\s*:\\s*\"(.*?)\"", Pattern.DOTALL);
private final String mask;
LinkMaskingResponsePostProcessor() {
this(DEFAULT_MASK);
}
LinkMaskingResponsePostProcessor(String mask) {
this.mask = mask;
}
@Override
protected String modifyContent(String originalContent) {
Matcher matcher = LINK_HREF.matcher(originalContent);
StringBuilder buffer = new StringBuilder();
int previous = 0;
while (matcher.find()) {
buffer.append(originalContent.substring(previous, matcher.start(1)));
buffer.append(this.mask);
previous = matcher.end(1);
}
if (previous < originalContent.length()) {
buffer.append(originalContent.substring(previous));
}
return buffer.toString();
}
}

View File

@@ -0,0 +1,88 @@
/*
* 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.response;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.List;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
class PrettyPrintingResponsePostProcessor extends ContentModifyingReponsePostProcessor {
private static final List<PrettyPrinter> prettyPrinters = Arrays.asList(
new JsonPrettyPrinter(), new XmlPrettyPrinter());
@Override
protected String modifyContent(String originalContent) {
if (StringUtils.hasText(originalContent)) {
for (PrettyPrinter prettyPrinter : prettyPrinters) {
try {
return prettyPrinter.prettyPrint(originalContent);
}
catch (Exception ex) {
// Continue
}
}
}
return originalContent;
}
private interface PrettyPrinter {
String prettyPrint(String string) throws Exception;
}
private static final class XmlPrettyPrinter implements PrettyPrinter {
@Override
public String prettyPrint(String original) throws Exception {
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount",
"4");
transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, "yes");
StringWriter transformed = new StringWriter();
transformer.transform(new StreamSource(new StringReader(original)),
new StreamResult(transformed));
return transformed.toString();
}
}
private static final class JsonPrettyPrinter implements PrettyPrinter {
@Override
public String prettyPrint(String original) throws IOException {
ObjectMapper objectMapper = new ObjectMapper().configure(
SerializationFeature.INDENT_OUTPUT, true);
return objectMapper.writeValueAsString(objectMapper.readTree(original));
}
}
}

View File

@@ -0,0 +1,39 @@
/*
* 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.response;
import org.springframework.mock.web.MockHttpServletResponse;
/**
* A {@code ResponsePostProcessor} is used to modify the response received from a MockMvc
* call prior to the response being documented.
*
* @author Andy Wilkinson
*/
public interface ResponsePostProcessor {
/**
* Post-processes the given {@code response}, returning a, possibly new,
* {@link MockHttpServletResponse} that should now be used.
*
* @param response The response to post-process
* @return The result of the post-processing
* @throws Exception if a failure occurs during the post-processing
*/
MockHttpServletResponse postProcess(MockHttpServletResponse response)
throws Exception;
}

View File

@@ -0,0 +1,74 @@
/*
* 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.response;
/**
* Static factory methods for accessing various {@link ResponsePostProcessor
* ResponsePostProcessors}.
*
* @author Andy Wilkinson
*/
public abstract class ResponsePostProcessors {
private ResponsePostProcessors() {
}
/**
* Returns a {@link ResponsePostProcessor} that will pretty print the content of the
* response.
*
* @return the response post-processor
*/
public static ResponsePostProcessor prettyPrintContent() {
return new PrettyPrintingResponsePostProcessor();
}
/**
* Returns a {@link ResponsePostProcessor} that will remove the headers with the given
* {@code headerNames} from the response.
*
* @param headerNames the name of the headers to remove
* @return the response post-processor
*/
public static ResponsePostProcessor removeHeaders(String... headerNames) {
return new HeaderRemovingResponsePostProcessor(headerNames);
}
/**
* Returns a {@link ResponsePostProcessor} that will update the content of the
* response to mask any links that it contains. Each link is masked my replacing its
* {@code href} with {@code ...}.
*
* @return the response post-processor
*/
public static ResponsePostProcessor maskLinks() {
return new LinkMaskingResponsePostProcessor();
}
/**
* Returns a {@link ResponsePostProcessor} that will update the content of the
* response to mask any links that it contains. Each link is masked my replacing its
* {@code href} with the given {@code mask}.
*
* @param mask the mask to apply
* @return the response post-processor
*/
public static ResponsePostProcessor maskLinksWith(String mask) {
return new LinkMaskingResponsePostProcessor(mask);
}
}

View File

@@ -0,0 +1,59 @@
/*
* 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;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.springframework.restdocs.test.StubMvcResult.result;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.restdocs.ResponseModifier.ResponseModifyingRestDocumentationResultHandler;
import org.springframework.restdocs.response.ResponsePostProcessor;
/**
* Tests for {@link ResponseModifier}
*
* @author Andy Wilkinson
*
*/
public class ResponseModifierTests {
@Test
public void postProcessorsAreApplied() throws Exception {
ResponsePostProcessor first = mock(ResponsePostProcessor.class);
ResponsePostProcessor second = mock(ResponsePostProcessor.class);
MockHttpServletResponse original = new MockHttpServletResponse();
MockHttpServletResponse afterFirst = new MockHttpServletResponse();
MockHttpServletResponse afterSecond = new MockHttpServletResponse();
given(first.postProcess(original)).willReturn(afterFirst);
given(second.postProcess(afterFirst)).willReturn(afterSecond);
RestDocumentationResultHandler resultHandler = new ResponseModifier(first, second)
.andDocument("test");
assertThat(
afterSecond,
is(equalTo(((ResponseModifyingRestDocumentationResultHandler) resultHandler)
.postProcessResponse(result(original)).getResponse())));
}
}

View File

@@ -16,12 +16,21 @@
package org.springframework.restdocs;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.springframework.restdocs.RestDocumentation.document;
import static org.springframework.restdocs.RestDocumentation.modifyResponseTo;
import static org.springframework.restdocs.response.ResponsePostProcessors.maskLinks;
import static org.springframework.restdocs.response.ResponsePostProcessors.prettyPrintContent;
import static org.springframework.restdocs.response.ResponsePostProcessors.removeHeaders;
import static org.springframework.restdocs.test.SnippetMatchers.httpResponse;
import static org.springframework.restdocs.test.SnippetMatchers.snippet;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.io.File;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
@@ -32,9 +41,13 @@ import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.restdocs.RestDocumentationIntegrationTests.TestConfiguration;
import org.springframework.restdocs.config.RestDocumentationConfigurer;
import org.springframework.restdocs.hypermedia.Link;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
@@ -122,7 +135,39 @@ public class RestDocumentationIntegrationTests {
assertExpectedSnippetFilesExist(
new File("build/generated-snippets/multi-step-3/"), "http-request.adoc",
"http-response.adoc", "curl-request.adoc");
}
@Test
public void postProcessedResponse() throws Exception {
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.apply(new RestDocumentationConfigurer()).build();
mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()).andDo(document("original"));
assertThat(
new File("build/generated-snippets/original/http-response.adoc"),
is(snippet().withContents(
httpResponse(HttpStatus.OK)
.header("a", "alpha")
.header("Content-Type", "application/json")
.content(
"{\"a\":\"alpha\",\"links\":[{\"rel\":\"rel\","
+ "\"href\":\"href\"}]}"))));
mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(modifyResponseTo(prettyPrintContent(), removeHeaders("a"),
maskLinks()).andDocument("post-processed"));
assertThat(
new File("build/generated-snippets/post-processed/http-response.adoc"),
is(snippet().withContents(
httpResponse(HttpStatus.OK).header("Content-Type",
"application/json").content(
String.format("{%n \"a\" : \"alpha\",%n \"links\" :"
+ " [ {%n \"rel\" : \"rel\",%n \"href\" :"
+ " \"...\"%n } ]%n}")))));
}
private void assertExpectedSnippetFilesExist(File directory, String... snippets) {
@@ -146,12 +191,15 @@ public class RestDocumentationIntegrationTests {
static class TestController {
@RequestMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE)
public Map<String, String> foo() {
Map<String, String> response = new HashMap<String, String>();
public ResponseEntity<Map<String, Object>> foo() {
Map<String, Object> response = new HashMap<>();
response.put("a", "alpha");
return response;
response.put("links", Arrays.asList(new Link("rel", "href")));
HttpHeaders headers = new HttpHeaders();
headers.add("a", "alpha");
return new ResponseEntity<Map<String, Object>>(response, headers,
HttpStatus.OK);
}
}
}

View File

@@ -0,0 +1,68 @@
/*
* 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.response;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.mock.web.MockHttpServletResponse;
public class ContentModifyingResponsePostProcessorTests {
private final MockHttpServletResponse original = new MockHttpServletResponse();
private final ContentModifyingReponsePostProcessor postProcessor = new TestContentModifyingResponsePostProcessor();
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void contentCanBeModified() throws Exception {
MockHttpServletResponse modified = this.postProcessor.postProcess(this.original);
assertThat(modified.getContentAsString(), is(equalTo("modified")));
}
@Test
public void nonContentMethodsAreDelegated() throws Exception {
this.original.addHeader("a", "alpha");
MockHttpServletResponse modified = this.postProcessor.postProcess(this.original);
assertThat(modified.getHeader("a"), is(equalTo("alpha")));
}
@Test
public void getContentAsByteArrayIsUnsupported() throws Exception {
this.thrown.expect(UnsupportedOperationException.class);
this.thrown.expectMessage(equalTo("Following modification, the response's"
+ " content should be accessed as a String"));
this.postProcessor.postProcess(this.original).getContentAsByteArray();
}
private static final class TestContentModifyingResponsePostProcessor extends
ContentModifyingReponsePostProcessor {
@Override
protected String modifyContent(String originalContent) throws Exception {
return "modified";
}
}
}

View File

@@ -0,0 +1,91 @@
/*
* 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.response;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.empty;
import static org.junit.Assert.assertThat;
import org.junit.Before;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletResponse;
/**
* Tests for {@link HeaderRemovingResponsePostProcessor}.
*
* @author Andy Wilkinson
*
*/
public class HeaderRemovingResponsePostProcessorTests {
private final MockHttpServletResponse response = new MockHttpServletResponse();
@Before
public void configureResponse() {
this.response.addHeader("a", "alpha");
this.response.addHeader("b", "bravo");
}
@Test
public void containsHeaderHonoursRemovedHeaders() {
MockHttpServletResponse response = removeHeaders("a");
assertThat(response.containsHeader("a"), is(false));
assertThat(response.containsHeader("b"), is(true));
}
@Test
public void getHeaderNamesHonoursRemovedHeaders() {
MockHttpServletResponse response = removeHeaders("a");
assertThat(response.getHeaderNames(), contains("b"));
}
@Test
public void getHeaderHonoursRemovedHeaders() {
MockHttpServletResponse response = removeHeaders("a");
assertThat(response.getHeader("a"), is(nullValue()));
assertThat(response.getHeader("b"), is("bravo"));
}
@Test
public void getHeadersHonoursRemovedHeaders() {
MockHttpServletResponse response = removeHeaders("a");
assertThat(response.getHeaders("a"), is(empty()));
assertThat(response.getHeaders("b"), contains("bravo"));
}
@Test
public void getHeaderValueHonoursRemovedHeaders() {
MockHttpServletResponse response = removeHeaders("a");
assertThat(response.getHeaderValue("a"), is(nullValue()));
assertThat(response.getHeaderValue("b"), is((Object) "bravo"));
}
@Test
public void getHeaderValuesHonoursRemovedHeaders() {
MockHttpServletResponse response = removeHeaders("a");
assertThat(response.getHeaderValues("a"), is(empty()));
assertThat(response.getHeaderValues("b"), contains((Object) "bravo"));
}
private MockHttpServletResponse removeHeaders(String... headerNames) {
return new HeaderRemovingResponsePostProcessor(headerNames)
.postProcess(this.response);
}
}

View File

@@ -0,0 +1,150 @@
/*
* 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.response;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.springframework.restdocs.hypermedia.Link;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
public class LinkMaskingResponsePostProcessorTests {
private final LinkMaskingResponsePostProcessor postProcessor = new LinkMaskingResponsePostProcessor();
private final Link[] links = new Link[] { new Link("a", "alpha"),
new Link("b", "bravo") };
private final Link[] maskedLinks = new Link[] { new Link("a", "..."),
new Link("b", "...") };
@Test
public void halLinksAreMasked() throws Exception {
assertThat(this.postProcessor.modifyContent(halPayloadWithLinks(this.links)),
is(equalTo(halPayloadWithLinks(this.maskedLinks))));
}
@Test
public void formattedHalLinksAreMasked() throws Exception {
assertThat(
this.postProcessor
.modifyContent(formattedHalPayloadWithLinks(this.links)),
is(equalTo(formattedHalPayloadWithLinks(this.maskedLinks))));
}
@Test
public void atomLinksAreMasked() throws Exception {
assertThat(this.postProcessor.modifyContent(atomPayloadWithLinks(this.links)),
is(equalTo(atomPayloadWithLinks(this.maskedLinks))));
}
@Test
public void formattedAtomLinksAreMasked() throws Exception {
assertThat(
this.postProcessor
.modifyContent(formattedAtomPayloadWithLinks(this.links)),
is(equalTo(formattedAtomPayloadWithLinks(this.maskedLinks))));
}
@Test
public void maskCanBeCustomized() throws Exception {
assertThat(
new LinkMaskingResponsePostProcessor("custom")
.modifyContent(formattedAtomPayloadWithLinks(this.links)),
is(equalTo(formattedAtomPayloadWithLinks(new Link("a", "custom"),
new Link("b", "custom")))));
}
private String atomPayloadWithLinks(Link... links) throws JsonProcessingException {
return new ObjectMapper().writeValueAsString(createAtomPayload(links));
}
private String formattedAtomPayloadWithLinks(Link... links)
throws JsonProcessingException {
return new ObjectMapper().configure(SerializationFeature.INDENT_OUTPUT, true)
.writeValueAsString(createAtomPayload(links));
}
private AtomPayload createAtomPayload(Link... links) {
AtomPayload payload = new AtomPayload();
payload.setLinks(Arrays.asList(links));
return payload;
}
private String halPayloadWithLinks(Link... links) throws JsonProcessingException {
return new ObjectMapper().writeValueAsString(createHalPayload(links));
}
private String formattedHalPayloadWithLinks(Link... links)
throws JsonProcessingException {
return new ObjectMapper().configure(SerializationFeature.INDENT_OUTPUT, true)
.writeValueAsString(createHalPayload(links));
}
private HalPayload createHalPayload(Link... links) {
HalPayload payload = new HalPayload();
Map<String, Object> linksMap = new LinkedHashMap<>();
for (Link link : links) {
Map<String, String> linkMap = new HashMap<>();
linkMap.put("href", link.getHref());
linksMap.put(link.getRel(), linkMap);
}
payload.setLinks(linksMap);
return payload;
}
static final class AtomPayload {
private List<Link> links;
public void setLinks(List<Link> links) {
this.links = links;
}
public List<Link> getLinks() {
return this.links;
}
}
static final class HalPayload {
private Map<String, Object> links;
@JsonProperty("_links")
public Map<String, Object> getLinks() {
return this.links;
}
public void setLinks(Map<String, Object> links) {
this.links = links;
}
}
}

View File

@@ -0,0 +1,59 @@
/*
* 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.response;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertThat;
import org.junit.Test;
/**
* Tests for {@link PrettyPrintingResponsePostProcessor}
*
* @author Andy Wilkinson
*
*/
public class PrettyPrintingResponsePostProcessorTests {
@Test
public void prettyPrintJson() throws Exception {
assertThat(new PrettyPrintingResponsePostProcessor().modifyContent("{\"a\":5}"),
equalTo(String.format("{%n \"a\" : 5%n}")));
}
@Test
public void prettyPrintXml() throws Exception {
assertThat(
new PrettyPrintingResponsePostProcessor()
.modifyContent("<one a=\"alpha\"><two b=\"bravo\"/></one>"),
equalTo(String.format("<?xml version=\"1.0\" encoding=\"UTF-8\"?>%n"
+ "<one a=\"alpha\">%n <two b=\"bravo\"/>%n</one>%n")));
}
@Test
public void empytContentIsHandledGracefully() throws Exception {
assertThat(new PrettyPrintingResponsePostProcessor().modifyContent(""),
equalTo(""));
}
@Test
public void nonJsonAndNonXmlContentIsHandledGracefully() throws Exception {
String content = "abcdefg";
assertThat(new PrettyPrintingResponsePostProcessor().modifyContent(content),
equalTo(content));
}
}

View File

@@ -16,26 +16,18 @@
package org.springframework.restdocs.test;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import org.hamcrest.Matcher;
import org.hamcrest.collection.IsIterableContainingInAnyOrder;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.springframework.restdocs.snippet.SnippetWritingResultHandler;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;
import org.springframework.restdocs.test.SnippetMatchers.SnippetMatcher;
/**
* The {@code ExpectedSnippet} rule is used to verify that a
@@ -49,7 +41,7 @@ public class ExpectedSnippet implements TestRule {
private String expectedType;
private Matcher<String> expectedContents;
private SnippetMatcher snippet = SnippetMatchers.snippet();
private File outputDir;
@@ -104,11 +96,7 @@ public class ExpectedSnippet implements TestRule {
if (this.outputDir != null && this.expectedName != null) {
File snippetDir = new File(this.outputDir, this.expectedName);
File snippetFile = new File(snippetDir, this.expectedType + ".adoc");
assertTrue("The file " + snippetFile + " does not exist or is not a file",
snippetFile.isFile());
if (this.expectedContents != null) {
assertThat(read(snippetFile), this.expectedContents);
}
assertThat(snippetFile, is(this.snippet));
}
}
@@ -154,45 +142,7 @@ public class ExpectedSnippet implements TestRule {
}
public void withContents(Matcher<String> matcher) {
this.expectedContents = matcher;
}
private String read(File snippetFile) throws IOException {
return FileCopyUtils.copyToString(new FileReader(snippetFile));
}
public Matcher<Iterable<? extends String>> asciidoctorTableWith(String[] header,
String[]... rows) {
Collection<Matcher<? super String>> matchers = new ArrayList<Matcher<? super String>>();
for (String headerItem : header) {
matchers.add(equalTo(headerItem));
}
for (String[] row : rows) {
for (String rowItem : row) {
matchers.add(equalTo(rowItem));
}
}
matchers.add(equalTo("|==="));
matchers.add(equalTo(""));
return new IsIterableContainingInAnyOrder<String>(matchers);
}
public String[] header(String... columns) {
String header = "|"
+ StringUtils.collectionToDelimitedString(Arrays.asList(columns), "|");
return new String[] { "", "|===", header, "" };
}
public String[] row(String... entries) {
List<String> lines = new ArrayList<String>();
for (String entry : entries) {
lines.add("|" + entry);
}
lines.add("");
return lines.toArray(new String[lines.size()]);
this.snippet.withContents(matcher);
}
}

View File

@@ -16,6 +16,9 @@
package org.springframework.restdocs.test;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
@@ -25,6 +28,7 @@ import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.springframework.http.HttpStatus;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMethod;
@@ -35,6 +39,10 @@ import org.springframework.web.bind.annotation.RequestMethod;
*/
public class SnippetMatchers {
public static SnippetMatcher snippet() {
return new SnippetMatcher();
}
public static AsciidoctorTableMatcher tableWithHeader(String... headers) {
return new AsciidoctorTableMatcher(headers);
}
@@ -52,7 +60,8 @@ public class SnippetMatchers {
return new AsciidoctorCodeBlockMatcher(language);
}
private static abstract class AbstractSnippetMatcher extends BaseMatcher<String> {
private static abstract class AbstractSnippetContentMatcher extends
BaseMatcher<String> {
private List<String> lines = new ArrayList<String>();
@@ -99,7 +108,7 @@ public class SnippetMatchers {
}
public static class AsciidoctorCodeBlockMatcher<T extends AsciidoctorCodeBlockMatcher<T>>
extends AbstractSnippetMatcher {
extends AbstractSnippetContentMatcher {
protected AsciidoctorCodeBlockMatcher(String language) {
this.addLine("");
@@ -152,7 +161,7 @@ public class SnippetMatchers {
}
public static class AsciidoctorTableMatcher extends AbstractSnippetMatcher {
public static class AsciidoctorTableMatcher extends AbstractSnippetContentMatcher {
private AsciidoctorTableMatcher(String... columns) {
this.addLine("");
@@ -174,4 +183,66 @@ public class SnippetMatchers {
return this;
}
}
public static class SnippetMatcher extends BaseMatcher<File> {
private Matcher<String> expectedContents;
@Override
public boolean matches(Object item) {
if (snippetFileExists(item)) {
if (this.expectedContents != null) {
try {
return this.expectedContents.matches(read((File) item));
}
catch (IOException e) {
return false;
}
}
return true;
}
return false;
}
private boolean snippetFileExists(Object item) {
return item instanceof File && ((File) item).isFile();
}
private String read(File snippetFile) throws IOException {
return FileCopyUtils.copyToString(new FileReader(snippetFile));
}
@Override
public void describeMismatch(Object item, Description description) {
if (!snippetFileExists(item)) {
description.appendText("The file " + item + " does not exist");
}
else if (this.expectedContents != null) {
try {
this.expectedContents
.describeMismatch(read((File) item), description);
}
catch (IOException e) {
description.appendText("The contents of " + item
+ " cound not be read");
}
}
}
@Override
public void describeTo(Description description) {
if (this.expectedContents != null) {
this.expectedContents.describeTo(description);
}
else {
description.appendText("Asciidoctor snippet");
}
}
public SnippetMatcher withContents(Matcher<String> matcher) {
this.expectedContents = matcher;
return this;
}
}
}

View File

@@ -45,6 +45,11 @@ public class StubMvcResult implements MvcResult {
return new StubMvcResult(requestBuilder);
}
public static StubMvcResult result(RequestBuilder requestBuilder,
MockHttpServletResponse response) {
return new StubMvcResult(requestBuilder, response);
}
public static StubMvcResult result(MockHttpServletRequest request) {
return new StubMvcResult(request);
}
@@ -65,6 +70,10 @@ public class StubMvcResult implements MvcResult {
this(new MockHttpServletRequest(), response);
}
private StubMvcResult(RequestBuilder requestBuilder, MockHttpServletResponse response) {
this(requestBuilder.buildRequest(new MockServletContext()), response);
}
private StubMvcResult(MockHttpServletRequest request, MockHttpServletResponse response) {
this.request = request;
this.response = response;