From 48f4d1343ac2cecd1ad379ed485da5fdf2c39574 Mon Sep 17 00:00:00 2001 From: andreasevers Date: Sun, 20 Sep 2015 12:44:33 +0200 Subject: [PATCH] Add support for documenting HTTP headers This commit adds support for documenting HTTP headers in both requests and responses. Closes gh-140 --- config/checkstyle/checkstyle.xml | 2 +- .../src/main/asciidoc/api-guide.adoc | 7 + .../com/example/notes/ApiDocumentation.java | 11 ++ .../headers/AbstractHeadersSnippet.java | 147 ++++++++++++++++ .../restdocs/headers/HeaderDescriptor.java | 72 ++++++++ .../restdocs/headers/HeaderDocumentation.java | 116 ++++++++++++ .../headers/RequestHeadersSnippet.java | 63 +++++++ .../headers/ResponseHeadersSnippet.java | 63 +++++++ .../restdocs/headers/package-info.java | 20 +++ .../templates/default-request-headers.snippet | 9 + .../default-response-headers.snippet | 9 + .../headers/RequestHeadersSnippetTests.java | 164 +++++++++++++++++ .../headers/ResponseHeadersSnippetTests.java | 165 ++++++++++++++++++ .../restdocs/test/ExpectedSnippet.java | 10 ++ .../request-headers-with-extra-column.snippet | 10 ++ .../request-headers-with-title.snippet | 10 ++ ...response-headers-with-extra-column.snippet | 10 ++ .../response-headers-with-title.snippet | 10 ++ 18 files changed, 897 insertions(+), 1 deletion(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDescriptor.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/package-info.java create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-request-headers.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-response-headers.snippet create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/request-headers-with-extra-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/request-headers-with-title.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/response-headers-with-extra-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/response-headers-with-title.snippet diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 117567d1..67682279 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -73,7 +73,7 @@ + value="org.junit.Assert.*, org.junit.Assume.*, org.hamcrest.CoreMatchers.*, org.hamcrest.Matchers.*, org.mockito.Mockito.*, org.mockito.BDDMockito.*, org.mockito.Matchers.*, org.springframework.restdocs.curl.CurlDocumentation.*, org.springframework.restdocs.hypermedia.HypermediaDocumentation.*, org.springframework.restdocs.mockmvc.IterableEnumeration.*, org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*, org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*, org.springframework.restdocs.operation.preprocess.Preprocessors.*, org.springframework.restdocs.payload.PayloadDocumentation.*, org.springframework.restdocs.headers.HeaderDocumentation.*, org.springframework.restdocs.request.RequestDocumentation.*, org.springframework.restdocs.snippet.Attributes.*, org.springframework.restdocs.test.SnippetMatchers.*, org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*, org.springframework.test.web.servlet.result.MockMvcResultMatchers.*" /> diff --git a/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc b/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc index 58c579bb..c0a42afd 100644 --- a/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc +++ b/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc @@ -58,6 +58,13 @@ use of HTTP status codes. | The requested resource did not exist |=== +[[overview-headers]] +== Headers + +Every response has the following header(s): + +include::{snippets}/headers-example/response-headers.adoc[] + [[overview-errors]] == Errors diff --git a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java index 81a7fef7..4da12b33 100644 --- a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java +++ b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java @@ -18,6 +18,8 @@ package com.example.notes; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; @@ -98,6 +100,15 @@ public class ApiDocumentation { .alwaysDo(this.document) .build(); } + + @Test + public void headersExample() throws Exception { + this.document.snippets(responseHeaders( + headerWithName("Content-Type").description("The Content-Type of the payload, e.g. `application/hal+json`"))); + + this.mockMvc.perform(get("/")) + .andExpect(status().isOk()); + } @Test public void errorExample() throws Exception { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java new file mode 100644 index 00000000..2d71e7fc --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java @@ -0,0 +1,147 @@ +/* + * 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.headers; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.snippet.TemplatedSnippet; +import org.springframework.util.Assert; + +/** + * Abstract {@link TemplatedSnippet} subclass that provides a base for snippets that + * document a RESTful resource's request or response headers. + * + * @author Andreas Evers + */ +public abstract class AbstractHeadersSnippet extends TemplatedSnippet { + + private List headerDescriptors; + + private String type; + + /** + * Creates a new {@code AbstractHeadersSnippet} that will produce a snippet named + * {@code -headers}. The headers will be documented using the given + * {@code descriptors} and the given {@code attributes} will be included in the model + * during template rendering. + * + * @param type the type of the headers + * @param descriptors the header descriptors + * @param attributes the additional attributes + */ + protected AbstractHeadersSnippet(String type, List descriptors, + Map attributes) { + super(type + "-headers", attributes); + for (HeaderDescriptor descriptor : descriptors) { + Assert.notNull(descriptor.getName()); + Assert.notNull(descriptor.getDescription()); + } + this.headerDescriptors = descriptors; + this.type = type; + } + + @Override + protected Map createModel(Operation operation) { + validateHeaderDocumentation(operation); + + Map model = new HashMap<>(); + List> headers = new ArrayList<>(); + model.put("headers", headers); + for (HeaderDescriptor descriptor : this.headerDescriptors) { + headers.add(createModelForDescriptor(descriptor)); + } + return model; + } + + private void validateHeaderDocumentation(Operation operation) { + List missingHeaders = findMissingHeaders(operation); + + if (!missingHeaders.isEmpty()) { + String message = ""; + if (!missingHeaders.isEmpty()) { + List names = new ArrayList(); + for (HeaderDescriptor headerDescriptor : missingHeaders) { + names.add(headerDescriptor.getName()); + } + message += "Headers with the following names were not found in the " + + this.type + ": " + names; + } + throw new SnippetException(message); + } + } + + /** + * Finds the headers that are missing from the operation. A header is missing if + * it is described by one of the {@code headerDescriptors} but is not present in the + * operation. + * + * @param operation the operation + * @return descriptors for the headers that are missing from the operation + */ + protected List findMissingHeaders(Operation operation) { + List missingHeaders = new ArrayList(); + for (HeaderDescriptor headerDescriptor : this.headerDescriptors) { + if (!headerDescriptor.isOptional() + && !getHeaders(operation).contains(headerDescriptor.getName())) { + missingHeaders.add(headerDescriptor); + } + } + + return missingHeaders; + } + + /** + * Returns the headers of the request or response extracted form the given + * {@code operation}. + * + * @param operation The operation + * @return The headers + */ + protected abstract Set getHeaders(Operation operation); + + /** + * Returns the list of {@link HeaderDescriptor HeaderDescriptors} that will be used to + * generate the documentation. + * + * @return the header descriptors + */ + protected final List getHeaderDescriptors() { + return this.headerDescriptors; + } + + /** + * Returns a model for the given {@code descriptor}. + * + * @param descriptor the descriptor + * @return the model + */ + protected Map createModelForDescriptor(HeaderDescriptor descriptor) { + Map model = new HashMap(); + model.put("name", descriptor.getName()); + model.put("description", descriptor.getDescription()); + model.put("optional", descriptor.isOptional()); + model.putAll(descriptor.getAttributes()); + return model; + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDescriptor.java new file mode 100644 index 00000000..8e544bc2 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDescriptor.java @@ -0,0 +1,72 @@ +/* + * Copyright 2014-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.restdocs.headers; + +import org.springframework.http.HttpHeaders; +import org.springframework.restdocs.snippet.AbstractDescriptor; + +/** + * A description of a header found in a request or response. + * + * @author Andreas Evers + * @see HeaderDocumentation#headerWithName(String) + */ +public class HeaderDescriptor extends AbstractDescriptor { + + private final String name; + + private boolean optional; + + /** + * Creates a new {@code HeaderDescriptor} describing the header with the given + * {@code name}. + * @param name the name + * @see HttpHeaders + */ + protected HeaderDescriptor(String name) { + this.name = name; + } + + /** + * Marks the header as optional. + * + * @return {@code this} + */ + public final HeaderDescriptor optional() { + this.optional = true; + return this; + } + + /** + * Returns the name for the header. + * + * @return the header name + */ + public String getName() { + return this.name; + } + + /** + * Returns {@code true} if the described header is optional, otherwise {@code false}. + * + * @return {@code true} if the described header is optional, otherwise {@code false} + */ + public final boolean isOptional() { + return this.optional; + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java new file mode 100644 index 00000000..a7d5afc6 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java @@ -0,0 +1,116 @@ +/* + * 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.headers; + +import java.util.Arrays; +import java.util.Map; + +import org.springframework.restdocs.snippet.Snippet; + +/** + * Static factory methods for documenting a RESTful API's request and response headers. + * + * @author Andreas Evers + */ +public abstract class HeaderDocumentation { + + private HeaderDocumentation() { + + } + + /** + * Creates a {@code HeaderDescriptor} that describes a header with the given + * {@code name}. + * + * @param name The name of the header + * @return a {@code HeaderDescriptor} ready for further configuration + */ + public static HeaderDescriptor headerWithName(String name) { + return new HeaderDescriptor(name); + } + + /** + * Returns a handler that will produce a snippet documenting the headers of the API + * call's request. + *

+ * If a header is documented, is not marked as optional, and is not present in the + * request, a failure will occur. If a header is present in the request, but is not + * documented by one of the descriptors, there will be no failure. + * + * @param descriptors The descriptions of the request's headers + * @return the handler + * @see #headerWithName(String) + */ + public static Snippet requestHeaders(HeaderDescriptor... descriptors) { + return new RequestHeadersSnippet(Arrays.asList(descriptors)); + } + + /** + * Returns a handler that will produce a snippet documenting the headers of the API + * call's request. The given {@code attributes} will be available during snippet + * generation. + *

+ * If a header is documented, is not marked as optional, and is not present in the + * request, a failure will occur. If a header is present in the request, but is not + * documented by one of the descriptors, there will be no failure. + * + * @param attributes Attributes made available during rendering of the snippet + * @param descriptors The descriptions of the request's headers + * @return the handler + * @see #headerWithName(String) + */ + public static Snippet requestHeaders(Map attributes, + HeaderDescriptor... descriptors) { + return new RequestHeadersSnippet(Arrays.asList(descriptors), attributes); + } + + /** + * Returns a handler that will produce a snippet documenting the headers of the API + * call's response. + *

+ * If a header is documented, is not marked as optional, and is not present in the + * response, a failure will occur. If a header is present in the response, but is not + * documented by one of the descriptors, there will be no failure. + * + * @param descriptors The descriptions of the response's headers + * @return the handler + * @see #headerWithName(String) + */ + public static Snippet responseHeaders(HeaderDescriptor... descriptors) { + return new ResponseHeadersSnippet(Arrays.asList(descriptors)); + } + + /** + * Returns a handler that will produce a snippet documenting the headers of the API + * call's response. The given {@code attributes} will be available during snippet + * generation. + *

+ * If a header is documented, is not marked as optional, and is not present in the + * response, a failure will occur. If a header is present in the response, but is not + * documented by one of the descriptors, there will be no failure. + * + * @param attributes Attributes made available during rendering of the snippet + * @param descriptors The descriptions of the response's headers + * @return the handler + * @see #headerWithName(String) + */ + public static Snippet responseHeaders(Map attributes, + HeaderDescriptor... descriptors) { + return new ResponseHeadersSnippet(Arrays.asList(descriptors), attributes); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java new file mode 100644 index 00000000..deb7efca --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java @@ -0,0 +1,63 @@ +/* + * 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.headers; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.snippet.Snippet; + +/** + * A {@link Snippet} that documents the headers in a request. + * + * @author Andreas Evers + * @see HeaderDocumentation#requestHeaders(HeaderDescriptor...) + * @see HeaderDocumentation#requestHeaders(Map, HeaderDescriptor...) + */ +public class RequestHeadersSnippet extends AbstractHeadersSnippet { + + /** + * Creates a new {@code RequestHeadersSnippet} that will document the headers in the + * request using the given {@code descriptors}. + * + * @param descriptors the descriptors + */ + protected RequestHeadersSnippet(List descriptors) { + this(descriptors, null); + } + + /** + * Creates a new {@code RequestHeadersSnippet} that will document the headers in the + * request using the given {@code descriptors}. The given {@code attributes} will be + * included in the model during template rendering. + * + * @param descriptors the descriptors + * @param attributes the additional attributes + */ + protected RequestHeadersSnippet(List descriptors, + Map attributes) { + super("request", descriptors, attributes); + } + + @Override + protected Set getHeaders(Operation operation) { + return operation.getRequest().getHeaders().keySet(); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java new file mode 100644 index 00000000..bb0d5d07 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java @@ -0,0 +1,63 @@ +/* + * 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.headers; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.snippet.Snippet; + +/** + * A {@link Snippet} that documents the headers in a response. + * + * @author Andreas Evers + * @see HeaderDocumentation#responseHeaders(HeaderDescriptor...) + * @see HeaderDocumentation#responseHeaders(Map, HeaderDescriptor...) + */ +public class ResponseHeadersSnippet extends AbstractHeadersSnippet { + + /** + * Creates a new {@code ResponseHeadersSnippet} that will document the headers in the + * response using the given {@code descriptors}. + * + * @param descriptors the descriptors + */ + protected ResponseHeadersSnippet(List descriptors) { + this(descriptors, null); + } + + /** + * Creates a new {@code ResponseHeadersSnippet} that will document the headers in the + * response using the given {@code descriptors}. The given {@code attributes} will be + * included in the model during template rendering. + * + * @param descriptors the descriptors + * @param attributes the additional attributes + */ + protected ResponseHeadersSnippet(List descriptors, + Map attributes) { + super("response", descriptors, attributes); + } + + @Override + protected Set getHeaders(Operation operation) { + return operation.getResponse().getHeaders().keySet(); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/package-info.java new file mode 100644 index 00000000..f27ba176 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Documenting the headers of a RESTful API's requests and responses. + */ +package org.springframework.restdocs.headers; diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-request-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-request-headers.snippet new file mode 100644 index 00000000..5f14875d --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-request-headers.snippet @@ -0,0 +1,9 @@ +|=== +|Name|Description + +{{#headers}} +|{{name}} +|{{description}} + +{{/headers}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-response-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-response-headers.snippet new file mode 100644 index 00000000..5f14875d --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-response-headers.snippet @@ -0,0 +1,9 @@ +|=== +|Name|Description + +{{#headers}} +|{{name}} +|{{description}} + +{{/headers}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java new file mode 100644 index 00000000..93e83136 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java @@ -0,0 +1,164 @@ +/* + * 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.headers; + +import java.io.IOException; +import java.util.Arrays; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.core.io.FileSystemResource; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateResourceResolver; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.OperationBuilder; + +import static org.hamcrest.CoreMatchers.endsWith; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; +import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; + +/** + * Tests for {@link RequestHeadersSnippet}. + * + * @author Andreas Evers + */ +public class RequestHeadersSnippetTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Rule + public final ExpectedSnippet snippet = new ExpectedSnippet(); + + @Test + public void requestWithHeaders() throws IOException { + this.snippet.expectRequestHeaders("request-with-headers").withContents(// + tableWithHeader("Name", "Description") // + .row("X-Test", "one") // + .row("Accept", "two") // + .row("Accept-Encoding", "three") // + .row("Accept-Language", "four") // + .row("Cache-Control", "five") // + .row("Connection", "six")); + new RequestHeadersSnippet(Arrays.asList( + headerWithName("X-Test").description("one"), // + headerWithName("Accept").description("two"), // + headerWithName("Accept-Encoding").description("three"), // + headerWithName("Accept-Language").description("four"), // + headerWithName("Cache-Control").description("five"), // + headerWithName("Connection").description("six"))) // + .document(new OperationBuilder("request-with-headers", this.snippet + .getOutputDirectory()) // + .request("http://localhost") // + .header("X-Test", "test") // + .header("Accept", "*/*") // + .header("Accept-Encoding", "gzip, deflate") // + .header("Accept-Language", "en-US,en;q=0.5") // + .header("Cache-Control", "max-age=0") // + .header("Connection", "keep-alive") // + .build()); + } + + @Test + public void undocumentedRequestHeader() throws IOException { + new RequestHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( + "one"))).document(new OperationBuilder("undocumented-request-header", + this.snippet.getOutputDirectory()).request("http://localhost") + .header("X-Test", "test").header("Accept", "*/*").build()); + } + + @Test + public void missingRequestHeader() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(equalTo("Headers with the following names were not found" + + " in the request: [Accept]")); + new RequestHeadersSnippet(Arrays.asList(headerWithName("Accept").description( + "one"))).document(new OperationBuilder("missing-request-headers", + this.snippet.getOutputDirectory()).request("http://localhost").build()); + } + + @Test + public void undocumentedRequestHeaderAndMissingRequestHeader() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(endsWith("Headers with the following names were not found" + + " in the request: [Accept]")); + new RequestHeadersSnippet(Arrays.asList(headerWithName("Accept").description( + "one"))).document(new OperationBuilder( + "undocumented-request-header-and-missing-request-header", this.snippet + .getOutputDirectory()).request("http://localhost") + .header("X-Test", "test").build()); + } + + @Test + public void requestHeadersWithCustomDescriptorAttributes() throws IOException { + this.snippet.expectRequestHeaders("request-headers-with-custom-attributes") + .withContents(// + tableWithHeader("Name", "Description", "Foo") // + .row("X-Test", "one", "alpha") // + .row("Accept-Encoding", "two", "bravo") // + .row("Accept", "three", "charlie")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("request-headers")).willReturn( + snippetResource("request-headers-with-extra-column")); + new RequestHeadersSnippet(Arrays.asList( + headerWithName("X-Test").description("one").attributes( + key("foo").value("alpha")), + headerWithName("Accept-Encoding").description("two").attributes( + key("foo").value("bravo")), + headerWithName("Accept").description("three").attributes( + key("foo").value("charlie")))).document(new OperationBuilder( + "request-headers-with-custom-attributes", this.snippet + .getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)).request("http://localhost") + .header("X-Test", "test").header("Accept-Encoding", "gzip, deflate") + .header("Accept", "*/*").build()); + } + + @Test + public void requestHeadersWithCustomAttributes() throws IOException { + this.snippet.expectRequestHeaders("request-headers-with-custom-attributes") + .withContents(startsWith(".Custom title")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("request-headers")).willReturn( + snippetResource("request-headers-with-title")); + new RequestHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( + "one")), attributes(key("title").value("Custom title"))) + .document(new OperationBuilder("request-headers-with-custom-attributes", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .request("http://localhost").header("X-Test", "test").build()); + } + + private FileSystemResource snippetResource(String name) { + return new FileSystemResource("src/test/resources/custom-snippet-templates/" + + name + ".snippet"); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java new file mode 100644 index 00000000..ac6a40ce --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java @@ -0,0 +1,165 @@ +/* + * 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.headers; + +import java.io.IOException; +import java.util.Arrays; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.core.io.FileSystemResource; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateResourceResolver; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.OperationBuilder; + +import static org.hamcrest.CoreMatchers.endsWith; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; +import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; + +/** + * Tests for {@link ReponseHeadersSnippet}. + * + * @author Andreas Evers + */ +public class ResponseHeadersSnippetTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Rule + public final ExpectedSnippet snippet = new ExpectedSnippet(); + + @Test + public void responseWithHeaders() throws IOException { + this.snippet.expectResponseHeaders("response-headers").withContents(// + tableWithHeader("Name", "Description") // + .row("X-Test", "one") // + .row("Content-Type", "two") // + .row("Etag", "three") // + .row("Content-Length", "four") // + .row("Cache-Control", "five") // + .row("Vary", "six")); + new ResponseHeadersSnippet(Arrays.asList( + headerWithName("X-Test").description("one"), // + headerWithName("Content-Type").description("two"), // + headerWithName("Etag").description("three"), // + headerWithName("Content-Length").description("four"), // + headerWithName("Cache-Control").description("five"), // + headerWithName("Vary").description("six"))) // + .document(new OperationBuilder("response-headers", this.snippet + .getOutputDirectory()) // + .response() // + .header("X-Test", "test") // + .header("Content-Type", "application/json") // + .header("Etag", "lskjadldj3ii32l2ij23") // + .header("Content-Length", "19166") // + .header("Cache-Control", "max-age=0") // + .header("Vary", "User-Agent") // + .build()); + } + + @Test + public void responseHeadersWithCustomDescriptorAttributes() throws IOException { + this.snippet.expectResponseHeaders("response-headers-with-custom-attributes") + .withContents(// + tableWithHeader("Name", "Description", "Foo") // + .row("X-Test", "one", "alpha") // + .row("Content-Type", "two", "bravo") // + .row("Etag", "three", "charlie")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("response-headers")).willReturn( + snippetResource("response-headers-with-extra-column")); + new ResponseHeadersSnippet(Arrays.asList( + headerWithName("X-Test").description("one").attributes( + key("foo").value("alpha")), + headerWithName("Content-Type").description("two").attributes( + key("foo").value("bravo")), + headerWithName("Etag").description("three").attributes( + key("foo").value("charlie")))).document(new OperationBuilder( + "response-headers-with-custom-attributes", this.snippet + .getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)).response() + .header("X-Test", "test").header("Content-Type", "application/json") + .header("Etag", "lskjadldj3ii32l2ij23").build()); + } + + @Test + public void responseHeadersWithCustomAttributes() throws IOException { + this.snippet.expectResponseHeaders("response-headers-with-custom-attributes") + .withContents(startsWith(".Custom title")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("response-headers")).willReturn( + snippetResource("response-headers-with-title")); + new ResponseHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( + "one")), attributes(key("title").value("Custom title"))) + .document(new OperationBuilder("response-headers-with-custom-attributes", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)).response() + .header("X-Test", "test").build()); + } + + @Test + public void undocumentedResponseHeader() throws IOException { + new ResponseHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( + "one"))).document(new OperationBuilder("undocumented-response-header", + this.snippet.getOutputDirectory()).response().header("X-Test", "test") + .header("Content-Type", "*/*").build()); + } + + @Test + public void missingResponseHeader() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(equalTo("Headers with the following names were not found" + + " in the response: [Content-Type]")); + new ResponseHeadersSnippet(Arrays.asList(headerWithName("Content-Type") + .description("one"))).document(new OperationBuilder( + "missing-response-headers", this.snippet.getOutputDirectory()).response() + .build()); + } + + @Test + public void undocumentedResponseHeaderAndMissingResponseHeader() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(endsWith("Headers with the following names were not found" + + " in the response: [Content-Type]")); + new ResponseHeadersSnippet(Arrays.asList(headerWithName("Content-Type") + .description("one"))).document(new OperationBuilder( + "undocumented-response-header-and-missing-response-header", this.snippet + .getOutputDirectory()).response().header("X-Test", "test") + .build()); + } + + private FileSystemResource snippetResource(String name) { + return new FileSystemResource("src/test/resources/custom-snippet-templates/" + + name + ".snippet"); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java index c20b0df0..475cb15c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java @@ -75,6 +75,16 @@ public class ExpectedSnippet implements TestRule { return this; } + public ExpectedSnippet expectRequestHeaders(String name) { + expect(name, "request-headers"); + return this; + } + + public ExpectedSnippet expectResponseHeaders(String name) { + expect(name, "response-headers"); + return this; + } + public ExpectedSnippet expectLinks(String name) { expect(name, "links"); return this; diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-headers-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-headers-with-extra-column.snippet new file mode 100644 index 00000000..29d68777 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-headers-with-extra-column.snippet @@ -0,0 +1,10 @@ +|=== +|Name|Description|Foo + +{{#headers}} +|{{name}} +|{{description}} +|{{foo}} + +{{/headers}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-headers-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-headers-with-title.snippet new file mode 100644 index 00000000..998090a0 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-headers-with-title.snippet @@ -0,0 +1,10 @@ +.{{title}} +|=== +|Name|Description + +{{#headers}} +|{{name}} +|{{description}} + +{{/headers}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/response-headers-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/response-headers-with-extra-column.snippet new file mode 100644 index 00000000..29d68777 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/response-headers-with-extra-column.snippet @@ -0,0 +1,10 @@ +|=== +|Name|Description|Foo + +{{#headers}} +|{{name}} +|{{description}} +|{{foo}} + +{{/headers}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/response-headers-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/response-headers-with-title.snippet new file mode 100644 index 00000000..998090a0 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/response-headers-with-title.snippet @@ -0,0 +1,10 @@ +.{{title}} +|=== +|Name|Description + +{{#headers}} +|{{name}} +|{{description}} + +{{/headers}} +|=== \ No newline at end of file