{
+
+ 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;
+ }
+
+}
diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/CookieDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/CookieDocumentation.java
new file mode 100644
index 00000000..20c20f43
--- /dev/null
+++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/CookieDocumentation.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright 2014-2022 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 Clyde Stubbs
+ * @author Andy Wilkinson
+ * @since 3.0
+ */
+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}.
+ *
+ * If a cookie is present in the request, but is not documented by one of the
+ * descriptors, a failure will occur when the snippet is invoked. Similarly, if a
+ * cookie is documented, is not marked as optional, and is not present in the request,
+ * a failure will also 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}.
+ *
+ * If a cookie is present in the request, but is not documented by one of the
+ * descriptors, a failure will occur when the snippet is invoked. Similarly, if a
+ * cookie is documented, is not marked as optional, and is not present in the request,
+ * a failure will also 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 descriptors) {
+ return new RequestCookiesSnippet(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}.
+ *
+ * If a cookie is documented, is not marked as optional, and is not present in the
+ * request, a failure will occur. Any undocumented cookies will be ignored.
+ * @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(CookieDescriptor... descriptors) {
+ return relaxedRequestCookies(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}.
+ *
+ * If a cookie is documented, is not marked as optional, and is not present in the
+ * request, a failure will occur. Any undocumented cookies will be ignored.
+ * @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(List descriptors) {
+ return new RequestCookiesSnippet(descriptors, true);
+ }
+
+ /**
+ * 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}.
+ *
+ * If a cookie is present in the request, but is not documented by one of the
+ * descriptors, a failure will occur when the snippet is invoked. Similarly, if a
+ * cookie is documented, is not marked as optional, and is not present in the request,
+ * a failure will also 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 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}.
+ *
+ * If a cookie is present in the request, but is not documented by one of the
+ * descriptors, a failure will occur when the snippet is invoked. Similarly, if a
+ * cookie is documented, is not marked as optional, and is not present in the request,
+ * a failure will also 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 attributes,
+ List descriptors) {
+ return new RequestCookiesSnippet(descriptors, attributes);
+ }
+
+ /**
+ * 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}.
+ *
+ * If a cookie is documented, is not marked as optional, and is not present in the
+ * request, a failure will occur. Any undocumented cookies will be ignored.
+ * @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 attributes,
+ CookieDescriptor... descriptors) {
+ return relaxedRequestCookies(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}.
+ *
+ * If a cookie is documented, is not marked as optional, and is not present in the
+ * request, a failure will occur. Any undocumented cookies will be ignored.
+ * @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 attributes,
+ List 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}.
+ *
+ * If a cookie is present in the response, but is not documented by one of the
+ * descriptors, a failure will occur when the snippet is invoked. Similarly, if a
+ * cookie is documented, is not marked as optional, and is not present in the
+ * response, a failure will also 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}.
+ *
+ * If a cookie is present in the response, but is not documented by one of the
+ * descriptors, a failure will occur when the snippet is invoked. Similarly, if a
+ * cookie is documented, is not marked as optional, and is not present in the
+ * response, a failure will also 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 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}.
+ *
+ * If a cookie is documented, is not marked as optional, and is not present in the
+ * response, a failure will occur. Any undocumented cookies will be ignored.
+ * @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(CookieDescriptor... descriptors) {
+ return relaxedResponseCookies(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}.
+ *
+ * If a cookie is documented, is not marked as optional, and is not present in the
+ * response, a failure will occur. Any undocumented cookies will be ignored.
+ * @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 descriptors) {
+ return new ResponseCookiesSnippet(descriptors, 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}.
+ *
+ * If a cookie is present in the response, but is not documented by one of the
+ * descriptors, a failure will occur when the snippet is invoked. Similarly, if a
+ * cookie is documented, is not marked as optional, and is not present in the
+ * response, a failure will also 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 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}.
+ *
+ * If a cookie is present in the response, but is not documented by one of the
+ * descriptors, a failure will occur when the snippet is invoked. Similarly, if a
+ * cookie is documented, is not marked as optional, and is not present in the
+ * response, a failure will also 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 attributes,
+ List descriptors) {
+ return new ResponseCookiesSnippet(descriptors, attributes);
+ }
+
+ /**
+ * 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}.
+ *
+ * If a cookie is documented, is not marked as optional, and is not present in the
+ * response, a failure will occur. Any undocumented cookies will be ignored.
+ * @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 attributes,
+ CookieDescriptor... descriptors) {
+ return relaxedResponseCookies(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}.
+ *
+ * If a cookie is documented, is not marked as optional, and is not present in the
+ * response, a failure will occur. Any undocumented cookies will be ignored.
+ * @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 attributes,
+ List descriptors) {
+ return new ResponseCookiesSnippet(descriptors, attributes, true);
+ }
+
+}
diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/RequestCookiesSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/RequestCookiesSnippet.java
new file mode 100644
index 00000000..e5f4d5d2
--- /dev/null
+++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/RequestCookiesSnippet.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2014-2022 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;
+import org.springframework.restdocs.snippet.SnippetException;
+
+/**
+ * A {@link Snippet} that documents the cookies in a request.
+ *
+ * @author Clyde Stubbs
+ * @author Andy Wilkinson
+ * @since 3.0
+ * @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 descriptors) {
+ this(descriptors, null, false);
+ }
+
+ /**
+ * Creates a new {@code RequestCookiesSnippet} that will document the cookies in the
+ * request using the given {@code descriptors}. If {@code ignoreUndocumentedCookies}
+ * is {@code true}, undocumented cookies will be ignored and will not trigger a
+ * failure.
+ * @param descriptors the descriptors
+ * @param ignoreUndocumentedCookies whether undocumented cookies should be ignored
+ */
+ protected RequestCookiesSnippet(List descriptors, boolean ignoreUndocumentedCookies) {
+ this(descriptors, null, 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 descriptors, Map attributes) {
+ this(descriptors, attributes, 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 whether undocumented cookies should be ignored
+ */
+ protected RequestCookiesSnippet(List descriptors, Map attributes,
+ boolean ignoreUndocumentedCookies) {
+ super("request", descriptors, attributes, ignoreUndocumentedCookies);
+ }
+
+ @Override
+ protected Set extractActualCookies(Operation operation) {
+ HashSet actualCookies = new HashSet<>();
+ for (RequestCookie cookie : operation.getRequest().getCookies()) {
+ actualCookies.add(cookie.getName());
+ }
+ return actualCookies;
+ }
+
+ @Override
+ protected void verificationFailed(Set undocumentedCookies, Set missingCookies) {
+ String message = "";
+ if (!undocumentedCookies.isEmpty()) {
+ message += "Cookies with the following names were not documented: " + undocumentedCookies;
+ }
+ if (!missingCookies.isEmpty()) {
+ if (message.length() > 0) {
+ message += ". ";
+ }
+ message += "Cookies with the following names were not found in the request: " + missingCookies;
+ }
+ throw new SnippetException(message);
+ }
+
+ /**
+ * 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 additionalDescriptors) {
+ List combinedDescriptors = new ArrayList<>(this.getCookieDescriptors().values());
+ combinedDescriptors.addAll(additionalDescriptors);
+ return new RequestCookiesSnippet(combinedDescriptors, getAttributes(), isIgnoreUndocumentedCookies());
+ }
+
+}
diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/ResponseCookiesSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/ResponseCookiesSnippet.java
new file mode 100644
index 00000000..f6a5176f
--- /dev/null
+++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/ResponseCookiesSnippet.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2014-2022 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;
+import org.springframework.restdocs.snippet.SnippetException;
+
+/**
+ * A {@link Snippet} that documents the cookies in a response.
+ *
+ * @author Clyde Stubbs
+ * @author Andy Wilkinson
+ * @since 3.0
+ * @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 descriptors) {
+ this(descriptors, null, false);
+ }
+
+ /**
+ * Creates a new {@code ResponseCookiesSnippet} that will document the cookies in the
+ * response using the given {@code descriptors}. If {@code ignoreUndocumentedCookies}
+ * is {@code true}, undocumented cookies will be ignored and will not trigger a
+ * failure.
+ * @param descriptors the descriptors
+ * @param ignoreUndocumentedCookies whether undocumented cookies should be ignored
+ */
+ protected ResponseCookiesSnippet(List descriptors, boolean ignoreUndocumentedCookies) {
+ this(descriptors, null, ignoreUndocumentedCookies);
+ }
+
+ /**
+ * 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 not be
+ * ignored.
+ * @param descriptors the descriptors
+ * @param attributes the additional attributes
+ */
+ protected ResponseCookiesSnippet(List descriptors, Map attributes) {
+ this(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 whether undocumented cookies should be ignored
+ */
+ protected ResponseCookiesSnippet(List descriptors, Map attributes,
+ boolean ignoreUndocumentedCookies) {
+ super("response", descriptors, attributes, ignoreUndocumentedCookies);
+ }
+
+ @Override
+ protected Set extractActualCookies(Operation operation) {
+ return operation.getResponse().getCookies().stream().map(ResponseCookie::getName).collect(Collectors.toSet());
+ }
+
+ @Override
+ protected void verificationFailed(Set undocumentedCookies, Set missingCookies) {
+ String message = "";
+ if (!undocumentedCookies.isEmpty()) {
+ message += "Cookies with the following names were not documented: " + undocumentedCookies;
+ }
+ if (!missingCookies.isEmpty()) {
+ if (message.length() > 0) {
+ message += ". ";
+ }
+ message += "Cookies with the following names were not found in the response: " + missingCookies;
+ }
+ throw new SnippetException(message);
+ }
+
+ /**
+ * 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 additionalDescriptors) {
+ List combinedDescriptors = new ArrayList<>(this.getCookieDescriptors().values());
+ combinedDescriptors.addAll(additionalDescriptors);
+ return new ResponseCookiesSnippet(combinedDescriptors, getAttributes(), isIgnoreUndocumentedCookies());
+ }
+
+}
diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/package-info.java
new file mode 100644
index 00000000..afc01d60
--- /dev/null
+++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2014-2022 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;
diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponse.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponse.java
index 378160d4..8490ade2 100644
--- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponse.java
+++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponse.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2014-2019 the original author or authors.
+ * Copyright 2014-2022 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.
@@ -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 3.0
+ */
+ Collection getCookies();
+
}
diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java
index 17f835a6..125f744b 100644
--- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java
+++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java
@@ -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 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) {
diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseCookie.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseCookie.java
new file mode 100644
index 00000000..9cf39302
--- /dev/null
+++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseCookie.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2014-2022 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 3.0
+ */
+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;
+ }
+
+}
diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java
index 3a169365..5d9045af 100644
--- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java
+++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2014-2019 the original author or authors.
+ * Copyright 2014-2022 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.
@@ -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 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 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 getCookies() {
+ return this.cookies;
+ }
+
}
diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/UriModifyingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/UriModifyingOperationPreprocessor.java
index e16ee4e5..231f4e2c 100644
--- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/UriModifyingOperationPreprocessor.java
+++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/UriModifyingOperationPreprocessor.java
@@ -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) {
diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-cookies.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-cookies.snippet
new file mode 100644
index 00000000..0c531505
--- /dev/null
+++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-cookies.snippet
@@ -0,0 +1,9 @@
+|===
+|Name|Description
+
+{{#cookies}}
+|{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}}
+|{{#tableCellContent}}{{description}}{{/tableCellContent}}
+
+{{/cookies}}
+|===
\ No newline at end of file
diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-cookies.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-cookies.snippet
new file mode 100644
index 00000000..0c531505
--- /dev/null
+++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-cookies.snippet
@@ -0,0 +1,9 @@
+|===
+|Name|Description
+
+{{#cookies}}
+|{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}}
+|{{#tableCellContent}}{{description}}{{/tableCellContent}}
+
+{{/cookies}}
+|===
\ No newline at end of file
diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-cookies.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-cookies.snippet
new file mode 100644
index 00000000..dbc046b8
--- /dev/null
+++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-cookies.snippet
@@ -0,0 +1,5 @@
+Name | Description
+---- | -----------
+{{#cookies}}
+`{{name}}` | {{description}}
+{{/cookies}}
diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-cookies.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-cookies.snippet
new file mode 100644
index 00000000..dbc046b8
--- /dev/null
+++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-cookies.snippet
@@ -0,0 +1,5 @@
+Name | Description
+---- | -----------
+{{#cookies}}
+`{{name}}` | {{description}}
+{{/cookies}}
diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/RequestCookiesSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/RequestCookiesSnippetFailureTests.java
new file mode 100644
index 00000000..6498b2d5
--- /dev/null
+++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/RequestCookiesSnippetFailureTests.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2014-2022 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.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 Clyde Stubbs
+ * @author Andy Wilkinson
+ */
+public class RequestCookiesSnippetFailureTests {
+
+ @Rule
+ public OperationBuilder operationBuilder = new OperationBuilder(TemplateFormats.asciidoctor());
+
+ @Test
+ public void missingRequestCookie() {
+ assertThatExceptionOfType(SnippetException.class)
+ .isThrownBy(() -> new RequestCookiesSnippet(
+ Collections.singletonList(CookieDocumentation.cookieWithName("JSESSIONID").description("one")))
+ .document(this.operationBuilder.request("http://localhost").build()))
+ .withMessage("Cookies with the following names were not found in the request: [JSESSIONID]");
+ }
+
+ @Test
+ public void undocumentedRequestCookie() {
+ assertThatExceptionOfType(SnippetException.class)
+ .isThrownBy(() -> new RequestCookiesSnippet(Collections.emptyList()).document(this.operationBuilder
+ .request("http://localhost").cookie("JSESSIONID", "1234abcd5678efgh").build()))
+ .withMessageEndingWith("Cookies with the following names were not documented: [JSESSIONID]");
+ }
+
+}
diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/RequestCookiesSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/RequestCookiesSnippetTests.java
new file mode 100644
index 00000000..1fa41c0b
--- /dev/null
+++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/RequestCookiesSnippetTests.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2014-2022 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.cookies.CookieDocumentation.cookieWithName;
+import static org.springframework.restdocs.snippet.Attributes.attributes;
+import static org.springframework.restdocs.snippet.Attributes.key;
+
+/**
+ * Tests for {@link RequestCookiesSnippet}.
+ *
+ * @author Clyde Stubbs
+ * @author Andy Wilkinson
+ */
+public class RequestCookiesSnippetTests extends AbstractSnippetTests {
+
+ public RequestCookiesSnippetTests(String name, TemplateFormat templateFormat) {
+ super(name, templateFormat);
+ }
+
+ @Test
+ public void requestWithCookies() throws IOException {
+ new RequestCookiesSnippet(
+ Arrays.asList(cookieWithName("tz").description("one"), cookieWithName("logged_in").description("two")))
+ .document(this.operationBuilder.request("http://localhost").cookie("tz", "Europe%2FLondon")
+ .cookie("logged_in", "true").build());
+ assertThat(this.generatedSnippets.requestCookies())
+ .is(tableWithHeader("Name", "Description").row("`tz`", "one").row("`logged_in`", "two"));
+ }
+
+ @Test
+ public void ignoredRequestCookie() throws IOException {
+ new RequestCookiesSnippet(
+ Arrays.asList(cookieWithName("tz").ignored(), cookieWithName("logged_in").description("two")))
+ .document(this.operationBuilder.request("http://localhost").cookie("tz", "Europe%2FLondon")
+ .cookie("logged_in", "true").build());
+ assertThat(this.generatedSnippets.requestCookies())
+ .is(tableWithHeader("Name", "Description").row("`logged_in`", "two"));
+ }
+
+ @Test
+ public void allUndocumentedCookiesCanBeIgnored() throws IOException {
+ new RequestCookiesSnippet(
+ Arrays.asList(cookieWithName("tz").description("one"), cookieWithName("logged_in").description("two")),
+ true).document(
+ this.operationBuilder.request("http://localhost").cookie("tz", "Europe%2FLondon")
+ .cookie("logged_in", "true").cookie("user_session", "abcd1234efgh5678").build());
+ assertThat(this.generatedSnippets.requestCookies())
+ .is(tableWithHeader("Name", "Description").row("`tz`", "one").row("`logged_in`", "two"));
+ }
+
+ @Test
+ public void missingOptionalCookie() throws IOException {
+ new RequestCookiesSnippet(Arrays.asList(cookieWithName("tz").description("one").optional(),
+ cookieWithName("logged_in").description("two"))).document(
+ this.operationBuilder.request("http://localhost").cookie("logged_in", "true").build());
+ assertThat(this.generatedSnippets.requestCookies())
+ .is(tableWithHeader("Name", "Description").row("`tz`", "one").row("`logged_in`", "two"));
+ }
+
+ @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(cookieWithName("tz").description("one")),
+ attributes(key("title").value("Custom title")))
+ .document(this.operationBuilder
+ .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver))
+ .request("http://localhost").cookie("tz", "Europe%2FLondon").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(cookieWithName("tz").description("one").attributes(key("foo").value("alpha")),
+ cookieWithName("logged_in").description("two").attributes(key("foo").value("bravo"))))
+ .document(this.operationBuilder
+ .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver))
+ .request("http://localhost").cookie("tz", "Europe%2FLondon")
+ .cookie("logged_in", "true").build());
+ assertThat(this.generatedSnippets.requestCookies()).is(//
+ tableWithHeader("Name", "Description", "Foo").row("tz", "one", "alpha").row("logged_in", "two",
+ "bravo"));
+ }
+
+ @Test
+ public void additionalDescriptors() throws IOException {
+ new RequestCookiesSnippet(
+ Arrays.asList(cookieWithName("tz").description("one"), cookieWithName("logged_in").description("two")))
+ .and(cookieWithName("user_session").description("three"))
+ .document(this.operationBuilder.request("http://localhost").cookie("tz", "Europe%2FLondon")
+ .cookie("logged_in", "true").cookie("user_session", "abcd1234efgh5678").build());
+ assertThat(this.generatedSnippets.requestCookies()).is(tableWithHeader("Name", "Description").row("`tz`", "one")
+ .row("`logged_in`", "two").row("`user_session`", "three"));
+ }
+
+ @Test
+ public void additionalDescriptorsWithRelaxedRequestCookies() throws IOException {
+ new RequestCookiesSnippet(
+ Arrays.asList(cookieWithName("tz").description("one"), cookieWithName("logged_in").description("two")),
+ true).and(cookieWithName("user_session").description("three"))
+ .document(this.operationBuilder.request("http://localhost").cookie("tz", "Europe%2FLondon")
+ .cookie("logged_in", "true").cookie("user_session", "abcd1234efgh5678")
+ .cookie("color_theme", "light").build());
+ assertThat(this.generatedSnippets.requestCookies()).is(tableWithHeader("Name", "Description").row("`tz`", "one")
+ .row("`logged_in`", "two").row("`user_session`", "three"));
+ }
+
+ @Test
+ public void tableCellContentIsEscapedWhenNecessary() throws IOException {
+ new RequestCookiesSnippet(Collections.singletonList(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("|", "\\|");
+ }
+
+}
diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/ResponseCookiesSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/ResponseCookiesSnippetFailureTests.java
new file mode 100644
index 00000000..93dfe5be
--- /dev/null
+++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/ResponseCookiesSnippetFailureTests.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2014-2022 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.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;
+import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName;
+
+/**
+ * Tests for failures when rendering {@link ResponseCookiesSnippet} due to missing or
+ * undocumented cookies.
+ *
+ * @author Clyde Stubbs
+ * @author Andy Wilkinson
+ */
+public class ResponseCookiesSnippetFailureTests {
+
+ @Rule
+ public OperationBuilder operationBuilder = new OperationBuilder(TemplateFormats.asciidoctor());
+
+ @Test
+ public void missingResponseCookie() {
+ assertThatExceptionOfType(SnippetException.class)
+ .isThrownBy(() -> new ResponseCookiesSnippet(
+ Collections.singletonList(cookieWithName("JSESSIONID").description("one")))
+ .document(this.operationBuilder.response().build()))
+ .withMessage("Cookies with the following names were not found in the response: [JSESSIONID]");
+ }
+
+ @Test
+ public void undocumentedResponseCookie() {
+ assertThatExceptionOfType(SnippetException.class)
+ .isThrownBy(() -> new ResponseCookiesSnippet(Collections.emptyList())
+ .document(this.operationBuilder.response().cookie("JSESSIONID", "1234abcd5678efgh").build()))
+ .withMessageEndingWith("Cookies with the following names were not documented: [JSESSIONID]");
+ }
+
+}
diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/ResponseCookiesSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/ResponseCookiesSnippetTests.java
new file mode 100644
index 00000000..39a08328
--- /dev/null
+++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/ResponseCookiesSnippetTests.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2014-2022 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.cookies.CookieDocumentation.cookieWithName;
+import static org.springframework.restdocs.snippet.Attributes.attributes;
+import static org.springframework.restdocs.snippet.Attributes.key;
+
+/**
+ * Tests for {@link ResponseCookiesSnippet}.
+ *
+ * @author Clyde Stubbs
+ * @author Andy Wilkinson
+ */
+public class ResponseCookiesSnippetTests extends AbstractSnippetTests {
+
+ public ResponseCookiesSnippetTests(String name, TemplateFormat templateFormat) {
+ super(name, templateFormat);
+ }
+
+ @Test
+ public void responseWithCookies() throws IOException {
+ new ResponseCookiesSnippet(Arrays.asList(cookieWithName("has_recent_activity").description("one"),
+ cookieWithName("user_session").description("two")))
+ .document(this.operationBuilder.response().cookie("has_recent_activity", "true")
+ .cookie("user_session", "1234abcd5678efgh").build());
+ assertThat(this.generatedSnippets.responseCookies()).is(tableWithHeader("Name", "Description")
+ .row("`has_recent_activity`", "one").row("`user_session`", "two"));
+ }
+
+ @Test
+ public void ignoredResponseCookie() throws IOException {
+ new ResponseCookiesSnippet(Arrays.asList(cookieWithName("has_recent_activity").ignored(),
+ cookieWithName("user_session").description("two")))
+ .document(this.operationBuilder.response().cookie("has_recent_activity", "true")
+ .cookie("user_session", "1234abcd5678efgh").build());
+ assertThat(this.generatedSnippets.responseCookies())
+ .is(tableWithHeader("Name", "Description").row("`user_session`", "two"));
+ }
+
+ @Test
+ public void allUndocumentedResponseCookiesCanBeIgnored() throws IOException {
+ new ResponseCookiesSnippet(Arrays.asList(cookieWithName("has_recent_activity").description("one"),
+ cookieWithName("user_session").description("two")), true)
+ .document(this.operationBuilder.response().cookie("has_recent_activity", "true")
+ .cookie("user_session", "1234abcd5678efgh").cookie("some_cookie", "value").build());
+ assertThat(this.generatedSnippets.responseCookies()).is(tableWithHeader("Name", "Description")
+ .row("`has_recent_activity`", "one").row("`user_session`", "two"));
+ }
+
+ @Test
+ public void missingOptionalResponseCookie() throws IOException {
+ new ResponseCookiesSnippet(Arrays.asList(cookieWithName("has_recent_activity").description("one").optional(),
+ cookieWithName("user_session").description("two")))
+ .document(this.operationBuilder.response().cookie("user_session", "1234abcd5678efgh").build());
+ assertThat(this.generatedSnippets.responseCookies()).is(tableWithHeader("Name", "Description")
+ .row("`has_recent_activity`", "one").row("`user_session`", "two"));
+ }
+
+ @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(cookieWithName("has_recent_activity").description("one")),
+ attributes(key("title").value("Custom title")))
+ .document(this.operationBuilder
+ .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver))
+ .response().cookie("has_recent_activity", "true").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(
+ cookieWithName("has_recent_activity").description("one").attributes(key("foo").value("alpha")),
+ cookieWithName("user_session").description("two").attributes(key("foo").value("bravo")),
+ cookieWithName("color_theme").description("three").attributes(key("foo").value("charlie"))))
+ .document(this.operationBuilder
+ .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver))
+ .response().cookie("has_recent_activity", "true")
+ .cookie("user_session", "1234abcd5678efgh").cookie("color_theme", "high_contrast")
+ .build());
+ assertThat(this.generatedSnippets.responseCookies())
+ .is(tableWithHeader("Name", "Description", "Foo").row("has_recent_activity", "one", "alpha")
+ .row("user_session", "two", "bravo").row("color_theme", "three", "charlie"));
+ }
+
+ @Test
+ public void additionalDescriptors() throws IOException {
+ CookieDocumentation
+ .responseCookies(cookieWithName("has_recent_activity").description("one"),
+ cookieWithName("user_session").description("two"))
+ .and(cookieWithName("color_theme").description("three"))
+ .document(this.operationBuilder.response().cookie("has_recent_activity", "true")
+ .cookie("user_session", "1234abcd5678efgh").cookie("color_theme", "light").build());
+ assertThat(this.generatedSnippets.responseCookies()).is(tableWithHeader("Name", "Description")
+ .row("`has_recent_activity`", "one").row("`user_session`", "two").row("`color_theme`", "three"));
+ }
+
+ @Test
+ public void additionalDescriptorsWithRelaxedResponseCookies() throws IOException {
+ CookieDocumentation.relaxedResponseCookies(cookieWithName("has_recent_activity").description("one"))
+ .and(cookieWithName("color_theme").description("two"))
+ .document(this.operationBuilder.response().cookie("has_recent_activity", "true")
+ .cookie("user_session", "1234abcd5678efgh").cookie("color_theme", "light").build());
+ assertThat(this.generatedSnippets.responseCookies()).is(
+ tableWithHeader("Name", "Description").row("`has_recent_activity`", "one").row("`color_theme`", "two"));
+ }
+
+ @Test
+ public void tableCellContentIsEscapedWhenNecessary() throws IOException {
+ new ResponseCookiesSnippet(Collections.singletonList(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("|", "\\|");
+ }
+
+}
diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-cookies-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-cookies-with-extra-column.snippet
new file mode 100644
index 00000000..62c9dad6
--- /dev/null
+++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-cookies-with-extra-column.snippet
@@ -0,0 +1,10 @@
+|===
+|Name|Description|Foo
+
+{{#cookies}}
+|{{name}}
+|{{description}}
+|{{foo}}
+
+{{/cookies}}
+|===
\ No newline at end of file
diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-cookies-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-cookies-with-title.snippet
new file mode 100644
index 00000000..5e4a4af4
--- /dev/null
+++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-cookies-with-title.snippet
@@ -0,0 +1,10 @@
+.{{title}}
+|===
+|Name|Description
+
+{{#cookies}}
+|{{name}}
+|{{description}}
+
+{{/cookies}}
+|===
\ No newline at end of file
diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-cookies-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-cookies-with-extra-column.snippet
new file mode 100644
index 00000000..62c9dad6
--- /dev/null
+++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-cookies-with-extra-column.snippet
@@ -0,0 +1,10 @@
+|===
+|Name|Description|Foo
+
+{{#cookies}}
+|{{name}}
+|{{description}}
+|{{foo}}
+
+{{/cookies}}
+|===
\ No newline at end of file
diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-cookies-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-cookies-with-title.snippet
new file mode 100644
index 00000000..5e4a4af4
--- /dev/null
+++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-cookies-with-title.snippet
@@ -0,0 +1,10 @@
+.{{title}}
+|===
+|Name|Description
+
+{{#cookies}}
+|{{name}}
+|{{description}}
+
+{{/cookies}}
+|===
\ No newline at end of file
diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-cookies-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-cookies-with-extra-column.snippet
new file mode 100644
index 00000000..8c741628
--- /dev/null
+++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-cookies-with-extra-column.snippet
@@ -0,0 +1,5 @@
+Name | Description | Foo
+---- | ----------- | ---
+{{#cookies}}
+{{name}} | {{description}} | {{foo}}
+{{/cookies}}
\ No newline at end of file
diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-cookies-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-cookies-with-title.snippet
new file mode 100644
index 00000000..e5e2c8bd
--- /dev/null
+++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-cookies-with-title.snippet
@@ -0,0 +1,6 @@
+{{title}}
+Name | Description
+---- | -----------
+{{#cookies}}
+{{name}} | {{description}}
+{{/cookies}}
\ No newline at end of file
diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-cookies-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-cookies-with-extra-column.snippet
new file mode 100644
index 00000000..8c741628
--- /dev/null
+++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-cookies-with-extra-column.snippet
@@ -0,0 +1,5 @@
+Name | Description | Foo
+---- | ----------- | ---
+{{#cookies}}
+{{name}} | {{description}} | {{foo}}
+{{/cookies}}
\ No newline at end of file
diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-cookies-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-cookies-with-title.snippet
new file mode 100644
index 00000000..e5e2c8bd
--- /dev/null
+++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-cookies-with-title.snippet
@@ -0,0 +1,6 @@
+{{title}}
+Name | Description
+---- | -----------
+{{#cookies}}
+{{name}} | {{description}}
+{{/cookies}}
\ No newline at end of file
diff --git a/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/GeneratedSnippets.java b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/GeneratedSnippets.java
index 8598f2b7..e619e02b 100644
--- a/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/GeneratedSnippets.java
+++ b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/GeneratedSnippets.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2014-2021 the original author or authors.
+ * Copyright 2014-2022 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.
@@ -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");
}
diff --git a/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/OperationBuilder.java b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/OperationBuilder.java
index 9308255b..a9299048 100644
--- a/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/OperationBuilder.java
+++ b/spring-restdocs-core/src/testFixtures/java/org/springframework/restdocs/testfixtures/OperationBuilder.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2014-2021 the original author or authors.
+ * Copyright 2014-2022 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.
@@ -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 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;
diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java
index 2ab8fcd8..053df890 100644
--- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java
+++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2014-2019 the original author or authors.
+ * Copyright 2014-2022 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.
@@ -51,6 +51,7 @@ import org.springframework.web.multipart.MultipartFile;
* {@link MockHttpServletRequest}.
*
* @author Andy Wilkinson
+ * @author Clyde Stubbs
*/
class MockMvcRequestConverter implements RequestConverter {
diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverter.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverter.java
index db0adc71..56d44f7c 100644
--- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverter.java
+++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverter.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2014-2019 the original author or authors.
+ * Copyright 2014-2022 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.
@@ -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,27 @@ import org.springframework.util.StringUtils;
* {@link MockHttpServletResponse}.
*
* @author Andy Wilkinson
+ * @author Clyde Stubbs
*/
class MockMvcResponseConverter implements ResponseConverter {
@Override
public OperationResponse convert(MockHttpServletResponse mockResponse) {
- return new OperationResponseFactory().create(mockResponse.getStatus(), extractHeaders(mockResponse),
- mockResponse.getContentAsByteArray());
+ HttpHeaders headers = extractHeaders(mockResponse);
+ Collection cookies = extractCookies(mockResponse);
+ return new OperationResponseFactory().create(mockResponse.getStatus(), headers,
+ mockResponse.getContentAsByteArray(), cookies);
+ }
+
+ private Collection extractCookies(MockHttpServletResponse mockResponse) {
+ if (mockResponse.getCookies() == null || mockResponse.getCookies().length == 0) {
+ return Collections.emptyList();
+ }
+ List cookies = new ArrayList<>();
+ for (Cookie cookie : mockResponse.getCookies()) {
+ cookies.add(new ResponseCookie(cookie.getName(), cookie.getValue()));
+ }
+ return cookies;
}
private HttpHeaders extractHeaders(MockHttpServletResponse response) {
diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverterTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverterTests.java
index c1a28b62..40338091 100644
--- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverterTests.java
+++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverterTests.java
@@ -26,6 +26,7 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.restdocs.operation.OperationResponse;
+import org.springframework.restdocs.operation.ResponseCookie;
import static org.assertj.core.api.Assertions.assertThat;
@@ -58,6 +59,9 @@ public class MockMvcResponseConverterTests {
assertThat(operationResponse.getHeaders()).hasSize(1);
assertThat(operationResponse.getHeaders()).containsEntry(HttpHeaders.SET_COOKIE,
Collections.singletonList("name=value; Domain=localhost; HttpOnly"));
+ assertThat(operationResponse.getCookies()).hasSize(1);
+ assertThat(operationResponse.getCookies()).first().extracting(ResponseCookie::getName).isEqualTo("name");
+ assertThat(operationResponse.getCookies()).first().extracting(ResponseCookie::getValue).isEqualTo("value");
}
@Test
diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java
index 9d50090a..f4e00b2e 100644
--- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java
+++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java
@@ -48,6 +48,7 @@ import org.springframework.util.StreamUtils;
* {@link FilterableRequestSpecification}.
*
* @author Andy Wilkinson
+ * @author Clyde Stubbs
*/
class RestAssuredRequestConverter implements RequestConverter {
diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java
index a8e44f99..09b95978 100644
--- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java
+++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java
@@ -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,34 @@ 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 {
@Override
public OperationResponse convert(Response response) {
+ HttpHeaders headers = extractHeaders(response);
+ Collection cookies = extractCookies(response, headers);
return new OperationResponseFactory().create(response.getStatusCode(), extractHeaders(response),
- extractContent(response));
+ extractContent(response), cookies);
+ }
+
+ private Collection extractCookies(Response response, HttpHeaders headers) {
+ if (response.getCookies() == null || response.getCookies().size() == 0) {
+ return Collections.emptyList();
+ }
+ List cookies = new ArrayList<>();
+ for (Map.Entry cookie : response.getCookies().entrySet()) {
+ cookies.add(new ResponseCookie(cookie.getKey(), cookie.getValue()));
+ }
+ return cookies;
}
private HttpHeaders extractHeaders(Response response) {
diff --git a/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientResponseConverter.java b/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientResponseConverter.java
index df0a6084..fc7d4147 100644
--- a/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientResponseConverter.java
+++ b/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientResponseConverter.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2014-2019 the original author or authors.
+ * Copyright 2014-2022 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.
@@ -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;
@@ -31,13 +33,15 @@ import org.springframework.util.StringUtils;
* {@link ExchangeResult}.
*
* @author Andy Wilkinson
+ * @author Clyde Stubbs
*/
class WebTestClientResponseConverter implements ResponseConverter {
@Override
public OperationResponse convert(ExchangeResult result) {
+ Collection cookies = extractCookies(result);
return new OperationResponseFactory().create(result.getStatus().value(), extractHeaders(result),
- result.getResponseBodyContent());
+ result.getResponseBodyContent(), cookies);
}
private HttpHeaders extractHeaders(ExchangeResult result) {
@@ -50,7 +54,7 @@ class WebTestClientResponseConverter implements ResponseConverter extractCookies(ExchangeResult result) {
+ return result.getResponseCookies().values().stream().flatMap(List::stream).map(this::createResponseCookie)
+ .collect(Collectors.toSet());
+ }
+
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);
diff --git a/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientResponseConverterTests.java b/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientResponseConverterTests.java
index ce3dfdf6..f420bcbf 100644
--- a/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientResponseConverterTests.java
+++ b/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientResponseConverterTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2014-2019 the original author or authors.
+ * Copyright 2014-2022 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.
@@ -23,8 +23,8 @@ import org.junit.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
-import org.springframework.http.ResponseCookie;
import org.springframework.restdocs.operation.OperationResponse;
+import org.springframework.restdocs.operation.ResponseCookie;
import org.springframework.test.web.reactive.server.ExchangeResult;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.server.RouterFunctions;
@@ -62,7 +62,8 @@ public class WebTestClientResponseConverterTests {
ExchangeResult result = WebTestClient
.bindToRouterFunction(RouterFunctions.route(GET("/foo"),
(req) -> ServerResponse.ok()
- .cookie(ResponseCookie.from("name", "value").domain("localhost").httpOnly(true).build())
+ .cookie(org.springframework.http.ResponseCookie.from("name", "value")
+ .domain("localhost").httpOnly(true).build())
.build()))
.configureClient().baseUrl("http://localhost").build().get().uri("/foo").exchange().expectBody()
.returnResult();
@@ -70,6 +71,9 @@ public class WebTestClientResponseConverterTests {
assertThat(response.getHeaders()).hasSize(1);
assertThat(response.getHeaders()).containsEntry(HttpHeaders.SET_COOKIE,
Collections.singletonList("name=value; Domain=localhost; HttpOnly"));
+ assertThat(response.getCookies()).hasSize(1);
+ assertThat(response.getCookies()).first().extracting(ResponseCookie::getName).isEqualTo("name");
+ assertThat(response.getCookies()).first().extracting(ResponseCookie::getValue).isEqualTo("value");
}
}