Skip to content

Commit d34962c

Browse files
committed
Merge pull request #592 from clydebarrow
* gh-592: Polish "Add support for documenting request and response cookies" Add support for documenting request and response cookies Closes gh-592
2 parents 15980d7 + d5522f5 commit d34962c

File tree

42 files changed

+1753
-27
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1753
-27
lines changed

config/checkstyle/checkstyle.xml

Lines changed: 2 additions & 1 deletion
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -989,6 +989,58 @@ When documenting HTTP Headers, the test fails if a documented header is not foun
989989

990990

991991

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2014-2022 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+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2014-2022 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() {
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+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2014-2022 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+
private WebTestClient webTestClient;
29+
30+
public void cookies() {
31+
// tag::cookies[]
32+
this.webTestClient.get().uri("/people").cookie("JSESSIONID", "ACBCDFD0FF93D5BB=") // <1>
33+
.exchange().expectStatus().isOk().expectBody().consumeWith(document("cookies", requestCookies(// <2>
34+
cookieWithName("JSESSIONID").description("Session token")), // <3>
35+
responseCookies(// <4>
36+
cookieWithName("JSESSIONID").description("Updated session token"),
37+
cookieWithName("logged_in").description("User is logged in"))));
38+
// end::cookies[]
39+
}
40+
41+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/*
2+
* Copyright 2014-2022 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.Collections;
21+
import java.util.HashMap;
22+
import java.util.HashSet;
23+
import java.util.LinkedHashMap;
24+
import java.util.List;
25+
import java.util.Map;
26+
import java.util.Map.Entry;
27+
import java.util.Set;
28+
29+
import org.springframework.restdocs.operation.Operation;
30+
import org.springframework.restdocs.snippet.TemplatedSnippet;
31+
import org.springframework.util.Assert;
32+
33+
/**
34+
* Abstract {@link TemplatedSnippet} subclass that provides a base for snippets that
35+
* document a RESTful resource's request or response cookies.
36+
*
37+
* @author Clyde Stubbs
38+
* @author Andy Wilkinson
39+
* @since 3.0
40+
*/
41+
public abstract class AbstractCookiesSnippet extends TemplatedSnippet {
42+
43+
private final Map<String, CookieDescriptor> descriptorsByName = new LinkedHashMap<>();
44+
45+
private final boolean ignoreUndocumentedCookies;
46+
47+
/**
48+
* Creates a new {@code AbstractCookiesSnippet} that will produce a snippet named
49+
* {@code <type>-cookies}. The cookies will be documented using the given
50+
* {@code descriptors} and the given {@code attributes} will be included in the model
51+
* during template rendering.
52+
* @param type the type of the cookies
53+
* @param descriptors the cookie descriptors
54+
* @param attributes the additional attributes
55+
* @param ignoreUndocumentedCookies whether undocumented cookies should be ignored
56+
*/
57+
protected AbstractCookiesSnippet(String type, List<CookieDescriptor> descriptors, Map<String, Object> attributes,
58+
boolean ignoreUndocumentedCookies) {
59+
super(type + "-cookies", attributes);
60+
for (CookieDescriptor descriptor : descriptors) {
61+
Assert.notNull(descriptor.getName(), "Cookie descriptors must have a name");
62+
if (!descriptor.isIgnored()) {
63+
Assert.notNull(descriptor.getDescription(), "The descriptor for cookie '" + descriptor.getName()
64+
+ "' must either have a description or be marked as ignored");
65+
}
66+
this.descriptorsByName.put(descriptor.getName(), descriptor);
67+
}
68+
this.ignoreUndocumentedCookies = ignoreUndocumentedCookies;
69+
}
70+
71+
@Override
72+
protected Map<String, Object> createModel(Operation operation) {
73+
verifyCookieDescriptors(operation);
74+
75+
Map<String, Object> model = new HashMap<>();
76+
List<Map<String, Object>> cookies = new ArrayList<>();
77+
for (CookieDescriptor descriptor : this.descriptorsByName.values()) {
78+
if (!descriptor.isIgnored()) {
79+
cookies.add(createModelForDescriptor(descriptor));
80+
}
81+
}
82+
model.put("cookies", cookies);
83+
return model;
84+
}
85+
86+
private void verifyCookieDescriptors(Operation operation) {
87+
Set<String> actualCookies = extractActualCookies(operation);
88+
Set<String> expectedCookies = new HashSet<>();
89+
for (Entry<String, CookieDescriptor> entry : this.descriptorsByName.entrySet()) {
90+
if (!entry.getValue().isOptional()) {
91+
expectedCookies.add(entry.getKey());
92+
}
93+
}
94+
Set<String> undocumentedCookies;
95+
if (this.ignoreUndocumentedCookies) {
96+
undocumentedCookies = Collections.emptySet();
97+
}
98+
else {
99+
undocumentedCookies = new HashSet<>(actualCookies);
100+
undocumentedCookies.removeAll(this.descriptorsByName.keySet());
101+
}
102+
Set<String> missingCookies = new HashSet<>(expectedCookies);
103+
missingCookies.removeAll(actualCookies);
104+
105+
if (!undocumentedCookies.isEmpty() || !missingCookies.isEmpty()) {
106+
verificationFailed(undocumentedCookies, missingCookies);
107+
}
108+
}
109+
110+
/**
111+
* Extracts the names of the cookies from the request or response of the given
112+
* {@code operation}.
113+
* @param operation the operation
114+
* @return the cookie names
115+
*/
116+
protected abstract Set<String> extractActualCookies(Operation operation);
117+
118+
/**
119+
* Called when the documented cookies do not match the actual cookies.
120+
* @param undocumentedCookies the cookies that were found in the operation but were
121+
* not documented
122+
* @param missingCookies the cookies that were documented but were not found in the
123+
* operation
124+
*/
125+
protected abstract void verificationFailed(Set<String> undocumentedCookies, Set<String> missingCookies);
126+
127+
/**
128+
* Returns the list of {@link CookieDescriptor CookieDescriptors} that will be used to
129+
* generate the documentation.
130+
* @return the cookie descriptors
131+
*/
132+
protected final Map<String, CookieDescriptor> getCookieDescriptors() {
133+
return this.descriptorsByName;
134+
}
135+
136+
/**
137+
* Returns whether or not this snippet ignores undocumented cookies.
138+
* @return {@code true} if undocumented cookies are ignored, otherwise {@code false}
139+
*/
140+
protected final boolean isIgnoreUndocumentedCookies() {
141+
return this.ignoreUndocumentedCookies;
142+
}
143+
144+
/**
145+
* Returns a model for the given {@code descriptor}.
146+
* @param descriptor the descriptor
147+
* @return the model
148+
*/
149+
protected Map<String, Object> createModelForDescriptor(CookieDescriptor descriptor) {
150+
Map<String, Object> model = new HashMap<>();
151+
model.put("name", descriptor.getName());
152+
model.put("description", descriptor.getDescription());
153+
model.put("optional", descriptor.isOptional());
154+
model.putAll(descriptor.getAttributes());
155+
return model;
156+
}
157+
158+
}

0 commit comments

Comments
 (0)