From d6460e0d578bfc56bbf431fdcba2444b3d7a1ad4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Wed, 5 Apr 2023 17:02:38 +0200 Subject: [PATCH] Add Cookie attributes + SameSite CookieResultMatchers in MockMvc This commit adds assertions to MockMvc's CookieresultMatchers: - `attribute` for arbitrary attributes - `sameSite` for the SameSite well-known attribute Note that the `sameSite` methods delegate to their `attribute` counterparts. Note also that Jakarta's `Cookie#getAttribute` method is case-insensitive, which is reflected in the documentation of the `attribute` assertion method and the tests. Closes gh-30285 --- .../servlet/result/CookieResultMatchers.java | 46 +++++++++++++ .../servlet/result/CookieResultMatchersDsl.kt | 28 ++++++++ .../resultmatchers/CookieAssertionTests.java | 67 +++++++++++++++++++ 3 files changed, 141 insertions(+) diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/result/CookieResultMatchers.java b/spring-test/src/main/java/org/springframework/test/web/servlet/result/CookieResultMatchers.java index 46c8533477e..b65da358f50 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/result/CookieResultMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/result/CookieResultMatchers.java @@ -146,6 +146,24 @@ public class CookieResultMatchers { }; } + /** + * Assert a cookie's SameSite attribute with a Hamcrest {@link Matcher}. + * @since 6.0.8 + * @see #attribute(String, String, Matcher) + */ + public ResultMatcher sameSite(String name, Matcher matcher) { + return attribute(name, "SameSite", matcher); + } + + /** + * Assert a cookie's SameSite attribute. + * @since 6.0.8 + * @see #attribute(String, String, String) + */ + public ResultMatcher sameSite(String name, String sameSite) { + return attribute(name, "SameSite", sameSite); + } + /** * Assert a cookie's comment with a Hamcrest {@link Matcher}. */ @@ -211,6 +229,34 @@ public class CookieResultMatchers { }; } + /** + * Assert a cookie's specified attribute with a Hamcrest {@link Matcher}. + * @param cookieAttribute the name of the Cookie attribute (case-insensitive) + * @since 6.0.8 + */ + public ResultMatcher attribute(String cookieName, String cookieAttribute, Matcher matcher) { + return result -> { + Cookie cookie = getCookie(result, cookieName); + String attribute = cookie.getAttribute(cookieAttribute); + assertNotNull("Response cookie '" + cookieName + "' doesn't have attribute '" + cookieAttribute + "'", attribute); + assertThat("Response cookie '" + cookieName + "' attribute '" + cookieAttribute + "'", + attribute, matcher); + }; + } + + /** + * Assert a cookie's specified attribute. + * @param cookieAttribute the name of the Cookie attribute (case-insensitive) + * @since 6.0.8 + */ + public ResultMatcher attribute(String cookieName, String cookieAttribute, String attributeValue) { + return result -> { + Cookie cookie = getCookie(result, cookieName); + assertEquals("Response cookie '" + cookieName + "' attribute '" + cookieAttribute + "'", + attributeValue, cookie.getAttribute(cookieAttribute)); + }; + } + private static Cookie getCookie(MvcResult result, String name) { Cookie cookie = result.getResponse().getCookie(name); diff --git a/spring-test/src/main/kotlin/org/springframework/test/web/servlet/result/CookieResultMatchersDsl.kt b/spring-test/src/main/kotlin/org/springframework/test/web/servlet/result/CookieResultMatchersDsl.kt index 987f7e19e3f..20b76fcf98a 100644 --- a/spring-test/src/main/kotlin/org/springframework/test/web/servlet/result/CookieResultMatchersDsl.kt +++ b/spring-test/src/main/kotlin/org/springframework/test/web/servlet/result/CookieResultMatchersDsl.kt @@ -99,6 +99,20 @@ class CookieResultMatchersDsl internal constructor (private val actions: ResultA actions.andExpect(matchers.domain(name, domain)) } + /** + * @see CookieResultMatchers.sameSite + */ + fun sameSite(name: String, matcher: Matcher) { + actions.andExpect(matchers.sameSite(name, matcher)) + } + + /** + * @see CookieResultMatchers.sameSite + */ + fun sameSite(name: String, sameSite: String) { + actions.andExpect(matchers.sameSite(name, sameSite)) + } + /** * @see CookieResultMatchers.comment */ @@ -140,4 +154,18 @@ class CookieResultMatchersDsl internal constructor (private val actions: ResultA fun httpOnly(name: String, httpOnly: Boolean) { actions.andExpect(matchers.httpOnly(name, httpOnly)) } + + /** + * @see CookieResultMatchers.attribute + */ + fun attribute(name: String, attributeName: String, matcher: Matcher) { + actions.andExpect(matchers.attribute(name, attributeName, matcher)) + } + + /** + * @see CookieResultMatchers.attribute + */ + fun attribute(name: String, attributeName: String, attributeValue: String) { + actions.andExpect(matchers.attribute(name, attributeName, attributeValue)) + } } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/resultmatchers/CookieAssertionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/resultmatchers/CookieAssertionTests.java index 901126a62c4..295ddb6b74d 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/resultmatchers/CookieAssertionTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/resultmatchers/CookieAssertionTests.java @@ -16,15 +16,22 @@ package org.springframework.test.web.servlet.samples.standalone.resultmatchers; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.stereotype.Controller; import org.springframework.test.web.servlet.MockMvc; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.i18n.CookieLocaleResolver; import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.hamcrest.CoreMatchers.anything; +import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.startsWith; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -41,6 +48,8 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standal public class CookieAssertionTests { private static final String COOKIE_NAME = CookieLocaleResolver.DEFAULT_COOKIE_NAME; + private static final String COOKIE_WITH_ATTRIBUTES_NAME = "SecondCookie"; + protected static final String SECOND_COOKIE_ATTRIBUTE = "COOKIE_ATTRIBUTE"; private MockMvc mockMvc; @@ -50,9 +59,21 @@ public class CookieAssertionTests { CookieLocaleResolver localeResolver = new CookieLocaleResolver(); localeResolver.setCookieDomain("domain"); localeResolver.setCookieHttpOnly(true); + localeResolver.setCookieSameSite("foo"); + + Cookie cookie = new Cookie(COOKIE_WITH_ATTRIBUTES_NAME, "value"); + cookie.setAttribute("sameSite", "Strict"); //intentionally camelCase + cookie.setAttribute(SECOND_COOKIE_ATTRIBUTE, "there"); this.mockMvc = standaloneSetup(new SimpleController()) .addInterceptors(new LocaleChangeInterceptor()) + .addInterceptors(new HandlerInterceptor() { + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + response.addCookie(cookie); + return true; + } + }) .setLocaleResolver(localeResolver) .defaultRequest(get("/").param("locale", "en_US")) .alwaysExpect(status().isOk()) @@ -91,6 +112,26 @@ public class CookieAssertionTests { this.mockMvc.perform(get("/")).andExpect(cookie().domain(COOKIE_NAME, "domain")); } + @Test + void testSameSite() throws Exception { + this.mockMvc.perform(get("/")).andExpect(cookie() + .sameSite(COOKIE_NAME, "foo")); + } + + @Test + void testSameSiteMatcher() throws Exception { + this.mockMvc.perform(get("/")).andExpect(cookie() + .sameSite(COOKIE_WITH_ATTRIBUTES_NAME, startsWith("Str"))); + } + + @Test + void testSameSiteNotEquals() throws Exception { + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> + this.mockMvc.perform(get("/")).andExpect(cookie() + .sameSite(COOKIE_WITH_ATTRIBUTES_NAME, "Str"))) + .withMessage("Response cookie 'SecondCookie' attribute 'SameSite' expected: but was:"); + } + @Test public void testVersion() throws Exception { this.mockMvc.perform(get("/")).andExpect(cookie().version(COOKIE_NAME, 0)); @@ -111,6 +152,32 @@ public class CookieAssertionTests { this.mockMvc.perform(get("/")).andExpect(cookie().httpOnly(COOKIE_NAME, true)); } + @Test + void testAttribute() throws Exception { + this.mockMvc.perform(get("/")).andExpect(cookie() + .attribute(COOKIE_WITH_ATTRIBUTES_NAME, SECOND_COOKIE_ATTRIBUTE, "there")); + } + + @Test + void testAttributeMatcher() throws Exception { + this.mockMvc.perform(get("/")).andExpect(cookie() + .attribute(COOKIE_WITH_ATTRIBUTES_NAME, SECOND_COOKIE_ATTRIBUTE, is("there"))); + } + + @Test + void testAttributeNotPresent() { + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> this.mockMvc.perform(get("/")) + .andExpect(cookie().attribute(COOKIE_WITH_ATTRIBUTES_NAME, "randomAttribute", anything()))) + .withMessage("Response cookie 'SecondCookie' doesn't have attribute 'randomAttribute'"); + } + + @Test + void testAttributeNotEquals() { + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> this.mockMvc.perform(get("/")) + .andExpect(cookie().attribute(COOKIE_WITH_ATTRIBUTES_NAME, SECOND_COOKIE_ATTRIBUTE, "foo"))) + .withMessage("Response cookie 'SecondCookie' attribute 'COOKIE_ATTRIBUTE' expected: but was:"); + } + @Controller private static class SimpleController {