diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 3de6d5ac..612ebd17 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -5,7 +5,8 @@ - + diff --git a/docs/build.gradle b/docs/build.gradle index ce49eb9a..c24ded86 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -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") diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 5fee0d69..0f267476 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -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 diff --git a/docs/src/test/java/com/example/mockmvc/HttpCookies.java b/docs/src/test/java/com/example/mockmvc/HttpCookies.java new file mode 100644 index 00000000..aab39db7 --- /dev/null +++ b/docs/src/test/java/com/example/mockmvc/HttpCookies.java @@ -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[] + } + +} diff --git a/docs/src/test/java/com/example/restassured/HttpCookies.java b/docs/src/test/java/com/example/restassured/HttpCookies.java new file mode 100644 index 00000000..f6b76293 --- /dev/null +++ b/docs/src/test/java/com/example/restassured/HttpCookies.java @@ -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[] + } + +} diff --git a/docs/src/test/java/com/example/webtestclient/HttpCookies.java b/docs/src/test/java/com/example/webtestclient/HttpCookies.java new file mode 100644 index 00000000..8979520d --- /dev/null +++ b/docs/src/test/java/com/example/webtestclient/HttpCookies.java @@ -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[] + } +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/AbstractCookiesSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/AbstractCookiesSnippet.java new file mode 100644 index 00000000..865ea397 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/AbstractCookiesSnippet.java @@ -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 cookieDescriptors; + + protected final boolean ignoreUndocumentedCookies; + + private String type; + + /** + * Creates a new {@code AbstractCookiesSnippet} that will produce a snippet named + * {@code -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 descriptors, Map 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 createModel(Operation operation) { + validateCookieDocumentation(operation); + + Map model = new HashMap<>(); + List> cookies = new ArrayList<>(); + model.put("cookies", cookies); + for (CookieDescriptor descriptor : this.cookieDescriptors) { + cookies.add(createModelForDescriptor(descriptor)); + } + return model; + } + + private void validateCookieDocumentation(Operation operation) { + List missingCookies = findMissingCookies(operation); + if (!missingCookies.isEmpty()) { + List 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 findMissingCookies(Operation operation) { + List missingCookies = new ArrayList<>(); + Set 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 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 getCookieDescriptors() { + return this.cookieDescriptors; + } + + /** + * Returns a model for the given {@code descriptor}. + * @param descriptor the descriptor + * @return the model + */ + protected Map createModelForDescriptor(CookieDescriptor descriptor) { + Map model = new HashMap<>(); + model.put("name", descriptor.getName()); + model.put("description", descriptor.getDescription()); + model.put("optional", descriptor.isOptional()); + model.putAll(descriptor.getAttributes()); + return model; + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/CookieDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/CookieDescriptor.java new file mode 100644 index 00000000..51a4787a --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/CookieDescriptor.java @@ -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 { + + 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..0a88d13a --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/CookieDocumentation.java @@ -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}. + *

+ * 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}. + *

+ * 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 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}. + *

+ * 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 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 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 attributes, + List 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}. + *

+ * 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 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 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}. + *

+ * 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 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 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 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}. + *

+ * 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 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 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 attributes, + List 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}. + *

+ * 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 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..1a8d7ccc --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/RequestCookiesSnippet.java @@ -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 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 descriptors, Map 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 descriptors, Map attributes) { + super("request", descriptors, attributes, false); + } + + @Override + protected Set extractActualCookies(Operation operation) { + HashSet 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 additionalDescriptors) { + List combinedDescriptors = new ArrayList<>(this.getCookieDescriptors()); + combinedDescriptors.addAll(additionalDescriptors); + return new RequestCookiesSnippet(combinedDescriptors, getAttributes(), false); + } + +} 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..99076115 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/ResponseCookiesSnippet.java @@ -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 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 descriptors, Map 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 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()); + } + + /** + * 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()); + combinedDescriptors.addAll(additionalDescriptors); + return new ResponseCookiesSnippet(combinedDescriptors, getAttributes(), this.ignoreUndocumentedCookies); + } + +} 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..d59b9452 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/package-info.java @@ -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; 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..4778f84f 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 @@ -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 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..69ed9271 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseCookie.java @@ -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; + } + +} 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..73676e6a 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 @@ -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..c10ab68d --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/RequestCookiesSnippetFailureTests.java @@ -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]"); + } + +} 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..bb2685f3 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/RequestCookiesSnippetTests.java @@ -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("|", "\\|"); + } + +} 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..100edc90 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/ResponseCookiesSnippetFailureTests.java @@ -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]"); + } + +} 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..ec05d3d9 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/ResponseCookiesSnippetTests.java @@ -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("|", "\\|"); + } + +} 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..0071ce91 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 @@ -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..de29ad39 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 @@ -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/MockMvcResponseConverter.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverter.java index db0adc71..2ffd33de 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 @@ -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 { @Override public OperationResponse convert(MockHttpServletResponse mockResponse) { - return new OperationResponseFactory().create(mockResponse.getStatus(), extractHeaders(mockResponse), - mockResponse.getContentAsByteArray()); + HttpHeaders headers = extractHeaders(mockResponse); + Collection cookies = extractCookies(mockResponse, headers); + return new OperationResponseFactory().create(mockResponse.getStatus(), headers, + mockResponse.getContentAsByteArray(), cookies); + } + + private Collection extractCookies(MockHttpServletResponse mockRequest, HttpHeaders headers) { + if (mockRequest.getCookies() == null || mockRequest.getCookies().length == 0) { + return Collections.emptyList(); + } + List 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) { 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..a1c1399b 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,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 { @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 servletCookie : response.getCookies().entrySet()) { + cookies.add(new ResponseCookie(servletCookie.getKey(), servletCookie.getValue())); + } + headers.remove(HttpHeaders.COOKIE); + 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..af5aeefc 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 @@ -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 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