1// Copyright 2013 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package cookiejar
6
7import (
8	"fmt"
9	"net/http"
10	"net/url"
11	"slices"
12	"strings"
13	"testing"
14	"time"
15)
16
17// tNow is the synthetic current time used as now during testing.
18var tNow = time.Date(2013, 1, 1, 12, 0, 0, 0, time.UTC)
19
20// testPSL implements PublicSuffixList with just two rules: "co.uk"
21// and the default rule "*".
22// The implementation has two intentional bugs:
23//
24//	PublicSuffix("www.buggy.psl") == "xy"
25//	PublicSuffix("www2.buggy.psl") == "com"
26type testPSL struct{}
27
28func (testPSL) String() string {
29	return "testPSL"
30}
31func (testPSL) PublicSuffix(d string) string {
32	if d == "co.uk" || strings.HasSuffix(d, ".co.uk") {
33		return "co.uk"
34	}
35	if d == "www.buggy.psl" {
36		return "xy"
37	}
38	if d == "www2.buggy.psl" {
39		return "com"
40	}
41	return d[strings.LastIndex(d, ".")+1:]
42}
43
44// newTestJar creates an empty Jar with testPSL as the public suffix list.
45func newTestJar() *Jar {
46	jar, err := New(&Options{PublicSuffixList: testPSL{}})
47	if err != nil {
48		panic(err)
49	}
50	return jar
51}
52
53var hasDotSuffixTests = [...]struct {
54	s, suffix string
55}{
56	{"", ""},
57	{"", "."},
58	{"", "x"},
59	{".", ""},
60	{".", "."},
61	{".", ".."},
62	{".", "x"},
63	{".", "x."},
64	{".", ".x"},
65	{".", ".x."},
66	{"x", ""},
67	{"x", "."},
68	{"x", ".."},
69	{"x", "x"},
70	{"x", "x."},
71	{"x", ".x"},
72	{"x", ".x."},
73	{".x", ""},
74	{".x", "."},
75	{".x", ".."},
76	{".x", "x"},
77	{".x", "x."},
78	{".x", ".x"},
79	{".x", ".x."},
80	{"x.", ""},
81	{"x.", "."},
82	{"x.", ".."},
83	{"x.", "x"},
84	{"x.", "x."},
85	{"x.", ".x"},
86	{"x.", ".x."},
87	{"com", ""},
88	{"com", "m"},
89	{"com", "om"},
90	{"com", "com"},
91	{"com", ".com"},
92	{"com", "x.com"},
93	{"com", "xcom"},
94	{"com", "xorg"},
95	{"com", "org"},
96	{"com", "rg"},
97	{"foo.com", ""},
98	{"foo.com", "m"},
99	{"foo.com", "om"},
100	{"foo.com", "com"},
101	{"foo.com", ".com"},
102	{"foo.com", "o.com"},
103	{"foo.com", "oo.com"},
104	{"foo.com", "foo.com"},
105	{"foo.com", ".foo.com"},
106	{"foo.com", "x.foo.com"},
107	{"foo.com", "xfoo.com"},
108	{"foo.com", "xfoo.org"},
109	{"foo.com", "foo.org"},
110	{"foo.com", "oo.org"},
111	{"foo.com", "o.org"},
112	{"foo.com", ".org"},
113	{"foo.com", "org"},
114	{"foo.com", "rg"},
115}
116
117func TestHasDotSuffix(t *testing.T) {
118	for _, tc := range hasDotSuffixTests {
119		got := hasDotSuffix(tc.s, tc.suffix)
120		want := strings.HasSuffix(tc.s, "."+tc.suffix)
121		if got != want {
122			t.Errorf("s=%q, suffix=%q: got %v, want %v", tc.s, tc.suffix, got, want)
123		}
124	}
125}
126
127var canonicalHostTests = map[string]string{
128	"www.example.com":         "www.example.com",
129	"WWW.EXAMPLE.COM":         "www.example.com",
130	"wWw.eXAmple.CoM":         "www.example.com",
131	"www.example.com:80":      "www.example.com",
132	"192.168.0.10":            "192.168.0.10",
133	"192.168.0.5:8080":        "192.168.0.5",
134	"2001:4860:0:2001::68":    "2001:4860:0:2001::68",
135	"[2001:4860:0:::68]:8080": "2001:4860:0:::68",
136	"www.bücher.de":           "www.xn--bcher-kva.de",
137	"www.example.com.":        "www.example.com",
138	// TODO: Fix canonicalHost so that all of the following malformed
139	// domain names trigger an error. (This list is not exhaustive, e.g.
140	// malformed internationalized domain names are missing.)
141	".":                       "",
142	"..":                      ".",
143	"...":                     "..",
144	".net":                    ".net",
145	".net.":                   ".net",
146	"a..":                     "a.",
147	"b.a..":                   "b.a.",
148	"weird.stuff...":          "weird.stuff..",
149	"[bad.unmatched.bracket:": "error",
150}
151
152func TestCanonicalHost(t *testing.T) {
153	for h, want := range canonicalHostTests {
154		got, err := canonicalHost(h)
155		if want == "error" {
156			if err == nil {
157				t.Errorf("%q: got %q and nil error, want non-nil", h, got)
158			}
159			continue
160		}
161		if err != nil {
162			t.Errorf("%q: %v", h, err)
163			continue
164		}
165		if got != want {
166			t.Errorf("%q: got %q, want %q", h, got, want)
167			continue
168		}
169	}
170}
171
172var hasPortTests = map[string]bool{
173	"www.example.com":      false,
174	"www.example.com:80":   true,
175	"127.0.0.1":            false,
176	"127.0.0.1:8080":       true,
177	"2001:4860:0:2001::68": false,
178	"[2001::0:::68]:80":    true,
179}
180
181func TestHasPort(t *testing.T) {
182	for host, want := range hasPortTests {
183		if got := hasPort(host); got != want {
184			t.Errorf("%q: got %t, want %t", host, got, want)
185		}
186	}
187}
188
189var jarKeyTests = map[string]string{
190	"foo.www.example.com": "example.com",
191	"www.example.com":     "example.com",
192	"example.com":         "example.com",
193	"com":                 "com",
194	"foo.www.bbc.co.uk":   "bbc.co.uk",
195	"www.bbc.co.uk":       "bbc.co.uk",
196	"bbc.co.uk":           "bbc.co.uk",
197	"co.uk":               "co.uk",
198	"uk":                  "uk",
199	"192.168.0.5":         "192.168.0.5",
200	"www.buggy.psl":       "www.buggy.psl",
201	"www2.buggy.psl":      "buggy.psl",
202	// The following are actual outputs of canonicalHost for
203	// malformed inputs to canonicalHost (see above).
204	"":              "",
205	".":             ".",
206	"..":            ".",
207	".net":          ".net",
208	"a.":            "a.",
209	"b.a.":          "a.",
210	"weird.stuff..": ".",
211}
212
213func TestJarKey(t *testing.T) {
214	for host, want := range jarKeyTests {
215		if got := jarKey(host, testPSL{}); got != want {
216			t.Errorf("%q: got %q, want %q", host, got, want)
217		}
218	}
219}
220
221var jarKeyNilPSLTests = map[string]string{
222	"foo.www.example.com": "example.com",
223	"www.example.com":     "example.com",
224	"example.com":         "example.com",
225	"com":                 "com",
226	"foo.www.bbc.co.uk":   "co.uk",
227	"www.bbc.co.uk":       "co.uk",
228	"bbc.co.uk":           "co.uk",
229	"co.uk":               "co.uk",
230	"uk":                  "uk",
231	"192.168.0.5":         "192.168.0.5",
232	// The following are actual outputs of canonicalHost for
233	// malformed inputs to canonicalHost.
234	"":              "",
235	".":             ".",
236	"..":            "..",
237	".net":          ".net",
238	"a.":            "a.",
239	"b.a.":          "a.",
240	"weird.stuff..": "stuff..",
241}
242
243func TestJarKeyNilPSL(t *testing.T) {
244	for host, want := range jarKeyNilPSLTests {
245		if got := jarKey(host, nil); got != want {
246			t.Errorf("%q: got %q, want %q", host, got, want)
247		}
248	}
249}
250
251var isIPTests = map[string]bool{
252	"127.0.0.1":            true,
253	"1.2.3.4":              true,
254	"2001:4860:0:2001::68": true,
255	"::1%zone":             true,
256	"example.com":          false,
257	"1.1.1.300":            false,
258	"www.foo.bar.net":      false,
259	"123.foo.bar.net":      false,
260}
261
262func TestIsIP(t *testing.T) {
263	for host, want := range isIPTests {
264		if got := isIP(host); got != want {
265			t.Errorf("%q: got %t, want %t", host, got, want)
266		}
267	}
268}
269
270var defaultPathTests = map[string]string{
271	"/":           "/",
272	"/abc":        "/",
273	"/abc/":       "/abc",
274	"/abc/xyz":    "/abc",
275	"/abc/xyz/":   "/abc/xyz",
276	"/a/b/c.html": "/a/b",
277	"":            "/",
278	"strange":     "/",
279	"//":          "/",
280	"/a//b":       "/a/",
281	"/a/./b":      "/a/.",
282	"/a/../b":     "/a/..",
283}
284
285func TestDefaultPath(t *testing.T) {
286	for path, want := range defaultPathTests {
287		if got := defaultPath(path); got != want {
288			t.Errorf("%q: got %q, want %q", path, got, want)
289		}
290	}
291}
292
293var domainAndTypeTests = [...]struct {
294	host         string // host Set-Cookie header was received from
295	domain       string // domain attribute in Set-Cookie header
296	wantDomain   string // expected domain of cookie
297	wantHostOnly bool   // expected host-cookie flag
298	wantErr      error  // expected error
299}{
300	{"www.example.com", "", "www.example.com", true, nil},
301	{"127.0.0.1", "", "127.0.0.1", true, nil},
302	{"2001:4860:0:2001::68", "", "2001:4860:0:2001::68", true, nil},
303	{"www.example.com", "example.com", "example.com", false, nil},
304	{"www.example.com", ".example.com", "example.com", false, nil},
305	{"www.example.com", "www.example.com", "www.example.com", false, nil},
306	{"www.example.com", ".www.example.com", "www.example.com", false, nil},
307	{"foo.sso.example.com", "sso.example.com", "sso.example.com", false, nil},
308	{"bar.co.uk", "bar.co.uk", "bar.co.uk", false, nil},
309	{"foo.bar.co.uk", ".bar.co.uk", "bar.co.uk", false, nil},
310	{"127.0.0.1", "127.0.0.1", "127.0.0.1", true, nil},
311	{"2001:4860:0:2001::68", "2001:4860:0:2001::68", "2001:4860:0:2001::68", true, nil},
312	{"www.example.com", ".", "", false, errMalformedDomain},
313	{"www.example.com", "..", "", false, errMalformedDomain},
314	{"www.example.com", "other.com", "", false, errIllegalDomain},
315	{"www.example.com", "com", "", false, errIllegalDomain},
316	{"www.example.com", ".com", "", false, errIllegalDomain},
317	{"foo.bar.co.uk", ".co.uk", "", false, errIllegalDomain},
318	{"127.www.0.0.1", "127.0.0.1", "", false, errIllegalDomain},
319	{"com", "", "com", true, nil},
320	{"com", "com", "com", true, nil},
321	{"com", ".com", "com", true, nil},
322	{"co.uk", "", "co.uk", true, nil},
323	{"co.uk", "co.uk", "co.uk", true, nil},
324	{"co.uk", ".co.uk", "co.uk", true, nil},
325}
326
327func TestDomainAndType(t *testing.T) {
328	jar := newTestJar()
329	for _, tc := range domainAndTypeTests {
330		domain, hostOnly, err := jar.domainAndType(tc.host, tc.domain)
331		if err != tc.wantErr {
332			t.Errorf("%q/%q: got %q error, want %v",
333				tc.host, tc.domain, err, tc.wantErr)
334			continue
335		}
336		if err != nil {
337			continue
338		}
339		if domain != tc.wantDomain || hostOnly != tc.wantHostOnly {
340			t.Errorf("%q/%q: got %q/%t want %q/%t",
341				tc.host, tc.domain, domain, hostOnly,
342				tc.wantDomain, tc.wantHostOnly)
343		}
344	}
345}
346
347// expiresIn creates an expires attribute delta seconds from tNow.
348func expiresIn(delta int) string {
349	t := tNow.Add(time.Duration(delta) * time.Second)
350	return "expires=" + t.Format(time.RFC1123)
351}
352
353// mustParseURL parses s to a URL and panics on error.
354func mustParseURL(s string) *url.URL {
355	u, err := url.Parse(s)
356	if err != nil || u.Scheme == "" || u.Host == "" {
357		panic(fmt.Sprintf("Unable to parse URL %s.", s))
358	}
359	return u
360}
361
362// jarTest encapsulates the following actions on a jar:
363//  1. Perform SetCookies with fromURL and the cookies from setCookies.
364//     (Done at time tNow + 0 ms.)
365//  2. Check that the entries in the jar matches content.
366//     (Done at time tNow + 1001 ms.)
367//  3. For each query in tests: Check that Cookies with toURL yields the
368//     cookies in want.
369//     (Query n done at tNow + (n+2)*1001 ms.)
370type jarTest struct {
371	description string   // The description of what this test is supposed to test
372	fromURL     string   // The full URL of the request from which Set-Cookie headers where received
373	setCookies  []string // All the cookies received from fromURL
374	content     string   // The whole (non-expired) content of the jar
375	queries     []query  // Queries to test the Jar.Cookies method
376}
377
378// query contains one test of the cookies returned from Jar.Cookies.
379type query struct {
380	toURL string // the URL in the Cookies call
381	want  string // the expected list of cookies (order matters)
382}
383
384// run runs the jarTest.
385func (test jarTest) run(t *testing.T, jar *Jar) {
386	now := tNow
387
388	// Populate jar with cookies.
389	setCookies := make([]*http.Cookie, len(test.setCookies))
390	for i, cs := range test.setCookies {
391		cookies := (&http.Response{Header: http.Header{"Set-Cookie": {cs}}}).Cookies()
392		if len(cookies) != 1 {
393			panic(fmt.Sprintf("Wrong cookie line %q: %#v", cs, cookies))
394		}
395		setCookies[i] = cookies[0]
396	}
397	jar.setCookies(mustParseURL(test.fromURL), setCookies, now)
398	now = now.Add(1001 * time.Millisecond)
399
400	// Serialize non-expired entries in the form "name1=val1 name2=val2".
401	var cs []string
402	for _, submap := range jar.entries {
403		for _, cookie := range submap {
404			if !cookie.Expires.After(now) {
405				continue
406			}
407
408			v := cookie.Value
409			if strings.ContainsAny(v, " ,") || cookie.Quoted {
410				v = `"` + v + `"`
411			}
412			cs = append(cs, cookie.Name+"="+v)
413		}
414	}
415	slices.Sort(cs)
416	got := strings.Join(cs, " ")
417
418	// Make sure jar content matches our expectations.
419	if got != test.content {
420		t.Errorf("Test %q Content\ngot  %q\nwant %q",
421			test.description, got, test.content)
422	}
423
424	// Test different calls to Cookies.
425	for i, query := range test.queries {
426		now = now.Add(1001 * time.Millisecond)
427		var s []string
428		for _, c := range jar.cookies(mustParseURL(query.toURL), now) {
429			s = append(s, c.String())
430		}
431		if got := strings.Join(s, " "); got != query.want {
432			t.Errorf("Test %q #%d\ngot  %q\nwant %q", test.description, i, got, query.want)
433		}
434	}
435}
436
437// basicsTests contains fundamental tests. Each jarTest has to be performed on
438// a fresh, empty Jar.
439var basicsTests = [...]jarTest{
440	{
441		"Retrieval of a plain host cookie.",
442		"http://www.host.test/",
443		[]string{"A=a"},
444		"A=a",
445		[]query{
446			{"http://www.host.test", "A=a"},
447			{"http://www.host.test/", "A=a"},
448			{"http://www.host.test/some/path", "A=a"},
449			{"https://www.host.test", "A=a"},
450			{"https://www.host.test/", "A=a"},
451			{"https://www.host.test/some/path", "A=a"},
452			{"ftp://www.host.test", ""},
453			{"ftp://www.host.test/", ""},
454			{"ftp://www.host.test/some/path", ""},
455			{"http://www.other.org", ""},
456			{"http://sibling.host.test", ""},
457			{"http://deep.www.host.test", ""},
458		},
459	},
460	{
461		"Secure cookies are not returned to http.",
462		"http://www.host.test/",
463		[]string{"A=a; secure"},
464		"A=a",
465		[]query{
466			{"http://www.host.test", ""},
467			{"http://www.host.test/", ""},
468			{"http://www.host.test/some/path", ""},
469			{"https://www.host.test", "A=a"},
470			{"https://www.host.test/", "A=a"},
471			{"https://www.host.test/some/path", "A=a"},
472		},
473	},
474	{
475		"Explicit path.",
476		"http://www.host.test/",
477		[]string{"A=a; path=/some/path"},
478		"A=a",
479		[]query{
480			{"http://www.host.test", ""},
481			{"http://www.host.test/", ""},
482			{"http://www.host.test/some", ""},
483			{"http://www.host.test/some/", ""},
484			{"http://www.host.test/some/path", "A=a"},
485			{"http://www.host.test/some/paths", ""},
486			{"http://www.host.test/some/path/foo", "A=a"},
487			{"http://www.host.test/some/path/foo/", "A=a"},
488		},
489	},
490	{
491		"Implicit path #1: path is a directory.",
492		"http://www.host.test/some/path/",
493		[]string{"A=a"},
494		"A=a",
495		[]query{
496			{"http://www.host.test", ""},
497			{"http://www.host.test/", ""},
498			{"http://www.host.test/some", ""},
499			{"http://www.host.test/some/", ""},
500			{"http://www.host.test/some/path", "A=a"},
501			{"http://www.host.test/some/paths", ""},
502			{"http://www.host.test/some/path/foo", "A=a"},
503			{"http://www.host.test/some/path/foo/", "A=a"},
504		},
505	},
506	{
507		"Implicit path #2: path is not a directory.",
508		"http://www.host.test/some/path/index.html",
509		[]string{"A=a"},
510		"A=a",
511		[]query{
512			{"http://www.host.test", ""},
513			{"http://www.host.test/", ""},
514			{"http://www.host.test/some", ""},
515			{"http://www.host.test/some/", ""},
516			{"http://www.host.test/some/path", "A=a"},
517			{"http://www.host.test/some/paths", ""},
518			{"http://www.host.test/some/path/foo", "A=a"},
519			{"http://www.host.test/some/path/foo/", "A=a"},
520		},
521	},
522	{
523		"Implicit path #3: no path in URL at all.",
524		"http://www.host.test",
525		[]string{"A=a"},
526		"A=a",
527		[]query{
528			{"http://www.host.test", "A=a"},
529			{"http://www.host.test/", "A=a"},
530			{"http://www.host.test/some/path", "A=a"},
531		},
532	},
533	{
534		"Cookies are sorted by path length.",
535		"http://www.host.test/",
536		[]string{
537			"A=a; path=/foo/bar",
538			"B=b; path=/foo/bar/baz/qux",
539			"C=c; path=/foo/bar/baz",
540			"D=d; path=/foo"},
541		"A=a B=b C=c D=d",
542		[]query{
543			{"http://www.host.test/foo/bar/baz/qux", "B=b C=c A=a D=d"},
544			{"http://www.host.test/foo/bar/baz/", "C=c A=a D=d"},
545			{"http://www.host.test/foo/bar", "A=a D=d"},
546		},
547	},
548	{
549		"Creation time determines sorting on same length paths.",
550		"http://www.host.test/",
551		[]string{
552			"A=a; path=/foo/bar",
553			"X=x; path=/foo/bar",
554			"Y=y; path=/foo/bar/baz/qux",
555			"B=b; path=/foo/bar/baz/qux",
556			"C=c; path=/foo/bar/baz",
557			"W=w; path=/foo/bar/baz",
558			"Z=z; path=/foo",
559			"D=d; path=/foo"},
560		"A=a B=b C=c D=d W=w X=x Y=y Z=z",
561		[]query{
562			{"http://www.host.test/foo/bar/baz/qux", "Y=y B=b C=c W=w A=a X=x Z=z D=d"},
563			{"http://www.host.test/foo/bar/baz/", "C=c W=w A=a X=x Z=z D=d"},
564			{"http://www.host.test/foo/bar", "A=a X=x Z=z D=d"},
565		},
566	},
567	{
568		"Sorting of same-name cookies.",
569		"http://www.host.test/",
570		[]string{
571			"A=1; path=/",
572			"A=2; path=/path",
573			"A=3; path=/quux",
574			"A=4; path=/path/foo",
575			"A=5; domain=.host.test; path=/path",
576			"A=6; domain=.host.test; path=/quux",
577			"A=7; domain=.host.test; path=/path/foo",
578		},
579		"A=1 A=2 A=3 A=4 A=5 A=6 A=7",
580		[]query{
581			{"http://www.host.test/path", "A=2 A=5 A=1"},
582			{"http://www.host.test/path/foo", "A=4 A=7 A=2 A=5 A=1"},
583		},
584	},
585	{
586		"Disallow domain cookie on public suffix.",
587		"http://www.bbc.co.uk",
588		[]string{
589			"a=1",
590			"b=2; domain=co.uk",
591		},
592		"a=1",
593		[]query{{"http://www.bbc.co.uk", "a=1"}},
594	},
595	{
596		"Host cookie on IP.",
597		"http://192.168.0.10",
598		[]string{"a=1"},
599		"a=1",
600		[]query{{"http://192.168.0.10", "a=1"}},
601	},
602	{
603		"Domain cookies on IP.",
604		"http://192.168.0.10",
605		[]string{
606			"a=1; domain=192.168.0.10",  // allowed
607			"b=2; domain=172.31.9.9",    // rejected, can't set cookie for other IP
608			"c=3; domain=.192.168.0.10", // rejected like in most browsers
609		},
610		"a=1",
611		[]query{
612			{"http://192.168.0.10", "a=1"},
613			{"http://172.31.9.9", ""},
614			{"http://www.fancy.192.168.0.10", ""},
615		},
616	},
617	{
618		"Port is ignored #1.",
619		"http://www.host.test/",
620		[]string{"a=1"},
621		"a=1",
622		[]query{
623			{"http://www.host.test", "a=1"},
624			{"http://www.host.test:8080/", "a=1"},
625		},
626	},
627	{
628		"Port is ignored #2.",
629		"http://www.host.test:8080/",
630		[]string{"a=1"},
631		"a=1",
632		[]query{
633			{"http://www.host.test", "a=1"},
634			{"http://www.host.test:8080/", "a=1"},
635			{"http://www.host.test:1234/", "a=1"},
636		},
637	},
638	{
639		"IPv6 zone is not treated as a host.",
640		"https://example.com/",
641		[]string{"a=1"},
642		"a=1",
643		[]query{
644			{"https://[::1%25.example.com]:80/", ""},
645		},
646	},
647	{
648		"Retrieval of cookies with quoted values", // issue #46443
649		"http://www.host.test/",
650		[]string{
651			`cookie-1="quoted"`,
652			`cookie-2="quoted with spaces"`,
653			`cookie-3="quoted,with,commas"`,
654			`cookie-4= ,`,
655		},
656		`cookie-1="quoted" cookie-2="quoted with spaces" cookie-3="quoted,with,commas" cookie-4=" ,"`,
657		[]query{
658			{
659				"http://www.host.test",
660				`cookie-1="quoted" cookie-2="quoted with spaces" cookie-3="quoted,with,commas" cookie-4=" ,"`,
661			},
662		},
663	},
664}
665
666func TestBasics(t *testing.T) {
667	for _, test := range basicsTests {
668		jar := newTestJar()
669		test.run(t, jar)
670	}
671}
672
673// updateAndDeleteTests contains jarTests which must be performed on the same
674// Jar.
675var updateAndDeleteTests = [...]jarTest{
676	{
677		"Set initial cookies.",
678		"http://www.host.test",
679		[]string{
680			"a=1",
681			"b=2; secure",
682			"c=3; httponly",
683			"d=4; secure; httponly"},
684		"a=1 b=2 c=3 d=4",
685		[]query{
686			{"http://www.host.test", "a=1 c=3"},
687			{"https://www.host.test", "a=1 b=2 c=3 d=4"},
688		},
689	},
690	{
691		"Update value via http.",
692		"http://www.host.test",
693		[]string{
694			"a=w",
695			"b=x; secure",
696			"c=y; httponly",
697			"d=z; secure; httponly"},
698		"a=w b=x c=y d=z",
699		[]query{
700			{"http://www.host.test", "a=w c=y"},
701			{"https://www.host.test", "a=w b=x c=y d=z"},
702		},
703	},
704	{
705		"Clear Secure flag from an http.",
706		"http://www.host.test/",
707		[]string{
708			"b=xx",
709			"d=zz; httponly"},
710		"a=w b=xx c=y d=zz",
711		[]query{{"http://www.host.test", "a=w b=xx c=y d=zz"}},
712	},
713	{
714		"Delete all.",
715		"http://www.host.test/",
716		[]string{
717			"a=1; max-Age=-1",                    // delete via MaxAge
718			"b=2; " + expiresIn(-10),             // delete via Expires
719			"c=2; max-age=-1; " + expiresIn(-10), // delete via both
720			"d=4; max-age=-1; " + expiresIn(10)}, // MaxAge takes precedence
721		"",
722		[]query{{"http://www.host.test", ""}},
723	},
724	{
725		"Refill #1.",
726		"http://www.host.test",
727		[]string{
728			"A=1",
729			"A=2; path=/foo",
730			"A=3; domain=.host.test",
731			"A=4; path=/foo; domain=.host.test"},
732		"A=1 A=2 A=3 A=4",
733		[]query{{"http://www.host.test/foo", "A=2 A=4 A=1 A=3"}},
734	},
735	{
736		"Refill #2.",
737		"http://www.google.com",
738		[]string{
739			"A=6",
740			"A=7; path=/foo",
741			"A=8; domain=.google.com",
742			"A=9; path=/foo; domain=.google.com"},
743		"A=1 A=2 A=3 A=4 A=6 A=7 A=8 A=9",
744		[]query{
745			{"http://www.host.test/foo", "A=2 A=4 A=1 A=3"},
746			{"http://www.google.com/foo", "A=7 A=9 A=6 A=8"},
747		},
748	},
749	{
750		"Delete A7.",
751		"http://www.google.com",
752		[]string{"A=; path=/foo; max-age=-1"},
753		"A=1 A=2 A=3 A=4 A=6 A=8 A=9",
754		[]query{
755			{"http://www.host.test/foo", "A=2 A=4 A=1 A=3"},
756			{"http://www.google.com/foo", "A=9 A=6 A=8"},
757		},
758	},
759	{
760		"Delete A4.",
761		"http://www.host.test",
762		[]string{"A=; path=/foo; domain=host.test; max-age=-1"},
763		"A=1 A=2 A=3 A=6 A=8 A=9",
764		[]query{
765			{"http://www.host.test/foo", "A=2 A=1 A=3"},
766			{"http://www.google.com/foo", "A=9 A=6 A=8"},
767		},
768	},
769	{
770		"Delete A6.",
771		"http://www.google.com",
772		[]string{"A=; max-age=-1"},
773		"A=1 A=2 A=3 A=8 A=9",
774		[]query{
775			{"http://www.host.test/foo", "A=2 A=1 A=3"},
776			{"http://www.google.com/foo", "A=9 A=8"},
777		},
778	},
779	{
780		"Delete A3.",
781		"http://www.host.test",
782		[]string{"A=; domain=host.test; max-age=-1"},
783		"A=1 A=2 A=8 A=9",
784		[]query{
785			{"http://www.host.test/foo", "A=2 A=1"},
786			{"http://www.google.com/foo", "A=9 A=8"},
787		},
788	},
789	{
790		"No cross-domain delete.",
791		"http://www.host.test",
792		[]string{
793			"A=; domain=google.com; max-age=-1",
794			"A=; path=/foo; domain=google.com; max-age=-1"},
795		"A=1 A=2 A=8 A=9",
796		[]query{
797			{"http://www.host.test/foo", "A=2 A=1"},
798			{"http://www.google.com/foo", "A=9 A=8"},
799		},
800	},
801	{
802		"Delete A8 and A9.",
803		"http://www.google.com",
804		[]string{
805			"A=; domain=google.com; max-age=-1",
806			"A=; path=/foo; domain=google.com; max-age=-1"},
807		"A=1 A=2",
808		[]query{
809			{"http://www.host.test/foo", "A=2 A=1"},
810			{"http://www.google.com/foo", ""},
811		},
812	},
813}
814
815func TestUpdateAndDelete(t *testing.T) {
816	jar := newTestJar()
817	for _, test := range updateAndDeleteTests {
818		test.run(t, jar)
819	}
820}
821
822func TestExpiration(t *testing.T) {
823	jar := newTestJar()
824	jarTest{
825		"Expiration.",
826		"http://www.host.test",
827		[]string{
828			"a=1",
829			"b=2; max-age=3",
830			"c=3; " + expiresIn(3),
831			"d=4; max-age=5",
832			"e=5; " + expiresIn(5),
833			"f=6; max-age=100",
834		},
835		"a=1 b=2 c=3 d=4 e=5 f=6", // executed at t0 + 1001 ms
836		[]query{
837			{"http://www.host.test", "a=1 b=2 c=3 d=4 e=5 f=6"}, // t0 + 2002 ms
838			{"http://www.host.test", "a=1 d=4 e=5 f=6"},         // t0 + 3003 ms
839			{"http://www.host.test", "a=1 d=4 e=5 f=6"},         // t0 + 4004 ms
840			{"http://www.host.test", "a=1 f=6"},                 // t0 + 5005 ms
841			{"http://www.host.test", "a=1 f=6"},                 // t0 + 6006 ms
842		},
843	}.run(t, jar)
844}
845
846//
847// Tests derived from Chromium's cookie_store_unittest.h.
848//
849
850// See http://src.chromium.org/viewvc/chrome/trunk/src/net/cookies/cookie_store_unittest.h?revision=159685&content-type=text/plain
851// Some of the original tests are in a bad condition (e.g.
852// DomainWithTrailingDotTest) or are not RFC 6265 conforming (e.g.
853// TestNonDottedAndTLD #1 and #6) and have not been ported.
854
855// chromiumBasicsTests contains fundamental tests. Each jarTest has to be
856// performed on a fresh, empty Jar.
857var chromiumBasicsTests = [...]jarTest{
858	{
859		"DomainWithTrailingDotTest.",
860		"http://www.google.com/",
861		[]string{
862			"a=1; domain=.www.google.com.",
863			"b=2; domain=.www.google.com.."},
864		"",
865		[]query{
866			{"http://www.google.com", ""},
867		},
868	},
869	{
870		"ValidSubdomainTest #1.",
871		"http://a.b.c.d.com",
872		[]string{
873			"a=1; domain=.a.b.c.d.com",
874			"b=2; domain=.b.c.d.com",
875			"c=3; domain=.c.d.com",
876			"d=4; domain=.d.com"},
877		"a=1 b=2 c=3 d=4",
878		[]query{
879			{"http://a.b.c.d.com", "a=1 b=2 c=3 d=4"},
880			{"http://b.c.d.com", "b=2 c=3 d=4"},
881			{"http://c.d.com", "c=3 d=4"},
882			{"http://d.com", "d=4"},
883		},
884	},
885	{
886		"ValidSubdomainTest #2.",
887		"http://a.b.c.d.com",
888		[]string{
889			"a=1; domain=.a.b.c.d.com",
890			"b=2; domain=.b.c.d.com",
891			"c=3; domain=.c.d.com",
892			"d=4; domain=.d.com",
893			"X=bcd; domain=.b.c.d.com",
894			"X=cd; domain=.c.d.com"},
895		"X=bcd X=cd a=1 b=2 c=3 d=4",
896		[]query{
897			{"http://b.c.d.com", "b=2 c=3 d=4 X=bcd X=cd"},
898			{"http://c.d.com", "c=3 d=4 X=cd"},
899		},
900	},
901	{
902		"InvalidDomainTest #1.",
903		"http://foo.bar.com",
904		[]string{
905			"a=1; domain=.yo.foo.bar.com",
906			"b=2; domain=.foo.com",
907			"c=3; domain=.bar.foo.com",
908			"d=4; domain=.foo.bar.com.net",
909			"e=5; domain=ar.com",
910			"f=6; domain=.",
911			"g=7; domain=/",
912			"h=8; domain=http://foo.bar.com",
913			"i=9; domain=..foo.bar.com",
914			"j=10; domain=..bar.com",
915			"k=11; domain=.foo.bar.com?blah",
916			"l=12; domain=.foo.bar.com/blah",
917			"m=12; domain=.foo.bar.com:80",
918			"n=14; domain=.foo.bar.com:",
919			"o=15; domain=.foo.bar.com#sup",
920		},
921		"", // Jar is empty.
922		[]query{{"http://foo.bar.com", ""}},
923	},
924	{
925		"InvalidDomainTest #2.",
926		"http://foo.com.com",
927		[]string{"a=1; domain=.foo.com.com.com"},
928		"",
929		[]query{{"http://foo.bar.com", ""}},
930	},
931	{
932		"DomainWithoutLeadingDotTest #1.",
933		"http://manage.hosted.filefront.com",
934		[]string{"a=1; domain=filefront.com"},
935		"a=1",
936		[]query{{"http://www.filefront.com", "a=1"}},
937	},
938	{
939		"DomainWithoutLeadingDotTest #2.",
940		"http://www.google.com",
941		[]string{"a=1; domain=www.google.com"},
942		"a=1",
943		[]query{
944			{"http://www.google.com", "a=1"},
945			{"http://sub.www.google.com", "a=1"},
946			{"http://something-else.com", ""},
947		},
948	},
949	{
950		"CaseInsensitiveDomainTest.",
951		"http://www.google.com",
952		[]string{
953			"a=1; domain=.GOOGLE.COM",
954			"b=2; domain=.www.gOOgLE.coM"},
955		"a=1 b=2",
956		[]query{{"http://www.google.com", "a=1 b=2"}},
957	},
958	{
959		"TestIpAddress #1.",
960		"http://1.2.3.4/foo",
961		[]string{"a=1; path=/"},
962		"a=1",
963		[]query{{"http://1.2.3.4/foo", "a=1"}},
964	},
965	{
966		"TestIpAddress #2.",
967		"http://1.2.3.4/foo",
968		[]string{
969			"a=1; domain=.1.2.3.4",
970			"b=2; domain=.3.4"},
971		"",
972		[]query{{"http://1.2.3.4/foo", ""}},
973	},
974	{
975		"TestIpAddress #3.",
976		"http://1.2.3.4/foo",
977		[]string{"a=1; domain=1.2.3.3"},
978		"",
979		[]query{{"http://1.2.3.4/foo", ""}},
980	},
981	{
982		"TestIpAddress #4.",
983		"http://1.2.3.4/foo",
984		[]string{"a=1; domain=1.2.3.4"},
985		"a=1",
986		[]query{{"http://1.2.3.4/foo", "a=1"}},
987	},
988	{
989		"TestNonDottedAndTLD #2.",
990		"http://com./index.html",
991		[]string{"a=1"},
992		"a=1",
993		[]query{
994			{"http://com./index.html", "a=1"},
995			{"http://no-cookies.com./index.html", ""},
996		},
997	},
998	{
999		"TestNonDottedAndTLD #3.",
1000		"http://a.b",
1001		[]string{
1002			"a=1; domain=.b",
1003			"b=2; domain=b"},
1004		"",
1005		[]query{{"http://bar.foo", ""}},
1006	},
1007	{
1008		"TestNonDottedAndTLD #4.",
1009		"http://google.com",
1010		[]string{
1011			"a=1; domain=.com",
1012			"b=2; domain=com"},
1013		"",
1014		[]query{{"http://google.com", ""}},
1015	},
1016	{
1017		"TestNonDottedAndTLD #5.",
1018		"http://google.co.uk",
1019		[]string{
1020			"a=1; domain=.co.uk",
1021			"b=2; domain=.uk"},
1022		"",
1023		[]query{
1024			{"http://google.co.uk", ""},
1025			{"http://else.co.com", ""},
1026			{"http://else.uk", ""},
1027		},
1028	},
1029	{
1030		"TestHostEndsWithDot.",
1031		"http://www.google.com",
1032		[]string{
1033			"a=1",
1034			"b=2; domain=.www.google.com."},
1035		"a=1",
1036		[]query{{"http://www.google.com", "a=1"}},
1037	},
1038	{
1039		"PathTest",
1040		"http://www.google.izzle",
1041		[]string{"a=1; path=/wee"},
1042		"a=1",
1043		[]query{
1044			{"http://www.google.izzle/wee", "a=1"},
1045			{"http://www.google.izzle/wee/", "a=1"},
1046			{"http://www.google.izzle/wee/war", "a=1"},
1047			{"http://www.google.izzle/wee/war/more/more", "a=1"},
1048			{"http://www.google.izzle/weehee", ""},
1049			{"http://www.google.izzle/", ""},
1050		},
1051	},
1052}
1053
1054func TestChromiumBasics(t *testing.T) {
1055	for _, test := range chromiumBasicsTests {
1056		jar := newTestJar()
1057		test.run(t, jar)
1058	}
1059}
1060
1061// chromiumDomainTests contains jarTests which must be executed all on the
1062// same Jar.
1063var chromiumDomainTests = [...]jarTest{
1064	{
1065		"Fill #1.",
1066		"http://www.google.izzle",
1067		[]string{"A=B"},
1068		"A=B",
1069		[]query{{"http://www.google.izzle", "A=B"}},
1070	},
1071	{
1072		"Fill #2.",
1073		"http://www.google.izzle",
1074		[]string{"C=D; domain=.google.izzle"},
1075		"A=B C=D",
1076		[]query{{"http://www.google.izzle", "A=B C=D"}},
1077	},
1078	{
1079		"Verify A is a host cookie and not accessible from subdomain.",
1080		"http://unused.nil",
1081		[]string{},
1082		"A=B C=D",
1083		[]query{{"http://foo.www.google.izzle", "C=D"}},
1084	},
1085	{
1086		"Verify domain cookies are found on proper domain.",
1087		"http://www.google.izzle",
1088		[]string{"E=F; domain=.www.google.izzle"},
1089		"A=B C=D E=F",
1090		[]query{{"http://www.google.izzle", "A=B C=D E=F"}},
1091	},
1092	{
1093		"Leading dots in domain attributes are optional.",
1094		"http://www.google.izzle",
1095		[]string{"G=H; domain=www.google.izzle"},
1096		"A=B C=D E=F G=H",
1097		[]query{{"http://www.google.izzle", "A=B C=D E=F G=H"}},
1098	},
1099	{
1100		"Verify domain enforcement works #1.",
1101		"http://www.google.izzle",
1102		[]string{"K=L; domain=.bar.www.google.izzle"},
1103		"A=B C=D E=F G=H",
1104		[]query{{"http://bar.www.google.izzle", "C=D E=F G=H"}},
1105	},
1106	{
1107		"Verify domain enforcement works #2.",
1108		"http://unused.nil",
1109		[]string{},
1110		"A=B C=D E=F G=H",
1111		[]query{{"http://www.google.izzle", "A=B C=D E=F G=H"}},
1112	},
1113}
1114
1115func TestChromiumDomain(t *testing.T) {
1116	jar := newTestJar()
1117	for _, test := range chromiumDomainTests {
1118		test.run(t, jar)
1119	}
1120
1121}
1122
1123// chromiumDeletionTests must be performed all on the same Jar.
1124var chromiumDeletionTests = [...]jarTest{
1125	{
1126		"Create session cookie a1.",
1127		"http://www.google.com",
1128		[]string{"a=1"},
1129		"a=1",
1130		[]query{{"http://www.google.com", "a=1"}},
1131	},
1132	{
1133		"Delete sc a1 via MaxAge.",
1134		"http://www.google.com",
1135		[]string{"a=1; max-age=-1"},
1136		"",
1137		[]query{{"http://www.google.com", ""}},
1138	},
1139	{
1140		"Create session cookie b2.",
1141		"http://www.google.com",
1142		[]string{"b=2"},
1143		"b=2",
1144		[]query{{"http://www.google.com", "b=2"}},
1145	},
1146	{
1147		"Delete sc b2 via Expires.",
1148		"http://www.google.com",
1149		[]string{"b=2; " + expiresIn(-10)},
1150		"",
1151		[]query{{"http://www.google.com", ""}},
1152	},
1153	{
1154		"Create persistent cookie c3.",
1155		"http://www.google.com",
1156		[]string{"c=3; max-age=3600"},
1157		"c=3",
1158		[]query{{"http://www.google.com", "c=3"}},
1159	},
1160	{
1161		"Delete pc c3 via MaxAge.",
1162		"http://www.google.com",
1163		[]string{"c=3; max-age=-1"},
1164		"",
1165		[]query{{"http://www.google.com", ""}},
1166	},
1167	{
1168		"Create persistent cookie d4.",
1169		"http://www.google.com",
1170		[]string{"d=4; max-age=3600"},
1171		"d=4",
1172		[]query{{"http://www.google.com", "d=4"}},
1173	},
1174	{
1175		"Delete pc d4 via Expires.",
1176		"http://www.google.com",
1177		[]string{"d=4; " + expiresIn(-10)},
1178		"",
1179		[]query{{"http://www.google.com", ""}},
1180	},
1181}
1182
1183func TestChromiumDeletion(t *testing.T) {
1184	jar := newTestJar()
1185	for _, test := range chromiumDeletionTests {
1186		test.run(t, jar)
1187	}
1188}
1189
1190// domainHandlingTests tests and documents the rules for domain handling.
1191// Each test must be performed on an empty new Jar.
1192var domainHandlingTests = [...]jarTest{
1193	{
1194		"Host cookie",
1195		"http://www.host.test",
1196		[]string{"a=1"},
1197		"a=1",
1198		[]query{
1199			{"http://www.host.test", "a=1"},
1200			{"http://host.test", ""},
1201			{"http://bar.host.test", ""},
1202			{"http://foo.www.host.test", ""},
1203			{"http://other.test", ""},
1204			{"http://test", ""},
1205		},
1206	},
1207	{
1208		"Domain cookie #1",
1209		"http://www.host.test",
1210		[]string{"a=1; domain=host.test"},
1211		"a=1",
1212		[]query{
1213			{"http://www.host.test", "a=1"},
1214			{"http://host.test", "a=1"},
1215			{"http://bar.host.test", "a=1"},
1216			{"http://foo.www.host.test", "a=1"},
1217			{"http://other.test", ""},
1218			{"http://test", ""},
1219		},
1220	},
1221	{
1222		"Domain cookie #2",
1223		"http://www.host.test",
1224		[]string{"a=1; domain=.host.test"},
1225		"a=1",
1226		[]query{
1227			{"http://www.host.test", "a=1"},
1228			{"http://host.test", "a=1"},
1229			{"http://bar.host.test", "a=1"},
1230			{"http://foo.www.host.test", "a=1"},
1231			{"http://other.test", ""},
1232			{"http://test", ""},
1233		},
1234	},
1235	{
1236		"Host cookie on IDNA domain #1",
1237		"http://www.bücher.test",
1238		[]string{"a=1"},
1239		"a=1",
1240		[]query{
1241			{"http://www.bücher.test", "a=1"},
1242			{"http://www.xn--bcher-kva.test", "a=1"},
1243			{"http://bücher.test", ""},
1244			{"http://xn--bcher-kva.test", ""},
1245			{"http://bar.bücher.test", ""},
1246			{"http://bar.xn--bcher-kva.test", ""},
1247			{"http://foo.www.bücher.test", ""},
1248			{"http://foo.www.xn--bcher-kva.test", ""},
1249			{"http://other.test", ""},
1250			{"http://test", ""},
1251		},
1252	},
1253	{
1254		"Host cookie on IDNA domain #2",
1255		"http://www.xn--bcher-kva.test",
1256		[]string{"a=1"},
1257		"a=1",
1258		[]query{
1259			{"http://www.bücher.test", "a=1"},
1260			{"http://www.xn--bcher-kva.test", "a=1"},
1261			{"http://bücher.test", ""},
1262			{"http://xn--bcher-kva.test", ""},
1263			{"http://bar.bücher.test", ""},
1264			{"http://bar.xn--bcher-kva.test", ""},
1265			{"http://foo.www.bücher.test", ""},
1266			{"http://foo.www.xn--bcher-kva.test", ""},
1267			{"http://other.test", ""},
1268			{"http://test", ""},
1269		},
1270	},
1271	{
1272		"Domain cookie on IDNA domain #1",
1273		"http://www.bücher.test",
1274		[]string{"a=1; domain=xn--bcher-kva.test"},
1275		"a=1",
1276		[]query{
1277			{"http://www.bücher.test", "a=1"},
1278			{"http://www.xn--bcher-kva.test", "a=1"},
1279			{"http://bücher.test", "a=1"},
1280			{"http://xn--bcher-kva.test", "a=1"},
1281			{"http://bar.bücher.test", "a=1"},
1282			{"http://bar.xn--bcher-kva.test", "a=1"},
1283			{"http://foo.www.bücher.test", "a=1"},
1284			{"http://foo.www.xn--bcher-kva.test", "a=1"},
1285			{"http://other.test", ""},
1286			{"http://test", ""},
1287		},
1288	},
1289	{
1290		"Domain cookie on IDNA domain #2",
1291		"http://www.xn--bcher-kva.test",
1292		[]string{"a=1; domain=xn--bcher-kva.test"},
1293		"a=1",
1294		[]query{
1295			{"http://www.bücher.test", "a=1"},
1296			{"http://www.xn--bcher-kva.test", "a=1"},
1297			{"http://bücher.test", "a=1"},
1298			{"http://xn--bcher-kva.test", "a=1"},
1299			{"http://bar.bücher.test", "a=1"},
1300			{"http://bar.xn--bcher-kva.test", "a=1"},
1301			{"http://foo.www.bücher.test", "a=1"},
1302			{"http://foo.www.xn--bcher-kva.test", "a=1"},
1303			{"http://other.test", ""},
1304			{"http://test", ""},
1305		},
1306	},
1307	{
1308		"Host cookie on TLD.",
1309		"http://com",
1310		[]string{"a=1"},
1311		"a=1",
1312		[]query{
1313			{"http://com", "a=1"},
1314			{"http://any.com", ""},
1315			{"http://any.test", ""},
1316		},
1317	},
1318	{
1319		"Domain cookie on TLD becomes a host cookie.",
1320		"http://com",
1321		[]string{"a=1; domain=com"},
1322		"a=1",
1323		[]query{
1324			{"http://com", "a=1"},
1325			{"http://any.com", ""},
1326			{"http://any.test", ""},
1327		},
1328	},
1329	{
1330		"Host cookie on public suffix.",
1331		"http://co.uk",
1332		[]string{"a=1"},
1333		"a=1",
1334		[]query{
1335			{"http://co.uk", "a=1"},
1336			{"http://uk", ""},
1337			{"http://some.co.uk", ""},
1338			{"http://foo.some.co.uk", ""},
1339			{"http://any.uk", ""},
1340		},
1341	},
1342	{
1343		"Domain cookie on public suffix is ignored.",
1344		"http://some.co.uk",
1345		[]string{"a=1; domain=co.uk"},
1346		"",
1347		[]query{
1348			{"http://co.uk", ""},
1349			{"http://uk", ""},
1350			{"http://some.co.uk", ""},
1351			{"http://foo.some.co.uk", ""},
1352			{"http://any.uk", ""},
1353		},
1354	},
1355}
1356
1357func TestDomainHandling(t *testing.T) {
1358	for _, test := range domainHandlingTests {
1359		jar := newTestJar()
1360		test.run(t, jar)
1361	}
1362}
1363
1364func TestIssue19384(t *testing.T) {
1365	cookies := []*http.Cookie{{Name: "name", Value: "value"}}
1366	for _, host := range []string{"", ".", "..", "..."} {
1367		jar, _ := New(nil)
1368		u := &url.URL{Scheme: "http", Host: host, Path: "/"}
1369		if got := jar.Cookies(u); len(got) != 0 {
1370			t.Errorf("host %q, got %v", host, got)
1371		}
1372		jar.SetCookies(u, cookies)
1373		if got := jar.Cookies(u); len(got) != 1 || got[0].Value != "value" {
1374			t.Errorf("host %q, got %v", host, got)
1375		}
1376	}
1377}
1378