package auth import ( "testing" "time" ) func TestGetServiceToken_NotCached(t *testing.T) { defaultCache.Clear() did := "did:plc:test123" holdDID := "did:web:hold.example.com" token, expiresAt := GetServiceToken(did, holdDID) if token != "" { t.Errorf("Expected empty token for uncached entry, got %q", token) } if !expiresAt.IsZero() { t.Error("Expected zero time for uncached entry") } } func TestSetServiceToken_ManualExpiry(t *testing.T) { defaultCache.Clear() did := "did:plc:test123" holdDID := "did:web:hold.example.com" token := "invalid_jwt_token" // Will fall back to 50s default // This should succeed with default 50s TTL since JWT parsing will fail err := SetServiceToken(did, holdDID, token) if err != nil { t.Fatalf("SetServiceToken() error = %v", err) } // Verify token was cached cachedToken, expiresAt := GetServiceToken(did, holdDID) if cachedToken != token { t.Errorf("Expected token %q, got %q", token, cachedToken) } if expiresAt.IsZero() { t.Error("Expected non-zero expiry time") } // Expiry should be approximately 50s from now (with 10s margin subtracted in some cases) expectedExpiry := time.Now().Add(50 * time.Second) diff := expiresAt.Sub(expectedExpiry) if diff < -5*time.Second || diff > 5*time.Second { t.Errorf("Expiry time off by %v (expected ~50s from now)", diff) } } func TestGetServiceToken_Expired(t *testing.T) { // Manually insert an expired token by reaching into the cache. Set() // would apply the safety margin which is the opposite of what we // want here — we want a guaranteed-stale entry. did := "did:plc:test123" holdDID := "did:web:hold.example.com" cacheKey := did + ":" + holdDID defaultCache.mu.Lock() defaultCache.tokens[cacheKey] = &serviceTokenEntry{ token: "expired_token", expiresAt: time.Now().Add(-1 * time.Hour), } defaultCache.mu.Unlock() // Try to get - should return empty since expired token, expiresAt := GetServiceToken(did, holdDID) if token != "" { t.Errorf("Expected empty token for expired entry, got %q", token) } if !expiresAt.IsZero() { t.Error("Expected zero time for expired entry") } // Verify token was removed from cache defaultCache.mu.RLock() _, exists := defaultCache.tokens[cacheKey] defaultCache.mu.RUnlock() if exists { t.Error("Expected expired token to be removed from cache") } } func TestInvalidateServiceToken(t *testing.T) { defaultCache.Clear() did := "did:plc:test123" holdDID := "did:web:hold.example.com" token := "test_token" err := SetServiceToken(did, holdDID, token) if err != nil { t.Fatalf("SetServiceToken() error = %v", err) } cachedToken, _ := GetServiceToken(did, holdDID) if cachedToken != token { t.Fatal("Token should be cached") } InvalidateServiceToken(did, holdDID) cachedToken, _ = GetServiceToken(did, holdDID) if cachedToken != "" { t.Error("Expected token to be invalidated") } } func TestCleanExpiredTokens(t *testing.T) { defaultCache.Clear() defaultCache.mu.Lock() defaultCache.tokens["expired:hold1"] = &serviceTokenEntry{ token: "expired1", expiresAt: time.Now().Add(-1 * time.Hour), } defaultCache.tokens["valid:hold2"] = &serviceTokenEntry{ token: "valid1", expiresAt: time.Now().Add(1 * time.Hour), } defaultCache.mu.Unlock() CleanExpiredTokens() defaultCache.mu.RLock() _, expiredExists := defaultCache.tokens["expired:hold1"] _, validExists := defaultCache.tokens["valid:hold2"] defaultCache.mu.RUnlock() if expiredExists { t.Error("Expected expired token to be removed") } if !validExists { t.Error("Expected valid token to remain") } } func TestGetCacheStats(t *testing.T) { defaultCache.Clear() defaultCache.mu.Lock() defaultCache.tokens["did1:hold1"] = &serviceTokenEntry{ token: "token1", expiresAt: time.Now().Add(1 * time.Hour), } defaultCache.tokens["did2:hold2"] = &serviceTokenEntry{ token: "token2", expiresAt: time.Now().Add(1 * time.Hour), } defaultCache.mu.Unlock() stats := GetCacheStats() if stats == nil { t.Fatal("Expected non-nil stats") } totalEntries, ok := stats["total_entries"].(int) if !ok { t.Fatalf("Expected total_entries in stats map, got: %v", stats) } if totalEntries != 2 { t.Errorf("Expected 2 entries, got %d", totalEntries) } validTokens, ok := stats["valid_tokens"].(int) if !ok { t.Fatal("Expected valid_tokens in stats map") } if validTokens != 2 { t.Errorf("Expected 2 valid tokens, got %d", validTokens) } } func TestCache_StructAPI_IsolatedInstances(t *testing.T) { // A freshly constructed Cache must not share state with defaultCache — // otherwise tests that expect isolation would silently fail. c1 := NewCache() c2 := NewCache() did := "did:plc:alice" holdDID := "did:web:hold.test" c1.mu.Lock() c1.tokens[did+":"+holdDID] = &serviceTokenEntry{ token: "tok-c1", expiresAt: time.Now().Add(1 * time.Hour), } c1.mu.Unlock() if tok, _ := c1.Get(did, holdDID); tok != "tok-c1" { t.Errorf("c1.Get() = %q, want tok-c1", tok) } if tok, _ := c2.Get(did, holdDID); tok != "" { t.Errorf("c2.Get() = %q, want empty (instances must not share state)", tok) } if tok, _ := GetServiceToken(did, holdDID); tok == "tok-c1" { t.Error("defaultCache should not see writes to c1") } c1.Clear() if tok, _ := c1.Get(did, holdDID); tok != "" { t.Errorf("c1.Get() after Clear() = %q, want empty", tok) } } func TestCache_PackageFunctionsDelegateToDefault(t *testing.T) { // The package-level wrappers must route to defaultCache. defaultCache.Clear() did := "did:plc:bob" holdDID := "did:web:hold.test" if err := SetServiceToken(did, holdDID, "wrapper-tok"); err != nil { t.Fatalf("SetServiceToken: %v", err) } tok, exp := DefaultCache().Get(did, holdDID) if tok != "wrapper-tok" { t.Errorf("DefaultCache().Get() = %q, want wrapper-tok", tok) } if exp.IsZero() { t.Error("DefaultCache().Get() expiry is zero") } InvalidateServiceToken(did, holdDID) if tok, _ := DefaultCache().Get(did, holdDID); tok != "" { t.Errorf("after InvalidateServiceToken, DefaultCache().Get() = %q, want empty", tok) } }