Skip to content

Commit bb2db87

Browse files
vpavicrstoyanchev
authored andcommitted
Add support for adding cookies as headers in MockHttpServletResponse
Issue: SPR-17110
1 parent a8a1fc6 commit bb2db87

File tree

6 files changed

+374
-1
lines changed

6 files changed

+374
-1
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
* Copyright 2002-2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.mock.web;
18+
19+
import javax.servlet.http.Cookie;
20+
21+
import org.springframework.lang.Nullable;
22+
23+
/**
24+
* A {@code Cookie} subclass with the additional cookie directives as defined in the
25+
* <a href="https://tools.ietf.org/html/rfc6265">RFC 6265</a>.
26+
*
27+
* @author Vedran Pavic
28+
* @since 5.1
29+
*/
30+
public class MockCookie extends Cookie {
31+
32+
private static final long serialVersionUID = 4312531139502726325L;
33+
34+
@Nullable
35+
private String sameSite;
36+
37+
/**
38+
* Constructs a {@code MockCookie} instance with the specified name and value.
39+
*
40+
* @param name the cookie name
41+
* @param value the cookie value
42+
* @see Cookie#Cookie(String, String)
43+
*/
44+
public MockCookie(String name, String value) {
45+
super(name, value);
46+
}
47+
48+
/**
49+
* Factory method create {@code MockCookie} instance from Set-Cookie header value.
50+
*
51+
* @param setCookieHeader the Set-Cookie header value
52+
* @return the created cookie instance
53+
*/
54+
public static MockCookie parse(String setCookieHeader) {
55+
String[] cookieParts = setCookieHeader.split("\\s*=\\s*", 2);
56+
if (cookieParts.length != 2) {
57+
throw new IllegalArgumentException("Invalid Set-Cookie header value");
58+
}
59+
String name = cookieParts[0];
60+
String[] valueAndDirectives = cookieParts[1].split("\\s*;\\s*", 2);
61+
String value = valueAndDirectives[0];
62+
String[] directives = valueAndDirectives[1].split("\\s*;\\s*");
63+
String domain = null;
64+
int maxAge = -1;
65+
String path = null;
66+
boolean secure = false;
67+
boolean httpOnly = false;
68+
String sameSite = null;
69+
for (String directive : directives) {
70+
if (directive.startsWith("Domain")) {
71+
domain = directive.split("=")[1];
72+
}
73+
else if (directive.startsWith("Max-Age")) {
74+
maxAge = Integer.parseInt(directive.split("=")[1]);
75+
}
76+
else if (directive.startsWith("Path")) {
77+
path = directive.split("=")[1];
78+
}
79+
else if (directive.startsWith("Secure")) {
80+
secure = true;
81+
}
82+
else if (directive.startsWith("HttpOnly")) {
83+
httpOnly = true;
84+
}
85+
else if (directive.startsWith("SameSite")) {
86+
sameSite = directive.split("=")[1];
87+
}
88+
}
89+
MockCookie cookie = new MockCookie(name, value);
90+
if (domain != null) {
91+
cookie.setDomain(domain);
92+
}
93+
cookie.setMaxAge(maxAge);
94+
cookie.setPath(path);
95+
cookie.setSecure(secure);
96+
cookie.setHttpOnly(httpOnly);
97+
cookie.setSameSite(sameSite);
98+
return cookie;
99+
}
100+
101+
/**
102+
* Return the cookie "SameSite" attribute, or {@code null} if not set.
103+
* <p>
104+
* This limits the scope of the cookie such that it will only be attached to same site
105+
* requests if {@code "Strict"} or cross-site requests if {@code "Lax"}.
106+
*
107+
* @see <a href="https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis#section-4.1.2.7">RFC6265 bis</a>
108+
*/
109+
@Nullable
110+
public String getSameSite() {
111+
return this.sameSite;
112+
}
113+
114+
/**
115+
* Add the "SameSite" attribute to the cookie.
116+
* <p>
117+
* This limits the scope of the cookie such that it will only be attached to same site
118+
* requests if {@code "Strict"} or cross-site requests if {@code "Lax"}.
119+
*
120+
* @see <a href="https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis#section-4.1.2.7">RFC6265 bis</a>
121+
*/
122+
public void setSameSite(@Nullable String sameSite) {
123+
this.sameSite = sameSite;
124+
}
125+
126+
}

spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
* @author Juergen Hoeller
5555
* @author Rod Johnson
5656
* @author Brian Clozel
57+
* @author Vedran Pavic
5758
* @since 1.0.2
5859
*/
5960
public class MockHttpServletResponse implements HttpServletResponse {
@@ -353,6 +354,12 @@ private String getCookieHeader(Cookie cookie) {
353354
if (cookie.isHttpOnly()) {
354355
buf.append("; HttpOnly");
355356
}
357+
if (cookie instanceof MockCookie) {
358+
MockCookie mockCookie = (MockCookie) cookie;
359+
if (StringUtils.hasText(mockCookie.getSameSite())) {
360+
buf.append("; SameSite=").append(mockCookie.getSameSite());
361+
}
362+
}
356363
return buf.toString();
357364
}
358365

@@ -596,6 +603,11 @@ else if (HttpHeaders.CONTENT_LANGUAGE.equalsIgnoreCase(name)) {
596603
this.locale = language != null ? language : Locale.getDefault();
597604
return true;
598605
}
606+
else if (HttpHeaders.SET_COOKIE.equalsIgnoreCase(name)) {
607+
MockCookie cookie = MockCookie.parse(value.toString());
608+
addCookie(cookie);
609+
return true;
610+
}
599611
else {
600612
return false;
601613
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2002-2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.mock.web;
18+
19+
import org.junit.Test;
20+
21+
import static org.junit.Assert.*;
22+
23+
/**
24+
* Unit tests for {@link MockCookie}.
25+
*
26+
* @author Vedran Pavic
27+
*/
28+
public class MockCookieTests {
29+
30+
@Test
31+
public void constructCookie() {
32+
MockCookie cookie = new MockCookie("SESSION", "123");
33+
34+
assertEquals("SESSION", cookie.getName());
35+
assertEquals("123", cookie.getValue());
36+
}
37+
38+
@Test
39+
public void setSameSite() {
40+
MockCookie cookie = new MockCookie("SESSION", "123");
41+
cookie.setSameSite("Strict");
42+
43+
assertEquals("Strict", cookie.getSameSite());
44+
}
45+
46+
@Test
47+
public void parseValidHeader() {
48+
MockCookie cookie = MockCookie.parse(
49+
"SESSION=123; Domain=example.com; Max-Age=60; Path=/; Secure; HttpOnly; SameSite=Lax");
50+
51+
assertEquals("SESSION", cookie.getName());
52+
assertEquals("123", cookie.getValue());
53+
assertEquals("example.com", cookie.getDomain());
54+
assertEquals(60, cookie.getMaxAge());
55+
assertEquals("/", cookie.getPath());
56+
assertTrue(cookie.getSecure());
57+
assertTrue(cookie.isHttpOnly());
58+
assertEquals("Lax", cookie.getSameSite());
59+
}
60+
61+
@Test(expected = IllegalArgumentException.class)
62+
public void parseInvalidHeader() {
63+
MockCookie.parse("invalid");
64+
}
65+
66+
}

spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2018 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -322,4 +322,35 @@ public void modifyStatusMessageAfterSendError() throws IOException {
322322
assertEquals(HttpServletResponse.SC_NOT_FOUND, response.getStatus());
323323
}
324324

325+
@Test
326+
public void setCookieHeaderValid() {
327+
response.addHeader(HttpHeaders.SET_COOKIE, "SESSION=123; Path=/; Secure; HttpOnly; SameSite=Lax");
328+
Cookie cookie = response.getCookie("SESSION");
329+
assertNotNull(cookie);
330+
assertTrue(cookie instanceof MockCookie);
331+
assertEquals("SESSION", cookie.getName());
332+
assertEquals("123", cookie.getValue());
333+
assertEquals("/", cookie.getPath());
334+
assertTrue(cookie.getSecure());
335+
assertTrue(cookie.isHttpOnly());
336+
assertEquals("Lax", ((MockCookie) cookie).getSameSite());
337+
}
338+
339+
@Test
340+
public void addMockCookie() {
341+
MockCookie mockCookie = new MockCookie("SESSION", "123");
342+
mockCookie.setPath("/");
343+
mockCookie.setDomain("example.com");
344+
mockCookie.setMaxAge(0);
345+
mockCookie.setSecure(true);
346+
mockCookie.setHttpOnly(true);
347+
mockCookie.setSameSite("Lax");
348+
349+
response.addCookie(mockCookie);
350+
351+
assertEquals("SESSION=123; Path=/; Domain=example.com; Max-Age=0; " +
352+
"Expires=Thu, 1 Jan 1970 00:00:00 GMT; Secure; HttpOnly; SameSite=Lax",
353+
response.getHeader(HttpHeaders.SET_COOKIE));
354+
}
355+
325356
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
* Copyright 2002-2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.mock.web.test;
18+
19+
import javax.servlet.http.Cookie;
20+
21+
import org.springframework.lang.Nullable;
22+
23+
/**
24+
* A {@code Cookie} subclass with the additional cookie directives as defined in the
25+
* <a href="https://tools.ietf.org/html/rfc6265">RFC 6265</a>.
26+
*
27+
* @author Vedran Pavic
28+
* @since 5.1
29+
*/
30+
public class MockCookie extends Cookie {
31+
32+
private static final long serialVersionUID = 4312531139502726325L;
33+
34+
@Nullable
35+
private String sameSite;
36+
37+
/**
38+
* Constructs a {@code MockCookie} instance with the specified name and value.
39+
*
40+
* @param name the cookie name
41+
* @param value the cookie value
42+
* @see Cookie#Cookie(String, String)
43+
*/
44+
public MockCookie(String name, String value) {
45+
super(name, value);
46+
}
47+
48+
/**
49+
* Factory method create {@code MockCookie} instance from Set-Cookie header value.
50+
*
51+
* @param setCookieHeader the Set-Cookie header value
52+
* @return the created cookie instance
53+
*/
54+
public static MockCookie parse(String setCookieHeader) {
55+
String[] cookieParts = setCookieHeader.split("\\s*=\\s*", 2);
56+
if (cookieParts.length != 2) {
57+
throw new IllegalArgumentException("Invalid Set-Cookie header value");
58+
}
59+
String name = cookieParts[0];
60+
String[] valueAndDirectives = cookieParts[1].split("\\s*;\\s*", 2);
61+
String value = valueAndDirectives[0];
62+
String[] directives = valueAndDirectives[1].split("\\s*;\\s*");
63+
String domain = null;
64+
int maxAge = -1;
65+
String path = null;
66+
boolean secure = false;
67+
boolean httpOnly = false;
68+
String sameSite = null;
69+
for (String directive : directives) {
70+
if (directive.startsWith("Domain")) {
71+
domain = directive.split("=")[1];
72+
}
73+
else if (directive.startsWith("Max-Age")) {
74+
maxAge = Integer.parseInt(directive.split("=")[1]);
75+
}
76+
else if (directive.startsWith("Path")) {
77+
path = directive.split("=")[1];
78+
}
79+
else if (directive.startsWith("Secure")) {
80+
secure = true;
81+
}
82+
else if (directive.startsWith("HttpOnly")) {
83+
httpOnly = true;
84+
}
85+
else if (directive.startsWith("SameSite")) {
86+
sameSite = directive.split("=")[1];
87+
}
88+
}
89+
MockCookie cookie = new MockCookie(name, value);
90+
if (domain != null) {
91+
cookie.setDomain(domain);
92+
}
93+
cookie.setMaxAge(maxAge);
94+
cookie.setPath(path);
95+
cookie.setSecure(secure);
96+
cookie.setHttpOnly(httpOnly);
97+
cookie.setSameSite(sameSite);
98+
return cookie;
99+
}
100+
101+
/**
102+
* Return the cookie "SameSite" attribute, or {@code null} if not set.
103+
* <p>
104+
* This limits the scope of the cookie such that it will only be attached to same site
105+
* requests if {@code "Strict"} or cross-site requests if {@code "Lax"}.
106+
*
107+
* @see <a href="https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis#section-4.1.2.7">RFC6265 bis</a>
108+
*/
109+
@Nullable
110+
public String getSameSite() {
111+
return this.sameSite;
112+
}
113+
114+
/**
115+
* Add the "SameSite" attribute to the cookie.
116+
* <p>
117+
* This limits the scope of the cookie such that it will only be attached to same site
118+
* requests if {@code "Strict"} or cross-site requests if {@code "Lax"}.
119+
*
120+
* @see <a href="https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis#section-4.1.2.7">RFC6265 bis</a>
121+
*/
122+
public void setSameSite(@Nullable String sameSite) {
123+
this.sameSite = sameSite;
124+
}
125+
126+
}

0 commit comments

Comments
 (0)