Skip to content

Commit 6f8e2ad

Browse files
committed
Implement support for documenting request and response cookies. Addresses enhancement request #306.
1 parent df9711b commit 6f8e2ad

File tree

37 files changed

+1666
-55
lines changed

37 files changed

+1666
-55
lines changed

build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ subprojects {
105105

106106
compileJava {
107107
options.compilerArgs = [ '-Xlint:deprecation', '-Xlint:-options', '-Werror' ]
108+
options.encoding = 'UTF-8'
108109
}
109110

110111
tasks.withType(JavaCompile) {
@@ -273,4 +274,4 @@ configurations {
273274

274275
artifacts {
275276
archives docsZip
276-
}
277+
}

docs/src/docs/asciidoc/documenting-your-api.adoc

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,6 +1111,61 @@ When documenting HTTP Headers, the test fails if a documented header is not foun
11111111
the request or response.
11121112

11131113

1114+
[[documenting-your-api-http-cookies]]
1115+
=== HTTP Cookies
1116+
1117+
You can document the cookies in a request or response by using `requestCookies` and
1118+
`responseCookies`, respectively. The following examples show how to do so:
1119+
1120+
====
1121+
[source,java,indent=0,role="primary"]
1122+
.MockMvc
1123+
----
1124+
include::{examples-dir}/com/example/mockmvc/HttpCookies.java[tags=cookies]
1125+
----
1126+
<1> Configure Spring REST Docs to produce a snippet describing the request's cookies.
1127+
Uses the static `requestCookies` method on
1128+
`org.springframework.restdocs.cookies.CookieDocumentation`.
1129+
<2> Document the `JSESSIONID` cookie. Uses the static `cookieWithName` method on
1130+
`org.springframework.restdocs.cookies.CookieDocumentation.
1131+
<3> Produce a snippet describing the response's cookies. Uses the static `responseCookies`
1132+
method on `org.springframework.restdocs.cookies.CookieDocumentation`.
1133+
<4> Configure the request with an `JSESSIONID` and an additional cookie `logged_in`.
1134+
1135+
[source,java,indent=0,role="secondary"]
1136+
.WebTestClient
1137+
----
1138+
include::{examples-dir}/com/example/webtestclient/HttpCookies.java[tags=cookies]
1139+
----
1140+
<1> Configure Spring REST Docs to produce a snippet describing the request's cookies.
1141+
Uses the static `requestCookies` method on
1142+
`org.springframework.restdocs.cookies.CookieDocumentation`.
1143+
<2> Document the `JSESSIONID` cookie. Uses the static `cookieWithName` method on
1144+
`org.springframework.restdocs.cookies.CookieDocumentation.
1145+
<3> Produce a snippet describing the response's cookies. Uses the static `responseCookies`
1146+
method on `org.springframework.restdocs.cookies.CookieDocumentation`.
1147+
<4> Configure the request with an `JSESSIONID` and an additional cookie `logged_in`.
1148+
1149+
[source,java,indent=0,role="secondary"]
1150+
.REST Assured
1151+
----
1152+
include::{examples-dir}/com/example/restassured/HttpCookies.java[tags=cookies]
1153+
----
1154+
<1> Configure Spring REST Docs to produce a snippet describing the request's cookies.
1155+
Uses the static `requestCookies` method on
1156+
`org.springframework.restdocs.cookies.CookieDocumentation`.
1157+
<2> Document the `JSESSIONID` cookie. Uses the static `cookieWithName` method on
1158+
`org.springframework.restdocs.cookies.CookieDocumentation.
1159+
<3> Produce a snippet describing the response's cookies. Uses the static `responseCookies`
1160+
method on `org.springframework.restdocs.cookies.CookieDocumentation`.
1161+
<4> Configure the request with an `JSESSIONID` and an additional cookie `logged_in`.
1162+
====
1163+
1164+
The result is a snippet named `request-cookies.adoc` and a snippet named
1165+
`response-cookies.adoc`. Each contains a table describing the cookies.
1166+
1167+
When documenting HTTP Cookies, the test fails if a documented cookie is not found in
1168+
the request or response.
11141169

11151170
[[documenting-your-api-reusing-snippets]]
11161171
=== Reusing Snippets
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2014-2016 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.mockmvc;
18+
19+
import javax.servlet.http.Cookie;
20+
21+
import org.springframework.test.web.servlet.MockMvc;
22+
23+
import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName;
24+
import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies;
25+
import static org.springframework.restdocs.cookies.CookieDocumentation.responseCookies;
26+
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
27+
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
28+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
29+
30+
public class HttpCookies {
31+
32+
private MockMvc mockMvc;
33+
34+
public void cookies() throws Exception {
35+
// tag::cookies[]
36+
this.mockMvc
37+
.perform(get("/").cookie(new Cookie("JSESSIONID", "ACBCDFD0FF93D5BB"))) // <1>
38+
.andExpect(status().isOk())
39+
.andDo(document("cookies",
40+
requestCookies( // <2>
41+
cookieWithName("JSESSIONID").description(
42+
"Session token")), // <3>
43+
responseCookies( // <4>
44+
cookieWithName("JSESSIONID").description(
45+
"Updated session token"),
46+
cookieWithName("logged_in").description(
47+
"Set to true if the user is currently logged in"))));
48+
// end::cookies[]
49+
}
50+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2014-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.restassured;
18+
19+
import io.restassured.RestAssured;
20+
import io.restassured.specification.RequestSpecification;
21+
22+
import static org.hamcrest.CoreMatchers.is;
23+
import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName;
24+
import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies;
25+
import static org.springframework.restdocs.cookies.CookieDocumentation.responseCookies;
26+
import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document;
27+
28+
public class HttpCookies {
29+
30+
private RequestSpecification spec;
31+
32+
public void cookies() throws Exception {
33+
// tag::cookies[]
34+
RestAssured.given(this.spec)
35+
.filter(document("cookies",
36+
requestCookies( // <1>
37+
cookieWithName("JSESSIONID").description(
38+
"Saved session token")), // <2>
39+
responseCookies( // <3>
40+
cookieWithName("logged_in").description(
41+
"If user is logged in"),
42+
cookieWithName("JSESSIONID").description(
43+
"Updated session token"))))
44+
.cookie("JSESSIONID", "ACBCDFD0FF93D5BB") // <4>
45+
.when().get("/people")
46+
.then().assertThat().statusCode(is(200));
47+
// end::cookies[]
48+
}
49+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2014-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.webtestclient;
18+
19+
import org.springframework.test.web.reactive.server.WebTestClient;
20+
21+
import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName;
22+
import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies;
23+
import static org.springframework.restdocs.cookies.CookieDocumentation.responseCookies;
24+
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document;
25+
26+
public class HttpCookies {
27+
28+
// @formatter:off
29+
30+
private WebTestClient webTestClient;
31+
32+
public void cookies() throws Exception {
33+
// tag::cookies[]
34+
this.webTestClient
35+
.get().uri("/people").cookie("JSESSIONID", "ACBCDFD0FF93D5BB=") // <1>
36+
.exchange().expectStatus().isOk().expectBody()
37+
.consumeWith(document("cookies",
38+
requestCookies( // <2>
39+
cookieWithName("JSESSIONID").description("Session token")), // <3>
40+
responseCookies( // <4>
41+
cookieWithName("JSESSIONID")
42+
.description("Updated session token"),
43+
cookieWithName("logged_in")
44+
.description("User is logged in"))));
45+
// end::cookies[]
46+
}
47+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*
2+
* Copyright 2014-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.restdocs.cookies;
18+
19+
import java.util.ArrayList;
20+
import java.util.HashMap;
21+
import java.util.List;
22+
import java.util.Map;
23+
import java.util.Set;
24+
25+
import org.springframework.restdocs.operation.Operation;
26+
import org.springframework.restdocs.snippet.SnippetException;
27+
import org.springframework.restdocs.snippet.TemplatedSnippet;
28+
import org.springframework.util.Assert;
29+
30+
/**
31+
* Abstract {@link TemplatedSnippet} subclass that provides a base for snippets that
32+
* document a RESTful resource's request or response cookies.
33+
*
34+
* @author Andreas Evers
35+
* @author Clyde Stubbs
36+
* @since 2.1
37+
*/
38+
public abstract class AbstractCookiesSnippet extends TemplatedSnippet {
39+
40+
private List<CookieDescriptor> cookieDescriptors;
41+
42+
protected final boolean ignoreUndocumentedCookies;
43+
44+
private String type;
45+
46+
/**
47+
* Creates a new {@code AbstractCookiesSnippet} that will produce a snippet named
48+
* {@code <type>-cookies}. The cookies will be documented using the given
49+
* {@code descriptors} and the given {@code attributes} will be included in the model
50+
* during template rendering.
51+
* @param type the type of the cookies
52+
* @param descriptors the cookie descriptors
53+
* @param attributes the additional attributes
54+
* @param ignoreUndocumentedCookies whether undocumented cookies should be ignored
55+
*/
56+
protected AbstractCookiesSnippet(String type, List<CookieDescriptor> descriptors,
57+
Map<String, Object> attributes, boolean ignoreUndocumentedCookies) {
58+
super(type + "-cookies", attributes);
59+
for (CookieDescriptor descriptor : descriptors) {
60+
Assert.notNull(descriptor.getName(),
61+
"The name of the cookie must not be null");
62+
if (!descriptor.isIgnored()) {
63+
Assert.notNull(descriptor.getDescription(),
64+
"The description of the cookie must not be null");
65+
}
66+
}
67+
this.cookieDescriptors = descriptors;
68+
this.type = type;
69+
this.ignoreUndocumentedCookies = ignoreUndocumentedCookies;
70+
}
71+
72+
@Override
73+
protected Map<String, Object> createModel(Operation operation) {
74+
validateCookieDocumentation(operation);
75+
76+
Map<String, Object> model = new HashMap<>();
77+
List<Map<String, Object>> cookies = new ArrayList<>();
78+
model.put("cookies", cookies);
79+
for (CookieDescriptor descriptor : this.cookieDescriptors) {
80+
cookies.add(createModelForDescriptor(descriptor));
81+
}
82+
return model;
83+
}
84+
85+
private void validateCookieDocumentation(Operation operation) {
86+
List<CookieDescriptor> missingCookies = findMissingCookies(operation);
87+
if (!missingCookies.isEmpty()) {
88+
List<String> names = new ArrayList<>();
89+
for (CookieDescriptor cookieDescriptor : missingCookies) {
90+
names.add(cookieDescriptor.getName());
91+
}
92+
throw new SnippetException("Cookies with the following names were not found"
93+
+ " in the " + this.type + ": " + names);
94+
}
95+
}
96+
97+
/**
98+
* Finds the cookies that are missing from the operation. A cookie is missing if it is
99+
* described by one of the {@code cookieDescriptors} but is not present in the
100+
* operation.
101+
* @param operation the operation
102+
* @return descriptors for the cookies that are missing from the operation
103+
*/
104+
protected List<CookieDescriptor> findMissingCookies(Operation operation) {
105+
List<CookieDescriptor> missingCookies = new ArrayList<>();
106+
Set<String> actualCookies = extractActualCookies(operation);
107+
for (CookieDescriptor cookieDescriptor : this.cookieDescriptors) {
108+
if (!cookieDescriptor.isOptional()
109+
&& !actualCookies.contains(cookieDescriptor.getName())) {
110+
missingCookies.add(cookieDescriptor);
111+
}
112+
}
113+
114+
return missingCookies;
115+
}
116+
117+
/**
118+
* Extracts the names of the cookies from the request or response of the given
119+
* {@code operation}.
120+
* @param operation the operation
121+
* @return the cookie names
122+
*/
123+
protected abstract Set<String> extractActualCookies(Operation operation);
124+
125+
/**
126+
* Returns the list of {@link CookieDescriptor CookieDescriptors} that will be used to
127+
* generate the documentation.
128+
* @return the cookie descriptors
129+
*/
130+
protected final List<CookieDescriptor> getCookieDescriptors() {
131+
return this.cookieDescriptors;
132+
}
133+
134+
/**
135+
* Returns a model for the given {@code descriptor}.
136+
* @param descriptor the descriptor
137+
* @return the model
138+
*/
139+
protected Map<String, Object> createModelForDescriptor(CookieDescriptor descriptor) {
140+
Map<String, Object> model = new HashMap<>();
141+
model.put("name", descriptor.getName());
142+
model.put("description", descriptor.getDescription());
143+
model.put("optional", descriptor.isOptional());
144+
model.putAll(descriptor.getAttributes());
145+
return model;
146+
}
147+
148+
}

0 commit comments

Comments
 (0)