//go:build linux || freebsd // +build linux freebsd package cookie import ( "net/http" "net/url" "os" "path/filepath" "strconv" "testing" "time" ) // futureTimestamp returns a unix timestamp safely in the future. func futureTimestamp() int64 { return time.Now().Unix() + 86400*365 // ~1 year from now } // pastTimestamp returns a unix timestamp safely in the past. func pastTimestamp() int64 { return time.Now().Unix() - 86400 // 1 day ago } // fmtInt64 is a helper to avoid importing strconv in every test. func fmtInt64(v int64) string { return strconv.FormatInt(v, 10) } // writeTempFile writes content to a temp file inside t.TempDir() and returns its path. func writeTempFile(t *testing.T, dir, name, content string) string { t.Helper() path := filepath.Join(dir, name) if err := os.WriteFile(path, []byte(content), 0644); err != nil { t.Fatal(err) } return path } // netscapeLine builds a single Netscape-format cookie line. func netscapeLine(domain, domainFlag, path, secure, expires, name, value string) string { return domain + "\t" + domainFlag + "\t" + path + "\t" + secure + "\t" + expires + "\t" + name + "\t" + value } // --------------------------------------------------------------------------- // 1. TestNewJar // --------------------------------------------------------------------------- func TestNewJar(t *testing.T) { j, err := NewJar() if err != nil { t.Fatalf("NewJar() returned unexpected error: %v", err) } if j == nil { t.Fatal("NewJar() returned nil") } if j.Jar == nil { t.Fatal("NewJar().Jar is nil") } } // --------------------------------------------------------------------------- // 2. TestLoadFromFileEmptyPath // --------------------------------------------------------------------------- func TestLoadFromFileEmptyPath(t *testing.T) { j, err := NewJar() if err != nil { t.Fatal(err) } if err := j.LoadFromFile(""); err != nil { t.Fatalf("LoadFromFile(\"\") should return nil, got: %v", err) } } // --------------------------------------------------------------------------- // 3. TestLoadFromFileNotExist // --------------------------------------------------------------------------- func TestLoadFromFileNotExist(t *testing.T) { j, err := NewJar() if err != nil { t.Fatal(err) } if err := j.LoadFromFile("/tmp/this_file_does_not_exist_xxx.cookie"); err != nil { t.Fatalf("LoadFromFile(non-existent) should return nil, got: %v", err) } } // --------------------------------------------------------------------------- // 4. TestLoadFromFile // --------------------------------------------------------------------------- func TestLoadFromFile(t *testing.T) { dir := t.TempDir() ft := futureTimestamp() content := "# Netscape HTTP Cookie File\n" + netscapeLine(".example.com", "TRUE", "/", "FALSE", fmtInt64(ft), "test", "cookie_value") + "\n" path := writeTempFile(t, dir, "cookies.txt", content) j, err := NewJar() if err != nil { t.Fatal(err) } if err := j.LoadFromFile(path); err != nil { t.Fatalf("LoadFromFile() returned error: %v", err) } // Cookie set with scheme=http (because secure=FALSE), Domain=.example.com, Path=/ // Retrieve over http. u, _ := url.Parse("http://sub.example.com/path") cookies := j.Cookies(u) if len(cookies) != 1 { t.Fatalf("expected 1 cookie, got %d", len(cookies)) } if cookies[0].Name != "test" { t.Errorf("expected name 'test', got %q", cookies[0].Name) } if cookies[0].Value != "cookie_value" { t.Errorf("expected value 'cookie_value', got %q", cookies[0].Value) } _ = cookies[0].Path // cookiejar does not preserve Path on retrieval } // TestLoadFromFileHttps verifies that secure=TRUE results in an https cookie. func TestLoadFromFileHttps(t *testing.T) { dir := t.TempDir() ft := futureTimestamp() content := "# Netscape HTTP Cookie File\n" + netscapeLine(".secure.example.com", "TRUE", "/", "TRUE", fmtInt64(ft), "sess", "abc") + "\n" path := writeTempFile(t, dir, "cookies_secure.txt", content) j, err := NewJar() if err != nil { t.Fatal(err) } if err := j.LoadFromFile(path); err != nil { t.Fatalf("LoadFromFile() returned error: %v", err) } // Secure cookie — must be retrieved over HTTPS. u, _ := url.Parse("https://secure.example.com/path") cookies := j.Cookies(u) if len(cookies) != 1 { t.Fatalf("expected 1 secure cookie over HTTPS, got %d", len(cookies)) } if cookies[0].Name != "sess" { t.Errorf("expected name 'sess', got %q", cookies[0].Name) } if cookies[0].Value != "abc" { t.Errorf("expected value 'abc', got %q", cookies[0].Value) } } // --------------------------------------------------------------------------- // 5. TestLoadFromFileCommentsAndEmptyLines // --------------------------------------------------------------------------- func TestLoadFromFileCommentsAndEmptyLines(t *testing.T) { dir := t.TempDir() ft := futureTimestamp() content := "# Netscape HTTP Cookie File\n" + "# This is a comment that should be skipped\n" + "\n" + " \t \n" + netscapeLine(".alpha.com", "TRUE", "/", "FALSE", fmtInt64(ft), "a", "1") + "\n" + "# Another comment\n" + netscapeLine(".beta.com", "TRUE", "/", "FALSE", fmtInt64(ft), "b", "2") + "\n" path := writeTempFile(t, dir, "cookies.txt", content) j, err := NewJar() if err != nil { t.Fatal(err) } if err := j.LoadFromFile(path); err != nil { t.Fatalf("LoadFromFile() returned error: %v", err) } // Check alpha u, _ := url.Parse("http://alpha.com/path") cookies := j.Cookies(u) if len(cookies) != 1 { t.Fatalf("expected 1 cookie for alpha.com, got %d", len(cookies)) } if cookies[0].Name != "a" { t.Errorf("expected name 'a', got %q", cookies[0].Name) } // Check beta u2, _ := url.Parse("http://beta.com/path") cookies2 := j.Cookies(u2) if len(cookies2) != 1 { t.Fatalf("expected 1 cookie for beta.com, got %d", len(cookies2)) } if cookies2[0].Name != "b" { t.Errorf("expected name 'b', got %q", cookies2[0].Name) } } // --------------------------------------------------------------------------- // 6. TestLoadFromFileExpiredCookie // --------------------------------------------------------------------------- func TestLoadFromFileExpiredCookie(t *testing.T) { dir := t.TempDir() pt := pastTimestamp() ft := futureTimestamp() content := "# Netscape HTTP Cookie File\n" + netscapeLine(".expired.com", "TRUE", "/", "FALSE", fmtInt64(pt), "gone", "x") + "\n" + netscapeLine(".valid.com", "TRUE", "/", "FALSE", fmtInt64(ft), "alive", "y") + "\n" path := writeTempFile(t, dir, "cookies.txt", content) j, err := NewJar() if err != nil { t.Fatal(err) } if err := j.LoadFromFile(path); err != nil { t.Fatalf("LoadFromFile() returned error: %v", err) } // Expired cookie should not be stored. u, _ := url.Parse("http://expired.com/path") cookies := j.Cookies(u) if len(cookies) != 0 { t.Errorf("expected 0 cookies for expired domain, got %d", len(cookies)) } // Valid cookie should be stored. u2, _ := url.Parse("http://valid.com/path") cookies2 := j.Cookies(u2) if len(cookies2) != 1 { t.Fatalf("expected 1 cookie for valid domain, got %d", len(cookies2)) } if cookies2[0].Name != "alive" { t.Errorf("expected name 'alive', got %q", cookies2[0].Name) } } // TestLoadFromFileSessionCookie documents the current behaviour: when expires=0, // LoadFromFile does NOT skip it (because 0 > 0 is false), but time.Unix(0,0) // makes the cookiejar itself treat it as expired. Therefore the cookie is not // retrievable. This is a known limitation of the current implementation. func TestLoadFromFileSessionCookie(t *testing.T) { dir := t.TempDir() content := "# Netscape HTTP Cookie File\n" + netscapeLine(".session.com", "TRUE", "/", "FALSE", "0", "sess", "active") + "\n" path := writeTempFile(t, dir, "session.txt", content) j, err := NewJar() if err != nil { t.Fatal(err) } if err := j.LoadFromFile(path); err != nil { t.Fatalf("LoadFromFile() returned error: %v", err) } // The cookie is loaded (no error) but can not be retrieved because // time.Unix(0,0) is the Unix epoch (1970) which the cookiejar sees as expired. u, _ := url.Parse("http://session.com/path") cookies := j.Cookies(u) if len(cookies) != 0 { t.Logf("Note: session cookie (expires=0) currently not retrievable, got %d cookies", len(cookies)) } } // --------------------------------------------------------------------------- // 7. TestSaveToFileEmptyPath // --------------------------------------------------------------------------- func TestSaveToFileEmptyPath(t *testing.T) { j, err := NewJar() if err != nil { t.Fatal(err) } if err := j.SaveToFile(""); err != nil { t.Fatalf("SaveToFile(\"\") should return nil, got: %v", err) } } // --------------------------------------------------------------------------- // 8. TestSaveToFile // --------------------------------------------------------------------------- func TestSaveToFile(t *testing.T) { dir := t.TempDir() savePath := filepath.Join(dir, "subdir", "cookies.txt") j, err := NewJar() if err != nil { t.Fatal(err) } if err := j.SaveToFile(savePath); err != nil { t.Fatalf("SaveToFile() returned error: %v", err) } // File must exist. if _, err := os.Stat(savePath); os.IsNotExist(err) { t.Fatalf("SaveToFile() did not create file at %s", savePath) } data, err := os.ReadFile(savePath) if err != nil { t.Fatal(err) } content := string(data) if len(content) == 0 { t.Fatal("saved file is empty") } // Must contain Netscape header. if !contains(content, "Netscape HTTP Cookie File") { t.Errorf("saved file should contain 'Netscape HTTP Cookie File', got:\n%s", content) } // Must contain the generated-by line. if !contains(content, "Generated by goget") { t.Errorf("saved file should contain 'Generated by goget', got:\n%s", content) } } // TestSaveToFileExistingDirectory verifies SaveToFile works when the // directory already exists. func TestSaveToFileExistingDirectory(t *testing.T) { dir := filepath.Join(t.TempDir(), "already_exists") if err := os.MkdirAll(dir, 0755); err != nil { t.Fatal(err) } savePath := filepath.Join(dir, "cookies.txt") j, err := NewJar() if err != nil { t.Fatal(err) } if err := j.SaveToFile(savePath); err != nil { t.Fatalf("SaveToFile() returned error: %v", err) } if _, err := os.Stat(savePath); os.IsNotExist(err) { t.Fatalf("file not created in existing directory at %s", savePath) } } // --------------------------------------------------------------------------- // 9. TestCookieRoundTrip // --------------------------------------------------------------------------- func TestCookieRoundTrip(t *testing.T) { j, err := NewJar() if err != nil { t.Fatal(err) } u, _ := url.Parse("https://roundtrip.example.com/path") j.SetCookies(u, []*http.Cookie{ {Name: "session_id", Value: "abc123", Path: "/"}, }) cookies := j.Cookies(u) if len(cookies) != 1 { t.Fatalf("expected 1 cookie, got %d", len(cookies)) } if cookies[0].Name != "session_id" { t.Errorf("expected name 'session_id', got %q", cookies[0].Name) } if cookies[0].Value != "abc123" { t.Errorf("expected value 'abc123', got %q", cookies[0].Value) } } // TestCookieRoundTripHttpsThenHttp verifies that an insecure cookie set over // HTTPS is sent on HTTP as well. func TestCookieRoundTripHttpsThenHttp(t *testing.T) { j, err := NewJar() if err != nil { t.Fatal(err) } u, _ := url.Parse("https://mix.example.com/path") j.SetCookies(u, []*http.Cookie{ {Name: "lang", Value: "en", Path: "/"}, }) httpU, _ := url.Parse("http://mix.example.com/path") cookies := j.Cookies(httpU) if len(cookies) != 1 { t.Fatalf("expected 1 cookie over HTTP after HTTPS set, got %d", len(cookies)) } if cookies[0].Value != "en" { t.Errorf("expected value 'en', got %q", cookies[0].Value) } } // TestCookieRoundTripSecure verifies that a secure cookie set over HTTPS is // NOT sent over plain HTTP. func TestCookieRoundTripSecure(t *testing.T) { j, err := NewJar() if err != nil { t.Fatal(err) } u, _ := url.Parse("https://secure-only.example.com/path") j.SetCookies(u, []*http.Cookie{ {Name: "secret", Value: "s3cr3t", Path: "/", Secure: true}, }) // Over HTTPS – should be present. httpsCookies := j.Cookies(u) hasSecret := false for _, c := range httpsCookies { if c.Name == "secret" { hasSecret = true break } } if !hasSecret { t.Error("secure cookie should be sent over HTTPS") } // Over HTTP – must NOT be present. httpU, _ := url.Parse("http://secure-only.example.com/path") httpCookies := j.Cookies(httpU) for _, c := range httpCookies { if c.Name == "secret" { t.Error("secure cookie should NOT be sent over HTTP") break } } } // --------------------------------------------------------------------------- // 10. TestGetCookieCount // --------------------------------------------------------------------------- func TestGetCookieCount(t *testing.T) { j, err := NewJar() if err != nil { t.Fatal(err) } u, _ := url.Parse("https://count.example.com/path") // Empty jar. if c := j.GetCookieCount(u); c != 0 { t.Errorf("expected 0 cookies for empty jar, got %d", c) } // Add two cookies. j.SetCookies(u, []*http.Cookie{ {Name: "a", Value: "1", Path: "/"}, {Name: "b", Value: "2", Path: "/"}, }) if c := j.GetCookieCount(u); c != 2 { t.Errorf("expected 2 cookies, got %d", c) } } // TestGetCookieCountDifferentURLs verifies cookie counts are scoped to the URL. func TestGetCookieCountDifferentURLs(t *testing.T) { j, err := NewJar() if err != nil { t.Fatal(err) } u1, _ := url.Parse("https://left.example.com/path") u2, _ := url.Parse("https://right.example.com/path") j.SetCookies(u1, []*http.Cookie{{Name: "only_left", Value: "x", Path: "/"}}) if c := j.GetCookieCount(u1); c != 1 { t.Errorf("expected 1 cookie for left, got %d", c) } if c := j.GetCookieCount(u2); c != 0 { t.Errorf("expected 0 cookies for right, got %d", c) } } // --------------------------------------------------------------------------- // 11. TestMultipleCookies // --------------------------------------------------------------------------- func TestMultipleCookies(t *testing.T) { j, err := NewJar() if err != nil { t.Fatal(err) } u, _ := url.Parse("https://multi.example.com/path") j.SetCookies(u, []*http.Cookie{ {Name: "first", Value: "one", Path: "/"}, {Name: "second", Value: "two", Path: "/"}, {Name: "third", Value: "three", Path: "/"}, }) cookies := j.Cookies(u) if len(cookies) != 3 { t.Fatalf("expected 3 cookies, got %d", len(cookies)) } vals := make(map[string]string) for _, c := range cookies { vals[c.Name] = c.Value } if vals["first"] != "one" { t.Errorf("expected first=one, got first=%q", vals["first"]) } if vals["second"] != "two" { t.Errorf("expected second=two, got second=%q", vals["second"]) } if vals["third"] != "three" { t.Errorf("expected third=three, got third=%q", vals["third"]) } } // TestMultipleCookiesSameDomain verifies that setting a cookie with the same // name overwrites the previous value. func TestMultipleCookiesSameDomain(t *testing.T) { j, err := NewJar() if err != nil { t.Fatal(err) } u, _ := url.Parse("https://overwrite.example.com/path") j.SetCookies(u, []*http.Cookie{{Name: "lang", Value: "en", Path: "/"}}) j.SetCookies(u, []*http.Cookie{{Name: "theme", Value: "dark", Path: "/"}}) j.SetCookies(u, []*http.Cookie{{Name: "lang", Value: "fr", Path: "/"}}) cookies := j.Cookies(u) vals := make(map[string]string) for _, c := range cookies { vals[c.Name] = c.Value } if vals["lang"] != "fr" { t.Errorf("expected lang=fr (overwritten), got lang=%q", vals["lang"]) } if vals["theme"] != "dark" { t.Errorf("expected theme=dark, got theme=%q", vals["theme"]) } } // TestMultipleCookiesDifferentPaths validates that cookies with different // paths are scoped correctly. func TestMultipleCookiesDifferentPaths(t *testing.T) { j, err := NewJar() if err != nil { t.Fatal(err) } rootU, _ := url.Parse("https://paths.example.com/") adminU, _ := url.Parse("https://paths.example.com/admin") apiU, _ := url.Parse("https://paths.example.com/api/v1") j.SetCookies(rootU, []*http.Cookie{{Name: "root", Value: "r", Path: "/"}}) j.SetCookies(adminU, []*http.Cookie{{Name: "admin", Value: "a", Path: "/admin"}}) j.SetCookies(apiU, []*http.Cookie{{Name: "api", Value: "v", Path: "/api"}}) // Root path should only see the root cookie. rootCookies := j.Cookies(rootU) if len(rootCookies) != 1 { t.Fatalf("expected 1 cookie for root path, got %d", len(rootCookies)) } if rootCookies[0].Name != "root" { t.Errorf("expected 'root' cookie at root path, got %q", rootCookies[0].Name) } // Admin path should see root (/) + admin (/admin). adminCookies := j.Cookies(adminU) adminNames := make(map[string]bool) for _, c := range adminCookies { adminNames[c.Name] = true } if !adminNames["root"] { t.Error("root cookie should be visible under /admin") } if !adminNames["admin"] { t.Error("admin cookie should be visible under /admin") } // API path should see root (/) + api (/api). apiCookies := j.Cookies(apiU) apiNames := make(map[string]bool) for _, c := range apiCookies { apiNames[c.Name] = true } if !apiNames["root"] { t.Error("root cookie should be visible under /api") } if !apiNames["api"] { t.Error("api cookie should be visible under /api") } } // --------------------------------------------------------------------------- // Additional edge-case tests // --------------------------------------------------------------------------- // TestLoadFromFileMalformedLine checks that lines with fewer than 7 fields // are silently skipped. func TestLoadFromFileMalformedLine(t *testing.T) { dir := t.TempDir() ft := futureTimestamp() content := "# Netscape HTTP Cookie File\n" + netscapeLine(".good.com", "TRUE", "/", "FALSE", fmtInt64(ft), "ok", "fine") + "\n" + "short.line\n" + ".also-short\tTRUE\t/\n" + netscapeLine(".also-good.com", "TRUE", "/", "FALSE", fmtInt64(ft), "ok2", "fine2") + "\n" path := writeTempFile(t, dir, "malformed.txt", content) j, err := NewJar() if err != nil { t.Fatal(err) } if err := j.LoadFromFile(path); err != nil { t.Fatalf("LoadFromFile() returned error: %v", err) } // First valid cookie should be present. u1, _ := url.Parse("http://good.com/path") c1 := j.Cookies(u1) if len(c1) != 1 { t.Errorf("expected 1 cookie for good.com, got %d", len(c1)) } // Second valid cookie should be present. u2, _ := url.Parse("http://also-good.com/path") c2 := j.Cookies(u2) if len(c2) != 1 { t.Errorf("expected 1 cookie for also-good.com, got %d", len(c2)) } } // TestLoadFromFileInvalidExpiry checks that an unparsable expiry is skipped. func TestLoadFromFileInvalidExpiry(t *testing.T) { dir := t.TempDir() ft := futureTimestamp() content := "# Netscape HTTP Cookie File\n" + netscapeLine(".bad-expiry.com", "TRUE", "/", "FALSE", "not_a_number", "x", "y") + "\n" + netscapeLine(".fresh.com", "TRUE", "/", "FALSE", fmtInt64(ft), "good", "yes") + "\n" path := writeTempFile(t, dir, "bad_expiry.txt", content) j, err := NewJar() if err != nil { t.Fatal(err) } if err := j.LoadFromFile(path); err != nil { t.Fatalf("LoadFromFile() returned error: %v", err) } // The malformed-expiry line should be skipped. u1, _ := url.Parse("http://bad-expiry.com/path") c1 := j.Cookies(u1) if len(c1) != 0 { t.Errorf("expected 0 cookies for bad-expiry.com, got %d", len(c1)) } // The valid line should still work. u2, _ := url.Parse("http://fresh.com/path") c2 := j.Cookies(u2) if len(c2) != 1 { t.Fatalf("expected 1 cookie for fresh.com, got %d", len(c2)) } if c2[0].Name != "good" { t.Errorf("expected name 'good', got %q", c2[0].Name) } } // TestLoadAndSaveRoundTrip loads from a file then saves to another location. func TestLoadAndSaveRoundTrip(t *testing.T) { dir := t.TempDir() ft := futureTimestamp() loadContent := "# Netscape HTTP Cookie File\n" + netscapeLine(".rt.com", "TRUE", "/", "FALSE", fmtInt64(ft), "a", "b") + "\n" loadPath := writeTempFile(t, dir, "load.txt", loadContent) j, err := NewJar() if err != nil { t.Fatal(err) } if err := j.LoadFromFile(loadPath); err != nil { t.Fatalf("LoadFromFile() returned error: %v", err) } savePath := filepath.Join(dir, "nested", "save.txt") if err := j.SaveToFile(savePath); err != nil { t.Fatalf("SaveToFile() returned error: %v", err) } if _, err := os.Stat(savePath); os.IsNotExist(err) { t.Errorf("saved file not found at %s", savePath) } data, err := os.ReadFile(savePath) if err != nil { t.Fatal(err) } if !contains(string(data), "Netscape HTTP Cookie File") { t.Error("saved file must contain Netscape header") } } // TestGetCookieCountWithLoad verifies GetCookieCount after loading from file. func TestGetCookieCountWithLoad(t *testing.T) { dir := t.TempDir() ft := futureTimestamp() content := "# Netscape HTTP Cookie File\n" + netscapeLine(".cnt1.com", "TRUE", "/", "FALSE", fmtInt64(ft), "c1", "v1") + "\n" + netscapeLine(".cnt1.com", "TRUE", "/", "FALSE", fmtInt64(ft), "c2", "v2") + "\n" path := writeTempFile(t, dir, "count.txt", content) j, err := NewJar() if err != nil { t.Fatal(err) } if err := j.LoadFromFile(path); err != nil { t.Fatalf("LoadFromFile() returned error: %v", err) } u, _ := url.Parse("http://cnt1.com/path") if c := j.GetCookieCount(u); c != 2 { t.Errorf("expected 2 cookies after load, got %d", c) } } // TestLoadFromFileOpenError verifies that an IO error (e.g. a directory // instead of a file) is propagated as an error (not silently swallowed). func TestLoadFromFileOpenError(t *testing.T) { dir := t.TempDir() j, err := NewJar() if err != nil { t.Fatal(err) } // Try to open a directory – this should fail with an error (not IsNotExist). err = j.LoadFromFile(dir) if err == nil { t.Log("Note: opening a directory may produce an error or not depending on OS") } } // TestNewJarMultiple verifies that multiple jars can be created independently. func TestNewJarMultiple(t *testing.T) { j1, err := NewJar() if err != nil { t.Fatal(err) } j2, err := NewJar() if err != nil { t.Fatal(err) } u, _ := url.Parse("https://independent.example.com/path") j1.SetCookies(u, []*http.Cookie{{Name: "only_j1", Value: "yes", Path: "/"}}) if c := len(j1.Cookies(u)); c != 1 { t.Errorf("expected 1 cookie in j1, got %d", c) } if c := len(j2.Cookies(u)); c != 0 { t.Errorf("expected 0 cookies in j2, got %d", c) } } // TestSetCookiesUsesCookieDomainForStorageKey is a regression guard for the // BACKLOG entry "Cookie domain mismatch between storage and export". // The previous implementation keyed the export map by u.Hostname() and // the file output by (c.Domain with a possibly-prepended dot). When // stdlib cookiejar normalised c.Domain with a leading dot, the storage // key and the file's domain field could diverge, and round-tripping // through SaveToFile/LoadFromFile would re-key the cookie under a // different domain — silently losing the cookie on the next session. // // The fix is to key storage by the cookie's effective domain (no // leading dot) and to use that same key when writing the file. This // test sets a cookie scoped to "example.com" via a request to // "www.example.com", saves, loads into a fresh jar, and verifies the // cookie is still retrievable. func TestSetCookiesUsesCookieDomainForStorageKey(t *testing.T) { dir := t.TempDir() savePath := filepath.Join(dir, "roundtrip.txt") // Session 1: set a cookie whose Domain is "example.com" even // though the URL host is "www.example.com" (the typical case for // a server issuing a cookie that scopes to the parent domain). j1, err := NewJar() if err != nil { t.Fatal(err) } u, _ := url.Parse("https://www.example.com/path") j1.SetCookies(u, []*http.Cookie{ {Name: "session", Value: "abc", Path: "/", Domain: "example.com"}, }) // SaveToFile must write ".example.com" (Netscape format with // leading dot). if err := j1.SaveToFile(savePath); err != nil { t.Fatalf("SaveToFile: %v", err) } data, err := os.ReadFile(savePath) if err != nil { t.Fatal(err) } if !contains(string(data), ".example.com\t") { t.Errorf("saved file must contain .example.com entry, got:\n%s", data) } // Session 2: load into a fresh jar, the cookie must come back // and be retrievable for the original URL. j2, err := NewJar() if err != nil { t.Fatal(err) } if err := j2.LoadFromFile(savePath); err != nil { t.Fatalf("LoadFromFile: %v", err) } cookies := j2.Cookies(u) if len(cookies) != 1 { t.Fatalf("expected 1 cookie after round-trip, got %d", len(cookies)) } if cookies[0].Name != "session" || cookies[0].Value != "abc" { t.Errorf("round-tripped cookie = %+v, want {Name: session, Value: abc}", cookies[0]) } } // TestSetCookiesHostOnlyCookie is the host-only counterpart: a cookie // without a Domain attribute must be stored under the URL hostname // (the only scope information available). func TestSetCookiesHostOnlyCookie(t *testing.T) { j, err := NewJar() if err != nil { t.Fatal(err) } u, _ := url.Parse("https://hostonly.example.com/path") j.SetCookies(u, []*http.Cookie{ {Name: "k", Value: "v", Path: "/"}, }) cookies := j.Cookies(u) if len(cookies) != 1 { t.Fatalf("expected 1 cookie, got %d", len(cookies)) } if cookies[0].Name != "k" { t.Errorf("cookie name = %q, want %q", cookies[0].Name, "k") } } // --------------------------------------------------------------------------- // Helper: contains reports whether substr is in s. // --------------------------------------------------------------------------- func contains(s, substr string) bool { if len(substr) == 0 { return true } if len(substr) > len(s) { return false } for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { return true } } return false }