Skip to content

Commit f72a9f1

Browse files
clydebarrowwilkinsona
authored andcommitted
Add support for documenting request and response cookies
See gh-592
1 parent 15980d7 commit f72a9f1

File tree

38 files changed

+1572
-17
lines changed

38 files changed

+1572
-17
lines changed

config/checkstyle/checkstyle.xml

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
<property name="file" value="${config_loc}/checkstyle-suppressions.xml"/>
66
</module>
77
<module name="io.spring.javaformat.checkstyle.SpringChecks">
8-
<property name="avoidStaticImportExcludes" value=" org.springframework.restdocs.cli.CliDocumentation.*"/>
8+
<property name="avoidStaticImportExcludes" value="org.springframework.restdocs.cli.CliDocumentation.*,
9+
org.springframework.restdocs.cookies.CookieDocumentation.*"/>
910
</module>
1011
<module name="com.puppycrawl.tools.checkstyle.TreeWalker">
1112
<module name="com.puppycrawl.tools.checkstyle.checks.imports.IllegalImportCheck">

docs/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ dependencies {
2121
testImplementation(project(":spring-restdocs-mockmvc"))
2222
testImplementation(project(":spring-restdocs-restassured"))
2323
testImplementation(project(":spring-restdocs-webtestclient"))
24+
testImplementation("jakarta.servlet:jakarta.servlet-api")
2425
testImplementation("jakarta.validation:jakarta.validation-api")
2526
testImplementation("junit:junit")
2627
testImplementation("org.testng:testng:6.9.10")

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

+55
Original file line numberDiff line numberDiff line change
@@ -988,6 +988,61 @@ Each contains a table describing the headers.
988988
When documenting HTTP Headers, the test fails if a documented header is not found in the request or response.
989989

990990

991+
[[documenting-your-api-http-cookies]]
992+
=== HTTP Cookies
993+
994+
You can document the cookies in a request or response by using `requestCookies` and
995+
`responseCookies`, respectively. The following examples show how to do so:
996+
997+
====
998+
[source,java,indent=0,role="primary"]
999+
.MockMvc
1000+
----
1001+
include::{examples-dir}/com/example/mockmvc/HttpCookies.java[tags=cookies]
1002+
----
1003+
<1> Configure Spring REST Docs to produce a snippet describing the request's cookies.
1004+
Uses the static `requestCookies` method on
1005+
`org.springframework.restdocs.cookies.CookieDocumentation`.
1006+
<2> Document the `JSESSIONID` cookie. Uses the static `cookieWithName` method on
1007+
`org.springframework.restdocs.cookies.CookieDocumentation.
1008+
<3> Produce a snippet describing the response's cookies. Uses the static `responseCookies`
1009+
method on `org.springframework.restdocs.cookies.CookieDocumentation`.
1010+
<4> Configure the request with an `JSESSIONID` and an additional cookie `logged_in`.
1011+
1012+
[source,java,indent=0,role="secondary"]
1013+
.WebTestClient
1014+
----
1015+
include::{examples-dir}/com/example/webtestclient/HttpCookies.java[tags=cookies]
1016+
----
1017+
<1> Configure Spring REST Docs to produce a snippet describing the request's cookies.
1018+
Uses the static `requestCookies` method on
1019+
`org.springframework.restdocs.cookies.CookieDocumentation`.
1020+
<2> Document the `JSESSIONID` cookie. Uses the static `cookieWithName` method on
1021+
`org.springframework.restdocs.cookies.CookieDocumentation.
1022+
<3> Produce a snippet describing the response's cookies. Uses the static `responseCookies`
1023+
method on `org.springframework.restdocs.cookies.CookieDocumentation`.
1024+
<4> Configure the request with an `JSESSIONID` and an additional cookie `logged_in`.
1025+
1026+
[source,java,indent=0,role="secondary"]
1027+
.REST Assured
1028+
----
1029+
include::{examples-dir}/com/example/restassured/HttpCookies.java[tags=cookies]
1030+
----
1031+
<1> Configure Spring REST Docs to produce a snippet describing the request's cookies.
1032+
Uses the static `requestCookies` method on
1033+
`org.springframework.restdocs.cookies.CookieDocumentation`.
1034+
<2> Document the `JSESSIONID` cookie. Uses the static `cookieWithName` method on
1035+
`org.springframework.restdocs.cookies.CookieDocumentation.
1036+
<3> Produce a snippet describing the response's cookies. Uses the static `responseCookies`
1037+
method on `org.springframework.restdocs.cookies.CookieDocumentation`.
1038+
<4> Configure the request with an `JSESSIONID` and an additional cookie `logged_in`.
1039+
====
1040+
1041+
The result is a snippet named `request-cookies.adoc` and a snippet named
1042+
`response-cookies.adoc`. Each contains a table describing the cookies.
1043+
1044+
When documenting HTTP Cookies, the test fails if a documented cookie is not found in
1045+
the request or response.
9911046

9921047
[[documenting-your-api-reusing-snippets]]
9931048
=== Reusing Snippets
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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+
* https://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 jakarta.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.perform(get("/").cookie(new Cookie("JSESSIONID", "ACBCDFD0FF93D5BB"))) // <1>
37+
.andExpect(status().isOk()).andDo(document("cookies", requestCookies(// <2>
38+
cookieWithName("JSESSIONID").description("Session token")), // <3>
39+
responseCookies(// <4>
40+
cookieWithName("JSESSIONID").description("Updated session token"),
41+
cookieWithName("logged_in")
42+
.description("Set to true if the user is currently logged in"))));
43+
// end::cookies[]
44+
}
45+
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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+
* https://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.restassured.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).filter(document("cookies", requestCookies(// <1>
35+
cookieWithName("JSESSIONID").description("Saved session token")), // <2>
36+
responseCookies(// <3>
37+
cookieWithName("logged_in").description("If user is logged in"),
38+
cookieWithName("JSESSIONID").description("Updated session token"))))
39+
.cookie("JSESSIONID", "ACBCDFD0FF93D5BB") // <4>
40+
.when().get("/people").then().assertThat().statusCode(is(200));
41+
// end::cookies[]
42+
}
43+
44+
}
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+
* https://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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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+
* https://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, Map<String, Object> attributes,
57+
boolean ignoreUndocumentedCookies) {
58+
super(type + "-cookies", attributes);
59+
for (CookieDescriptor descriptor : descriptors) {
60+
Assert.notNull(descriptor.getName(), "The name of the cookie must not be null");
61+
if (!descriptor.isIgnored()) {
62+
Assert.notNull(descriptor.getDescription(), "The description of the cookie must not be null");
63+
}
64+
}
65+
this.cookieDescriptors = descriptors;
66+
this.type = type;
67+
this.ignoreUndocumentedCookies = ignoreUndocumentedCookies;
68+
}
69+
70+
@Override
71+
protected Map<String, Object> createModel(Operation operation) {
72+
validateCookieDocumentation(operation);
73+
74+
Map<String, Object> model = new HashMap<>();
75+
List<Map<String, Object>> cookies = new ArrayList<>();
76+
model.put("cookies", cookies);
77+
for (CookieDescriptor descriptor : this.cookieDescriptors) {
78+
cookies.add(createModelForDescriptor(descriptor));
79+
}
80+
return model;
81+
}
82+
83+
private void validateCookieDocumentation(Operation operation) {
84+
List<CookieDescriptor> missingCookies = findMissingCookies(operation);
85+
if (!missingCookies.isEmpty()) {
86+
List<String> names = new ArrayList<>();
87+
for (CookieDescriptor cookieDescriptor : missingCookies) {
88+
names.add(cookieDescriptor.getName());
89+
}
90+
throw new SnippetException(
91+
"Cookies with the following names were not found" + " in the " + this.type + ": " + names);
92+
}
93+
}
94+
95+
/**
96+
* Finds the cookies that are missing from the operation. A cookie is missing if it is
97+
* described by one of the {@code cookieDescriptors} but is not present in the
98+
* operation.
99+
* @param operation the operation
100+
* @return descriptors for the cookies that are missing from the operation
101+
*/
102+
protected List<CookieDescriptor> findMissingCookies(Operation operation) {
103+
List<CookieDescriptor> missingCookies = new ArrayList<>();
104+
Set<String> actualCookies = extractActualCookies(operation);
105+
for (CookieDescriptor cookieDescriptor : this.cookieDescriptors) {
106+
if (!cookieDescriptor.isOptional() && !actualCookies.contains(cookieDescriptor.getName())) {
107+
missingCookies.add(cookieDescriptor);
108+
}
109+
}
110+
111+
return missingCookies;
112+
}
113+
114+
/**
115+
* Extracts the names of the cookies from the request or response of the given
116+
* {@code operation}.
117+
* @param operation the operation
118+
* @return the cookie names
119+
*/
120+
protected abstract Set<String> extractActualCookies(Operation operation);
121+
122+
/**
123+
* Returns the list of {@link CookieDescriptor CookieDescriptors} that will be used to
124+
* generate the documentation.
125+
* @return the cookie descriptors
126+
*/
127+
protected final List<CookieDescriptor> getCookieDescriptors() {
128+
return this.cookieDescriptors;
129+
}
130+
131+
/**
132+
* Returns a model for the given {@code descriptor}.
133+
* @param descriptor the descriptor
134+
* @return the model
135+
*/
136+
protected Map<String, Object> createModelForDescriptor(CookieDescriptor descriptor) {
137+
Map<String, Object> model = new HashMap<>();
138+
model.put("name", descriptor.getName());
139+
model.put("description", descriptor.getDescription());
140+
model.put("optional", descriptor.isOptional());
141+
model.putAll(descriptor.getAttributes());
142+
return model;
143+
}
144+
145+
}

0 commit comments

Comments
 (0)