package auth import ( "crypto/tls" "net/http" "net/http/httptest" "testing" "time" ) func TestGenerateToken(t *testing.T) { token, err := GenerateToken() if err != nil { t.Fatalf("GenerateToken() error = %v", err) } if len(token) != 64 { t.Errorf("GenerateToken() length = %d, want 64", len(token)) } // Uniqueness check tokens := make(map[string]bool) for i := 0; i < 1000; i++ { tok, err := GenerateToken() if err != nil { t.Fatalf("GenerateToken() iteration %d error = %v", i, err) } if tokens[tok] { t.Errorf("GenerateToken() produced duplicate token at iteration %d", i) } tokens[tok] = true } } func TestSessionStore_Validate(t *testing.T) { store := NewSessionStore() // Empty token if store.Validate("") { t.Error("Validate(\"\") = true, want false") } // Missing token if store.Validate("nonexistent") { t.Error("Validate(\"nonexistent\") = true, want false") } // Valid token token, _ := GenerateToken() store.Add(token, time.Now().Add(1*time.Hour)) if !store.Validate(token) { t.Error("Validate(valid) = false, want true") } // Expired token expiredToken, _ := GenerateToken() store.Add(expiredToken, time.Now().Add(-1*time.Hour)) if store.Validate(expiredToken) { t.Error("Validate(expired) = true, want false") } // Expired token should be cleaned up if store.Validate(expiredToken) { t.Error("Validate(expired) second call = true, want false (should be cleaned)") } } func TestSessionStore_AddRemove(t *testing.T) { store := NewSessionStore() token, _ := GenerateToken() store.Add(token, time.Now().Add(1*time.Hour)) if !store.Validate(token) { t.Error("Validate after Add = false, want true") } store.Remove(token) if store.Validate(token) { t.Error("Validate after Remove = true, want false") } } func TestSessionStore_CountActive(t *testing.T) { store := NewSessionStore() if count := store.CountActive(); count != 0 { t.Errorf("CountActive() = %d, want 0", count) } t1, _ := GenerateToken() t2, _ := GenerateToken() t3, _ := GenerateToken() store.Add(t1, time.Now().Add(1*time.Hour)) store.Add(t2, time.Now().Add(1*time.Hour)) store.Add(t3, time.Now().Add(-1*time.Hour)) // expired if count := store.CountActive(); count != 2 { t.Errorf("CountActive() = %d, want 2 (expired should be cleaned)", count) } } func TestCookieSecure(t *testing.T) { tests := []struct { name string forwardedProto string tls bool expected bool }{ {"https forwarded", "https", false, true}, {"http forwarded", "http", false, false}, {"empty forwarded, no TLS", "", false, false}, {"empty forwarded, TLS", "", true, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req := httptest.NewRequest("GET", "/", nil) if tt.forwardedProto != "" { req.Header.Set("X-Forwarded-Proto", tt.forwardedProto) } if tt.tls { req.TLS = &tls.ConnectionState{} } if got := CookieSecure(req); got != tt.expected { t.Errorf("CookieSecure() = %v, want %v", got, tt.expected) } }) } } func TestSetCookie(t *testing.T) { req := httptest.NewRequest("GET", "/", nil) req.Header.Set("X-Forwarded-Proto", "https") w := httptest.NewRecorder() expiresAt := time.Now().Add(1 * time.Hour) SetCookie(w, req, "test_cookie", "abc123", expiresAt) cookies := w.Result().Cookies() if len(cookies) != 1 { t.Fatalf("expected 1 cookie, got %d", len(cookies)) } c := cookies[0] if c.Name != "test_cookie" { t.Errorf("cookie name = %q, want test_cookie", c.Name) } if c.Value != "abc123" { t.Errorf("cookie value = %q, want abc123", c.Value) } if !c.HttpOnly { t.Error("cookie HttpOnly = false, want true") } if c.SameSite != http.SameSiteStrictMode { t.Errorf("cookie SameSite = %v, want Strict", c.SameSite) } if !c.Secure { t.Error("cookie Secure = false, want true (X-Forwarded-Proto: https)") } if c.Path != "/" { t.Errorf("cookie Path = %q, want /", c.Path) } } func TestClearCookie(t *testing.T) { req := httptest.NewRequest("GET", "/", nil) w := httptest.NewRecorder() ClearCookie(w, req, "test_cookie") cookies := w.Result().Cookies() if len(cookies) != 1 { t.Fatalf("expected 1 cookie, got %d", len(cookies)) } c := cookies[0] if c.Name != "test_cookie" { t.Errorf("cookie name = %q, want test_cookie", c.Name) } if c.Value != "" { t.Errorf("cookie value = %q, want empty", c.Value) } if c.MaxAge != -1 { t.Errorf("cookie MaxAge = %d, want -1", c.MaxAge) } } func TestGetTokenFromCookie(t *testing.T) { req := httptest.NewRequest("GET", "/", nil) req.AddCookie(&http.Cookie{Name: "session", Value: "token123"}) token, ok := GetTokenFromCookie(req, "session") if !ok { t.Error("GetTokenFromCookie() ok = false, want true") } if token != "token123" { t.Errorf("GetTokenFromCookie() = %q, want token123", token) } // Missing cookie _, ok = GetTokenFromCookie(req, "nonexistent") if ok { t.Error("GetTokenFromCookie() for missing cookie ok = true, want false") } // Empty value req2 := httptest.NewRequest("GET", "/", nil) req2.AddCookie(&http.Cookie{Name: "session", Value: ""}) _, ok = GetTokenFromCookie(req2, "session") if ok { t.Error("GetTokenFromCookie() for empty value ok = true, want false") } } func TestRequireAdmin(t *testing.T) { store := NewSessionStore() token, _ := GenerateToken() store.Add(token, time.Now().Add(1*time.Hour)) handler := RequireAdmin(store, "session")(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("ok")) }) // Valid session req := httptest.NewRequest("GET", "/", nil) req.AddCookie(&http.Cookie{Name: "session", Value: token}) w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } // No cookie req2 := httptest.NewRequest("GET", "/", nil) w2 := httptest.NewRecorder() handler(w2, req2) if w2.Code != http.StatusUnauthorized { t.Errorf("status = %d, want 401", w2.Code) } // Invalid token req3 := httptest.NewRequest("GET", "/", nil) req3.AddCookie(&http.Cookie{Name: "session", Value: "invalid"}) w3 := httptest.NewRecorder() handler(w3, req3) if w3.Code != http.StatusUnauthorized { t.Errorf("status = %d, want 401", w3.Code) } } func TestUserSessionStore(t *testing.T) { store := NewUserSessionStore() if _, ok := store.Username(""); ok { t.Error("Username(\"\") = true, want false") } token, _ := GenerateToken() store.Add(token, "alice", time.Now().Add(1*time.Hour)) username, ok := store.Username(token) if !ok || username != "alice" { t.Errorf("Username() = %q, %v; want alice, true", username, ok) } // Expired session expired, _ := GenerateToken() store.Add(expired, "bob", time.Now().Add(-1*time.Hour)) if _, ok := store.Username(expired); ok { t.Error("Username(expired) = true, want false") } // Remove store.Remove(token) if _, ok := store.Username(token); ok { t.Error("Username after Remove = true, want false") } }