From e5f35c98f371e7132e114fdcffca5f0c14fc40d2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 30 Apr 2015 11:12:55 +0100 Subject: [PATCH] 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 --- build.gradle | 4 +- .../restdocs/ResponseModifier.java | 117 ++++++++++++++ .../restdocs/RestDocumentation.java | 17 ++ .../RestDocumentationResultHandler.java | 17 +- .../ContentModifyingReponsePostProcessor.java | 96 +++++++++++ .../HeaderRemovingResponsePostProcessor.java | 132 +++++++++++++++ .../LinkMaskingResponsePostProcessor.java | 55 +++++++ .../PrettyPrintingResponsePostProcessor.java | 88 ++++++++++ .../response/ResponsePostProcessor.java | 39 +++++ .../response/ResponsePostProcessors.java | 74 +++++++++ .../restdocs/ResponseModifierTests.java | 59 +++++++ .../RestDocumentationIntegrationTests.java | 56 ++++++- ...ntModifyingResponsePostProcessorTests.java | 68 ++++++++ ...derRemovingResponsePostProcessorTests.java | 91 +++++++++++ ...LinkMaskingResponsePostProcessorTests.java | 150 ++++++++++++++++++ ...ttyPrintingResponsePostProcessorTests.java | 59 +++++++ .../restdocs/test/ExpectedSnippet.java | 60 +------ .../restdocs/test/SnippetMatchers.java | 77 ++++++++- .../restdocs/test/StubMvcResult.java | 9 ++ 19 files changed, 1196 insertions(+), 72 deletions(-) create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/ResponseModifier.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/response/ContentModifyingReponsePostProcessor.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/response/HeaderRemovingResponsePostProcessor.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/response/LinkMaskingResponsePostProcessor.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/response/PrettyPrintingResponsePostProcessor.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/response/ResponsePostProcessor.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/response/ResponsePostProcessors.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/ResponseModifierTests.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/response/ContentModifyingResponsePostProcessorTests.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/response/HeaderRemovingResponsePostProcessorTests.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/response/LinkMaskingResponsePostProcessorTests.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/response/PrettyPrintingResponsePostProcessorTests.java diff --git a/build.gradle b/build.gradle index 9bbf6b06..ef09e161 100644 --- a/build.gradle +++ b/build.gradle @@ -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" diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/ResponseModifier.java b/spring-restdocs/src/main/java/org/springframework/restdocs/ResponseModifier.java new file mode 100644 index 00000000..40c04285 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/ResponseModifier.java @@ -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 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)); + } + + } + + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentation.java index 87cd79e4..f0320e91 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentation.java @@ -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); + } + } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java index 912d4650..4beea6a7 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java @@ -49,7 +49,7 @@ public class RestDocumentationResultHandler implements ResultHandler { private final String outputDir; - private List delegates; + private List 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); + } + } + } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/response/ContentModifyingReponsePostProcessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/response/ContentModifyingReponsePostProcessor.java new file mode 100644 index 00000000..67bc6ddc --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/response/ContentModifyingReponsePostProcessor.java @@ -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)); + } + + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/response/HeaderRemovingResponsePostProcessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/response/HeaderRemovingResponsePostProcessor.java new file mode 100644 index 00000000..e3b926d0 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/response/HeaderRemovingResponsePostProcessor.java @@ -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 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 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 hiddenHeaders; + + private HeaderHidingMethodInterceptor(Set 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 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; + } + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/response/LinkMaskingResponsePostProcessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/response/LinkMaskingResponsePostProcessor.java new file mode 100644 index 00000000..6ad58e4c --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/response/LinkMaskingResponsePostProcessor.java @@ -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(); + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/response/PrettyPrintingResponsePostProcessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/response/PrettyPrintingResponsePostProcessor.java new file mode 100644 index 00000000..41bd7f5b --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/response/PrettyPrintingResponsePostProcessor.java @@ -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 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)); + } + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/response/ResponsePostProcessor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/response/ResponsePostProcessor.java new file mode 100644 index 00000000..c685626f --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/response/ResponsePostProcessor.java @@ -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; +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/response/ResponsePostProcessors.java b/spring-restdocs/src/main/java/org/springframework/restdocs/response/ResponsePostProcessors.java new file mode 100644 index 00000000..b04128a9 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/response/ResponsePostProcessors.java @@ -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); + } +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/ResponseModifierTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/ResponseModifierTests.java new file mode 100644 index 00000000..63a3862c --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/ResponseModifierTests.java @@ -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()))); + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java index 0f51b483..646623b3 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java @@ -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 foo() { - Map response = new HashMap(); + public ResponseEntity> foo() { + Map 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>(response, headers, + HttpStatus.OK); } - } } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/response/ContentModifyingResponsePostProcessorTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/response/ContentModifyingResponsePostProcessorTests.java new file mode 100644 index 00000000..dd07830a --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/response/ContentModifyingResponsePostProcessorTests.java @@ -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"; + } + + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/response/HeaderRemovingResponsePostProcessorTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/response/HeaderRemovingResponsePostProcessorTests.java new file mode 100644 index 00000000..1891eadc --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/response/HeaderRemovingResponsePostProcessorTests.java @@ -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); + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/response/LinkMaskingResponsePostProcessorTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/response/LinkMaskingResponsePostProcessorTests.java new file mode 100644 index 00000000..ddc66d45 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/response/LinkMaskingResponsePostProcessorTests.java @@ -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 linksMap = new LinkedHashMap<>(); + for (Link link : links) { + Map linkMap = new HashMap<>(); + linkMap.put("href", link.getHref()); + linksMap.put(link.getRel(), linkMap); + } + payload.setLinks(linksMap); + return payload; + } + + static final class AtomPayload { + + private List links; + + public void setLinks(List links) { + this.links = links; + } + + public List getLinks() { + return this.links; + } + + } + + static final class HalPayload { + + private Map links; + + @JsonProperty("_links") + public Map getLinks() { + return this.links; + } + + public void setLinks(Map links) { + this.links = links; + } + + } +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/response/PrettyPrintingResponsePostProcessorTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/response/PrettyPrintingResponsePostProcessorTests.java new file mode 100644 index 00000000..5a5b4577 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/response/PrettyPrintingResponsePostProcessorTests.java @@ -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(""), + equalTo(String.format("%n" + + "%n %n%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)); + } +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java b/spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java index d7ec0d71..1bc2dda0 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java @@ -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 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 matcher) { - this.expectedContents = matcher; - } - - private String read(File snippetFile) throws IOException { - return FileCopyUtils.copyToString(new FileReader(snippetFile)); - } - - public Matcher> asciidoctorTableWith(String[] header, - String[]... rows) { - Collection> matchers = new ArrayList>(); - 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(matchers); - } - - public String[] header(String... columns) { - String header = "|" - + StringUtils.collectionToDelimitedString(Arrays.asList(columns), "|"); - return new String[] { "", "|===", header, "" }; - } - - public String[] row(String... entries) { - List lines = new ArrayList(); - for (String entry : entries) { - lines.add("|" + entry); - } - lines.add(""); - return lines.toArray(new String[lines.size()]); + this.snippet.withContents(matcher); } } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java b/spring-restdocs/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java index 801ad07f..a8cdb4d0 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java @@ -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 { + private static abstract class AbstractSnippetContentMatcher extends + BaseMatcher { private List lines = new ArrayList(); @@ -99,7 +108,7 @@ public class SnippetMatchers { } public static class AsciidoctorCodeBlockMatcher> - 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 { + + private Matcher 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 matcher) { + this.expectedContents = matcher; + return this; + } + + } } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java b/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java index ab11f19d..1d4d4651 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java @@ -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;