Add support for documenting request and response cookies

See gh-592
This commit is contained in:
clydebarrow
2019-03-07 14:26:31 +11:00
committed by Andy Wilkinson
parent 15980d7486
commit f72a9f1067
38 changed files with 1572 additions and 17 deletions

View File

@@ -5,7 +5,8 @@
<property name="file" value="${config_loc}/checkstyle-suppressions.xml"/>
</module>
<module name="io.spring.javaformat.checkstyle.SpringChecks">
<property name="avoidStaticImportExcludes" value=" org.springframework.restdocs.cli.CliDocumentation.*"/>
<property name="avoidStaticImportExcludes" value="org.springframework.restdocs.cli.CliDocumentation.*,
org.springframework.restdocs.cookies.CookieDocumentation.*"/>
</module>
<module name="com.puppycrawl.tools.checkstyle.TreeWalker">
<module name="com.puppycrawl.tools.checkstyle.checks.imports.IllegalImportCheck">

View File

@@ -21,6 +21,7 @@ dependencies {
testImplementation(project(":spring-restdocs-mockmvc"))
testImplementation(project(":spring-restdocs-restassured"))
testImplementation(project(":spring-restdocs-webtestclient"))
testImplementation("jakarta.servlet:jakarta.servlet-api")
testImplementation("jakarta.validation:jakarta.validation-api")
testImplementation("junit:junit")
testImplementation("org.testng:testng:6.9.10")

View File

@@ -988,6 +988,61 @@ Each contains a table describing the headers.
When documenting HTTP Headers, the test fails if a documented header is not found in the request or response.
[[documenting-your-api-http-cookies]]
=== HTTP Cookies
You can document the cookies in a request or response by using `requestCookies` and
`responseCookies`, respectively. The following examples show how to do so:
====
[source,java,indent=0,role="primary"]
.MockMvc
----
include::{examples-dir}/com/example/mockmvc/HttpCookies.java[tags=cookies]
----
<1> Configure Spring REST Docs to produce a snippet describing the request's cookies.
Uses the static `requestCookies` method on
`org.springframework.restdocs.cookies.CookieDocumentation`.
<2> Document the `JSESSIONID` cookie. Uses the static `cookieWithName` method on
`org.springframework.restdocs.cookies.CookieDocumentation.
<3> Produce a snippet describing the response's cookies. Uses the static `responseCookies`
method on `org.springframework.restdocs.cookies.CookieDocumentation`.
<4> Configure the request with an `JSESSIONID` and an additional cookie `logged_in`.
[source,java,indent=0,role="secondary"]
.WebTestClient
----
include::{examples-dir}/com/example/webtestclient/HttpCookies.java[tags=cookies]
----
<1> Configure Spring REST Docs to produce a snippet describing the request's cookies.
Uses the static `requestCookies` method on
`org.springframework.restdocs.cookies.CookieDocumentation`.
<2> Document the `JSESSIONID` cookie. Uses the static `cookieWithName` method on
`org.springframework.restdocs.cookies.CookieDocumentation.
<3> Produce a snippet describing the response's cookies. Uses the static `responseCookies`
method on `org.springframework.restdocs.cookies.CookieDocumentation`.
<4> Configure the request with an `JSESSIONID` and an additional cookie `logged_in`.
[source,java,indent=0,role="secondary"]
.REST Assured
----
include::{examples-dir}/com/example/restassured/HttpCookies.java[tags=cookies]
----
<1> Configure Spring REST Docs to produce a snippet describing the request's cookies.
Uses the static `requestCookies` method on
`org.springframework.restdocs.cookies.CookieDocumentation`.
<2> Document the `JSESSIONID` cookie. Uses the static `cookieWithName` method on
`org.springframework.restdocs.cookies.CookieDocumentation.
<3> Produce a snippet describing the response's cookies. Uses the static `responseCookies`
method on `org.springframework.restdocs.cookies.CookieDocumentation`.
<4> Configure the request with an `JSESSIONID` and an additional cookie `logged_in`.
====
The result is a snippet named `request-cookies.adoc` and a snippet named
`response-cookies.adoc`. Each contains a table describing the cookies.
When documenting HTTP Cookies, the test fails if a documented cookie is not found in
the request or response.
[[documenting-your-api-reusing-snippets]]
=== Reusing Snippets

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2014-2016 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
*
* https://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.mockmvc;
import jakarta.servlet.http.Cookie;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName;
import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies;
import static org.springframework.restdocs.cookies.CookieDocumentation.responseCookies;
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 HttpCookies {
private MockMvc mockMvc;
public void cookies() throws Exception {
// tag::cookies[]
this.mockMvc.perform(get("/").cookie(new Cookie("JSESSIONID", "ACBCDFD0FF93D5BB"))) // <1>
.andExpect(status().isOk()).andDo(document("cookies", requestCookies(// <2>
cookieWithName("JSESSIONID").description("Session token")), // <3>
responseCookies(// <4>
cookieWithName("JSESSIONID").description("Updated session token"),
cookieWithName("logged_in")
.description("Set to true if the user is currently logged in"))));
// end::cookies[]
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2014-2017 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
*
* https://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.restassured;
import io.restassured.RestAssured;
import io.restassured.specification.RequestSpecification;
import static org.hamcrest.CoreMatchers.is;
import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName;
import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies;
import static org.springframework.restdocs.cookies.CookieDocumentation.responseCookies;
import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document;
public class HttpCookies {
private RequestSpecification spec;
public void cookies() throws Exception {
// tag::cookies[]
RestAssured.given(this.spec).filter(document("cookies", requestCookies(// <1>
cookieWithName("JSESSIONID").description("Saved session token")), // <2>
responseCookies(// <3>
cookieWithName("logged_in").description("If user is logged in"),
cookieWithName("JSESSIONID").description("Updated session token"))))
.cookie("JSESSIONID", "ACBCDFD0FF93D5BB") // <4>
.when().get("/people").then().assertThat().statusCode(is(200));
// end::cookies[]
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2014-2017 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
*
* https://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.webtestclient;
import org.springframework.test.web.reactive.server.WebTestClient;
import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName;
import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies;
import static org.springframework.restdocs.cookies.CookieDocumentation.responseCookies;
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document;
public class HttpCookies {
// @formatter:off
private WebTestClient webTestClient;
public void cookies() throws Exception {
// tag::cookies[]
this.webTestClient
.get().uri("/people").cookie("JSESSIONID", "ACBCDFD0FF93D5BB=") // <1>
.exchange().expectStatus().isOk().expectBody()
.consumeWith(document("cookies",
requestCookies(// <2>
cookieWithName("JSESSIONID").description("Session token")), // <3>
responseCookies(// <4>
cookieWithName("JSESSIONID")
.description("Updated session token"),
cookieWithName("logged_in")
.description("User is logged in"))));
// end::cookies[]
}
}

View File

@@ -0,0 +1,145 @@
/*
* Copyright 2014-2017 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
*
* https://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 org.springframework.restdocs.cookies;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.snippet.SnippetException;
import org.springframework.restdocs.snippet.TemplatedSnippet;
import org.springframework.util.Assert;
/**
* Abstract {@link TemplatedSnippet} subclass that provides a base for snippets that
* document a RESTful resource's request or response cookies.
*
* @author Andreas Evers
* @author Clyde Stubbs
* @since 2.1
*/
public abstract class AbstractCookiesSnippet extends TemplatedSnippet {
private List<CookieDescriptor> cookieDescriptors;
protected final boolean ignoreUndocumentedCookies;
private String type;
/**
* Creates a new {@code AbstractCookiesSnippet} that will produce a snippet named
* {@code <type>-cookies}. The cookies will be documented using the given
* {@code descriptors} and the given {@code attributes} will be included in the model
* during template rendering.
* @param type the type of the cookies
* @param descriptors the cookie descriptors
* @param attributes the additional attributes
* @param ignoreUndocumentedCookies whether undocumented cookies should be ignored
*/
protected AbstractCookiesSnippet(String type, List<CookieDescriptor> descriptors, Map<String, Object> attributes,
boolean ignoreUndocumentedCookies) {
super(type + "-cookies", attributes);
for (CookieDescriptor descriptor : descriptors) {
Assert.notNull(descriptor.getName(), "The name of the cookie must not be null");
if (!descriptor.isIgnored()) {
Assert.notNull(descriptor.getDescription(), "The description of the cookie must not be null");
}
}
this.cookieDescriptors = descriptors;
this.type = type;
this.ignoreUndocumentedCookies = ignoreUndocumentedCookies;
}
@Override
protected Map<String, Object> createModel(Operation operation) {
validateCookieDocumentation(operation);
Map<String, Object> model = new HashMap<>();
List<Map<String, Object>> cookies = new ArrayList<>();
model.put("cookies", cookies);
for (CookieDescriptor descriptor : this.cookieDescriptors) {
cookies.add(createModelForDescriptor(descriptor));
}
return model;
}
private void validateCookieDocumentation(Operation operation) {
List<CookieDescriptor> missingCookies = findMissingCookies(operation);
if (!missingCookies.isEmpty()) {
List<String> names = new ArrayList<>();
for (CookieDescriptor cookieDescriptor : missingCookies) {
names.add(cookieDescriptor.getName());
}
throw new SnippetException(
"Cookies with the following names were not found" + " in the " + this.type + ": " + names);
}
}
/**
* Finds the cookies that are missing from the operation. A cookie is missing if it is
* described by one of the {@code cookieDescriptors} but is not present in the
* operation.
* @param operation the operation
* @return descriptors for the cookies that are missing from the operation
*/
protected List<CookieDescriptor> findMissingCookies(Operation operation) {
List<CookieDescriptor> missingCookies = new ArrayList<>();
Set<String> actualCookies = extractActualCookies(operation);
for (CookieDescriptor cookieDescriptor : this.cookieDescriptors) {
if (!cookieDescriptor.isOptional() && !actualCookies.contains(cookieDescriptor.getName())) {
missingCookies.add(cookieDescriptor);
}
}
return missingCookies;
}
/**
* Extracts the names of the cookies from the request or response of the given
* {@code operation}.
* @param operation the operation
* @return the cookie names
*/
protected abstract Set<String> extractActualCookies(Operation operation);
/**
* Returns the list of {@link CookieDescriptor CookieDescriptors} that will be used to
* generate the documentation.
* @return the cookie descriptors
*/
protected final List<CookieDescriptor> getCookieDescriptors() {
return this.cookieDescriptors;
}
/**
* Returns a model for the given {@code descriptor}.
* @param descriptor the descriptor
* @return the model
*/
protected Map<String, Object> createModelForDescriptor(CookieDescriptor descriptor) {
Map<String, Object> model = new HashMap<>();
model.put("name", descriptor.getName());
model.put("description", descriptor.getDescription());
model.put("optional", descriptor.isOptional());
model.putAll(descriptor.getAttributes());
return model;
}
}

View File

@@ -0,0 +1,69 @@
/*
* 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
*
* https://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 org.springframework.restdocs.cookies;
import org.springframework.restdocs.snippet.IgnorableDescriptor;
/**
* A description of a cookie found in a request or response.
*
* @author Andreas Evers
* @author Clyde Stubbs
* @since 2.1
* @see CookieDocumentation#cookieWithName(String)
*/
public class CookieDescriptor extends IgnorableDescriptor<CookieDescriptor> {
private final String name;
private boolean optional;
/**
* Creates a new {@code CookieDescriptor} describing the cookie with the given
* {@code name}.
* @param name the name
*/
protected CookieDescriptor(String name) {
this.name = name;
}
/**
* Marks the cookie as optional.
* @return {@code this}
*/
public final CookieDescriptor optional() {
this.optional = true;
return this;
}
/**
* Returns the name for the cookie.
* @return the cookie name
*/
public final String getName() {
return this.name;
}
/**
* Returns {@code true} if the described cookie is optional, otherwise {@code false}.
* @return {@code true} if the described cookie is optional, otherwise {@code false}
*/
public final boolean isOptional() {
return this.optional;
}
}

View File

@@ -0,0 +1,232 @@
/*
* Copyright 2014-2016 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
*
* https://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 org.springframework.restdocs.cookies;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.springframework.restdocs.snippet.Snippet;
/**
* Static factory methods for documenting a RESTful API's request and response cookies.
*
* @author Andreas Evers
* @author Andy Wilkinson
* @author Marcel Overdijk
* @author Clyde Stubbs
* @since 2.1
*/
public abstract class CookieDocumentation {
private CookieDocumentation() {
}
/**
* Creates a {@code CookieDescriptor} that describes a cookie with the given
* {@code name}.
* @param name the name of the cookie
* @return a {@code CookieDescriptor} ready for further configuration
*/
public static CookieDescriptor cookieWithName(String name) {
return new CookieDescriptor(name);
}
/**
* Returns a new {@link Snippet} that will document the cookies of the API operation's
* request. The cookies will be documented using the given {@code descriptors}.
* <p>
* If a cookie is documented, is not marked as optional, and is not present in the
* request, a failure will occur.
* @param descriptors the descriptions of the request's cookies
* @return the snippet that will document the request cookies
* @see #cookieWithName(String)
*/
public static RequestCookiesSnippet requestCookies(CookieDescriptor... descriptors) {
return requestCookies(Arrays.asList(descriptors));
}
/**
* Returns a new {@link Snippet} that will document the cookies of the API operation's
* request. The cookies will be documented using the given {@code descriptors}.
* <p>
* If a cookie is documented, is not marked as optional, and is not present in the
* request, a failure will occur.
* @param descriptors the descriptions of the request's cookies
* @return the snippet that will document the request cookies
* @see #cookieWithName(String)
*/
public static RequestCookiesSnippet requestCookies(List<CookieDescriptor> descriptors) {
return new RequestCookiesSnippet(descriptors);
}
/**
* Returns a new {@link Snippet} that will document the cookies of the API
* operations's request. The given {@code attributes} will be available during snippet
* generation and the cookies will be documented using the given {@code descriptors}.
* <p>
* If a cookie is documented, is not marked as optional, and is not present in the
* request, a failure will occur.
* @param attributes the attributes
* @param descriptors the descriptions of the request's cookies
* @return the snippet that will document the request cookies
* @see #cookieWithName(String)
*/
public static RequestCookiesSnippet requestCookies(Map<String, Object> attributes,
CookieDescriptor... descriptors) {
return requestCookies(attributes, Arrays.asList(descriptors));
}
/**
* Returns a new {@link Snippet} that will document the cookies of the API
* operations's request. The given {@code attributes} will be available during snippet
* generation and the cookies will be documented using the given {@code descriptors}.
* <p>
* If a cookie is documented, is not marked as optional, and is not present in the
* request, a failure will occur. Any cookies present in the request that are not
* documented will result in an error.
* @param attributes the attributes
* @param descriptors the descriptions of the request's cookies
* @return the snippet that will document the request cookies
* @see #cookieWithName(String)
*/
public static RequestCookiesSnippet requestCookies(Map<String, Object> attributes,
List<CookieDescriptor> descriptors) {
return new RequestCookiesSnippet(descriptors, attributes, false);
}
/**
* Returns a new {@link Snippet} that will document the cookies of the API
* operations's request. The given {@code attributes} will be available during snippet
* generation and the cookies will be documented using the given {@code descriptors}.
* <p>
* If a cookie is documented, is not marked as optional, and is not present in the
* request, a failure will occur. An undocumented cookie in the request will not
* generate an error.
* @param attributes the attributes
* @param descriptors the descriptions of the request's cookies
* @return the snippet that will document the request cookies
* @see #cookieWithName(String)
*/
public static RequestCookiesSnippet relaxedRequestCookies(Map<String, Object> attributes,
List<CookieDescriptor> descriptors) {
return new RequestCookiesSnippet(descriptors, attributes, true);
}
/**
* Returns a new {@link Snippet} that will document the cookies of the API operation's
* response. The cookies will be documented using the given {@code descriptors}.
* <p>
* If a cookie is documented, is not marked as optional or ignored, and is not present
* in the request, a failure will occur.
* @param descriptors the descriptions of the response's cookies
* @return the snippet that will document the response cookies
* @see #cookieWithName(String)
*/
public static ResponseCookiesSnippet responseCookies(CookieDescriptor... descriptors) {
return responseCookies(Arrays.asList(descriptors));
}
/**
* Returns a new {@link Snippet} that will document the cookies of the API operation's
* response. The cookies will be documented using the given {@code descriptors}.
* <p>
* If a cookie is documented, is not marked as optional or ignored, and is not present
* in the request, a failure will occur. If a cookie is present in the response but is
* undocumented a failure will occur.
* @param descriptors the descriptions of the response's cookies
* @return the snippet that will document the response cookies
* @see #cookieWithName(String)
*/
public static ResponseCookiesSnippet responseCookies(List<CookieDescriptor> descriptors) {
return new ResponseCookiesSnippet(descriptors);
}
/**
* Returns a new {@link Snippet} that will document the cookies of the API operation's
* response. The cookies will be documented using the given {@code descriptors}.
* <p>
* If a cookie is documented, is not marked as optional or ignored, and is not present
* in the request, a failure will occur. No failure will occur if a cookie is present
* but undocumented.
* @param descriptors the descriptions of the response's cookies
* @return the snippet that will document the response cookies
* @see #cookieWithName(String)
*/
public static ResponseCookiesSnippet relaxedResponseCookies(List<CookieDescriptor> descriptors) {
return new ResponseCookiesSnippet(descriptors, null, true);
}
/**
* Returns a new {@link Snippet} that will document the cookies of the API
* operations's response. The given {@code attributes} will be available during
* snippet generation and the cookies will be documented using the given
* {@code descriptors}.
* <p>
* If a cookie is documented, is not marked as optional, and is not present in the
* response, a failure will occur. If a cookie is present in the response but is
* undocumented a failure will occur.
* @param attributes the attributes
* @param descriptors the descriptions of the response's cookies
* @return the snippet that will document the response cookies
* @see #cookieWithName(String)
*/
public static ResponseCookiesSnippet responseCookies(Map<String, Object> attributes,
CookieDescriptor... descriptors) {
return responseCookies(attributes, Arrays.asList(descriptors));
}
/**
* Returns a new {@link Snippet} that will document the cookies of the API
* operations's response. The given {@code attributes} will be available during
* snippet generation and the cookies will be documented using the given
* {@code descriptors}.
* <p>
* If a cookie is documented, is not marked as optional, and is not present in the
* response, a failure will occur. If a cookie is present in the response but is
* undocumented a failure will occur.
* @param attributes the attributes
* @param descriptors the descriptions of the response's cookies
* @return the snippet that will document the response cookies
* @see #cookieWithName(String)
*/
public static ResponseCookiesSnippet responseCookies(Map<String, Object> attributes,
List<CookieDescriptor> descriptors) {
return new ResponseCookiesSnippet(descriptors, attributes, false);
}
/**
* Returns a new {@link Snippet} that will document the cookies of the API
* operations's response. The given {@code attributes} will be available during
* snippet generation and the cookies will be documented using the given
* {@code descriptors}.
* <p>
* If a cookie is documented, is not marked as optional, and is not present in the
* response, a failure will occur. No failure will occur if a cookie is present but
* undocumented.
* @param attributes the attributes
* @param descriptors the descriptions of the response's cookies
* @return the snippet that will document the response cookies
* @see #cookieWithName(String)
*/
public static ResponseCookiesSnippet relaxedResponseCookies(Map<String, Object> attributes,
List<CookieDescriptor> descriptors) {
return new ResponseCookiesSnippet(descriptors, attributes, true);
}
}

View File

@@ -0,0 +1,109 @@
/*
* Copyright 2014-2016 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
*
* https://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 org.springframework.restdocs.cookies;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.operation.RequestCookie;
import org.springframework.restdocs.snippet.Snippet;
/**
* A {@link Snippet} that documents the cookies in a request.
*
* @author Andreas Evers
* @author Andy Wilkinson
* @author Clyde Stubbs
* @since 2.1
* @see CookieDocumentation#requestCookies(CookieDescriptor...)
* @see CookieDocumentation#requestCookies(Map, CookieDescriptor...)
*/
public class RequestCookiesSnippet extends AbstractCookiesSnippet {
/**
* Creates a new {@code RequestCookiesSnippet} that will document the cookies in the
* request using the given {@code descriptors}.
* @param descriptors the descriptors
*/
protected RequestCookiesSnippet(List<CookieDescriptor> descriptors) {
this(descriptors, null, false);
}
/**
* Creates a new {@code RequestCookiesSnippet} that will document the cookies in the
* request using the given {@code descriptors}. The given {@code attributes} will be
* included in the model during template rendering.
* @param descriptors the descriptors
* @param attributes the additional attributes
* @param ignoreUndocumentedCookies if set undocumented cookies will be ignored
*/
protected RequestCookiesSnippet(List<CookieDescriptor> descriptors, Map<String, Object> attributes,
boolean ignoreUndocumentedCookies) {
super("request", descriptors, attributes, ignoreUndocumentedCookies);
}
/**
* Creates a new {@code RequestCookiesSnippet} that will document the cookies in the
* request using the given {@code descriptors}. The given {@code attributes} will be
* included in the model during template rendering. Undocumented cookies will not be
* ignored.
* @param descriptors the descriptors
* @param attributes the additional attributes
*/
protected RequestCookiesSnippet(List<CookieDescriptor> descriptors, Map<String, Object> attributes) {
super("request", descriptors, attributes, false);
}
@Override
protected Set<String> extractActualCookies(Operation operation) {
HashSet<String> actualCookies = new HashSet<>();
for (RequestCookie cookie : operation.getRequest().getCookies()) {
actualCookies.add(cookie.getName());
}
return actualCookies;
}
/**
* Returns a new {@code RequestCookiesSnippet} configured with this snippet's
* attributes and its descriptors combined with the given
* {@code additionalDescriptors}.
* @param additionalDescriptors the additional descriptors
* @return the new snippet
*/
public final RequestCookiesSnippet and(CookieDescriptor... additionalDescriptors) {
return and(Arrays.asList(additionalDescriptors));
}
/**
* Returns a new {@code RequestCookiesSnippet} configured with this snippet's
* attributes and its descriptors combined with the given
* {@code additionalDescriptors}.
* @param additionalDescriptors the additional descriptors
* @return the new snippet
*/
public final RequestCookiesSnippet and(List<CookieDescriptor> additionalDescriptors) {
List<CookieDescriptor> combinedDescriptors = new ArrayList<>(this.getCookieDescriptors());
combinedDescriptors.addAll(additionalDescriptors);
return new RequestCookiesSnippet(combinedDescriptors, getAttributes(), false);
}
}

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2014-2016 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
*
* https://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 org.springframework.restdocs.cookies;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.operation.ResponseCookie;
import org.springframework.restdocs.snippet.Snippet;
/**
* A {@link Snippet} that documents the cookies in a response.
*
* @author Andreas Evers
* @author Andy Wilkinson
* @author Clyde Stubbs
* @since 2.1
* @see CookieDocumentation#responseCookies(CookieDescriptor...)
* @see CookieDocumentation#responseCookies(Map, CookieDescriptor...)
*/
public class ResponseCookiesSnippet extends AbstractCookiesSnippet {
/**
* Creates a new {@code ResponseCookiesSnippet} that will document the cookies in the
* response using the given {@code descriptors}.
* @param descriptors the descriptors
*/
protected ResponseCookiesSnippet(List<CookieDescriptor> descriptors) {
this(descriptors, null, false);
}
/**
* Creates a new {@code ResponseCookiesSnippet} that will document the cookies in the
* response using the given {@code descriptors}. The given {@code attributes} will be
* included in the model during template rendering. Undocumented cookies will cause a
* failure.
* @param descriptors the descriptors
* @param attributes the additional attributes
*/
protected ResponseCookiesSnippet(List<CookieDescriptor> descriptors, Map<String, Object> attributes) {
super("response", descriptors, attributes, false);
}
/**
* Creates a new {@code ResponseCookiesSnippet} that will document the cookies in the
* response using the given {@code descriptors}. The given {@code attributes} will be
* included in the model during template rendering.
* @param descriptors the descriptors
* @param attributes the additional attributes
* @param ignoreUndocumentedCookies ignore any cookies that are undocumented
*/
protected ResponseCookiesSnippet(List<CookieDescriptor> descriptors, Map<String, Object> attributes,
boolean ignoreUndocumentedCookies) {
super("response", descriptors, attributes, ignoreUndocumentedCookies);
}
@Override
protected Set<String> extractActualCookies(Operation operation) {
return operation.getResponse().getCookies().stream().map(ResponseCookie::getName).collect(Collectors.toSet());
}
/**
* Returns a new {@code ResponseCookiesSnippet} configured with this snippet's
* attributes and its descriptors combined with the given
* {@code additionalDescriptors}.
* @param additionalDescriptors the additional descriptors
* @return the new snippet
*/
public final ResponseCookiesSnippet and(CookieDescriptor... additionalDescriptors) {
return and(Arrays.asList(additionalDescriptors));
}
/**
* Returns a new {@code ResponseCookiesSnippet} configured with this snippet's
* attributes and its descriptors combined with the given
* {@code additionalDescriptors}.
* @param additionalDescriptors the additional descriptors
* @return the new snippet
*/
public final ResponseCookiesSnippet and(List<CookieDescriptor> additionalDescriptors) {
List<CookieDescriptor> combinedDescriptors = new ArrayList<>(this.getCookieDescriptors());
combinedDescriptors.addAll(additionalDescriptors);
return new ResponseCookiesSnippet(combinedDescriptors, getAttributes(), this.ignoreUndocumentedCookies);
}
}

View File

@@ -0,0 +1,20 @@
/*
* 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
*
* https://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.
*/
/**
* Documenting the cookies of a RESTful API's requests and responses.
*/
package org.springframework.restdocs.cookies;

View File

@@ -16,6 +16,8 @@
package org.springframework.restdocs.operation;
import java.util.Collection;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
@@ -23,6 +25,7 @@ import org.springframework.http.HttpStatus;
* The response that was received as part of performing an operation on a RESTful service.
*
* @author Andy Wilkinson
* @author Clyde Stubbs
* @see Operation
* @see Operation#getRequest()
*/
@@ -64,4 +67,12 @@ public interface OperationResponse {
*/
String getContentAsString();
/**
* Returns the {@link ResponseCookie cookies} returned with the response. If no
* cookies were returned an empty collection is returned.
* @return the cookies, never {@code null}
* @since 2.1
*/
Collection<ResponseCookie> getCookies();
}

View File

@@ -16,26 +16,47 @@
package org.springframework.restdocs.operation;
import java.util.Collection;
import java.util.Collections;
import org.springframework.http.HttpHeaders;
/**
* A factory for creating {@link OperationResponse OperationResponses}.
*
* @author Andy Wilkinson
* @author Clyde Stubbs
*/
public class OperationResponseFactory {
/**
* Creates a new {@link OperationResponse} without cookies. If the response has any
* content, the given {@code headers} will be augmented to ensure that they include a
* {@code Content-Length} header.
* @param status the status of the response
* @param headers the request's headers
* @param content the content of the request
* @return the {@code OperationResponse}
*/
public OperationResponse create(int status, HttpHeaders headers, byte[] content) {
return new StandardOperationResponse(status, augmentHeaders(headers, content), content,
Collections.emptyList());
}
/**
* Creates a new {@link OperationResponse}. If the response has any content, the given
* {@code headers} will be augmented to ensure that they include a
* {@code Content-Length} header.
* @param status the status of the response
* @param headers the response's headers
* @param content the content of the response
* @param headers the request's headers
* @param content the content of the request
* @param cookies the cookies
* @return the {@code OperationResponse}
* @since 3.0
*/
public OperationResponse create(int status, HttpHeaders headers, byte[] content) {
return new StandardOperationResponse(status, augmentHeaders(headers, content), content);
public OperationResponse create(int status, HttpHeaders headers, byte[] content,
Collection<ResponseCookie> cookies) {
return new StandardOperationResponse(status, augmentHeaders(headers, content), content, cookies);
}
/**
@@ -49,7 +70,7 @@ public class OperationResponseFactory {
*/
public OperationResponse createFrom(OperationResponse original, byte[] newContent) {
return new StandardOperationResponse(original.getStatusCode(),
getUpdatedHeaders(original.getHeaders(), newContent), newContent);
getUpdatedHeaders(original.getHeaders(), newContent), newContent, original.getCookies());
}
/**
@@ -60,7 +81,8 @@ public class OperationResponseFactory {
* @return the new response with the new headers
*/
public OperationResponse createFrom(OperationResponse original, HttpHeaders newHeaders) {
return new StandardOperationResponse(original.getStatusCode(), newHeaders, original.getContent());
return new StandardOperationResponse(original.getStatusCode(), newHeaders, original.getContent(),
original.getCookies());
}
private HttpHeaders augmentHeaders(HttpHeaders originalHeaders, byte[] content) {

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2014-2018 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
*
* https://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 org.springframework.restdocs.operation;
/**
* A representation of a Cookie returned in a response.
*
* @author Clyde Stubbs
* @since 2.1
*/
public final class ResponseCookie {
private final String name;
private final String value;
/**
* Creates a new {@code ResponseCookie} with the given {@code name} and {@code value}.
* @param name the name of the cookie
* @param value the value of the cookie
*/
public ResponseCookie(String name, String value) {
this.name = name;
this.value = value;
}
/**
* Returns the name of the cookie.
* @return the name
*/
public String getName() {
return this.name;
}
/**
* Returns the value of the cookie.
* @return the value
*/
public String getValue() {
return this.value;
}
}

View File

@@ -16,6 +16,8 @@
package org.springframework.restdocs.operation;
import java.util.Collection;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
@@ -23,21 +25,26 @@ import org.springframework.http.HttpStatus;
* Standard implementation of {@link OperationResponse}.
*
* @author Andy Wilkinson
* @author Clyde Stubbs
*/
class StandardOperationResponse extends AbstractOperationMessage implements OperationResponse {
private final int status;
private Collection<ResponseCookie> cookies;
/**
* Creates a new response with the given {@code status}, {@code headers}, and
* {@code content}.
* @param status the status of the response
* @param headers the headers of the response
* @param content the content of the response
* @param cookies any cookies included in the response
*/
StandardOperationResponse(int status, HttpHeaders headers, byte[] content) {
StandardOperationResponse(int status, HttpHeaders headers, byte[] content, Collection<ResponseCookie> cookies) {
super(content, headers);
this.status = status;
this.cookies = cookies;
}
@Override
@@ -50,4 +57,9 @@ class StandardOperationResponse extends AbstractOperationMessage implements Oper
return this.status;
}
@Override
public Collection<ResponseCookie> getCookies() {
return this.cookies;
}
}

View File

@@ -142,7 +142,7 @@ public class UriModifyingOperationPreprocessor implements OperationPreprocessor
@Override
public OperationResponse preprocess(OperationResponse response) {
return this.contentModifyingDelegate.preprocess(new OperationResponseFactory().create(response.getStatusCode(),
modify(response.getHeaders()), response.getContent()));
modify(response.getHeaders()), response.getContent(), response.getCookies()));
}
private HttpHeaders modify(HttpHeaders headers) {

View File

@@ -0,0 +1,9 @@
|===
|Name|Description
{{#cookies}}
|{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}}
|{{#tableCellContent}}{{description}}{{/tableCellContent}}
{{/cookies}}
|===

View File

@@ -0,0 +1,9 @@
|===
|Name|Description
{{#cookies}}
|{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}}
|{{#tableCellContent}}{{description}}{{/tableCellContent}}
{{/cookies}}
|===

View File

@@ -0,0 +1,5 @@
Name | Description
---- | -----------
{{#cookies}}
`{{name}}` | {{description}}
{{/cookies}}

View File

@@ -0,0 +1,5 @@
Name | Description
---- | -----------
{{#cookies}}
`{{name}}` | {{description}}
{{/cookies}}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2014-2018 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
*
* https://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 org.springframework.restdocs.cookies;
import java.io.IOException;
import java.util.Collections;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.restdocs.snippet.SnippetException;
import org.springframework.restdocs.templates.TemplateFormats;
import org.springframework.restdocs.testfixtures.OperationBuilder;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for failures when rendering {@link RequestCookiesSnippet} due to missing or
* undocumented cookies.
*
* @author Andy Wilkinson
* @author Clyde Stubbs
*/
public class RequestCookiesSnippetFailureTests {
@Rule
public OperationBuilder operationBuilder = new OperationBuilder(TemplateFormats.asciidoctor());
@Test
public void missingRequestCookie() throws IOException {
assertThatExceptionOfType(SnippetException.class)
.isThrownBy(() -> new RequestCookiesSnippet(
Collections.singletonList(CookieDocumentation.cookieWithName("Accept").description("one")))
.document(this.operationBuilder.request("http://localhost").build()))
.withMessage("Cookies with the following names were not found" + " in the request: [Accept]");
}
@Test
public void undocumentedRequestCookieAndMissingRequestHeader() throws IOException {
assertThatExceptionOfType(SnippetException.class).isThrownBy(() -> new RequestCookiesSnippet(
Collections.singletonList(CookieDocumentation.cookieWithName("Accept").description("one")))
.document(this.operationBuilder.request("http://localhost").cookie("X-Test", "test").build()))
.withMessageEndingWith("Cookies with the following names were not found" + " in the request: [Accept]");
}
}

View File

@@ -0,0 +1,144 @@
/*
* Copyright 2014-2018 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
*
* https://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 org.springframework.restdocs.cookies;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import org.junit.Test;
import org.springframework.restdocs.AbstractSnippetTests;
import org.springframework.restdocs.templates.TemplateEngine;
import org.springframework.restdocs.templates.TemplateFormat;
import org.springframework.restdocs.templates.TemplateFormats;
import org.springframework.restdocs.templates.TemplateResourceResolver;
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.springframework.restdocs.snippet.Attributes.attributes;
import static org.springframework.restdocs.snippet.Attributes.key;
/**
* Tests for {@link RequestCookiesSnippet}.
*
* @author Andreas Evers
* @author Andy Wilkinson
* @author Clyde Stubbs
*/
public class RequestCookiesSnippetTests extends AbstractSnippetTests {
public RequestCookiesSnippetTests(String name, TemplateFormat templateFormat) {
super(name, templateFormat);
}
@Test
public void requestWithCookies() throws IOException {
new RequestCookiesSnippet(Arrays.asList(CookieDocumentation.cookieWithName("Session").description("one"),
CookieDocumentation.cookieWithName("User").description("two"),
CookieDocumentation.cookieWithName("Timeout").description("three"),
CookieDocumentation.cookieWithName("Preference").description("four"),
CookieDocumentation.cookieWithName("Connection").description("five")))
.document(this.operationBuilder.request("http://localhost").cookie("Session", "test")
.cookie("User", "nobody").cookie("Timeout", "3600").cookie("Preference", "inverse")
.cookie("Connection", "secure").build());
assertThat(this.generatedSnippets.requestCookies())
.is(tableWithHeader("Name", "Description").row("`Session`", "one").row("`User`", "two")
.row("`Timeout`", "three").row("`Preference`", "four").row("`Connection`", "five"));
}
@Test
public void undocumentedRequestCookie() throws IOException {
new RequestCookiesSnippet(
Collections.singletonList(CookieDocumentation.cookieWithName("X-Test").description("one")))
.document(this.operationBuilder.request("http://localhost").cookie("X-Test", "test")
.cookie("Second", "*/*").build());
assertThat(this.generatedSnippets.requestCookies())
.is(tableWithHeader("Name", "Description").row("`X-Test`", "one"));
}
@Test
public void requestCookiesWithCustomAttributes() throws IOException {
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
given(resolver.resolveTemplateResource("request-cookies"))
.willReturn(snippetResource("request-cookies-with-title"));
new RequestCookiesSnippet(
Collections.singletonList(CookieDocumentation.cookieWithName("X-Test").description("one")),
attributes(key("title").value("Custom title")))
.document(this.operationBuilder
.attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver))
.request("http://localhost").cookie("X-Test", "test").build());
assertThat(this.generatedSnippets.requestCookies()).contains("Custom title");
}
@Test
public void requestCookiesWithCustomDescriptorAttributes() throws IOException {
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
given(resolver.resolveTemplateResource("request-cookies"))
.willReturn(snippetResource("request-cookies-with-extra-column"));
new RequestCookiesSnippet(Arrays.asList(
CookieDocumentation.cookieWithName("X-Test").description("one").attributes(key("foo").value("alpha")),
CookieDocumentation.cookieWithName("Accept-Encoding").description("two")
.attributes(key("foo").value("bravo")),
CookieDocumentation.cookieWithName("Accept").description("three")
.attributes(key("foo").value("charlie"))))
.document(this.operationBuilder
.attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver))
.request("http://localhost").cookie("X-Test", "test")
.cookie("Accept-Encoding", "gzip, deflate").cookie("Accept", "*/*").build());
assertThat(this.generatedSnippets.requestCookies()).is(//
tableWithHeader("Name", "Description", "Foo").row("X-Test", "one", "alpha")
.row("Accept-Encoding", "two", "bravo").row("Accept", "three", "charlie"));
}
@Test
public void additionalDescriptors() throws IOException {
CookieDocumentation
.requestCookies(CookieDocumentation.cookieWithName("X-Test").description("one"),
CookieDocumentation.cookieWithName("Accept").description("two"),
CookieDocumentation.cookieWithName("Accept-Encoding").description("three"),
CookieDocumentation.cookieWithName("Accept-Language").description("four"))
.and(CookieDocumentation.cookieWithName("Cache-Control").description("five"),
CookieDocumentation.cookieWithName("Connection").description("six"))
.document(this.operationBuilder.request("http://localhost").cookie("X-Test", "test")
.cookie("Accept", "*/*").cookie("Accept-Encoding", "gzip, deflate")
.cookie("Accept-Language", "en-US,en;q=0.5").cookie("Cache-Control", "max-age=0")
.cookie("Connection", "keep-alive").build());
assertThat(this.generatedSnippets.requestCookies()).is(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"));
}
@Test
public void tableCellContentIsEscapedWhenNecessary() throws IOException {
new RequestCookiesSnippet(
Collections.singletonList(CookieDocumentation.cookieWithName("Foo|Bar").description("one|two")))
.document(this.operationBuilder.request("http://localhost").cookie("Foo|Bar", "baz").build());
assertThat(this.generatedSnippets.requestCookies()).is(tableWithHeader("Name", "Description")
.row(escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two")));
}
private String escapeIfNecessary(String input) {
if (this.templateFormat.getId().equals(TemplateFormats.markdown().getId())) {
return input;
}
return input.replace("|", "\\|");
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2014-2018 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
*
* https://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 org.springframework.restdocs.cookies;
import java.io.IOException;
import java.util.Collections;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.restdocs.snippet.SnippetException;
import org.springframework.restdocs.templates.TemplateFormats;
import org.springframework.restdocs.testfixtures.OperationBuilder;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for failures when rendering {@link ResponseCookiesSnippet} due to missing or
* undocumented cookies.
*
* @author Andy Wilkinson
* @author Clyde Stubbs
*/
public class ResponseCookiesSnippetFailureTests {
@Rule
public OperationBuilder operationBuilder = new OperationBuilder(TemplateFormats.asciidoctor());
@Test
public void missingResponseCookie() throws IOException {
assertThatExceptionOfType(SnippetException.class)
.isThrownBy(() -> new ResponseCookiesSnippet(Collections
.singletonList(CookieDocumentation.cookieWithName("Content-Type").description("one")))
.document(this.operationBuilder.response().build()))
.withMessage("Cookies with the following names were not found" + " in the response: [Content-Type]");
}
@Test
public void undocumentedResponseCookieAndMissingResponseHeader() throws IOException {
assertThatExceptionOfType(SnippetException.class)
.isThrownBy(() -> new ResponseCookiesSnippet(Collections
.singletonList(CookieDocumentation.cookieWithName("Content-Type").description("one")))
.document(this.operationBuilder.response().cookie("X-Test", "test").build()))
.withMessageEndingWith(
"Cookies with the following names were not found" + " in the response: [Content-Type]");
}
}

View File

@@ -0,0 +1,141 @@
/*
* Copyright 2014-2018 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
*
* https://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 org.springframework.restdocs.cookies;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import org.junit.Test;
import org.springframework.restdocs.AbstractSnippetTests;
import org.springframework.restdocs.templates.TemplateEngine;
import org.springframework.restdocs.templates.TemplateFormat;
import org.springframework.restdocs.templates.TemplateFormats;
import org.springframework.restdocs.templates.TemplateResourceResolver;
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.springframework.restdocs.snippet.Attributes.attributes;
import static org.springframework.restdocs.snippet.Attributes.key;
/**
* Tests for {@link ResponseCookiesSnippet}.
*
* @author Andreas Evers
* @author Andy Wilkinson
* @author Clyde Stubbs
*/
public class ResponseCookiesSnippetTests extends AbstractSnippetTests {
public ResponseCookiesSnippetTests(String name, TemplateFormat templateFormat) {
super(name, templateFormat);
}
@Test
public void responseWithCookies() throws IOException {
new ResponseCookiesSnippet(Arrays.asList(CookieDocumentation.cookieWithName("X-Test").description("one"),
CookieDocumentation.cookieWithName("Content-Type").description("two"),
CookieDocumentation.cookieWithName("Etag").description("three"),
CookieDocumentation.cookieWithName("Cache-Control").description("five"),
CookieDocumentation.cookieWithName("Vary").description("six")))
.document(this.operationBuilder.response().cookie("X-Test", "test")
.cookie("Content-Type", "application/json").cookie("Etag", "lskjadldj3ii32l2ij23")
.cookie("Cache-Control", "max-age=0").cookie("Vary", "User-Agent").build());
assertThat(this.generatedSnippets.responseCookies())
.is(tableWithHeader("Name", "Description").row("`X-Test`", "one").row("`Content-Type`", "two")
.row("`Etag`", "three").row("`Cache-Control`", "five").row("`Vary`", "six"));
}
@Test
public void undocumentedResponseCookie() throws IOException {
new ResponseCookiesSnippet(
Collections.singletonList(CookieDocumentation.cookieWithName("X-Test").description("one")))
.document(this.operationBuilder.response().cookie("X-Test", "test")
.cookie("Content-Type", "*/*").build());
assertThat(this.generatedSnippets.responseCookies())
.is(tableWithHeader("Name", "Description").row("`X-Test`", "one"));
}
@Test
public void responseCookiesWithCustomAttributes() throws IOException {
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
given(resolver.resolveTemplateResource("response-cookies"))
.willReturn(snippetResource("response-cookies-with-title"));
new ResponseCookiesSnippet(
Collections.singletonList(CookieDocumentation.cookieWithName("X-Test").description("one")),
attributes(key("title").value("Custom title")))
.document(this.operationBuilder
.attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver))
.response().cookie("X-Test", "test").build());
assertThat(this.generatedSnippets.responseCookies()).contains("Custom title");
}
@Test
public void responseCookiesWithCustomDescriptorAttributes() throws IOException {
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
given(resolver.resolveTemplateResource("response-cookies"))
.willReturn(snippetResource("response-cookies-with-extra-column"));
new ResponseCookiesSnippet(Arrays.asList(
CookieDocumentation.cookieWithName("X-Test").description("one").attributes(key("foo").value("alpha")),
CookieDocumentation.cookieWithName("Content-Type").description("two")
.attributes(key("foo").value("bravo")),
CookieDocumentation.cookieWithName("Etag").description("three")
.attributes(key("foo").value("charlie"))))
.document(this.operationBuilder
.attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver))
.response().cookie("X-Test", "test").cookie("Content-Type", "application/json")
.cookie("Etag", "lskjadldj3ii32l2ij23").build());
assertThat(this.generatedSnippets.responseCookies()).is(tableWithHeader("Name", "Description", "Foo")
.row("X-Test", "one", "alpha").row("Content-Type", "two", "bravo").row("Etag", "three", "charlie"));
}
@Test
public void additionalDescriptors() throws IOException {
CookieDocumentation
.responseCookies(CookieDocumentation.cookieWithName("X-Test").description("one"),
CookieDocumentation.cookieWithName("Content-Type").description("two"),
CookieDocumentation.cookieWithName("Etag").description("three"))
.and(CookieDocumentation.cookieWithName("Cache-Control").description("five"),
CookieDocumentation.cookieWithName("Vary").description("six"))
.document(this.operationBuilder.response().cookie("X-Test", "test")
.cookie("Content-Type", "application/json").cookie("Etag", "lskjadldj3ii32l2ij23")
.cookie("Cache-Control", "max-age=0").cookie("Vary", "User-Agent").build());
assertThat(this.generatedSnippets.responseCookies())
.is(tableWithHeader("Name", "Description").row("`X-Test`", "one").row("`Content-Type`", "two")
.row("`Etag`", "three").row("`Cache-Control`", "five").row("`Vary`", "six"));
}
@Test
public void tableCellContentIsEscapedWhenNecessary() throws IOException {
new ResponseCookiesSnippet(
Collections.singletonList(CookieDocumentation.cookieWithName("Foo|Bar").description("one|two")))
.document(this.operationBuilder.response().cookie("Foo|Bar", "baz").build());
assertThat(this.generatedSnippets.responseCookies()).is(tableWithHeader("Name", "Description")
.row(escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two")));
}
private String escapeIfNecessary(String input) {
if (this.templateFormat.getId().equals(TemplateFormats.markdown().getId())) {
return input;
}
return input.replace("|", "\\|");
}
}

View File

@@ -0,0 +1,10 @@
|===
|Name|Description|Foo
{{#cookies}}
|{{name}}
|{{description}}
|{{foo}}
{{/cookies}}
|===

View File

@@ -0,0 +1,10 @@
.{{title}}
|===
|Name|Description
{{#cookies}}
|{{name}}
|{{description}}
{{/cookies}}
|===

View File

@@ -0,0 +1,10 @@
|===
|Name|Description|Foo
{{#cookies}}
|{{name}}
|{{description}}
|{{foo}}
{{/cookies}}
|===

View File

@@ -0,0 +1,10 @@
.{{title}}
|===
|Name|Description
{{#cookies}}
|{{name}}
|{{description}}
{{/cookies}}
|===

View File

@@ -0,0 +1,5 @@
Name | Description | Foo
---- | ----------- | ---
{{#cookies}}
{{name}} | {{description}} | {{foo}}
{{/cookies}}

View File

@@ -0,0 +1,6 @@
{{title}}
Name | Description
---- | -----------
{{#cookies}}
{{name}} | {{description}}
{{/cookies}}

View File

@@ -0,0 +1,5 @@
Name | Description | Foo
---- | ----------- | ---
{{#cookies}}
{{name}} | {{description}} | {{foo}}
{{/cookies}}

View File

@@ -0,0 +1,6 @@
{{title}}
Name | Description
---- | -----------
{{#cookies}}
{{name}} | {{description}}
{{/cookies}}

View File

@@ -70,6 +70,14 @@ public class GeneratedSnippets extends OperationTestRule {
return snippet("response-headers");
}
public String requestCookies() {
return snippet("request-cookies");
}
public String responseCookies() {
return snippet("response-cookies");
}
public String httpRequest() {
return snippet("http-request");
}

View File

@@ -22,8 +22,10 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.runners.model.Statement;
@@ -42,6 +44,7 @@ import org.springframework.restdocs.operation.OperationResponse;
import org.springframework.restdocs.operation.OperationResponseFactory;
import org.springframework.restdocs.operation.Parameters;
import org.springframework.restdocs.operation.RequestCookie;
import org.springframework.restdocs.operation.ResponseCookie;
import org.springframework.restdocs.operation.StandardOperation;
import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolverFactory;
import org.springframework.restdocs.snippet.StandardWriterResolver;
@@ -94,7 +97,6 @@ public class OperationBuilder extends OperationTestRule {
this.name = operationName;
this.outputDirectory = outputDirectory;
this.requestBuilder = null;
this.requestBuilder = null;
this.attributes.clear();
}
@@ -265,10 +267,12 @@ public class OperationBuilder extends OperationTestRule {
private HttpHeaders headers = new HttpHeaders();
private Set<ResponseCookie> cookies = new HashSet<>();
private byte[] content = new byte[0];
private OperationResponse buildResponse() {
return new OperationResponseFactory().create(this.status, this.headers, this.content);
return new OperationResponseFactory().create(this.status, this.headers, this.content, this.cookies);
}
public OperationResponseBuilder status(int status) {
@@ -281,6 +285,11 @@ public class OperationBuilder extends OperationTestRule {
return this;
}
public OperationResponseBuilder cookie(String name, String value) {
this.cookies.add(new ResponseCookie(name, value));
return this;
}
public OperationResponseBuilder content(byte[] content) {
this.content = content;
return this;

View File

@@ -16,6 +16,11 @@
package org.springframework.restdocs.mockmvc;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import jakarta.servlet.http.Cookie;
import org.springframework.http.HttpHeaders;
@@ -23,6 +28,7 @@ import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.restdocs.operation.OperationResponse;
import org.springframework.restdocs.operation.OperationResponseFactory;
import org.springframework.restdocs.operation.ResponseConverter;
import org.springframework.restdocs.operation.ResponseCookie;
import org.springframework.util.StringUtils;
/**
@@ -30,13 +36,28 @@ import org.springframework.util.StringUtils;
* {@link MockHttpServletResponse}.
*
* @author Andy Wilkinson
* @author Clyde Stubbs
*/
class MockMvcResponseConverter implements ResponseConverter<MockHttpServletResponse> {
@Override
public OperationResponse convert(MockHttpServletResponse mockResponse) {
return new OperationResponseFactory().create(mockResponse.getStatus(), extractHeaders(mockResponse),
mockResponse.getContentAsByteArray());
HttpHeaders headers = extractHeaders(mockResponse);
Collection<ResponseCookie> cookies = extractCookies(mockResponse, headers);
return new OperationResponseFactory().create(mockResponse.getStatus(), headers,
mockResponse.getContentAsByteArray(), cookies);
}
private Collection<ResponseCookie> extractCookies(MockHttpServletResponse mockRequest, HttpHeaders headers) {
if (mockRequest.getCookies() == null || mockRequest.getCookies().length == 0) {
return Collections.emptyList();
}
List<ResponseCookie> cookies = new ArrayList<>();
for (Cookie servletCookie : mockRequest.getCookies()) {
cookies.add(new ResponseCookie(servletCookie.getName(), servletCookie.getValue()));
}
headers.remove(HttpHeaders.COOKIE);
return cookies;
}
private HttpHeaders extractHeaders(MockHttpServletResponse response) {

View File

@@ -16,6 +16,12 @@
package org.springframework.restdocs.restassured;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import io.restassured.http.Header;
import io.restassured.response.Response;
@@ -23,19 +29,35 @@ import org.springframework.http.HttpHeaders;
import org.springframework.restdocs.operation.OperationResponse;
import org.springframework.restdocs.operation.OperationResponseFactory;
import org.springframework.restdocs.operation.ResponseConverter;
import org.springframework.restdocs.operation.ResponseCookie;
/**
* A converter for creating an {@link OperationResponse} from a REST Assured
* {@link Response}.
*
* @author Andy Wilkinson
* @author Clyde Stubbs
*/
class RestAssuredResponseConverter implements ResponseConverter<Response> {
@Override
public OperationResponse convert(Response response) {
HttpHeaders headers = extractHeaders(response);
Collection<ResponseCookie> cookies = extractCookies(response, headers);
return new OperationResponseFactory().create(response.getStatusCode(), extractHeaders(response),
extractContent(response));
extractContent(response), cookies);
}
private Collection<ResponseCookie> extractCookies(Response response, HttpHeaders headers) {
if (response.getCookies() == null || response.getCookies().size() == 0) {
return Collections.emptyList();
}
List<ResponseCookie> cookies = new ArrayList<>();
for (Map.Entry<String, String> servletCookie : response.getCookies().entrySet()) {
cookies.add(new ResponseCookie(servletCookie.getKey(), servletCookie.getValue()));
}
headers.remove(HttpHeaders.COOKIE);
return cookies;
}
private HttpHeaders extractHeaders(Response response) {

View File

@@ -17,12 +17,14 @@
package org.springframework.restdocs.webtestclient;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.restdocs.operation.OperationResponse;
import org.springframework.restdocs.operation.OperationResponseFactory;
import org.springframework.restdocs.operation.ResponseConverter;
import org.springframework.restdocs.operation.ResponseCookie;
import org.springframework.test.web.reactive.server.ExchangeResult;
import org.springframework.util.StringUtils;
@@ -36,8 +38,9 @@ class WebTestClientResponseConverter implements ResponseConverter<ExchangeResult
@Override
public OperationResponse convert(ExchangeResult result) {
Collection<ResponseCookie> cookies = extractCookies(result, result.getResponseHeaders());
return new OperationResponseFactory().create(result.getStatus().value(), extractHeaders(result),
result.getResponseBodyContent());
result.getResponseBodyContent(), cookies);
}
private HttpHeaders extractHeaders(ExchangeResult result) {
@@ -50,7 +53,7 @@ class WebTestClientResponseConverter implements ResponseConverter<ExchangeResult
return headers;
}
private String generateSetCookieHeader(ResponseCookie cookie) {
private String generateSetCookieHeader(org.springframework.http.ResponseCookie cookie) {
StringBuilder header = new StringBuilder();
header.append(cookie.getName());
header.append('=');
@@ -71,12 +74,26 @@ class WebTestClientResponseConverter implements ResponseConverter<ExchangeResult
return header.toString();
}
private Collection<ResponseCookie> extractCookies(ExchangeResult result, HttpHeaders headers) {
List<String> cookieHeaders = headers.get(HttpHeaders.COOKIE);
if (cookieHeaders == null) {
return result.getResponseCookies().values().stream().flatMap(List::stream).map(this::createResponseCookie)
.collect(Collectors.toSet());
}
headers.remove(HttpHeaders.COOKIE);
return cookieHeaders.stream().map(this::createResponseCookie).collect(Collectors.toList());
}
private void appendIfAvailable(StringBuilder header, String value) {
if (StringUtils.hasText(value)) {
header.append(value);
}
}
private ResponseCookie createResponseCookie(org.springframework.http.ResponseCookie original) {
return new ResponseCookie(original.getName(), original.getValue());
}
private void appendIfAvailable(StringBuilder header, String name, String value) {
if (StringUtils.hasText(value)) {
header.append(name);
@@ -84,4 +101,9 @@ class WebTestClientResponseConverter implements ResponseConverter<ExchangeResult
}
}
private ResponseCookie createResponseCookie(String header) {
String[] components = header.split("=");
return new ResponseCookie(components[0], components[1]);
}
}