diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 67682279..b7908d74 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.headers.HeaderDocumentation.*, org.springframework.restdocs.hypermedia.HypermediaDocumentation.*, org.springframework.restdocs.mockmvc.IterableEnumeration.*, org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*, org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*, org.springframework.restdocs.payload.PayloadDocumentation.*, org.springframework.restdocs.operation.preprocess.Preprocessors.*, 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/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 7fd84cdf..af979cb0 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -288,6 +288,31 @@ built using one of the methods on `RestDocumentationRequestBuilders` rather than +[[documenting-your-api-http-headers]] +=== HTTP headers + +The headers in a request or response can be documented using `requestHeaders` and +`responseHeaders` respectively. For example: + +[source,java,indent=0] +---- +include::{examples-dir}/com/example/HttpHeaders.java[tags=headers] +---- +<1> Perform a `GET` request with an `Authorization` header that uses basic authentication +<2> Produce a snippet describing the request's headers. Uses the static `requestHeaders` +method on `org.springframework.restdocs.headers.HeaderDocumentation`. +<3> Document the `Authorization` header. Uses the static `headerWithName` method on +`org.springframework.restdocs.headers.HeaderDocumentation. +<4> Produce a snippet describing the response's headers. Uses the static `responseHeaders` +method on `org.springframework.restdocs.headers.HeaderDocumentation`. + +The result is a snippet named `request-headers.adoc` and a snippet named +`response-headers.adoc`. Each contains a table describing the headers. + +When documenting HTTP Headers, the test will fail if a documented header is not found in +the request or response. + + [[documenting-your-api-constraints]] === Documenting constraints diff --git a/docs/src/test/java/com/example/HttpHeaders.java b/docs/src/test/java/com/example/HttpHeaders.java new file mode 100644 index 00000000..f47e3971 --- /dev/null +++ b/docs/src/test/java/com/example/HttpHeaders.java @@ -0,0 +1,50 @@ +/* + * 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 com.example; + +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class HttpHeaders { + + private MockMvc mockMvc; + + public void headers() throws Exception { + // tag::headers[] + this.mockMvc + .perform(get("/people").header("Authorization", "Basic dXNlcjpzZWNyZXQ=")) // <1> + .andExpect(status().isOk()) + .andDo(document("headers", + requestHeaders( // <2> + headerWithName("Authorization").description( + "Basic auth credentials")), // <3> + responseHeaders( // <4> + headerWithName("X-RateLimit-Limit").description( + "The total number of requests permitted per period"), + headerWithName("X-RateLimit-Remaining").description( + "Remaining requests permitted in current period"), + headerWithName("X-RateLimit-Reset").description( + "Time at which the rate limit period will reset")))); + // end::headers[] + } +} 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 index 2d71e7fc..5c3099b9 100644 --- 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 @@ -91,8 +91,8 @@ public abstract class AbstractHeadersSnippet extends TemplatedSnippet { } /** - * 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 + * 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 @@ -100,9 +100,10 @@ public abstract class AbstractHeadersSnippet extends TemplatedSnippet { */ protected List findMissingHeaders(Operation operation) { List missingHeaders = new ArrayList(); + Set actualHeaders = extractActualHeaders(operation); for (HeaderDescriptor headerDescriptor : this.headerDescriptors) { if (!headerDescriptor.isOptional() - && !getHeaders(operation).contains(headerDescriptor.getName())) { + && !actualHeaders.contains(headerDescriptor.getName())) { missingHeaders.add(headerDescriptor); } } @@ -111,13 +112,13 @@ public abstract class AbstractHeadersSnippet extends TemplatedSnippet { } /** - * Returns the headers of the request or response extracted form the given + * Extracts the names of the headers from the request or response of the given * {@code operation}. * - * @param operation The operation - * @return The headers + * @param operation the operation + * @return the header names */ - protected abstract Set getHeaders(Operation operation); + protected abstract Set extractActualHeaders(Operation operation); /** * Returns the list of {@link HeaderDescriptor HeaderDescriptors} that will be used to 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 index 8e544bc2..b0e332ab 100644 --- 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 @@ -56,7 +56,7 @@ public class HeaderDescriptor extends AbstractDescriptor { * * @return the header name */ - public String getName() { + public final String getName() { return this.name; } 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 index a7d5afc6..85800ecf 100644 --- 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 @@ -25,6 +25,7 @@ import org.springframework.restdocs.snippet.Snippet; * Static factory methods for documenting a RESTful API's request and response headers. * * @author Andreas Evers + * @author Andy Wilkinson */ public abstract class HeaderDocumentation { @@ -44,15 +45,14 @@ public abstract class HeaderDocumentation { } /** - * Returns a handler that will produce a snippet documenting the headers of the API - * call's request. + * Returns a new {@link Snippet} that will document the headers of the API operation's + * request. The headers will be documented using the given {@code descriptors}. *

* 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. + * request, a failure will occur. * - * @param descriptors The descriptions of the request's headers - * @return the handler + * @param descriptors the descriptions of the request's headers + * @return the snippet that will document the request headers * @see #headerWithName(String) */ public static Snippet requestHeaders(HeaderDescriptor... descriptors) { @@ -60,17 +60,16 @@ public abstract class HeaderDocumentation { } /** - * 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. + * Returns a new {@link Snippet} that will document the headers of the API + * operations's request. The given {@code attributes} will be available during snippet + * generation and the headers will be documented using the given {@code descriptors}. *

* 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. + * request, a failure will occur. * - * @param attributes Attributes made available during rendering of the snippet - * @param descriptors The descriptions of the request's headers - * @return the handler + * @param attributes the attributes + * @param descriptors the descriptions of the request's headers + * @return the snippet that will document the request headers * @see #headerWithName(String) */ public static Snippet requestHeaders(Map attributes, @@ -79,15 +78,14 @@ public abstract class HeaderDocumentation { } /** - * Returns a handler that will produce a snippet documenting the headers of the API - * call's response. + * Returns a new {@link Snippet} that will document the headers of the API operation's + * response. The headers will be documented using the given {@code descriptors}. *

* 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. + * request, a failure will occur. * - * @param descriptors The descriptions of the response's headers - * @return the handler + * @param descriptors the descriptions of the response's headers + * @return the snippet that will document the response headers * @see #headerWithName(String) */ public static Snippet responseHeaders(HeaderDescriptor... descriptors) { @@ -95,17 +93,17 @@ public abstract class HeaderDocumentation { } /** - * 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. + * Returns a new {@link Snippet} that will document the headers of the API + * operations's response. The given {@code attributes} will be available during + * snippet generation and the headers will be documented using the given + * {@code descriptors}. *

* 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. + * response, a failure will occur. * - * @param attributes Attributes made available during rendering of the snippet - * @param descriptors The descriptions of the response's headers - * @return the handler + * @param attributes the attributes + * @param descriptors the descriptions of the response's headers + * @return the snippet that will document the response headers * @see #headerWithName(String) */ public static Snippet responseHeaders(Map 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 index deb7efca..83dd6b6d 100644 --- 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 @@ -56,7 +56,7 @@ public class RequestHeadersSnippet extends AbstractHeadersSnippet { } @Override - protected Set getHeaders(Operation operation) { + protected Set extractActualHeaders(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 index bb0d5d07..979083c4 100644 --- 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 @@ -56,7 +56,7 @@ public class ResponseHeadersSnippet extends AbstractHeadersSnippet { } @Override - protected Set getHeaders(Operation operation) { + protected Set extractActualHeaders(Operation operation) { return operation.getResponse().getHeaders().keySet(); } 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 index 93e83136..b5b2d1e4 100644 --- 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 @@ -44,6 +44,7 @@ import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; * Tests for {@link RequestHeadersSnippet}. * * @author Andreas Evers + * @author Andy Wilkinson */ public class RequestHeadersSnippetTests { @@ -55,31 +56,36 @@ public class RequestHeadersSnippetTests { @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") // + 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"))) // + 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()); + .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 caseInsensitiveRequestHeaders() throws IOException { + this.snippet + .expectRequestHeaders("case-insensitive-request-headers") + .withContents(tableWithHeader("Name", "Description").row("X-Test", "one")); + new RequestHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( + "one"))).document(new OperationBuilder( + "case-insensitive-request-headers", this.snippet.getOutputDirectory()) + .request("/").header("X-test", "test").build()); } @Test @@ -118,9 +124,9 @@ public class RequestHeadersSnippetTests { 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") // + 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( 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 index ac6a40ce..24487094 100644 --- 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 @@ -41,9 +41,10 @@ import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; /** - * Tests for {@link ReponseHeadersSnippet}. + * Tests for {@link ResponseHeadersSnippet}. * * @author Andreas Evers + * @author Andy Wilkinson */ public class ResponseHeadersSnippetTests { @@ -55,40 +56,41 @@ public class ResponseHeadersSnippetTests { @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")); + this.snippet.expectResponseHeaders("response-headers").withContents( + tableWithHeader("Name", "Description").row("X-Test", "one") + .row("Content-Type", "two").row("Etag", "three") + .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"))) // + headerWithName("X-Test").description("one"), + headerWithName("Content-Type").description("two"), headerWithName("Etag") + .description("three"), 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()); + .getOutputDirectory()).response().header("X-Test", "test") + .header("Content-Type", "application/json") + .header("Etag", "lskjadldj3ii32l2ij23") + .header("Cache-Control", "max-age=0") + .header("Vary", "User-Agent").build()); + } + + @Test + public void caseInsensitiveResponseHeaders() throws IOException { + this.snippet + .expectResponseHeaders("case-insensitive-response-headers") + .withContents(tableWithHeader("Name", "Description").row("X-Test", "one")); + new ResponseHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( + "one"))).document(new OperationBuilder( + "case-insensitive-response-headers", this.snippet.getOutputDirectory()) + .response().header("X-test", "test").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") // + .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( 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 475cb15c..63364f6a 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 @@ -34,6 +34,7 @@ import static org.junit.Assert.assertThat; * generated the expected snippet. * * @author Andy Wilkinson + * @author Andreas Evers */ public class ExpectedSnippet implements TestRule {