From 0f0769245d05e0a1f9bc77f920a7b4a433ace9da Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Sun, 12 Nov 2017 16:48:19 +0000 Subject: [PATCH] Add cookie handling to WebTestClient request and response converters Closes gh-447 --- build.gradle | 3 +- .../WebTestClientRequestConverter.java | 22 ++++++-- .../WebTestClientResponseConverter.java | 56 ++++++++++++++++++- .../WebTestClientRequestConverterTests.java | 27 +++++++++ .../WebTestClientResponseConverterTests.java | 22 ++++++++ ...ientRestDocumentationIntegrationTests.java | 51 ++++++++++++++++- 6 files changed, 172 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index 975d50be..8b4c4283 100644 --- a/build.gradle +++ b/build.gradle @@ -16,6 +16,7 @@ allprojects { group = 'org.springframework.restdocs' repositories { mavenCentral() + maven { url 'https://repo.spring.io/libs-snapshot' } } } @@ -34,7 +35,7 @@ sonarqube { } ext { - springVersion = '5.0.1.RELEASE' + springVersion = '5.0.2.BUILD-SNAPSHOT' javadocLinks = [ 'http://docs.oracle.com/javase/8/docs/api/', "http://docs.spring.io/spring-framework/docs/$springVersion/javadoc-api/", diff --git a/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverter.java b/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverter.java index e85af924..d603773d 100644 --- a/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverter.java +++ b/spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverter.java @@ -69,10 +69,10 @@ class WebTestClientRequestConverter implements RequestConverter @Override public OperationRequest convert(ExchangeResult result) { + HttpHeaders headers = extractRequestHeaders(result); return new OperationRequestFactory().create(result.getUrl(), result.getMethod(), - result.getRequestBodyContent(), extractRequestHeaders(result), - extractParameters(result), extractRequestParts(result), - extractCookies(result)); + result.getRequestBodyContent(), headers, extractParameters(result), + extractRequestParts(result), extractCookies(headers)); } private HttpHeaders extractRequestHeaders(ExchangeResult result) { @@ -123,9 +123,19 @@ class WebTestClientRequestConverter implements RequestConverter return contentStream; } - private Collection extractCookies(ExchangeResult result) { - // Cookies are not available. See https://jira.spring.io/browse/SPR-16124. - return Collections.emptyList(); + private Collection extractCookies(HttpHeaders headers) { + List cookieHeaders = headers.get(HttpHeaders.COOKIE); + if (cookieHeaders == null) { + return Collections.emptyList(); + } + headers.remove(HttpHeaders.COOKIE); + return cookieHeaders.stream().map(this::createRequestCookie) + .collect(Collectors.toList()); + } + + private RequestCookie createRequestCookie(String header) { + String[] components = header.split("="); + return new RequestCookie(components[0], components[1]); } private final class ExchangeResultReactiveHttpInputMessage 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 79fae8ba..9251bd19 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 @@ -16,10 +16,16 @@ package org.springframework.restdocs.webtestclient; +import java.util.Collection; + +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.test.web.reactive.server.ExchangeResult; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; /** * A {@link ResponseConverter} for creating an {@link OperationResponse} derived from an @@ -31,8 +37,56 @@ class WebTestClientResponseConverter implements ResponseConverter responseCookies = result + .getResponseCookies(); return new OperationResponseFactory().create(result.getStatus(), - result.getResponseHeaders(), result.getResponseBodyContent()); + extractHeaders(result), result.getResponseBodyContent()); + } + + private HttpHeaders extractHeaders(ExchangeResult result) { + HttpHeaders headers = result.getResponseHeaders(); + if (result.getResponseCookies().isEmpty() + || headers.containsKey(HttpHeaders.SET_COOKIE)) { + return headers; + } + result.getResponseCookies().values().stream().flatMap(Collection::stream) + .forEach((cookie) -> headers.add(HttpHeaders.SET_COOKIE, + generateSetCookieHeader(cookie))); + return headers; + } + + 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"); + } + if (cookie.isHttpOnly()) { + header.append("; HttpOnly"); + } + return header.toString(); + } + + private void appendIfAvailable(StringBuilder header, String value) { + if (StringUtils.hasText(value)) { + header.append(value); + } + } + + private void appendIfAvailable(StringBuilder header, String name, String value) { + if (StringUtils.hasText(value)) { + header.append(name); + header.append(value); + } } } diff --git a/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverterTests.java b/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverterTests.java index 6ec4c22f..ee3356f2 100644 --- a/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverterTests.java +++ b/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverterTests.java @@ -18,15 +18,18 @@ package org.springframework.restdocs.webtestclient; import java.net.URI; import java.util.Arrays; +import java.util.Iterator; import org.junit.Test; import org.springframework.core.io.ByteArrayResource; import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.RequestCookie; import org.springframework.test.web.reactive.server.ExchangeResult; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.util.LinkedMultiValueMap; @@ -37,6 +40,7 @@ import org.springframework.web.reactive.function.server.RouterFunctions; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.Matchers.hasEntry; import static org.junit.Assert.assertThat; @@ -205,4 +209,27 @@ public class WebTestClientRequestConverterTests { assertThat(part.getContent(), is(equalTo(new byte[] { 1, 2, 3, 4 }))); } + @Test + public void requestWithCookies() throws Exception { + ExchangeResult result = WebTestClient + .bindToRouterFunction(RouterFunctions.route(GET("/foo"), (req) -> null)) + .configureClient().baseUrl("http://localhost").build().get().uri("/foo") + .cookie("cookieName1", "cookieVal1").cookie("cookieName2", "cookieVal2") + .exchange().expectBody().returnResult(); + assertThat(result.getRequestHeaders().get(HttpHeaders.COOKIE), + is(notNullValue())); + OperationRequest request = this.converter.convert(result); + assertThat(request.getUri(), is(URI.create("http://localhost/foo"))); + assertThat(request.getMethod(), is(HttpMethod.GET)); + assertThat(request.getCookies().size(), is(equalTo(2))); + assertThat(request.getHeaders().get(HttpHeaders.COOKIE), is(nullValue())); + Iterator cookieIterator = request.getCookies().iterator(); + RequestCookie cookie1 = cookieIterator.next(); + assertThat(cookie1.getName(), is(equalTo("cookieName1"))); + assertThat(cookie1.getValue(), is(equalTo("cookieVal1"))); + RequestCookie cookie2 = cookieIterator.next(); + assertThat(cookie2.getName(), is(equalTo("cookieName2"))); + assertThat(cookie2.getValue(), is(equalTo("cookieVal2"))); + } + } diff --git a/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientResponseConverterTests.java b/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientResponseConverterTests.java index 4c273926..e64bcfb4 100644 --- a/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientResponseConverterTests.java +++ b/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientResponseConverterTests.java @@ -16,10 +16,14 @@ package org.springframework.restdocs.webtestclient; +import java.util.Collections; + import org.junit.Test; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.ResponseCookie; import org.springframework.restdocs.operation.OperationResponse; import org.springframework.test.web.reactive.server.ExchangeResult; import org.springframework.test.web.reactive.server.WebTestClient; @@ -29,6 +33,7 @@ import org.springframework.web.reactive.function.server.ServerResponse; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; import static org.springframework.web.reactive.function.server.RequestPredicates.GET; /** @@ -55,4 +60,21 @@ public class WebTestClientResponseConverterTests { assertThat(response.getHeaders().getContentLength(), is(13L)); } + @Test + public void responseWithCookie() { + ExchangeResult result = WebTestClient + .bindToRouterFunction(RouterFunctions.route(GET("/foo"), + (req) -> ServerResponse.ok() + .cookie(ResponseCookie.from("name", "value") + .domain("localhost").httpOnly(true).build()) + .build())) + .configureClient().baseUrl("http://localhost").build().get().uri("/foo") + .exchange().expectBody().returnResult(); + OperationResponse response = this.converter.convert(result); + assertThat(response.getHeaders().size(), is(1)); + assertTrue(response.getHeaders().containsKey(HttpHeaders.SET_COOKIE)); + assertThat(response.getHeaders().get(HttpHeaders.SET_COOKIE), equalTo( + Collections.singletonList("name=value; Domain=localhost; HttpOnly"))); + } + } diff --git a/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientRestDocumentationIntegrationTests.java b/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientRestDocumentationIntegrationTests.java index 5d393344..dbbc0bcc 100644 --- a/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientRestDocumentationIntegrationTests.java +++ b/spring-restdocs-webtestclient/src/test/java/org/springframework/restdocs/webtestclient/WebTestClientRestDocumentationIntegrationTests.java @@ -28,7 +28,10 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseCookie; import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.test.web.reactive.server.EntityExchangeResult; @@ -52,6 +55,8 @@ import static org.springframework.restdocs.request.RequestDocumentation.pathPara import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; import static org.springframework.restdocs.request.RequestDocumentation.requestParts; import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; +import static org.springframework.restdocs.test.SnippetMatchers.codeBlock; +import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; import static org.springframework.restdocs.test.SnippetMatchers.snippet; import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; import static org.springframework.restdocs.test.SnippetMatchers.tableWithTitleAndHeader; @@ -84,7 +89,11 @@ public class WebTestClientRestDocumentationIntegrationTests { return request.body(BodyExtractors.toMultipartData()).map((parts) -> { return ServerResponse.status(HttpStatus.OK).build().block(); }); - }); + }).andRoute(RequestPredicates.GET("/set-cookie"), + (request) -> ServerResponse.ok() + .cookie(ResponseCookie.from("name", "value") + .domain("localhost").httpOnly(true).build()) + .build()); this.webTestClient = WebTestClient.bindToRouterFunction(route).configureClient() .baseUrl("https://api.example.com") .filter(documentationConfiguration(this.restDocumentation)).build(); @@ -151,6 +160,46 @@ public class WebTestClientRestDocumentationIntegrationTests { "Part b")))); } + @Test + public void responseWithSetCookie() throws Exception { + this.webTestClient.get().uri("/set-cookie").exchange().expectStatus().isOk() + .expectBody().consumeWith(document("set-cookie")); + assertThat(new File("build/generated-snippets/set-cookie/http-response.adoc"), + is(snippet(asciidoctor()) + .withContents(httpResponse(asciidoctor(), HttpStatus.OK).header( + HttpHeaders.SET_COOKIE, + "name=value; Domain=localhost; HttpOnly")))); + } + + @Test + public void curlSnippetWithCookies() throws Exception { + this.webTestClient.get().uri("/").cookie("cookieName", "cookieVal") + .accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk() + .expectBody().consumeWith(document("curl-snippet-with-cookies")); + assertThat( + new File( + "build/generated-snippets/curl-snippet-with-cookies/curl-request.adoc"), + is(snippet(asciidoctor()).withContents(codeBlock(asciidoctor(), "bash") + .content(String.format("$ curl 'https://api.example.com/' -i \\%n" + + " -H 'Accept: application/json' \\%n" + + " --cookie 'cookieName=cookieVal'"))))); + } + + @Test + public void httpieSnippetWithCookies() throws Exception { + this.webTestClient.get().uri("/").cookie("cookieName", "cookieVal") + .accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk() + .expectBody().consumeWith(document("httpie-snippet-with-cookies")); + assertThat( + new File( + "build/generated-snippets/httpie-snippet-with-cookies/httpie-request.adoc"), + is(snippet(asciidoctor()) + .withContents(codeBlock(asciidoctor(), "bash").content( + String.format("$ http GET 'https://api.example.com/' \\%n" + + " 'Accept:application/json' \\%n" + + " 'Cookie:cookieName=cookieVal'"))))); + } + private void assertExpectedSnippetFilesExist(File directory, String... snippets) { Set actual = new HashSet<>(Arrays.asList(directory.listFiles())); Set expected = Stream.of(snippets)