diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 71c12f5e4..570473714 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -1111,6 +1111,61 @@ When documenting HTTP Headers, the test fails if a documented header is not foun 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 000000000..30d0ec7ab --- /dev/null +++ b/docs/src/test/java/com/example/mockmvc/HttpCookies.java @@ -0,0 +1,50 @@ +/* + * 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 + * + * http://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 javax.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 000000000..4157faa9c --- /dev/null +++ b/docs/src/test/java/com/example/restassured/HttpCookies.java @@ -0,0 +1,49 @@ +/* + * 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 + * + * http://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.restassured3.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 000000000..0c532f4a4 --- /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 + * + * http://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 000000000..70754cf19 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/AbstractCookiesSnippet.java @@ -0,0 +1,148 @@ +/* + * 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 + * + * http://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 000000000..35ce134d1 --- /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 + * + * http://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 000000000..81b848e36 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/CookieDocumentation.java @@ -0,0 +1,236 @@ +/* + * 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 + * + * http://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 000000000..9bb0db712 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/RequestCookiesSnippet.java @@ -0,0 +1,111 @@ +/* + * 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 + * + * http://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 000000000..3336ba067 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cookies/ResponseCookiesSnippet.java @@ -0,0 +1,110 @@ +/* + * 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 + * + * http://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(), + 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 000000000..4ce74b8e6 --- /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 + * + * http://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 268595d0a..08b6d10d6 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 @@ * 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() */ @@ -56,4 +59,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 6d075df17..278620abe 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,6 +16,9 @@ package org.springframework.restdocs.operation; +import java.util.Collection; +import java.util.Collections; + import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -23,6 +26,7 @@ * A factory for creating {@link OperationResponse OperationResponses}. * * @author Andy Wilkinson + * @author Clyde Stubbs */ public class OperationResponseFactory { @@ -33,12 +37,29 @@ public class OperationResponseFactory { * @param status the status 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 2.1 + */ + public OperationResponse create(HttpStatus status, HttpHeaders headers, + byte[] content, Collection cookies) { + return new StandardOperationResponse(status, augmentHeaders(headers, content), + content, cookies); + } + + /** + * 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(HttpStatus status, HttpHeaders headers, byte[] content) { return new StandardOperationResponse(status, augmentHeaders(headers, content), - content); + content, Collections.emptyList()); } /** @@ -52,7 +73,8 @@ public OperationResponse create(HttpStatus status, HttpHeaders headers, */ public OperationResponse createFrom(OperationResponse original, byte[] newContent) { return new StandardOperationResponse(original.getStatus(), - getUpdatedHeaders(original.getHeaders(), newContent), newContent); + getUpdatedHeaders(original.getHeaders(), newContent), newContent, + original.getCookies()); } /** @@ -65,7 +87,7 @@ public OperationResponse createFrom(OperationResponse original, byte[] newConten public OperationResponse createFrom(OperationResponse original, HttpHeaders newHeaders) { return new StandardOperationResponse(original.getStatus(), newHeaders, - original.getContent()); + 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 000000000..2ab2bf866 --- /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 + * + * http://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 00b18d987..3b93743c0 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,22 +25,28 @@ * Standard implementation of {@link OperationResponse}. * * @author Andy Wilkinson + * @author Clyde Stubbs */ class StandardOperationResponse extends AbstractOperationMessage implements OperationResponse { private final HttpStatus 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(HttpStatus status, HttpHeaders headers, byte[] content) { + StandardOperationResponse(HttpStatus status, HttpHeaders headers, byte[] content, + Collection cookies) { super(content, headers); this.status = status; + this.cookies = cookies; } @Override @@ -46,4 +54,9 @@ public HttpStatus getStatus() { 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 aefddf134..922ce772d 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,9 +142,9 @@ public OperationRequest preprocess(OperationRequest request) { @Override public OperationResponse preprocess(OperationResponse response) { - return this.contentModifyingDelegate - .preprocess(new OperationResponseFactory().create(response.getStatus(), - modify(response.getHeaders()), response.getContent())); + return this.contentModifyingDelegate.preprocess(new OperationResponseFactory() + .create(response.getStatus(), 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 000000000..0c5315051 --- /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 000000000..0c5315051 --- /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 000000000..dbc046b82 --- /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 000000000..dbc046b82 --- /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 000000000..120517b63 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/RequestCookiesSnippetFailureTests.java @@ -0,0 +1,73 @@ +/* + * 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 + * + * http://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.junit.rules.ExpectedException; + +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.templates.TemplateFormats; +import org.springframework.restdocs.test.OperationBuilder; + +import static org.hamcrest.CoreMatchers.endsWith; +import static org.hamcrest.CoreMatchers.equalTo; + +/** + * 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()); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void missingRequestCookie() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(equalTo("Cookies with the following names were not found" + + " in the request: [Accept]")); + new RequestCookiesSnippet(Collections.singletonList( + CookieDocumentation.cookieWithName("Accept").description("one"))) + .document(this.operationBuilder.request("http://localhost") + .build()); + } + + @Test + public void undocumentedRequestCookieAndMissingRequestHeader() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(endsWith("Cookies with the following names were not found" + + " in the request: [Accept]")); + new RequestCookiesSnippet(Collections.singletonList( + CookieDocumentation.cookieWithName("Accept").description("one"))) + .document(this.operationBuilder.request("http://localhost") + .cookie("X-Test", "test").build()); + } + +} 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 000000000..ccf2b5c85 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/RequestCookiesSnippetTests.java @@ -0,0 +1,169 @@ +/* + * 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 + * + * http://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 000000000..0c7d6e755 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/ResponseCookiesSnippetFailureTests.java @@ -0,0 +1,72 @@ +/* + * 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 + * + * http://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.junit.rules.ExpectedException; + +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.templates.TemplateFormats; +import org.springframework.restdocs.test.OperationBuilder; + +import static org.hamcrest.CoreMatchers.endsWith; +import static org.hamcrest.CoreMatchers.equalTo; + +/** + * 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()); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void missingResponseCookie() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(equalTo("Cookies with the following names were not found" + + " in the response: [Content-Type]")); + new ResponseCookiesSnippet(Collections.singletonList( + CookieDocumentation.cookieWithName("Content-Type").description("one"))) + .document(this.operationBuilder.response().build()); + } + + @Test + public void undocumentedResponseCookieAndMissingResponseHeader() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(endsWith("Cookies with the following names were not found" + + " in the response: [Content-Type]")); + new ResponseCookiesSnippet(Collections.singletonList( + CookieDocumentation.cookieWithName("Content-Type").description("one"))) + .document(this.operationBuilder.response() + .cookie("X-Test", "test").build()); + } + +} 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 000000000..49b7b3f38 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cookies/ResponseCookiesSnippetTests.java @@ -0,0 +1,165 @@ +/* + * 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 + * + * http://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/java/org/springframework/restdocs/test/GeneratedSnippets.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/GeneratedSnippets.java index 2c141f560..d96992062 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/GeneratedSnippets.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/GeneratedSnippets.java @@ -70,6 +70,14 @@ public String responseHeaders() { 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/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index 5f280de06..976d9e7d6 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -22,8 +22,10 @@ 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.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 @@ private void prepare(String operationName, File outputDirectory) { this.name = operationName; this.outputDirectory = outputDirectory; this.requestBuilder = null; - this.requestBuilder = null; this.attributes.clear(); } @@ -273,11 +275,13 @@ public final class OperationResponseBuilder { 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); + this.content, this.cookies); } public OperationResponseBuilder status(int status) { @@ -290,6 +294,11 @@ public OperationResponseBuilder header(String name, String value) { 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-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 000000000..62c9dad68 --- /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 000000000..5e4a4af43 --- /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 000000000..62c9dad68 --- /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 000000000..5e4a4af43 --- /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 000000000..8c7416287 --- /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 000000000..e5e2c8bda --- /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 000000000..8c7416287 --- /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 000000000..e5e2c8bda --- /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-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverter.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverter.java index dc85f4900..7ccdb1ef7 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 javax.servlet.http.Cookie; import org.springframework.http.HttpHeaders; @@ -24,6 +29,7 @@ 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; /** @@ -31,14 +37,31 @@ * {@link MockHttpServletResponse}. * * @author Andy Wilkinson + * @author Clyde Stubbs */ class MockMvcResponseConverter implements ResponseConverter { @Override public OperationResponse convert(MockHttpServletResponse mockResponse) { + HttpHeaders headers = extractHeaders(mockResponse); + Collection cookies = extractCookies(mockResponse, headers); return new OperationResponseFactory().create( - HttpStatus.valueOf(mockResponse.getStatus()), - extractHeaders(mockResponse), mockResponse.getContentAsByteArray()); + HttpStatus.valueOf(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 (javax.servlet.http.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/restassured3/RestAssuredResponseConverter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/RestAssuredResponseConverter.java index e93263d93..679363fad 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/RestAssuredResponseConverter.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured3/RestAssuredResponseConverter.java @@ -16,6 +16,12 @@ package org.springframework.restdocs.restassured3; +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; @@ -24,20 +30,38 @@ 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( HttpStatus.valueOf(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 71669c37f..2b3519900 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,14 +17,15 @@ 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; /** * A {@link ResponseConverter} for creating an {@link OperationResponse} derived from an @@ -36,54 +37,32 @@ class WebTestClientResponseConverter implements ResponseConverter headers.add(HttpHeaders.SET_COOKIE, - generateSetCookieHeader(cookie))); - return headers; + Collection cookies = extractCookies(result, headers); + return new OperationResponseFactory().create(result.getStatus(), headers, + result.getResponseBodyContent(), cookies); } - private String generateSetCookieHeader(ResponseCookie cookie) { - StringBuilder header = new StringBuilder(); - header.append(cookie.getName()); - header.append('='); - appendIfAvailable(header, cookie.getValue()); - long maxAge = cookie.getMaxAge().getSeconds(); - if (maxAge > -1) { - header.append("; Max-Age="); - header.append(maxAge); - } - appendIfAvailable(header, "; Domain=", cookie.getDomain()); - appendIfAvailable(header, "; Path=", cookie.getPath()); - if (cookie.isSecure()) { - header.append("; Secure"); + private Collection extractCookies(ExchangeResult result, + HttpHeaders headers) { + List cookieHeaders = headers.get(HttpHeaders.COOKIE); + if (cookieHeaders == null) { + return result.getResponseCookies().values().stream().flatMap(List::stream) + .map(this::createResponseCookie).collect(Collectors.toSet()); } - if (cookie.isHttpOnly()) { - header.append("; HttpOnly"); - } - return header.toString(); + headers.remove(HttpHeaders.COOKIE); + return cookieHeaders.stream().map(this::createResponseCookie) + .collect(Collectors.toList()); } - private void appendIfAvailable(StringBuilder header, String value) { - if (StringUtils.hasText(value)) { - header.append(value); - } + private ResponseCookie createResponseCookie( + org.springframework.http.ResponseCookie original) { + return new ResponseCookie(original.getName(), original.getValue()); } - private void appendIfAvailable(StringBuilder header, String name, String value) { - if (StringUtils.hasText(value)) { - header.append(name); - header.append(value); - } + private ResponseCookie createResponseCookie(String header) { + String[] components = header.split("="); + return new ResponseCookie(components[0], components[1]); } }