Skip to content

Commit d6460e0

Browse files
committed
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
1 parent 842490b commit d6460e0

File tree

3 files changed

+141
-0
lines changed

3 files changed

+141
-0
lines changed

spring-test/src/main/java/org/springframework/test/web/servlet/result/CookieResultMatchers.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,24 @@ public ResultMatcher domain(String name, String domain) {
146146
};
147147
}
148148

149+
/**
150+
* Assert a cookie's SameSite attribute with a Hamcrest {@link Matcher}.
151+
* @since 6.0.8
152+
* @see #attribute(String, String, Matcher)
153+
*/
154+
public ResultMatcher sameSite(String name, Matcher<? super String> matcher) {
155+
return attribute(name, "SameSite", matcher);
156+
}
157+
158+
/**
159+
* Assert a cookie's SameSite attribute.
160+
* @since 6.0.8
161+
* @see #attribute(String, String, String)
162+
*/
163+
public ResultMatcher sameSite(String name, String sameSite) {
164+
return attribute(name, "SameSite", sameSite);
165+
}
166+
149167
/**
150168
* Assert a cookie's comment with a Hamcrest {@link Matcher}.
151169
*/
@@ -211,6 +229,34 @@ public ResultMatcher httpOnly(String name, boolean httpOnly) {
211229
};
212230
}
213231

232+
/**
233+
* Assert a cookie's specified attribute with a Hamcrest {@link Matcher}.
234+
* @param cookieAttribute the name of the Cookie attribute (case-insensitive)
235+
* @since 6.0.8
236+
*/
237+
public ResultMatcher attribute(String cookieName, String cookieAttribute, Matcher<? super String> matcher) {
238+
return result -> {
239+
Cookie cookie = getCookie(result, cookieName);
240+
String attribute = cookie.getAttribute(cookieAttribute);
241+
assertNotNull("Response cookie '" + cookieName + "' doesn't have attribute '" + cookieAttribute + "'", attribute);
242+
assertThat("Response cookie '" + cookieName + "' attribute '" + cookieAttribute + "'",
243+
attribute, matcher);
244+
};
245+
}
246+
247+
/**
248+
* Assert a cookie's specified attribute.
249+
* @param cookieAttribute the name of the Cookie attribute (case-insensitive)
250+
* @since 6.0.8
251+
*/
252+
public ResultMatcher attribute(String cookieName, String cookieAttribute, String attributeValue) {
253+
return result -> {
254+
Cookie cookie = getCookie(result, cookieName);
255+
assertEquals("Response cookie '" + cookieName + "' attribute '" + cookieAttribute + "'",
256+
attributeValue, cookie.getAttribute(cookieAttribute));
257+
};
258+
}
259+
214260

215261
private static Cookie getCookie(MvcResult result, String name) {
216262
Cookie cookie = result.getResponse().getCookie(name);

spring-test/src/main/kotlin/org/springframework/test/web/servlet/result/CookieResultMatchersDsl.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,20 @@ class CookieResultMatchersDsl internal constructor (private val actions: ResultA
9999
actions.andExpect(matchers.domain(name, domain))
100100
}
101101

102+
/**
103+
* @see CookieResultMatchers.sameSite
104+
*/
105+
fun sameSite(name: String, matcher: Matcher<String>) {
106+
actions.andExpect(matchers.sameSite(name, matcher))
107+
}
108+
109+
/**
110+
* @see CookieResultMatchers.sameSite
111+
*/
112+
fun sameSite(name: String, sameSite: String) {
113+
actions.andExpect(matchers.sameSite(name, sameSite))
114+
}
115+
102116
/**
103117
* @see CookieResultMatchers.comment
104118
*/
@@ -140,4 +154,18 @@ class CookieResultMatchersDsl internal constructor (private val actions: ResultA
140154
fun httpOnly(name: String, httpOnly: Boolean) {
141155
actions.andExpect(matchers.httpOnly(name, httpOnly))
142156
}
157+
158+
/**
159+
* @see CookieResultMatchers.attribute
160+
*/
161+
fun attribute(name: String, attributeName: String, matcher: Matcher<String>) {
162+
actions.andExpect(matchers.attribute(name, attributeName, matcher))
163+
}
164+
165+
/**
166+
* @see CookieResultMatchers.attribute
167+
*/
168+
fun attribute(name: String, attributeName: String, attributeValue: String) {
169+
actions.andExpect(matchers.attribute(name, attributeName, attributeValue))
170+
}
143171
}

spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/resultmatchers/CookieAssertionTests.java

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,22 @@
1616

1717
package org.springframework.test.web.servlet.samples.standalone.resultmatchers;
1818

19+
import jakarta.servlet.http.Cookie;
20+
import jakarta.servlet.http.HttpServletRequest;
21+
import jakarta.servlet.http.HttpServletResponse;
1922
import org.junit.jupiter.api.BeforeEach;
2023
import org.junit.jupiter.api.Test;
2124

2225
import org.springframework.stereotype.Controller;
2326
import org.springframework.test.web.servlet.MockMvc;
2427
import org.springframework.web.bind.annotation.RequestMapping;
28+
import org.springframework.web.servlet.HandlerInterceptor;
2529
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
2630
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
2731

32+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
33+
import static org.hamcrest.CoreMatchers.anything;
34+
import static org.hamcrest.CoreMatchers.is;
2835
import static org.hamcrest.Matchers.equalTo;
2936
import static org.hamcrest.Matchers.startsWith;
3037
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@@ -41,6 +48,8 @@
4148
public class CookieAssertionTests {
4249

4350
private static final String COOKIE_NAME = CookieLocaleResolver.DEFAULT_COOKIE_NAME;
51+
private static final String COOKIE_WITH_ATTRIBUTES_NAME = "SecondCookie";
52+
protected static final String SECOND_COOKIE_ATTRIBUTE = "COOKIE_ATTRIBUTE";
4453

4554
private MockMvc mockMvc;
4655

@@ -50,9 +59,21 @@ public void setup() {
5059
CookieLocaleResolver localeResolver = new CookieLocaleResolver();
5160
localeResolver.setCookieDomain("domain");
5261
localeResolver.setCookieHttpOnly(true);
62+
localeResolver.setCookieSameSite("foo");
63+
64+
Cookie cookie = new Cookie(COOKIE_WITH_ATTRIBUTES_NAME, "value");
65+
cookie.setAttribute("sameSite", "Strict"); //intentionally camelCase
66+
cookie.setAttribute(SECOND_COOKIE_ATTRIBUTE, "there");
5367

5468
this.mockMvc = standaloneSetup(new SimpleController())
5569
.addInterceptors(new LocaleChangeInterceptor())
70+
.addInterceptors(new HandlerInterceptor() {
71+
@Override
72+
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
73+
response.addCookie(cookie);
74+
return true;
75+
}
76+
})
5677
.setLocaleResolver(localeResolver)
5778
.defaultRequest(get("/").param("locale", "en_US"))
5879
.alwaysExpect(status().isOk())
@@ -91,6 +112,26 @@ public void testDomain() throws Exception {
91112
this.mockMvc.perform(get("/")).andExpect(cookie().domain(COOKIE_NAME, "domain"));
92113
}
93114

115+
@Test
116+
void testSameSite() throws Exception {
117+
this.mockMvc.perform(get("/")).andExpect(cookie()
118+
.sameSite(COOKIE_NAME, "foo"));
119+
}
120+
121+
@Test
122+
void testSameSiteMatcher() throws Exception {
123+
this.mockMvc.perform(get("/")).andExpect(cookie()
124+
.sameSite(COOKIE_WITH_ATTRIBUTES_NAME, startsWith("Str")));
125+
}
126+
127+
@Test
128+
void testSameSiteNotEquals() throws Exception {
129+
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
130+
this.mockMvc.perform(get("/")).andExpect(cookie()
131+
.sameSite(COOKIE_WITH_ATTRIBUTES_NAME, "Str")))
132+
.withMessage("Response cookie 'SecondCookie' attribute 'SameSite' expected:<Str> but was:<Strict>");
133+
}
134+
94135
@Test
95136
public void testVersion() throws Exception {
96137
this.mockMvc.perform(get("/")).andExpect(cookie().version(COOKIE_NAME, 0));
@@ -111,6 +152,32 @@ public void testHttpOnly() throws Exception {
111152
this.mockMvc.perform(get("/")).andExpect(cookie().httpOnly(COOKIE_NAME, true));
112153
}
113154

155+
@Test
156+
void testAttribute() throws Exception {
157+
this.mockMvc.perform(get("/")).andExpect(cookie()
158+
.attribute(COOKIE_WITH_ATTRIBUTES_NAME, SECOND_COOKIE_ATTRIBUTE, "there"));
159+
}
160+
161+
@Test
162+
void testAttributeMatcher() throws Exception {
163+
this.mockMvc.perform(get("/")).andExpect(cookie()
164+
.attribute(COOKIE_WITH_ATTRIBUTES_NAME, SECOND_COOKIE_ATTRIBUTE, is("there")));
165+
}
166+
167+
@Test
168+
void testAttributeNotPresent() {
169+
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> this.mockMvc.perform(get("/"))
170+
.andExpect(cookie().attribute(COOKIE_WITH_ATTRIBUTES_NAME, "randomAttribute", anything())))
171+
.withMessage("Response cookie 'SecondCookie' doesn't have attribute 'randomAttribute'");
172+
}
173+
174+
@Test
175+
void testAttributeNotEquals() {
176+
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> this.mockMvc.perform(get("/"))
177+
.andExpect(cookie().attribute(COOKIE_WITH_ATTRIBUTES_NAME, SECOND_COOKIE_ATTRIBUTE, "foo")))
178+
.withMessage("Response cookie 'SecondCookie' attribute 'COOKIE_ATTRIBUTE' expected:<foo> but was:<there>");
179+
}
180+
114181

115182
@Controller
116183
private static class SimpleController {

0 commit comments

Comments
 (0)