Files
goget/internal/cookie/jar_test.go
T

875 lines
25 KiB
Go
Raw Normal View History

//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
}