// Copyright (c) 2026 Zeni Kim // Use of this source code is governed by MIT-style // license that can be found in the LICENSE file. package core import ( "encoding/hex" "net/http" "net/http/httptest" "os" "strings" "testing" ) // testCookieSecret is a 32 byte (AES-256) key expressed as a hex string, suitable // for the COOKIE_SECRET env var used by SetCookie/ClearCookie. const testCookieSecret = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" // enableTemplateCookieEnv enables the template engine (required for cookie handling) // and sets a deterministic cookie secret, restoring both env vars on cleanup. func enableTemplateCookieEnv(t *testing.T) { t.Helper() prevTemplate := os.Getenv("TEMPLATE_ENABLE") prevSecret := os.Getenv("COOKIE_SECRET") prevSecure := os.Getenv("COOKIE_SECURE") os.Setenv("TEMPLATE_ENABLE", "true") os.Setenv("COOKIE_SECRET", testCookieSecret) os.Setenv("COOKIE_SECURE", "false") t.Cleanup(func() { os.Setenv("TEMPLATE_ENABLE", prevTemplate) os.Setenv("COOKIE_SECRET", prevSecret) os.Setenv("COOKIE_SECURE", prevSecure) }) } func decodeSecret(t *testing.T) []byte { t.Helper() key, err := hex.DecodeString(testCookieSecret) if err != nil { t.Fatalf("failed decoding test cookie secret: %v", err) } return key } func TestCookieWrite(t *testing.T) { w := httptest.NewRecorder() cookie := http.Cookie{Name: "goffee", Value: "hello", Path: "/"} if err := CookieWrite(w, cookie); err != nil { t.Fatalf("failed testing cookie write: %v", err) } rsp := w.Result() cookies := rsp.Cookies() if len(cookies) != 1 { t.Fatalf("expected 1 cookie, got %d", len(cookies)) } if cookies[0].Name != "goffee" { t.Errorf("expected cookie name 'goffee', got %q", cookies[0].Name) } } func TestCookieWriteTooLong(t *testing.T) { w := httptest.NewRecorder() // A value large enough that the base64-encoded cookie string exceeds 4096 bytes. cookie := http.Cookie{Name: "goffee", Value: strings.Repeat("a", 5000), Path: "/"} err := CookieWrite(w, cookie) if err == nil { t.Errorf("expected ErrValueTooLong, got nil") } } func TestCookieRead(t *testing.T) { // Build a request carrying a base64-encoded cookie value. w := httptest.NewRecorder() cookie := http.Cookie{Name: "goffee", Value: "hello-world", Path: "/"} if err := CookieWrite(w, cookie); err != nil { t.Fatalf("failed writing cookie: %v", err) } r := httptest.NewRequest(GET, LOCALHOST, nil) for _, c := range w.Result().Cookies() { r.AddCookie(c) } val, err := CookieRead(r, "goffee") if err != nil { t.Fatalf("failed testing cookie read: %v", err) } if val != "hello-world" { t.Errorf("expected 'hello-world', got %q", val) } } func TestCookieReadMissing(t *testing.T) { r := httptest.NewRequest(GET, LOCALHOST, nil) _, err := CookieRead(r, "goffee") if err == nil { t.Errorf("expected error reading missing cookie") } } func TestCookieReadInvalidBase64(t *testing.T) { r := httptest.NewRequest(GET, LOCALHOST, nil) r.AddCookie(&http.Cookie{Name: "goffee", Value: "!!!not-base64!!!"}) _, err := CookieRead(r, "goffee") if err == nil { t.Errorf("expected error reading invalid base64 cookie") } } func TestCookieWriteReadEncrypted(t *testing.T) { key := decodeSecret(t) w := httptest.NewRecorder() cookie := http.Cookie{Name: "goffee", Value: "secret-value", Path: "/"} if err := CookieWriteEncrypted(w, cookie, key); err != nil { t.Fatalf("failed writing encrypted cookie: %v", err) } r := httptest.NewRequest(GET, LOCALHOST, nil) for _, c := range w.Result().Cookies() { r.AddCookie(c) } val, err := CookieReadEncrypted(r, "goffee", key) if err != nil { t.Fatalf("failed reading encrypted cookie: %v", err) } if val != "secret-value" { t.Errorf("expected 'secret-value', got %q", val) } } func TestCookieReadEncryptedWrongKey(t *testing.T) { key := decodeSecret(t) w := httptest.NewRecorder() cookie := http.Cookie{Name: "goffee", Value: "secret-value", Path: "/"} if err := CookieWriteEncrypted(w, cookie, key); err != nil { t.Fatalf("failed writing encrypted cookie: %v", err) } // A different but valid AES key must fail decryption. wrongKey, _ := hex.DecodeString("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff") r := httptest.NewRequest(GET, LOCALHOST, nil) for _, c := range w.Result().Cookies() { r.AddCookie(c) } if _, err := CookieReadEncrypted(r, "goffee", wrongKey); err == nil { t.Errorf("expected error decrypting with wrong key") } } func TestCookieReadEncryptedWrongName(t *testing.T) { key := decodeSecret(t) w := httptest.NewRecorder() cookie := http.Cookie{Name: "goffee", Value: "secret-value", Path: "/"} if err := CookieWriteEncrypted(w, cookie, key); err != nil { t.Fatalf("failed writing encrypted cookie: %v", err) } r := httptest.NewRequest(GET, LOCALHOST, nil) for _, c := range w.Result().Cookies() { r.AddCookie(c) } // Reading with a different name must fail the authenticated name check. if _, err := CookieReadEncrypted(r, "other", key); err == nil { t.Errorf("expected error decrypting with wrong cookie name") } } func TestSetCookie(t *testing.T) { enableTemplateCookieEnv(t) w := httptest.NewRecorder() if err := SetCookie(w, "user@example.com", "token-123"); err != nil { t.Fatalf("failed testing set cookie: %v", err) } cookies := w.Result().Cookies() if len(cookies) != 1 { t.Fatalf("expected 1 cookie, got %d", len(cookies)) } if cookies[0].Name != "goffee" { t.Errorf("expected cookie name 'goffee', got %q", cookies[0].Name) } if !cookies[0].HttpOnly { t.Errorf("expected cookie to be HttpOnly") } if cookies[0].MaxAge <= 0 { t.Errorf("expected a positive MaxAge, got %d", cookies[0].MaxAge) } } func TestSetCookieUsesJWT_Lifetime(t *testing.T) { enableTemplateCookieEnv(t) prev := os.Getenv("JWT_LIFESPAN_MINUTES") os.Setenv("JWT_LIFESPAN_MINUTES", "5") t.Cleanup(func() { os.Setenv("JWT_LIFESPAN_MINUTES", prev) }) w := httptest.NewRecorder() if err := SetCookie(w, "user@example.com", "token-123"); err != nil { t.Fatalf("failed testing set cookie: %v", err) } cookies := w.Result().Cookies() if cookies[0].MaxAge != 5*60 { t.Errorf("expected MaxAge of %d, got %d", 5*60, cookies[0].MaxAge) } } func TestSetCookieWithMaxAge(t *testing.T) { enableTemplateCookieEnv(t) w := httptest.NewRecorder() if err := SetCookieWithMaxAge(w, "user@example.com", "token-abc", 120); err != nil { t.Fatalf("failed testing set cookie with max age: %v", err) } cookies := w.Result().Cookies() if cookies[0].MaxAge != 120 { t.Errorf("expected MaxAge of 120, got %d", cookies[0].MaxAge) } } func TestSetCookieTemplatesDisabledPanics(t *testing.T) { prev := os.Getenv("TEMPLATE_ENABLE") os.Setenv("TEMPLATE_ENABLE", "false") t.Cleanup(func() { os.Setenv("TEMPLATE_ENABLE", prev) }) defer func() { if r := recover(); r == nil { t.Errorf("expected panic when templates are disabled") } }() w := httptest.NewRecorder() _ = SetCookieWithMaxAge(w, "user@example.com", "token", 60) } func TestClearCookie(t *testing.T) { prevSecure := os.Getenv("COOKIE_SECURE") os.Setenv("COOKIE_SECURE", "false") t.Cleanup(func() { os.Setenv("COOKIE_SECURE", prevSecure) }) w := httptest.NewRecorder() if err := ClearCookie(w); err != nil { t.Fatalf("failed testing clear cookie: %v", err) } cookies := w.Result().Cookies() if len(cookies) != 1 { t.Fatalf("expected 1 cookie, got %d", len(cookies)) } c := cookies[0] if c.Name != "goffee" { t.Errorf("expected cookie name 'goffee', got %q", c.Name) } if c.Value != "" { t.Errorf("expected empty cookie value, got %q", c.Value) } if c.MaxAge != -1 { t.Errorf("expected MaxAge of -1 to delete the cookie, got %d", c.MaxAge) } if !c.HttpOnly { t.Errorf("expected cleared cookie to be HttpOnly") } } func TestGetCookieRoundTrip(t *testing.T) { enableTemplateCookieEnv(t) // Set a cookie and feed it back into a request, then decrypt it. w := httptest.NewRecorder() if err := SetCookie(w, "user@example.com", "token-xyz"); err != nil { t.Fatalf("failed setting cookie: %v", err) } r := httptest.NewRequest(GET, LOCALHOST, nil) for _, c := range w.Result().Cookies() { r.AddCookie(c) } user, err := GetCookie(r) if err != nil { t.Fatalf("failed testing get cookie: %v", err) } if user.Email != "user@example.com" { t.Errorf("expected email 'user@example.com', got %q", user.Email) } if user.Token != "token-xyz" { t.Errorf("expected token 'token-xyz', got %q", user.Token) } } func TestGetCookieTemplatesDisabledPanics(t *testing.T) { prev := os.Getenv("TEMPLATE_ENABLE") os.Setenv("TEMPLATE_ENABLE", "false") t.Cleanup(func() { os.Setenv("TEMPLATE_ENABLE", prev) }) defer func() { if r := recover(); r == nil { t.Errorf("expected panic when templates are disabled") } }() r := httptest.NewRequest(GET, LOCALHOST, nil) _, _ = GetCookie(r) }