Polish contribution that added support for documenting HTTP headers

The main changes are:

- Update javadoc to align with changes made in 5a01016
- Update documentation with details of the new support for documenting
  HTTP headers
- Add tests to verify that HTTP headers are matched case-insensitively

Closes gh-71
This commit is contained in:
Andy Wilkinson
2015-09-29 12:31:04 +01:00
parent 48f4d1343a
commit ea8f736259
11 changed files with 175 additions and 92 deletions

View File

@@ -73,7 +73,7 @@
<module name="AvoidStarImport" />
<module name="AvoidStaticImport">
<property name="excludes"
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.*" />
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.*" />
</module>
<module name="IllegalImport" />
<module name="RedundantImport" />

View File

@@ -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

View File

@@ -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[]
}
}

View File

@@ -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<HeaderDescriptor> findMissingHeaders(Operation operation) {
List<HeaderDescriptor> missingHeaders = new ArrayList<HeaderDescriptor>();
Set<String> 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<String> getHeaders(Operation operation);
protected abstract Set<String> extractActualHeaders(Operation operation);
/**
* Returns the list of {@link HeaderDescriptor HeaderDescriptors} that will be used to

View File

@@ -56,7 +56,7 @@ public class HeaderDescriptor extends AbstractDescriptor<HeaderDescriptor> {
*
* @return the header name
*/
public String getName() {
public final String getName() {
return this.name;
}

View File

@@ -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}.
* <p>
* 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}.
* <p>
* 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<String, Object> 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}.
* <p>
* 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}.
* <p>
* 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<String, Object> attributes,

View File

@@ -56,7 +56,7 @@ public class RequestHeadersSnippet extends AbstractHeadersSnippet {
}
@Override
protected Set<String> getHeaders(Operation operation) {
protected Set<String> extractActualHeaders(Operation operation) {
return operation.getRequest().getHeaders().keySet();
}

View File

@@ -56,7 +56,7 @@ public class ResponseHeadersSnippet extends AbstractHeadersSnippet {
}
@Override
protected Set<String> getHeaders(Operation operation) {
protected Set<String> extractActualHeaders(Operation operation) {
return operation.getResponse().getHeaders().keySet();
}

View File

@@ -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(

View File

@@ -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(

View File

@@ -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 {