From 128acaff8a0318e67cc6bf1ff77f36e877abbe8a Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 19 Aug 2020 21:11:53 +0100 Subject: [PATCH] WebTestClient cookie assertion support See gh-19647 --- .../reactive/MockClientHttpResponse.java | 2 +- .../web/reactive/server/CookieAssertions.java | 220 ++++++++++++++++++ .../reactive/server/DefaultWebTestClient.java | 5 + .../web/reactive/server/WebTestClient.java | 6 + .../reactive/server/CookieAssertionTests.java | 138 +++++++++++ 5 files changed, 370 insertions(+), 1 deletion(-) create mode 100644 spring-test/src/main/java/org/springframework/test/web/reactive/server/CookieAssertions.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/reactive/server/CookieAssertionTests.java diff --git a/spring-test/src/main/java/org/springframework/mock/http/client/reactive/MockClientHttpResponse.java b/spring-test/src/main/java/org/springframework/mock/http/client/reactive/MockClientHttpResponse.java index 8c584848089..c2e950025f4 100644 --- a/spring-test/src/main/java/org/springframework/mock/http/client/reactive/MockClientHttpResponse.java +++ b/spring-test/src/main/java/org/springframework/mock/http/client/reactive/MockClientHttpResponse.java @@ -80,7 +80,7 @@ public class MockClientHttpResponse implements ClientHttpResponse { public HttpHeaders getHeaders() { if (!getCookies().isEmpty() && this.headers.get(HttpHeaders.SET_COOKIE) == null) { getCookies().values().stream().flatMap(Collection::stream) - .forEach(cookie -> getHeaders().add(HttpHeaders.SET_COOKIE, cookie.toString())); + .forEach(cookie -> this.headers.add(HttpHeaders.SET_COOKIE, cookie.toString())); } return this.headers; } diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/CookieAssertions.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/CookieAssertions.java new file mode 100644 index 00000000000..9d1cad06598 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/CookieAssertions.java @@ -0,0 +1,220 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.test.web.reactive.server; + +import java.time.Duration; +import java.util.function.Consumer; + +import org.hamcrest.Matcher; +import org.hamcrest.MatcherAssert; + +import org.springframework.http.ResponseCookie; +import org.springframework.test.util.AssertionErrors; + +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Assertions on cookies of the response. + * @author Rossen Stoyanchev + */ +public class CookieAssertions { + + private final ExchangeResult exchangeResult; + + private final WebTestClient.ResponseSpec responseSpec; + + + public CookieAssertions(ExchangeResult exchangeResult, WebTestClient.ResponseSpec responseSpec) { + this.exchangeResult = exchangeResult; + this.responseSpec = responseSpec; + } + + + /** + * Expect a header with the given name to match the specified values. + */ + public WebTestClient.ResponseSpec valueEquals(String name, String value) { + this.exchangeResult.assertWithDiagnostics(() -> { + String message = getMessage(name); + AssertionErrors.assertEquals(message, value, getCookie(name).getValue()); + }); + return this.responseSpec; + } + + /** + * Assert the first value of the response cookie with a Hamcrest {@link Matcher}. + */ + public WebTestClient.ResponseSpec value(String name, Matcher matcher) { + String value = getCookie(name).getValue(); + this.exchangeResult.assertWithDiagnostics(() -> { + String message = getMessage(name); + MatcherAssert.assertThat(message, value, matcher); + }); + return this.responseSpec; + } + + /** + * Consume the value of the response cookie. + */ + public WebTestClient.ResponseSpec value(String name, Consumer consumer) { + String value = getCookie(name).getValue(); + this.exchangeResult.assertWithDiagnostics(() -> consumer.accept(value)); + return this.responseSpec; + } + + /** + * Expect that the cookie with the given name is present. + */ + public WebTestClient.ResponseSpec exists(String name) { + getCookie(name); + return this.responseSpec; + } + + /** + * Expect that the cookie with the given name is not present. + */ + public WebTestClient.ResponseSpec doesNotExist(String name) { + ResponseCookie cookie = this.exchangeResult.getResponseCookies().getFirst(name); + if (cookie != null) { + String message = getMessage(name) + " exists with value=[" + cookie.getValue() + "]"; + this.exchangeResult.assertWithDiagnostics(() -> AssertionErrors.fail(message)); + } + return this.responseSpec; + } + + /** + * Assert a cookie's maxAge attribute. + */ + public WebTestClient.ResponseSpec maxAge(String name, Duration expected) { + Duration maxAge = getCookie(name).getMaxAge(); + this.exchangeResult.assertWithDiagnostics(() -> { + String message = getMessage(name) + " maxAge"; + AssertionErrors.assertEquals(message, expected, maxAge); + }); + return this.responseSpec; + } + + /** + * Assert a cookie's maxAge attribute with a Hamcrest {@link Matcher}. + */ + public WebTestClient.ResponseSpec maxAge(String name, Matcher matcher) { + long maxAge = getCookie(name).getMaxAge().getSeconds(); + this.exchangeResult.assertWithDiagnostics(() -> { + String message = getMessage(name) + " maxAge"; + assertThat(message, maxAge, matcher); + }); + return this.responseSpec; + } + + /** + * Assert a cookie's path attribute. + */ + public WebTestClient.ResponseSpec path(String name, String expected) { + String path = getCookie(name).getPath(); + this.exchangeResult.assertWithDiagnostics(() -> { + String message = getMessage(name) + " path"; + AssertionErrors.assertEquals(message, expected, path); + }); + return this.responseSpec; + } + + /** + * Assert a cookie's path attribute with a Hamcrest {@link Matcher}. + */ + public WebTestClient.ResponseSpec path(String name, Matcher matcher) { + String path = getCookie(name).getPath(); + this.exchangeResult.assertWithDiagnostics(() -> { + String message = getMessage(name) + " path"; + assertThat(message, path, matcher); + }); + return this.responseSpec; + } + + /** + * Assert a cookie's domain attribute. + */ + public WebTestClient.ResponseSpec domain(String name, String expected) { + String path = getCookie(name).getDomain(); + this.exchangeResult.assertWithDiagnostics(() -> { + String message = getMessage(name) + " domain"; + AssertionErrors.assertEquals(message, expected, path); + }); + return this.responseSpec; + } + + /** + * Assert a cookie's domain attribute with a Hamcrest {@link Matcher}. + */ + public WebTestClient.ResponseSpec domain(String name, Matcher matcher) { + String domain = getCookie(name).getDomain(); + this.exchangeResult.assertWithDiagnostics(() -> { + String message = getMessage(name) + " domain"; + assertThat(message, domain, matcher); + }); + return this.responseSpec; + } + + /** + * Assert a cookie's secure attribute. + */ + public WebTestClient.ResponseSpec secure(String name, boolean expected) { + boolean isSecure = getCookie(name).isSecure(); + this.exchangeResult.assertWithDiagnostics(() -> { + String message = getMessage(name) + " secure"; + AssertionErrors.assertEquals(message, expected, isSecure); + }); + return this.responseSpec; + } + + /** + * Assert a cookie's httpOnly attribute. + */ + public WebTestClient.ResponseSpec httpOnly(String name, boolean expected) { + boolean isHttpOnly = getCookie(name).isHttpOnly(); + this.exchangeResult.assertWithDiagnostics(() -> { + String message = getMessage(name) + " secure"; + AssertionErrors.assertEquals(message, expected, isHttpOnly); + }); + return this.responseSpec; + } + + /** + * Assert a cookie's sameSite attribute. + */ + public WebTestClient.ResponseSpec sameSite(String name, String expected) { + String sameSite = getCookie(name).getSameSite(); + this.exchangeResult.assertWithDiagnostics(() -> { + String message = getMessage(name) + " secure"; + AssertionErrors.assertEquals(message, expected, sameSite); + }); + return this.responseSpec; + } + + + private ResponseCookie getCookie(String name) { + ResponseCookie cookie = this.exchangeResult.getResponseCookies().getFirst(name); + if (cookie == null) { + String message = "No cookie with name '" + name + "'"; + this.exchangeResult.assertWithDiagnostics(() -> AssertionErrors.fail(message)); + } + return cookie; + } + + private String getMessage(String cookie) { + return "Response cookie '" + cookie + "'"; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java index 56a41deaa15..2c3bff24944 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java @@ -337,6 +337,11 @@ class DefaultWebTestClient implements WebTestClient { return new HeaderAssertions(this.exchangeResult, this); } + @Override + public CookieAssertions expectCookie() { + return new CookieAssertions(this.exchangeResult, this); + } + @Override public BodySpec expectBody(Class bodyType) { B body = this.response.bodyToMono(bodyType).block(this.timeout); diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java index ed9b1ecde7a..4018ac1acde 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java @@ -764,6 +764,12 @@ public interface WebTestClient { */ HeaderAssertions expectHeader(); + /** + * Assertions on the cookies of the response. + * @since 5.3 + */ + CookieAssertions expectCookie(); + /** * Consume and decode the response body to a single object of type * {@code } and then apply assertions. diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/CookieAssertionTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/CookieAssertionTests.java new file mode 100644 index 00000000000..426dc7b3445 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/CookieAssertionTests.java @@ -0,0 +1,138 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.test.web.reactive.server; + +import java.net.URI; +import java.time.Duration; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.MonoProcessor; +import reactor.core.publisher.Sinks; + +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseCookie; +import org.springframework.mock.http.client.reactive.MockClientHttpRequest; +import org.springframework.mock.http.client.reactive.MockClientHttpResponse; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link CookieAssertions} + * @author Rossen Stoyanchev + */ +public class CookieAssertionTests { + + private final ResponseCookie cookie = ResponseCookie.from("foo", "bar") + .maxAge(Duration.ofMinutes(30)) + .domain("foo.com") + .path("/foo") + .secure(true) + .httpOnly(true) + .sameSite("Lax") + .build(); + + private final CookieAssertions assertions = cookieAssertions(cookie); + + + @Test + void valueEquals() { + assertions.valueEquals("foo", "bar"); + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.valueEquals("what?!", "bar")); + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.valueEquals("foo", "what?!")); + } + + @Test + void value() { + assertions.value("foo", equalTo("bar")); + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.value("foo", equalTo("what?!"))); + } + + @Test + void exists() { + assertions.exists("foo"); + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.exists("what?!")); + } + + @Test + void doesNotExist() { + assertions.doesNotExist("what?!"); + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.doesNotExist("foo")); + } + + @Test + void maxAge() { + assertions.maxAge("foo", Duration.ofMinutes(30)); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertions.maxAge("foo", Duration.ofMinutes(29))); + + assertions.maxAge("foo", equalTo(Duration.ofMinutes(30).getSeconds())); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertions.maxAge("foo", equalTo(Duration.ofMinutes(29).getSeconds()))); + } + + @Test + void domain() { + assertions.domain("foo", "foo.com"); + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.domain("foo", "what.com")); + + assertions.domain("foo", equalTo("foo.com")); + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.domain("foo", equalTo("what.com"))); + } + + @Test + void path() { + assertions.path("foo", "/foo"); + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.path("foo", "/what")); + + assertions.path("foo", equalTo("/foo")); + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.path("foo", equalTo("/what"))); + } + + @Test + void secure() { + assertions.secure("foo", true); + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.secure("foo", false)); + } + + @Test + void httpOnly() { + assertions.httpOnly("foo", true); + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.httpOnly("foo", false)); + } + + @Test + void sameSite() { + assertions.sameSite("foo", "Lax"); + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.sameSite("foo", "Strict")); + } + + + private CookieAssertions cookieAssertions(ResponseCookie cookie) { + MockClientHttpRequest request = new MockClientHttpRequest(HttpMethod.GET, URI.create("/")); + MockClientHttpResponse response = new MockClientHttpResponse(HttpStatus.OK); + response.getCookies().add(cookie.getName(), cookie); + + MonoProcessor emptyContent = MonoProcessor.fromSink(Sinks.one()); + emptyContent.onComplete(); + + ExchangeResult result = new ExchangeResult(request, response, emptyContent, emptyContent, Duration.ZERO, null, null); + return new CookieAssertions(result, mock(WebTestClient.ResponseSpec.class)); + } + +}