View Javadoc
1   /*
2    * Copyright (c) 2002-2025 Gargoyle Software Inc.
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    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit;
16  
17  import java.net.URL;
18  import java.nio.charset.StandardCharsets;
19  import java.util.ArrayList;
20  import java.util.Arrays;
21  import java.util.Collections;
22  import java.util.Date;
23  import java.util.List;
24  import java.util.Map;
25  
26  import org.apache.http.client.utils.DateUtils;
27  import org.htmlunit.junit.annotation.Alerts;
28  import org.htmlunit.util.MimeType;
29  import org.htmlunit.util.NameValuePair;
30  import org.junit.jupiter.api.Test;
31  import org.openqa.selenium.By;
32  import org.openqa.selenium.WebDriver;
33  import org.openqa.selenium.htmlunit.HtmlUnitDriver;
34  
35  /**
36   * Unit tests for {@link CookieManager}.
37   *
38   * @author Daniel Gredler
39   * @author Ahmed Ashour
40   * @author Marc Guillemot
41   * @author Frank Danek
42   * @author Ronald Brill
43   */
44  public class CookieManagerTest extends WebDriverTestCase {
45  
46      /** HTML code with JS code <code>alert(document.cookie)</code>. */
47      public static final String HTML_ALERT_COOKIE = DOCTYPE_HTML
48          + "<html><head>\n"
49          + "<script>\n"
50          + LOG_TITLE_FUNCTION
51          + "  function test() {\n"
52          // there is no fixed order, sort for stable testing
53          + "    var c = document.cookie;\n"
54          + "    c = c.split('; ').sort().join('; ');\n"
55          + "    log(c);\n"
56          + "  }\n"
57          + "</script>\n"
58          + "</head>\n"
59          + "<body onload='test()'>\n"
60          + "</body></html>";
61  
62      /**
63       * @throws Exception if the test fails
64       */
65      @Test
66      @Alerts("my_key=\"Hello, big, big, world\"; yet_another_key=Hi")
67      public void comma() throws Exception {
68          final List<NameValuePair> responseHeader = new ArrayList<>();
69          responseHeader.add(new NameValuePair("Set-Cookie", "my_key=\"Hello, big, big, world\""));
70          responseHeader.add(new NameValuePair("Set-Cookie", "yet_another_key=Hi"));
71          getMockWebConnection().setDefaultResponse(HTML_ALERT_COOKIE, 200, "OK", MimeType.TEXT_HTML, responseHeader);
72  
73          loadPage2(URL_FIRST, StandardCharsets.ISO_8859_1);
74          verifyTitle2(getWebDriver(), getExpectedAlerts());
75      }
76  
77      /**
78       * @throws Exception if the test fails
79       */
80      @Test
81      @Alerts("a_key=helloA; b_key=helloB; c_key=helloC")
82      public void orderFromServer() throws Exception {
83          final List<NameValuePair> responseHeader = new ArrayList<>();
84          responseHeader.add(new NameValuePair("Set-Cookie", "c_key=helloC"));
85          responseHeader.add(new NameValuePair("Set-Cookie", "a_key=helloA"));
86          responseHeader.add(new NameValuePair("Set-Cookie", "b_key=helloB"));
87          getMockWebConnection().setDefaultResponse(HTML_ALERT_COOKIE, 200, "OK", MimeType.TEXT_HTML, responseHeader);
88  
89          loadPage2(URL_FIRST, StandardCharsets.ISO_8859_1);
90          verifyTitle2(getWebDriver(), getExpectedAlerts());
91      }
92  
93      /**
94       * @throws Exception if the test fails
95       */
96      @Test
97      public void orderCookiesByPath_fromJs() throws Exception {
98          final String html = DOCTYPE_HTML
99              + "<html><body><script>\n"
100             + "document.cookie = 'exampleCookie=rootPath;path=/';\n"
101             + "document.cookie = 'exampleCookie=currentPath;path=/testpages/';\n"
102             + "</script>\n"
103             + "<a href='/testpages/next.html'>next page</a>\n"
104             + "</body></html>";
105 
106         getMockWebConnection().setDefaultResponse("");
107 
108         final WebDriver webDriver = getWebDriver();
109         webDriver.manage().deleteAllCookies();
110 
111         loadPage2(html);
112         webDriver.findElement(By.linkText("next page")).click();
113 
114         final WebRequest lastRequest = getMockWebConnection().getLastWebRequest();
115         assertEquals("exampleCookie=currentPath; exampleCookie=rootPath",
116             lastRequest.getAdditionalHeaders().get(HttpHeader.COOKIE));
117     }
118 
119     /**
120      * @throws Exception if the test fails
121      */
122     @Test
123     @Alerts("key1=; key2=")
124     public void emptyCookie() throws Exception {
125         final List<NameValuePair> responseHeader = new ArrayList<>();
126         responseHeader.add(new NameValuePair("Set-Cookie", "key1="));
127         responseHeader.add(new NameValuePair("Set-Cookie", "key2="));
128         getMockWebConnection().setDefaultResponse(HTML_ALERT_COOKIE, 200, "OK", MimeType.TEXT_HTML, responseHeader);
129 
130         loadPage2(URL_FIRST, StandardCharsets.ISO_8859_1);
131         verifyTitle2(getWebDriver(), getExpectedAlerts());
132     }
133 
134     /**
135      * @throws Exception if the test fails
136      */
137     @Test
138     @Alerts("value1")
139     public void emptyCookieName() throws Exception {
140         final List<NameValuePair> responseHeader = new ArrayList<>();
141         responseHeader.add(new NameValuePair("Set-Cookie", "=value1"));
142         getMockWebConnection().setDefaultResponse(HTML_ALERT_COOKIE, 200, "OK", MimeType.TEXT_HTML, responseHeader);
143 
144         loadPage2(URL_FIRST, StandardCharsets.ISO_8859_1);
145         verifyTitle2(getWebDriver(), getExpectedAlerts());
146     }
147 
148     /**
149      * Test that " are not discarded.
150      * Regression test for issue 2973040.
151      * When a cookie is set with value within quotes, this value should be sent within quotes
152      * in the following requests. This is a bug in HttpClient which is not fixed in HttpClient-4.0.1.
153      * @see <a href="https://issues.apache.org/jira/browse/HTTPCLIENT-1006">HttpClient bug 1006</a>
154      * @throws Exception if the test fails
155      */
156     @Test
157     public void valueQuoted() throws Exception {
158         final List<NameValuePair> responseHeader = new ArrayList<>();
159         responseHeader.add(new NameValuePair("Set-Cookie", "key=value"));
160         responseHeader.add(new NameValuePair("Set-Cookie", "quoted=\"hello world\""));
161         responseHeader.add(new NameValuePair("Set-Cookie", "quotedEquals=\"aa= xx==\""));
162         getMockWebConnection().setResponse(URL_FIRST, "", 200, "OK", MimeType.TEXT_HTML, responseHeader);
163         getMockWebConnection().setDefaultResponse("");
164 
165         final WebDriver driver = loadPageWithAlerts2(URL_FIRST);
166 
167         driver.get(URL_SECOND.toExternalForm());
168 
169         final String lastCookies = getMockWebConnection().getLastAdditionalHeaders().get(HttpHeader.COOKIE);
170         final List<String> cookies = Arrays.asList(lastCookies.split(";\\s"));
171         Collections.sort(cookies);
172         assertEquals(new String[] {"key=value", "quoted=\"hello world\"", "quotedEquals=\"aa= xx==\""}, cookies);
173     }
174 
175     /**
176      * Regression test for issue 1735.
177      * @throws Exception if the test fails
178      */
179     @Test
180     public void valueEmpty() throws Exception {
181         ensureCookieValueIsSentBackUnquoted("SID=");
182     }
183 
184     /**
185      * @throws Exception if the test fails
186      */
187     @Test
188     public void valueUnsafe() throws Exception {
189         // fails with jetty 9.4.6.v20170531 but was working before
190         // ensureCookieValueIsSentBackUnquoted("SID=\"");
191 
192         ensureCookieValueIsSentBackUnquoted("SID=\"\"");
193         ensureCookieValueIsSentBackUnquoted("SID=ab\"cd");
194 
195         ensureCookieValueIsSentBackUnquoted("SID=\\");
196         ensureCookieValueIsSentBackUnquoted("SID=ab\\cd");
197     }
198 
199     /**
200      * If a Version 1 cookie is set with a value that requires quoting,
201      * but wasn't quoted by the server, then this value should be
202      * sent back unquoted as well.
203      * @throws Exception if the test fails
204      */
205     @Test
206     public void unquotedCookieValueIsSentBackUnquotedAsWell() throws Exception {
207         ensureCookieValueIsSentBackUnquoted("SID=1234");
208 
209         // even if there are special chars
210         ensureCookieValueIsSentBackUnquoted("SID=1234=");
211         ensureCookieValueIsSentBackUnquoted("SID=1234:");
212         ensureCookieValueIsSentBackUnquoted("SID=1234<");
213     }
214 
215     private void ensureCookieValueIsSentBackUnquoted(final String cookie) throws Exception {
216         final List<NameValuePair> responseHeaders = new ArrayList<>();
217         responseHeaders.add(new NameValuePair("Set-Cookie", cookie + "; Path=/; Version=1"));
218         getMockWebConnection().setResponse(URL_FIRST, "", 200, "OK", MimeType.TEXT_HTML, responseHeaders);
219         getMockWebConnection().setDefaultResponse("");
220 
221         final WebDriver driver = loadPageWithAlerts2(URL_FIRST);
222         driver.get(URL_SECOND.toExternalForm());
223 
224         final String lastCookie = getMockWebConnection().getLastAdditionalHeaders().get(HttpHeader.COOKIE);
225         assertEquals(cookie, lastCookie);
226     }
227 
228     /**
229      * @throws Exception if the test fails
230      */
231     @Test
232     @Alerts("nbCalls=1")
233     public void serverModifiesCookieValue() throws Exception {
234         final List<NameValuePair> responseHeader = new ArrayList<>();
235         responseHeader.add(new NameValuePair("Set-Cookie", "nbCalls=1"));
236         getMockWebConnection().setDefaultResponse(HTML_ALERT_COOKIE, 200, "OK", MimeType.TEXT_HTML, responseHeader);
237 
238         loadPage2(URL_FIRST, StandardCharsets.ISO_8859_1);
239         verifyTitle2(getWebDriver(), getExpectedAlerts());
240 
241         final List<NameValuePair> responseHeader2 = new ArrayList<>();
242         responseHeader2.add(new NameValuePair("Set-Cookie", "nbCalls=2"));
243         getMockWebConnection().setResponse(URL_FIRST, HTML_ALERT_COOKIE, 200, "OK", MimeType.TEXT_HTML,
244                 responseHeader2);
245 
246         loadPage2(URL_FIRST, StandardCharsets.ISO_8859_1);
247         verifyTitle2(getWebDriver(), "nbCalls=2");
248     }
249 
250     /**
251      * @throws Exception if the test fails
252      */
253     @Test
254     @Alerts("first=1")
255     public void cookie2() throws Exception {
256         final List<NameValuePair> responseHeader1 = new ArrayList<>();
257         responseHeader1.add(new NameValuePair("Set-Cookie", "first=1"));
258         getMockWebConnection().setResponse(URL_FIRST, HTML_ALERT_COOKIE, 200, "OK", MimeType.TEXT_HTML,
259                 responseHeader1);
260 
261         loadPage2(URL_FIRST, StandardCharsets.ISO_8859_1);
262         verifyTitle2(getWebDriver(), getExpectedAlerts());
263 
264         final List<NameValuePair> responseHeader2 = new ArrayList<>();
265         responseHeader2.add(new NameValuePair("Set-Cookie", "second=2"));
266         getMockWebConnection().setResponse(URL_FIRST, HTML_ALERT_COOKIE, 200, "OK", MimeType.TEXT_HTML,
267                 responseHeader2);
268 
269         loadPage2(URL_FIRST, StandardCharsets.ISO_8859_1);
270         verifyTitle2(getWebDriver(), "first=1; second=2");
271     }
272 
273     /**
274      * Tests for expiration date.
275      * Tests as well for bug 3421201 (with expiration date enclosed with quotes).
276      * @throws Exception if the test fails
277      */
278     @Test
279     @Alerts("second=2; visitor=f2")
280     public void setCookieExpired() throws Exception {
281         final Date aBitLater = new Date(new Date().getTime() + 60 * 60 * 1000); // one hour later
282         final List<NameValuePair> responseHeader1 = new ArrayList<>();
283         responseHeader1.add(new NameValuePair("Set-Cookie", "first=1;expires=Fri, 02-Jan-1970 00:00:00 GMT"));
284         responseHeader1.add(new NameValuePair("Set-Cookie",
285             "second=2;expires=" + DateUtils.formatDate(aBitLater)));
286         responseHeader1.add(new NameValuePair("Set-Cookie", "visit=fo; expires=\"Sat, 07-Apr-2002 13:11:33 GMT\";"));
287         responseHeader1.add(new NameValuePair("Set-Cookie", "visitor=f2; expires=\"Sat, 07-Apr-2092 13:11:33 GMT\";"));
288         getMockWebConnection().setResponse(URL_FIRST, HTML_ALERT_COOKIE, 200, "OK", MimeType.TEXT_HTML,
289                 responseHeader1);
290 
291         loadPage2(URL_FIRST, StandardCharsets.ISO_8859_1);
292         verifyTitle2(getWebDriver(), getExpectedAlerts());
293     }
294 
295     /**
296      * Test for document.cookie for cookies expired after the page was loaded.
297      * @throws Exception if the test fails
298      */
299     @Test
300     @Alerts({"cookies: first=1", "cookies: "})
301     public void setCookieTimeout() throws Exception {
302         final String html = DOCTYPE_HTML
303                 + "<html><head>\n"
304                 + "<script>\n"
305                 + LOG_TITLE_FUNCTION
306                 + "  function alertCookies() {\n"
307                 + "    log('cookies: ' + document.cookie);\n"
308                 + "  }\n"
309 
310                 + "  function test() {\n"
311                 + "    alertCookies();\n"
312                 + "    window.setTimeout(alertCookies, 2500);\n"
313                 + "  }\n"
314                 + "</script></head><body onload='test()'>\n"
315                 + "</body></html>";
316 
317         final List<NameValuePair> responseHeader1 = new ArrayList<>();
318         final String expires = DateUtils.formatDate(new Date(System.currentTimeMillis() + 2_000));
319         responseHeader1.add(new NameValuePair("Set-Cookie", "first=1; expires=" + expires + ";"));
320         getMockWebConnection().setResponse(URL_FIRST, html, 200, "OK", MimeType.TEXT_HTML, responseHeader1);
321 
322         loadPage2(URL_FIRST, StandardCharsets.ISO_8859_1);
323         verifyTitle2(DEFAULT_WAIT_TIME.multipliedBy(4), getWebDriver(), getExpectedAlerts());
324     }
325 
326     /**
327      * Regression test for bug 3081652: it seems that expiration date should be ignored if format is incorrect.
328      * @throws Exception if the test fails
329      */
330     @Test
331     @Alerts(DEFAULT = "fourth=4; third=3",
332             FF = "first=1; second=2; third=3",
333             FF_ESR = "first=1; second=2; third=3")
334     public void setCookieExpired_badDateFormat() throws Exception {
335         final List<NameValuePair> responseHeader1 = new ArrayList<>();
336         responseHeader1.add(new NameValuePair("Set-Cookie", "first=1;expires=Dec-1-94 16:00:00"));
337         responseHeader1.add(new NameValuePair("Set-Cookie", "second=2;expires=Dec-1-1994 16:00:00"));
338         responseHeader1.add(new NameValuePair("Set-Cookie", "third=3;expires=Dec-1-2094 16:00:00"));
339         responseHeader1.add(new NameValuePair("Set-Cookie", "fourth=4;expires=1/1/2000; path=/"));
340         getMockWebConnection().setResponse(URL_FIRST, HTML_ALERT_COOKIE, 200, "OK", MimeType.TEXT_HTML,
341                 responseHeader1);
342 
343         loadPage2(URL_FIRST, StandardCharsets.ISO_8859_1);
344         verifyTitle2(getWebDriver(), getExpectedAlerts());
345     }
346 
347     /**
348      * Two digits years should be interpreted as 20xx if before 1970 and as 19xx otherwise.
349      * @throws Exception if the test fails
350      */
351     @Test
352     @Alerts("cookie1=1; cookie2=2; cookie3=3")
353     public void setCookieExpires_twoDigits() throws Exception {
354         final List<NameValuePair> responseHeader1 = new ArrayList<>();
355         responseHeader1.add(new NameValuePair("Set-Cookie", "cookie1=1;expires=Sun 01-Dec-68 16:00:00 GMT"));
356         responseHeader1.add(new NameValuePair("Set-Cookie", "cookie2=2;expires=Thu 01-Dec-69 16:00:00 GMT"));
357         responseHeader1.add(new NameValuePair("Set-Cookie", "cookie3=3;expires=Mon 31-Dec-69 16:00:00 GMT"));
358         responseHeader1.add(new NameValuePair("Set-Cookie", "cookie4=4;expires=Thu 01-Jan-70 16:00:00 GMT"));
359         responseHeader1.add(new NameValuePair("Set-Cookie", "cookie5=5;expires=Tue 01-Dec-70 16:00:00 GMT"));
360         responseHeader1.add(new NameValuePair("Set-Cookie", "cookie6=6;expires=Wed 01-Dec-71 16:00:00 GMT"));
361         getMockWebConnection().setResponse(URL_FIRST, HTML_ALERT_COOKIE, 200, "OK", MimeType.TEXT_HTML,
362                 responseHeader1);
363 
364         loadPage2(URL_FIRST, StandardCharsets.ISO_8859_1);
365         verifyTitle2(getWebDriver(), getExpectedAlerts());
366     }
367 
368     /**
369      * Two digits years should be interpreted as 20xx if before 1970 and as 19xx otherwise.
370      * Same as the test before, only different formating was used.
371      * @throws Exception if the test fails
372      */
373     @Test
374     @Alerts("cookie1=1; cookie2=2; cookie3=3")
375     public void setCookieExpires_twoDigits2() throws Exception {
376         final List<NameValuePair> responseHeader1 = new ArrayList<>();
377         responseHeader1.add(new NameValuePair("Set-Cookie", "cookie1=1;expires=Sun,01 Dec 68 16:00:00 GMT"));
378         responseHeader1.add(new NameValuePair("Set-Cookie", "cookie2=2;expires=Thu,01 Dec 69 16:00:00 GMT"));
379         responseHeader1.add(new NameValuePair("Set-Cookie", "cookie3=3;expires=Mon,31 Dec 69 16:00:00 GMT"));
380         responseHeader1.add(new NameValuePair("Set-Cookie", "cookie4=4;expires=Thu,01 Jan 70 16:00:00 GMT"));
381         responseHeader1.add(new NameValuePair("Set-Cookie", "cookie5=5;expires=Tue,01 Dec 70 16:00:00 GMT"));
382         responseHeader1.add(new NameValuePair("Set-Cookie", "cookie6=6;expires=Wed,01 Dec 71 16:00:00 GMT"));
383         getMockWebConnection().setResponse(URL_FIRST, HTML_ALERT_COOKIE, 200, "OK", MimeType.TEXT_HTML,
384                 responseHeader1);
385 
386         loadPage2(URL_FIRST, StandardCharsets.ISO_8859_1);
387         verifyTitle2(getWebDriver(), getExpectedAlerts());
388     }
389 
390     /**
391      * Two digits years should be interpreted as 20xx if before 1970 and as 19xx otherwise,
392      * even with a quite strange date format.
393      * @throws Exception if the test fails
394      */
395     @Test
396     @Alerts("cookie1=1")
397     public void cookieExpires_TwoDigits3() throws Exception {
398         final List<NameValuePair> responseHeader1 = new ArrayList<>();
399         responseHeader1.add(new NameValuePair("Set-Cookie", "cookie1=1;expires=Sun-01 Dec 68 16:00:00 GMT"));
400         responseHeader1.add(new NameValuePair("Set-Cookie", "cookie6=6;expires=Wed-01 Dec 71 16:00:00 GMT"));
401         getMockWebConnection().setResponse(URL_FIRST, HTML_ALERT_COOKIE, 200, "OK", MimeType.TEXT_HTML,
402                 responseHeader1);
403 
404         loadPage2(URL_FIRST, StandardCharsets.ISO_8859_1);
405         verifyTitle2(getWebDriver(), getExpectedAlerts());
406     }
407 
408     /**
409      * @throws Exception if the test fails
410      */
411     @Test
412     @Alerts("cookie1=1")
413     public void cookieExpires_GMT() throws Exception {
414         final List<NameValuePair> responseHeader = new ArrayList<>();
415         responseHeader.add(new NameValuePair("Set-Cookie",
416                                                 "cookie1=1;expires=Sun Jan 20 2042 17:45:00 GMT+0800 (CST)"));
417         responseHeader.add(new NameValuePair("Set-Cookie",
418                                                 "cookie2=2;expires=Sun Jan 20 2004 17:45:00 GMT+0800 (CST)"));
419         getMockWebConnection().setResponse(URL_FIRST, HTML_ALERT_COOKIE, 200, "OK", MimeType.TEXT_HTML, responseHeader);
420 
421         loadPage2(URL_FIRST, StandardCharsets.ISO_8859_1);
422         verifyTitle2(getWebDriver(), getExpectedAlerts());
423     }
424 
425     /**
426      * Test some formating errors.
427      * @throws Exception if the test fails
428      */
429     @Test
430     @Alerts("fifth=5; first=1; fourth=4; second=2; sixth=6; third=3")
431     public void cookieExpires_badDateFormat() throws Exception {
432         final List<NameValuePair> responseHeader1 = new ArrayList<>();
433         responseHeader1.add(new NameValuePair("Set-Cookie", "first=1;expires=Thu 01-Dec-42 16:00:00 GMT"));
434         responseHeader1.add(new NameValuePair("Set-Cookie", "second=2;expires=Thu 01 Dec 42 16:00:00 GMT"));
435         responseHeader1.add(new NameValuePair("Set-Cookie", "third=3;expires=Thu, 01-Dec-42 16:00:00 GMT"));
436         responseHeader1.add(new NameValuePair("Set-Cookie", "fourth=4;expires=Thu, 01 Dec 42 16:00:00 GMT"));
437         responseHeader1.add(new NameValuePair("Set-Cookie", "fifth=5;expires=Thu,01-Dec-42 16:00:00 GMT"));
438         responseHeader1.add(new NameValuePair("Set-Cookie", "sixth=6;expires=Thu,01 Dec 42 16:00:00 GMT"));
439         getMockWebConnection().setResponse(URL_FIRST, HTML_ALERT_COOKIE, 200, "OK", MimeType.TEXT_HTML,
440                 responseHeader1);
441 
442         loadPage2(URL_FIRST, StandardCharsets.ISO_8859_1);
443         verifyTitle2(getWebDriver(), getExpectedAlerts());
444     }
445 
446     /**
447      * @throws Exception if the test fails
448      */
449     @Test
450     @Alerts("first=1")
451     public void cookieMaxAge() throws Exception {
452         final List<NameValuePair> responseHeader1 = new ArrayList<>();
453         responseHeader1.add(new NameValuePair("Set-Cookie", "first=1;max-age=1000"));
454         responseHeader1.add(new NameValuePair("Set-Cookie", "second=2;max-age=-1"));
455         getMockWebConnection().setResponse(URL_FIRST, HTML_ALERT_COOKIE, 200, "OK", MimeType.TEXT_HTML,
456                 responseHeader1);
457 
458         loadPage2(URL_FIRST, StandardCharsets.ISO_8859_1);
459         verifyTitle2(getWebDriver(), getExpectedAlerts());
460     }
461 
462     /**
463      * @throws Exception if the test fails
464      */
465     @Test
466     @Alerts({"first=1", ""})
467     public void cookieMaxAge_zeroDeletes() throws Exception {
468         List<NameValuePair> responseHeader1 = new ArrayList<>();
469         responseHeader1.add(new NameValuePair("Set-Cookie", "first=1;max-age=1000"));
470         getMockWebConnection().setResponse(URL_FIRST, HTML_ALERT_COOKIE, 200, "OK", MimeType.TEXT_HTML,
471                 responseHeader1);
472 
473         loadPage2(URL_FIRST, StandardCharsets.ISO_8859_1);
474         verifyTitle2(getWebDriver(), getExpectedAlerts()[0]);
475 
476         responseHeader1 = new ArrayList<>();
477         responseHeader1.add(new NameValuePair("Set-Cookie", "first=1;max-age=0"));
478         getMockWebConnection().setResponse(URL_FIRST, HTML_ALERT_COOKIE, 200, "OK", MimeType.TEXT_HTML,
479                 responseHeader1);
480 
481         loadPage2(URL_FIRST, StandardCharsets.ISO_8859_1);
482         verifyTitle2(getWebDriver(), getExpectedAlerts()[1]);
483     }
484 
485     /**
486      * @throws Exception if the test fails
487      */
488     @Test
489     @Alerts({"first=1", ""})
490     public void cookieMaxAge_negativeDeletes() throws Exception {
491         List<NameValuePair> responseHeader1 = new ArrayList<>();
492         responseHeader1.add(new NameValuePair("Set-Cookie", "first=1;max-age=1000"));
493         getMockWebConnection().setResponse(URL_FIRST, HTML_ALERT_COOKIE, 200, "OK", MimeType.TEXT_HTML,
494                 responseHeader1);
495 
496         loadPage2(URL_FIRST, StandardCharsets.ISO_8859_1);
497         verifyTitle2(getWebDriver(), getExpectedAlerts()[0]);
498 
499         responseHeader1 = new ArrayList<>();
500         responseHeader1.add(new NameValuePair("Set-Cookie", "first=1;max-age=-1"));
501         getMockWebConnection().setResponse(URL_FIRST, HTML_ALERT_COOKIE, 200, "OK", MimeType.TEXT_HTML,
502                 responseHeader1);
503 
504         loadPage2(URL_FIRST, StandardCharsets.ISO_8859_1);
505         verifyTitle2(getWebDriver(), getExpectedAlerts()[1]);
506     }
507 
508     /**
509      * @throws Exception if the test fails
510      */
511     @Test
512     @Alerts({"", "dog=dalmation"})
513     public void trailing_slash() throws Exception {
514         final String[] expectedAlerts = getExpectedAlerts();
515         final WebDriver driver = getWebDriver();
516 
517         setExpectedAlerts(expectedAlerts[0]);
518         loadPage2(HTML_ALERT_COOKIE, URL_SECOND);
519         verifyTitle2(getWebDriver(), getExpectedAlerts());
520 
521         setExpectedAlerts(expectedAlerts[1]);
522         driver.manage().addCookie(new org.openqa.selenium.Cookie("dog", "dalmation", "/second/"));
523         loadPage2(HTML_ALERT_COOKIE, URL_SECOND);
524         verifyTitle2(getWebDriver(), getExpectedAlerts());
525     }
526 
527     /**
528      * Regression test for bug 3032380: expired cookies shouldn't be returned.
529      * @throws Exception if the test fails
530      */
531     @Test
532     @Alerts({"Cookies: cookie1=value1; cookie2=value2", "Cookies: cookie2=value2"})
533     public void cookieExpiresAfterBeingSet() throws Exception {
534         final String html = DOCTYPE_HTML
535             + "<html><head><script>\n"
536             + LOG_TITLE_FUNCTION
537             + "  function f() {\n"
538             + "    log('Cookies: ' + document.cookie);\n"
539             + "  }\n"
540 
541             + "  function test() {\n"
542             + "    var date1 = new Date();\n"
543             + "    date1.setTime(date1.getTime() + 1000);\n"
544             + "    document.cookie = 'cookie1=value1; expires=' + date1.toGMTString() + '; path=/';\n"
545             + "    var date2 = new Date();\n"
546             + "    date2.setTime(date2.getTime() + 60 * 1000);\n"
547             + "    document.cookie = 'cookie2=value2; expires=' + date2.toGMTString() + '; path=/';\n"
548             + "    f();\n"
549             + "    setTimeout(f, 1500);\n"
550             + "  }\n"
551             + "</script></head><body onload='test()'>\n"
552             + "</body></html>";
553 
554         loadPage2(html);
555         verifyTitle2(DEFAULT_WAIT_TIME.multipliedBy(4), getWebDriver(), getExpectedAlerts());
556     }
557 
558     /**
559      * Regression test for bug 3181695 (introduced during 2.9-SNAPSHOT).
560      * @throws Exception if the test fails
561      */
562     @Test
563     public void removeExpiredCookies() throws Exception {
564         final List<NameValuePair> responseHeader = new ArrayList<>();
565         responseHeader.add(new NameValuePair("Set-Cookie", "key1=value1"));
566         responseHeader.add(new NameValuePair("Set-Cookie", "key2=value2"));
567         getMockWebConnection().setDefaultResponse(HTML_ALERT_COOKIE, 200, "OK", MimeType.TEXT_HTML, responseHeader);
568 
569         loadPage2(URL_FIRST, StandardCharsets.ISO_8859_1);
570         verifyTitle2(getWebDriver(), "key1=value1; key2=value2");
571 
572         responseHeader.clear();
573         responseHeader.add(new NameValuePair("Set-Cookie", "key1=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT"));
574         responseHeader.add(new NameValuePair("Set-Cookie", "key2=value2"));
575         getMockWebConnection().setDefaultResponse(HTML_ALERT_COOKIE, 200, "OK", MimeType.TEXT_HTML, responseHeader);
576 
577         loadPage2(URL_FIRST, StandardCharsets.ISO_8859_1);
578         verifyTitle2(getWebDriver(), "key2=value2");
579     }
580 
581     /**
582      * Response to / sets cookie to /foo/blah.
583      * @throws Exception if the test fails
584      */
585     @Test
586     @Alerts("first=1; second=2")
587     public void setCookieSubPath() throws Exception {
588         final List<NameValuePair> responseHeader1 = new ArrayList<>();
589         responseHeader1.add(new NameValuePair("Set-Cookie", "first=1;path=/foo/blah"));
590         responseHeader1.add(new NameValuePair("Set-Cookie", "second=2;path=/foo/blah/test"));
591         responseHeader1.add(new NameValuePair("Location", "/foo/blah/test"));
592 
593         getMockWebConnection().setDefaultResponse(HTML_ALERT_COOKIE);
594         getMockWebConnection().setResponse(URL_FIRST, "", 302, "Moved", MimeType.TEXT_HTML, responseHeader1);
595 
596         loadPage2(URL_FIRST, StandardCharsets.ISO_8859_1);
597         verifyTitle2(getWebDriver(), getExpectedAlerts());
598     }
599 
600     /**
601      * Response to /a/b sets cookie to /foo/blah.
602      * @throws Exception if the test fails
603      */
604     @Test
605     @Alerts("first=1; second=2")
606     public void setCookieDifferentPath() throws Exception {
607         final List<NameValuePair> responseHeader1 = new ArrayList<>();
608         responseHeader1.add(new NameValuePair("Set-Cookie", "first=1; path=/foo/blah"));
609         responseHeader1.add(new NameValuePair("Set-Cookie", "second=2; path=/foo/blah/test"));
610         responseHeader1.add(new NameValuePair("Set-Cookie", "third=3; path=/foo/other"));
611         responseHeader1.add(new NameValuePair("Set-Cookie", "fourth=4; path=/other/path"));
612         responseHeader1.add(new NameValuePair("Location", "/foo/blah/test"));
613 
614         getMockWebConnection().setDefaultResponse(HTML_ALERT_COOKIE);
615         final URL firstUrl = new URL(URL_FIRST, "/a/b");
616         getMockWebConnection().setResponse(firstUrl, "", 302, "Moved", MimeType.TEXT_HTML, responseHeader1);
617 
618         loadPage2(firstUrl, StandardCharsets.ISO_8859_1);
619         verifyTitle2(getWebDriver(), getExpectedAlerts());
620     }
621 
622     /**
623      * Response to /a/b sets cookie to /foo/blah.
624      * @throws Exception if the test fails
625      */
626     @Test
627     @Alerts("first=1; second=2")
628     public void setCookieDifferentPathSlashAtEnd() throws Exception {
629         final List<NameValuePair> responseHeader1 = new ArrayList<>();
630         responseHeader1.add(new NameValuePair("Set-Cookie", "first=1; path=/foo"));
631         responseHeader1.add(new NameValuePair("Set-Cookie", "second=2; path=/foo/"));
632         responseHeader1.add(new NameValuePair("Set-Cookie", "third=3; path=/foo/other"));
633         responseHeader1.add(new NameValuePair("Set-Cookie", "fourth=4; path=/other/path"));
634         responseHeader1.add(new NameValuePair("Location", "/foo/"));
635 
636         getMockWebConnection().setDefaultResponse(HTML_ALERT_COOKIE);
637         final URL firstUrl = new URL(URL_FIRST, "/a/b");
638         getMockWebConnection().setResponse(firstUrl, "", 302, "Moved", MimeType.TEXT_HTML, responseHeader1);
639 
640         loadPage2(firstUrl, StandardCharsets.ISO_8859_1);
641         verifyTitle2(getWebDriver(), getExpectedAlerts());
642     }
643 
644     /**
645      * Test for document.cookie for cookies expired after the page was loaded.
646      * @throws Exception if the test fails
647      */
648     @Test
649     @Alerts({"cookies: first=1", "cookies: "})
650     public void setCookieDuring302() throws Exception {
651         final String html = DOCTYPE_HTML
652                 + "<html><head>\n"
653                 + "<script>\n"
654                 + LOG_TITLE_FUNCTION
655                 + "  function alertCookies() {\n"
656                 + "    log('cookies: ' + document.cookie);\n"
657                 + "  }\n"
658 
659                 + "  function test() {\n"
660                 + "    alertCookies();\n"
661                 + "    window.setTimeout(alertCookies, 2500);\n"
662                 + "  }\n"
663                 + "</script></head><body onload='test()'>\n"
664                 + "</body></html>";
665 
666         getMockWebConnection().setDefaultResponse(html);
667         final URL firstUrl = new URL(URL_FIRST, "/foo/test.html");
668 
669         final List<NameValuePair> responseHeader1 = new ArrayList<>();
670         final String expires = DateUtils.formatDate(new Date(System.currentTimeMillis() + 2_000));
671         responseHeader1.add(new NameValuePair("Set-Cookie", "first=1; expires=" + expires + "; path=/foo"));
672         responseHeader1.add(new NameValuePair("Location", "/foo/content.html"));
673         getMockWebConnection().setResponse(firstUrl, "", 302, "Moved", MimeType.TEXT_HTML, responseHeader1);
674 
675         loadPage2(firstUrl, StandardCharsets.ISO_8859_1);
676         verifyTitle2(DEFAULT_WAIT_TIME.multipliedBy(4), getWebDriver(), getExpectedAlerts());
677     }
678 
679     /**
680      * HttpOnly cookies should not be available from JS.
681      * @throws Exception if the test fails
682      */
683     @Test
684     @Alerts("second=2")
685     public void httpOnly() throws Exception {
686         final List<NameValuePair> responseHeader = new ArrayList<>();
687         responseHeader.add(new NameValuePair("Set-Cookie", "first=1; path=/; HttpOnly"));
688         responseHeader.add(new NameValuePair("Set-Cookie", "second=2; path=/;"));
689 
690         getMockWebConnection().setDefaultResponse("");
691         getMockWebConnection().setResponse(URL_FIRST, HTML_ALERT_COOKIE, 200, "OK", MimeType.TEXT_HTML, responseHeader);
692 
693         final WebDriver driver = loadPage2(URL_FIRST, StandardCharsets.ISO_8859_1);
694         verifyTitle2(getWebDriver(), getExpectedAlerts());
695 
696         loadPage2(URL_FIRST, StandardCharsets.ISO_8859_1);
697         verifyTitle2(getWebDriver(), getExpectedAlerts());
698         driver.get(URL_FIRST + "foo");
699 
700         final Map<String, String> lastHeaders = getMockWebConnection().getLastAdditionalHeaders();
701 
702         // strange check, but there is no order
703         final String lastCookies = lastHeaders.get(HttpHeader.COOKIE);
704         assertEquals(17, lastCookies.length());
705         assertTrue("lastCookies: " + lastCookies, lastCookies.contains("first=1")
706                     && lastCookies.contains("second=2")
707                     && lastCookies.contains("; "));
708 
709         if (driver instanceof HtmlUnitDriver) {
710             final CookieManager mgr = getWebClient().getCookieManager();
711             assertEquals(2, mgr.getCookies().size());
712             assertTrue(mgr.getCookie("first").isHttpOnly());
713             assertFalse(mgr.getCookie("second").isHttpOnly());
714         }
715     }
716 
717     /**
718      * @throws Exception if the test fails
719      */
720     @Test
721     @Alerts({"first=1; second=2; third=3", "26"})
722     public void sameSite() throws Exception {
723         final List<NameValuePair> responseHeader = new ArrayList<>();
724         responseHeader.add(new NameValuePair("Set-Cookie", "first=1; path=/; SameSite=None; Secure"));
725         responseHeader.add(new NameValuePair("Set-Cookie", "second=2; path=/; SameSite=Lax"));
726         responseHeader.add(new NameValuePair("Set-Cookie", "third=3; path=/; SameSite=Strict"));
727 
728         getMockWebConnection().setDefaultResponse("");
729         getMockWebConnection().setResponse(URL_FIRST, HTML_ALERT_COOKIE, 200, "OK", MimeType.TEXT_HTML, responseHeader);
730 
731         final WebDriver driver = loadPage2(URL_FIRST, StandardCharsets.ISO_8859_1);
732         verifyTitle2(driver, getExpectedAlerts()[0]);
733         driver.get(URL_FIRST + "foo");
734 
735         final Map<String, String> lastHeaders = getMockWebConnection().getLastAdditionalHeaders();
736 
737         // strange check, but there is no order
738         final String lastCookies = lastHeaders.get(HttpHeader.COOKIE);
739         assertEquals(Integer.parseInt(getExpectedAlerts()[1]), lastCookies.length());
740         assertTrue("lastCookies: " + lastCookies,
741                     /* lastCookies.contains("first=1")
742                     && */ lastCookies.contains("second=2")
743                     && lastCookies.contains("third=3")
744                     && lastCookies.contains("; "));
745 
746         if (driver instanceof HtmlUnitDriver) {
747             final CookieManager mgr = getWebClient().getCookieManager();
748             assertEquals(3, mgr.getCookies().size());
749             assertEquals("none", mgr.getCookie("first").getSameSite());
750             assertEquals("lax", mgr.getCookie("second").getSameSite());
751             assertEquals("strict", mgr.getCookie("third").getSameSite());
752         }
753     }
754 
755     /**
756      * A cookie set with document.cookie without path applies only for the current path
757      * and overrides cookie previously set for this path.
758      * @throws Exception if the test fails
759      */
760     @Test
761     @Alerts("first=new")
762     public void cookieSetFromJSWithoutPathUsesCurrentLocation() throws Exception {
763         final List<NameValuePair> responseHeader1 = new ArrayList<>();
764         responseHeader1.add(new NameValuePair("Set-Cookie", "first=1"));
765 
766         final String html = DOCTYPE_HTML
767             + "<html>\n"
768             + "<head></head>\n"
769             + "<body><script>\n"
770             + "  document.cookie = 'first=new';\n"
771             + "  location.replace('/a/b/-');\n"
772             + "</script></body>\n"
773             + "</html>";
774 
775         getMockWebConnection().setDefaultResponse(HTML_ALERT_COOKIE);
776         final URL firstUrl = new URL(URL_FIRST, "/a/b");
777         getMockWebConnection().setResponse(firstUrl, html, 200, "Ok", MimeType.TEXT_HTML, responseHeader1);
778 
779         loadPage2(firstUrl, StandardCharsets.ISO_8859_1);
780         verifyTitle2(getWebDriver(), getExpectedAlerts());
781     }
782 
783     /**
784      * A cookie set with document.cookie without path applies only for the current path.
785      * @throws Exception if the test fails
786      */
787     @Test
788     @Alerts("first=1")
789     public void cookieSetFromJSWithoutPathUsesCurrentLocation2() throws Exception {
790         final List<NameValuePair> responseHeader1 = new ArrayList<>();
791         responseHeader1.add(new NameValuePair("Set-Cookie", "first=1; path=/c"));
792 
793         final String html = DOCTYPE_HTML
794             + "<html>\n"
795             + "<head></head>\n"
796             + "<body><script>\n"
797             + "  document.cookie = 'first=new';\n"
798             + "  location.replace('/c/test.html');\n"
799             + "</script></body>\n"
800             + "</html>";
801 
802         getMockWebConnection().setDefaultResponse(HTML_ALERT_COOKIE);
803         final URL firstUrl = new URL(URL_FIRST, "/a/b");
804         getMockWebConnection().setResponse(firstUrl, html, 200, "Ok", MimeType.TEXT_HTML, responseHeader1);
805 
806         loadPage2(firstUrl, StandardCharsets.ISO_8859_1);
807         verifyTitle2(getWebDriver(), getExpectedAlerts());
808     }
809 }