mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-05 22:44:17 +00:00
1b49e133da
* Fix bug affecting Azure OIDC authentication ( and most likely others ) * Fixes issue #51 * Ensure that appended roles are unique. Update the documentation. * Improvements targetting possible memory usage spikes. * Additional fixes and cleanup * Refactoring code to fix the issues identified by the users. * Modernize run * Fieldalignment * Multiple changes to improve performance and reduce complexity. - Optimise the errors and recovery. - Deduplicate code in metadata cache. - Remove unused performance monitoring code. - Simplify session management and settings handling. * Fix claims issue. * Add ability to overwrite the default scopes in the settings file * Well.. that escalated quickly. Completely forgot that Traefik uses outdated Yaegi and requires compatibility with 1.20 ( pre-generic Go code ). * Bugfix #51: Ensures that user provided scopes overrides work. * fixup! Bugfix #51: Ensures that user provided scopes overrides work. * fixup! fixup! Bugfix #51: Ensures that user provided scopes overrides work. * Abstract the provider logic into a separate package. * Additional micro fixes and cleanups. * Simplify all the things. * fixup! Simplify all the things. * fixup! fixup! Simplify all the things. * fixup! fixup! fixup! Simplify all the things. * fixup! fixup! fixup! fixup! Simplify all the things. * ... * Cleanup tests. * fixup! Cleanup tests. * fixup! fixup! fixup! Cleanup tests. * fixup! fixup! fixup! fixup! Cleanup tests. * fixup! fixup! fixup! fixup! fixup! Cleanup tests. * Issue #53: Fix CSRF token handling in reverse proxy 1. ✅ HTTPS Detection Fixed (session.go:723) - Now uses X-Forwarded-Proto header instead of r.URL.Scheme - Properly detects HTTPS in reverse proxy environments 2. ✅ SameSite Cookie Attribute Fixed - Removed automatic SameSiteStrictMode for HTTPS (would break OAuth) - Keeps SameSiteLaxMode to allow OAuth callbacks from external domains - Only uses Strict for AJAX requests which don't involve OAuth redirects 3. ✅ Cookie Domain Handling Fixed - Now respects X-Forwarded-Host header for cookie domain - Ensures cookies are set for the public domain, not internal proxy domain 4. ✅ EnhanceSessionSecurity Properly Integrated - Function is now actually called during session save - Applies security enhancements without breaking OAuth flow Why Issue #53 Failed Before: 1. Cookies were not marked Secure in HTTPS environments (browser wouldn't send them back) 2. If they had been Secure with SameSite=Strict, Azure callbacks would still fail 3. Cookie domain might have been wrong (internal vs public domain) Why It Works Now: 1. Cookies are properly marked Secure for HTTPS 2. Uses SameSite=Lax to allow OAuth provider callbacks 3. Cookie domain uses public domain from X-Forwarded-Host 4. CSRF token persists through the entire OAuth flow * Next set of enhancements together with memory usage improvements. * Memory leak fixes and optimisations. * CSRF and Cookie Domain fixes * fixup! CSRF and Cookie Domain fixes * Metadata cache leak fix + profiling * fixup! Metadata cache leak fix + profiling * Memory leaks hunting, part 1337. * Further pursue of perfection. * fixup! Further pursue of perfection. * fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * Clear race conditions * fixup! Clear race conditions * Weekend fun with memory leaks * Splitting code into multiple files with reasonable testing coverage. ``` ok github.com/lukaszraczylo/traefikoidc 117.017s coverage: 72.6% of statements ok github.com/lukaszraczylo/traefikoidc/auth 0.505s coverage: 87.1% of statements ok github.com/lukaszraczylo/traefikoidc/circuit_breaker 0.283s coverage: 99.0% of statements github.com/lukaszraczylo/traefikoidc/config coverage: 0.0% of statements ok github.com/lukaszraczylo/traefikoidc/handlers 0.349s coverage: 98.2% of statements ok github.com/lukaszraczylo/traefikoidc/internal/providers (cached) coverage: 94.3% of statements ok github.com/lukaszraczylo/traefikoidc/middleware 0.808s coverage: 78.0% of statements ok github.com/lukaszraczylo/traefikoidc/recovery 0.653s coverage: 100.0% of statements ok github.com/lukaszraczylo/traefikoidc/session/chunking (cached) coverage: 87.8% of statements ok github.com/lukaszraczylo/traefikoidc/session/core (cached) coverage: 85.6% of statements ok github.com/lukaszraczylo/traefikoidc/session/crypto (cached) coverage: 81.8% of statements ok github.com/lukaszraczylo/traefikoidc/session/storage (cached) coverage: 93.5% of statements ok github.com/lukaszraczylo/traefikoidc/session/validators (cached) coverage: 98.8% of statements ```` * fixup! Splitting code into multiple files with reasonable testing coverage. * fixup! fixup! Splitting code into multiple files with reasonable testing coverage. * Weekend fun with further optimisations. * fixup! Weekend fun with further optimisations. * fixup! fixup! Weekend fun with further optimisations. * fixup! fixup! fixup! Weekend fun with further optimisations. * fixup! fixup! fixup! fixup! Weekend fun with further optimisations. * fixup! fixup! fixup! fixup! fixup! Weekend fun with further optimisations. * Pre-release cleanup. * Enhance test coverage. * fixup! Enhance test coverage. * fixup! fixup! Enhance test coverage. * fixup! fixup! fixup! Enhance test coverage.
2041 lines
50 KiB
Go
2041 lines
50 KiB
Go
package cache
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestCacheBasicOperations(t *testing.T) {
|
|
config := DefaultConfig()
|
|
cache := New(config)
|
|
defer cache.Close()
|
|
|
|
// Test Set and Get
|
|
key := "test-key"
|
|
value := "test-value"
|
|
ttl := 1 * time.Hour
|
|
|
|
err := cache.Set(key, value, ttl)
|
|
if err != nil {
|
|
t.Fatalf("Failed to set cache value: %v", err)
|
|
}
|
|
|
|
retrieved, exists := cache.Get(key)
|
|
if !exists {
|
|
t.Fatal("Expected value to exist in cache")
|
|
}
|
|
|
|
if retrieved != value {
|
|
t.Fatalf("Expected %s, got %v", value, retrieved)
|
|
}
|
|
|
|
// Test Delete
|
|
cache.Delete(key)
|
|
_, exists = cache.Get(key)
|
|
if exists {
|
|
t.Fatal("Expected value to be deleted from cache")
|
|
}
|
|
}
|
|
|
|
func TestCacheConcurrency(t *testing.T) {
|
|
config := DefaultConfig()
|
|
cache := New(config)
|
|
defer cache.Close()
|
|
|
|
var wg sync.WaitGroup
|
|
numGoroutines := 100
|
|
numOperations := 100
|
|
|
|
// Concurrent writes
|
|
wg.Add(numGoroutines)
|
|
for i := 0; i < numGoroutines; i++ {
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
for j := 0; j < numOperations; j++ {
|
|
key := "key"
|
|
value := id*numOperations + j
|
|
_ = cache.Set(key, value, 1*time.Hour)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
// Concurrent reads
|
|
wg.Add(numGoroutines)
|
|
for i := 0; i < numGoroutines; i++ {
|
|
go func() {
|
|
defer wg.Done()
|
|
for j := 0; j < numOperations; j++ {
|
|
cache.Get("key")
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
func TestTypedCache(t *testing.T) {
|
|
config := DefaultConfig()
|
|
baseCache := New(config)
|
|
defer baseCache.Close()
|
|
|
|
// Test TokenCache
|
|
tokenCache := NewTokenCache(baseCache)
|
|
|
|
token := "test-token"
|
|
claims := map[string]interface{}{
|
|
"sub": "user123",
|
|
"exp": time.Now().Add(1 * time.Hour).Unix(),
|
|
}
|
|
|
|
err := tokenCache.Set(token, claims, 1*time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("Failed to set token: %v", err)
|
|
}
|
|
|
|
retrievedClaims, exists := tokenCache.Get(token)
|
|
if !exists {
|
|
t.Fatal("Expected token to exist in cache")
|
|
}
|
|
|
|
if retrievedClaims["sub"] != claims["sub"] {
|
|
t.Fatalf("Claims mismatch: expected %v, got %v", claims["sub"], retrievedClaims["sub"])
|
|
}
|
|
|
|
// Test blacklist
|
|
err = tokenCache.SetBlacklisted(token, 24*time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("Failed to blacklist token: %v", err)
|
|
}
|
|
|
|
if !tokenCache.IsBlacklisted(token) {
|
|
t.Fatal("Expected token to be blacklisted")
|
|
}
|
|
}
|
|
|
|
func TestCacheManager(t *testing.T) {
|
|
manager := NewManager(nil)
|
|
defer manager.Close()
|
|
|
|
// Test getting different cache types
|
|
tokenCache := manager.GetTokenCache()
|
|
if tokenCache == nil {
|
|
t.Fatal("Expected token cache to be initialized")
|
|
}
|
|
|
|
metadataCache := manager.GetMetadataCache()
|
|
if metadataCache == nil {
|
|
t.Fatal("Expected metadata cache to be initialized")
|
|
}
|
|
|
|
jwkCache := manager.GetJWKCache()
|
|
if jwkCache == nil {
|
|
t.Fatal("Expected JWK cache to be initialized")
|
|
}
|
|
|
|
sessionCache := manager.GetSessionCache()
|
|
if sessionCache == nil {
|
|
t.Fatal("Expected session cache to be initialized")
|
|
}
|
|
|
|
// Test stats
|
|
stats := manager.GetStats()
|
|
if len(stats) != 5 {
|
|
t.Fatalf("Expected 5 cache stats, got %d", len(stats))
|
|
}
|
|
}
|
|
|
|
func TestCacheEviction(t *testing.T) {
|
|
config := DefaultConfig()
|
|
config.MaxSize = 3
|
|
cache := New(config)
|
|
defer cache.Close()
|
|
|
|
// Add items to fill the cache
|
|
_ = cache.Set("key1", "value1", 1*time.Hour)
|
|
_ = cache.Set("key2", "value2", 1*time.Hour)
|
|
_ = cache.Set("key3", "value3", 1*time.Hour)
|
|
|
|
// Verify all items exist
|
|
for i := 1; i <= 3; i++ {
|
|
key := "key" + string(rune('0'+i))
|
|
if _, exists := cache.Get(key); !exists {
|
|
t.Fatalf("Expected %s to exist", key)
|
|
}
|
|
}
|
|
|
|
// Add another item to trigger eviction
|
|
_ = cache.Set("key4", "value4", 1*time.Hour)
|
|
|
|
// Check that we still have only 3 items
|
|
if cache.Size() != 3 {
|
|
t.Fatalf("Expected cache size to be 3, got %d", cache.Size())
|
|
}
|
|
|
|
// The least recently used item (key1) should be evicted
|
|
if _, exists := cache.Get("key1"); exists {
|
|
t.Fatal("Expected key1 to be evicted")
|
|
}
|
|
|
|
// Other items should still exist
|
|
if _, exists := cache.Get("key4"); !exists {
|
|
t.Fatal("Expected key4 to exist")
|
|
}
|
|
}
|
|
|
|
func TestCacheExpiration(t *testing.T) {
|
|
config := DefaultConfig()
|
|
cache := New(config)
|
|
defer cache.Close()
|
|
|
|
// Set item with short TTL
|
|
_ = cache.Set("short-ttl", "value", 100*time.Millisecond)
|
|
|
|
// Item should exist immediately
|
|
if _, exists := cache.Get("short-ttl"); !exists {
|
|
t.Fatal("Expected item to exist immediately after setting")
|
|
}
|
|
|
|
// Wait for expiration
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
// Item should be expired
|
|
if _, exists := cache.Get("short-ttl"); exists {
|
|
t.Fatal("Expected item to be expired")
|
|
}
|
|
}
|
|
|
|
func BenchmarkCacheSet(b *testing.B) {
|
|
config := DefaultConfig()
|
|
cache := New(config)
|
|
defer cache.Close()
|
|
|
|
b.ResetTimer()
|
|
b.RunParallel(func(pb *testing.PB) {
|
|
i := 0
|
|
for pb.Next() {
|
|
key := "key"
|
|
_ = cache.Set(key, i, 1*time.Hour)
|
|
i++
|
|
}
|
|
})
|
|
}
|
|
|
|
func BenchmarkCacheGet(b *testing.B) {
|
|
config := DefaultConfig()
|
|
cache := New(config)
|
|
defer cache.Close()
|
|
|
|
// Pre-populate cache
|
|
for i := 0; i < 1000; i++ {
|
|
key := "key"
|
|
_ = cache.Set(key, i, 1*time.Hour)
|
|
}
|
|
|
|
b.ResetTimer()
|
|
b.RunParallel(func(pb *testing.PB) {
|
|
for pb.Next() {
|
|
cache.Get("key")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestCacheConfiguration tests various configuration options
|
|
func TestCacheConfiguration(t *testing.T) {
|
|
// Test default config
|
|
config := DefaultConfig()
|
|
if config.MaxSize != 1000 {
|
|
t.Errorf("Expected default max size 1000, got %d", config.MaxSize)
|
|
}
|
|
|
|
if config.DefaultTTL != 10*time.Minute {
|
|
t.Errorf("Expected default TTL 10 minutes, got %v", config.DefaultTTL)
|
|
}
|
|
|
|
if config.Type != TypeGeneral {
|
|
t.Errorf("Expected default type General, got %v", config.Type)
|
|
}
|
|
|
|
// Test custom config
|
|
customConfig := Config{
|
|
Type: TypeToken,
|
|
MaxSize: 500,
|
|
MaxMemoryBytes: 1024 * 1024,
|
|
DefaultTTL: 30 * time.Minute,
|
|
CleanupInterval: 5 * time.Minute,
|
|
EnableCompression: true,
|
|
EnableMetrics: true,
|
|
EnableAutoCleanup: true,
|
|
EnableMemoryLimit: true,
|
|
}
|
|
|
|
cache := New(customConfig)
|
|
defer cache.Close()
|
|
|
|
if cache.config.Type != TypeToken {
|
|
t.Errorf("Expected cache type Token, got %v", cache.config.Type)
|
|
}
|
|
}
|
|
|
|
// TestCacheStats tests cache statistics
|
|
func TestCacheStats(t *testing.T) {
|
|
config := DefaultConfig()
|
|
cache := New(config)
|
|
defer cache.Close()
|
|
|
|
// Initial stats
|
|
stats := cache.GetStats()
|
|
if stats["size"].(int64) != 0 {
|
|
t.Errorf("Expected initial size 0, got %d", stats["size"])
|
|
}
|
|
if stats["hits"].(int64) != 0 {
|
|
t.Errorf("Expected initial hits 0, got %d", stats["hits"])
|
|
}
|
|
if stats["misses"].(int64) != 0 {
|
|
t.Errorf("Expected initial misses 0, got %d", stats["misses"])
|
|
}
|
|
|
|
// Add item and check stats
|
|
_ = cache.Set("key1", "value1", 1*time.Hour)
|
|
stats = cache.GetStats()
|
|
if stats["size"].(int64) != 1 {
|
|
t.Errorf("Expected size 1, got %d", stats["size"])
|
|
}
|
|
|
|
// Cache hit
|
|
_, exists := cache.Get("key1")
|
|
if !exists {
|
|
t.Error("Expected key1 to exist")
|
|
}
|
|
stats = cache.GetStats()
|
|
if stats["hits"].(int64) != 1 {
|
|
t.Errorf("Expected hits 1, got %d", stats["hits"])
|
|
}
|
|
|
|
// Cache miss
|
|
_, exists = cache.Get("nonexistent")
|
|
if exists {
|
|
t.Error("Expected nonexistent key to not exist")
|
|
}
|
|
stats = cache.GetStats()
|
|
if stats["misses"].(int64) != 1 {
|
|
t.Errorf("Expected misses 1, got %d", stats["misses"])
|
|
}
|
|
}
|
|
|
|
// TestCacheMemoryLimit tests memory-based eviction
|
|
func TestCacheMemoryLimit(t *testing.T) {
|
|
config := DefaultConfig()
|
|
config.MaxMemoryBytes = 1024 // Very small limit
|
|
config.EnableMemoryLimit = true
|
|
cache := New(config)
|
|
defer cache.Close()
|
|
|
|
// Add items that exceed memory limit
|
|
largeValue := string(make([]byte, 500))
|
|
_ = cache.Set("key1", largeValue, 1*time.Hour)
|
|
_ = cache.Set("key2", largeValue, 1*time.Hour)
|
|
_ = cache.Set("key3", largeValue, 1*time.Hour)
|
|
|
|
// Check that memory limit is enforced
|
|
stats := cache.GetStats()
|
|
memoryUsage := stats["memory"].(int64)
|
|
if memoryUsage > config.MaxMemoryBytes*2 { // Allow some overhead
|
|
t.Errorf("Memory usage %d exceeds limit %d by too much", memoryUsage, config.MaxMemoryBytes)
|
|
}
|
|
}
|
|
|
|
// TestCacheCompression tests compression functionality
|
|
func TestCacheCompression(t *testing.T) {
|
|
config := DefaultConfig()
|
|
config.EnableCompression = true
|
|
cache := New(config)
|
|
defer cache.Close()
|
|
|
|
// Test with large compressible data
|
|
largeValueBytes := make([]byte, 1000)
|
|
for i := range largeValueBytes {
|
|
largeValueBytes[i] = byte('A') // Highly compressible
|
|
}
|
|
largeValue := string(largeValueBytes)
|
|
|
|
err := cache.Set("compressed", largeValue, 1*time.Hour)
|
|
if err != nil {
|
|
t.Errorf("Failed to set compressed value: %v", err)
|
|
}
|
|
|
|
retrieved, exists := cache.Get("compressed")
|
|
if !exists {
|
|
t.Error("Expected compressed value to exist")
|
|
}
|
|
|
|
if retrieved != largeValue {
|
|
t.Error("Compressed value doesn't match original")
|
|
}
|
|
}
|
|
|
|
// TestCacheCleanup tests automatic cleanup
|
|
func TestCacheCleanup(t *testing.T) {
|
|
config := DefaultConfig()
|
|
config.CleanupInterval = 50 * time.Millisecond
|
|
config.EnableAutoCleanup = true
|
|
cache := New(config)
|
|
defer cache.Close()
|
|
|
|
// Add expired item
|
|
_ = cache.Set("expired", "value", 25*time.Millisecond)
|
|
|
|
// Wait for expiration and cleanup
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Item should be cleaned up
|
|
_, exists := cache.Get("expired")
|
|
if exists {
|
|
t.Error("Expected expired item to be cleaned up")
|
|
}
|
|
}
|
|
|
|
// TestCacheClose tests cache shutdown
|
|
func TestCacheClose(t *testing.T) {
|
|
config := DefaultConfig()
|
|
cache := New(config)
|
|
|
|
_ = cache.Set("key", "value", 1*time.Hour)
|
|
|
|
// Close should not error
|
|
err := cache.Close()
|
|
if err != nil {
|
|
t.Errorf("Close should not error: %v", err)
|
|
}
|
|
|
|
// Double close should return an error since cache is already closed
|
|
err = cache.Close()
|
|
if err == nil {
|
|
t.Error("Double close should return an error")
|
|
}
|
|
}
|
|
|
|
// TestCacheContext tests context-based operations
|
|
func TestCacheContext(t *testing.T) {
|
|
config := DefaultConfig()
|
|
cache := New(config)
|
|
defer cache.Close()
|
|
|
|
_, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
|
defer cancel()
|
|
|
|
// Test context cancellation during operation
|
|
go func() {
|
|
time.Sleep(50 * time.Millisecond)
|
|
cancel()
|
|
}()
|
|
|
|
// This should respect context cancellation (if supported by cache implementation)
|
|
_ = cache.Set("key", "value", 1*time.Hour)
|
|
}
|
|
|
|
// TestCacheErrors tests error conditions
|
|
func TestCacheErrors(t *testing.T) {
|
|
config := DefaultConfig()
|
|
cache := New(config)
|
|
defer cache.Close()
|
|
|
|
// Test setting with zero TTL
|
|
err := cache.Set("zero-ttl", "value", 0)
|
|
if err != nil {
|
|
t.Errorf("Setting with zero TTL should not error: %v", err)
|
|
}
|
|
|
|
// Test setting with negative TTL
|
|
err = cache.Set("negative-ttl", "value", -1*time.Hour)
|
|
if err != nil {
|
|
t.Errorf("Setting with negative TTL should not error: %v", err)
|
|
}
|
|
|
|
// Test empty key
|
|
err = cache.Set("", "value", 1*time.Hour)
|
|
if err != nil {
|
|
t.Errorf("Setting with empty key should not error: %v", err)
|
|
}
|
|
|
|
// Test nil value
|
|
err = cache.Set("nil-value", nil, 1*time.Hour)
|
|
if err != nil {
|
|
t.Errorf("Setting nil value should not error: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestCacheTypeSpecificConfigs tests type-specific configurations
|
|
func TestCacheTypeSpecificConfigs(t *testing.T) {
|
|
// Test Token cache config
|
|
tokenConfig := &TokenConfig{
|
|
BlacklistTTL: 24 * time.Hour,
|
|
RefreshTokenTTL: 7 * 24 * time.Hour,
|
|
EnableTokenRotation: true,
|
|
}
|
|
|
|
config := DefaultConfig()
|
|
config.Type = TypeToken
|
|
config.TokenConfig = tokenConfig
|
|
|
|
cache := New(config)
|
|
defer cache.Close()
|
|
|
|
if cache.config.TokenConfig.BlacklistTTL != 24*time.Hour {
|
|
t.Errorf("Expected blacklist TTL 24h, got %v", cache.config.TokenConfig.BlacklistTTL)
|
|
}
|
|
|
|
// Test Metadata cache config
|
|
metadataConfig := &MetadataConfig{
|
|
GracePeriod: 30 * time.Minute,
|
|
ExtendedGracePeriod: 2 * time.Hour,
|
|
MaxGracePeriod: 24 * time.Hour,
|
|
SecurityCriticalMaxGracePeriod: 5 * time.Minute,
|
|
SecurityCriticalFields: []string{"issuer", "jwks_uri"},
|
|
}
|
|
|
|
config.Type = TypeMetadata
|
|
config.MetadataConfig = metadataConfig
|
|
|
|
cache2 := New(config)
|
|
defer cache2.Close()
|
|
|
|
if cache2.config.MetadataConfig.GracePeriod != 30*time.Minute {
|
|
t.Errorf("Expected grace period 30m, got %v", cache2.config.MetadataConfig.GracePeriod)
|
|
}
|
|
|
|
// Test JWK cache config
|
|
jwkConfig := &JWKConfig{
|
|
RefreshInterval: 15 * time.Minute,
|
|
MinRefreshTime: 1 * time.Minute,
|
|
MaxKeyAge: 24 * time.Hour,
|
|
}
|
|
|
|
config.Type = TypeJWK
|
|
config.JWKConfig = jwkConfig
|
|
|
|
cache3 := New(config)
|
|
defer cache3.Close()
|
|
|
|
if cache3.config.JWKConfig.RefreshInterval != 15*time.Minute {
|
|
t.Errorf("Expected refresh interval 15m, got %v", cache3.config.JWKConfig.RefreshInterval)
|
|
}
|
|
}
|
|
|
|
// TestCacheGetOrSet tests the GetOrSet functionality if it exists
|
|
func TestCacheGetOrSet(t *testing.T) {
|
|
config := DefaultConfig()
|
|
cache := New(config)
|
|
defer cache.Close()
|
|
|
|
key := "get-or-set-key"
|
|
value := "initial-value"
|
|
|
|
// Test set if not exists behavior
|
|
_ = cache.Set(key, value, 1*time.Hour)
|
|
|
|
retrieved, exists := cache.Get(key)
|
|
if !exists {
|
|
t.Error("Expected value to exist after set")
|
|
}
|
|
if retrieved != value {
|
|
t.Errorf("Expected %s, got %v", value, retrieved)
|
|
}
|
|
|
|
// Test get existing
|
|
retrieved, exists = cache.Get(key)
|
|
if !exists {
|
|
t.Error("Expected value to still exist")
|
|
}
|
|
if retrieved != value {
|
|
t.Errorf("Expected %s, got %v", value, retrieved)
|
|
}
|
|
}
|
|
|
|
// TestCacheUpdateTTL tests TTL updates if supported
|
|
func TestCacheUpdateTTL(t *testing.T) {
|
|
config := DefaultConfig()
|
|
cache := New(config)
|
|
defer cache.Close()
|
|
|
|
key := "ttl-test"
|
|
value := "value"
|
|
|
|
// Set with short TTL
|
|
_ = cache.Set(key, value, 50*time.Millisecond)
|
|
|
|
// Update with longer TTL
|
|
_ = cache.Set(key, value, 1*time.Hour)
|
|
|
|
// Wait past original TTL
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Should still exist due to updated TTL
|
|
_, exists := cache.Get(key)
|
|
if !exists {
|
|
t.Error("Expected item to exist after TTL update")
|
|
}
|
|
}
|
|
|
|
// TestCacheDisabledFeatures tests behavior with disabled features
|
|
func TestCacheDisabledFeatures(t *testing.T) {
|
|
config := DefaultConfig()
|
|
config.EnableMetrics = false
|
|
config.EnableAutoCleanup = false
|
|
config.EnableCompression = false
|
|
config.EnableMemoryLimit = false
|
|
|
|
cache := New(config)
|
|
defer cache.Close()
|
|
|
|
// Should still work with all features disabled
|
|
_ = cache.Set("key", "value", 1*time.Hour)
|
|
|
|
retrieved, exists := cache.Get("key")
|
|
if !exists {
|
|
t.Error("Expected basic functionality to work with disabled features")
|
|
}
|
|
if retrieved != "value" {
|
|
t.Error("Expected value to match")
|
|
}
|
|
}
|
|
|
|
// TestCacheSize tests size tracking
|
|
func TestCacheSize(t *testing.T) {
|
|
config := DefaultConfig()
|
|
cache := New(config)
|
|
defer cache.Close()
|
|
|
|
// Initial size should be 0
|
|
if cache.Size() != 0 {
|
|
t.Errorf("Expected initial size 0, got %d", cache.Size())
|
|
}
|
|
|
|
// Add items
|
|
for i := 0; i < 5; i++ {
|
|
key := fmt.Sprintf("key%d", i)
|
|
_ = cache.Set(key, fmt.Sprintf("value%d", i), 1*time.Hour)
|
|
}
|
|
|
|
if cache.Size() != 5 {
|
|
t.Errorf("Expected size 5, got %d", cache.Size())
|
|
}
|
|
|
|
// Delete item
|
|
cache.Delete("key0")
|
|
|
|
if cache.Size() != 4 {
|
|
t.Errorf("Expected size 4 after delete, got %d", cache.Size())
|
|
}
|
|
}
|
|
|
|
// TestCacheClear tests clearing the entire cache
|
|
func TestCacheClear(t *testing.T) {
|
|
config := DefaultConfig()
|
|
cache := New(config)
|
|
defer cache.Close()
|
|
|
|
// Add multiple items
|
|
for i := 0; i < 10; i++ {
|
|
key := fmt.Sprintf("key%d", i)
|
|
_ = cache.Set(key, fmt.Sprintf("value%d", i), 1*time.Hour)
|
|
}
|
|
|
|
if cache.Size() != 10 {
|
|
t.Errorf("Expected size 10, got %d", cache.Size())
|
|
}
|
|
|
|
// Clear cache
|
|
cache.Clear()
|
|
|
|
if cache.Size() != 0 {
|
|
t.Errorf("Expected size 0 after clear, got %d", cache.Size())
|
|
}
|
|
|
|
// Verify all items are gone
|
|
for i := 0; i < 10; i++ {
|
|
key := fmt.Sprintf("key%d", i)
|
|
if _, exists := cache.Get(key); exists {
|
|
t.Errorf("Expected %s to be cleared", key)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestCacheSetMaxSize tests dynamic max size updates
|
|
func TestCacheSetMaxSize(t *testing.T) {
|
|
config := DefaultConfig()
|
|
config.MaxSize = 5
|
|
cache := New(config)
|
|
defer cache.Close()
|
|
|
|
// Add items up to limit
|
|
for i := 0; i < 5; i++ {
|
|
key := fmt.Sprintf("key%d", i)
|
|
_ = cache.Set(key, fmt.Sprintf("value%d", i), 1*time.Hour)
|
|
}
|
|
|
|
if cache.Size() != 5 {
|
|
t.Errorf("Expected size 5, got %d", cache.Size())
|
|
}
|
|
|
|
// Reduce max size
|
|
cache.SetMaxSize(3)
|
|
|
|
// Cache should evict items to fit new limit
|
|
if cache.Size() > 3 {
|
|
t.Errorf("Expected size <= 3 after reducing max size, got %d", cache.Size())
|
|
}
|
|
|
|
// Increase max size
|
|
cache.SetMaxSize(10)
|
|
|
|
// Should be able to add more items
|
|
for i := 5; i < 8; i++ {
|
|
key := fmt.Sprintf("key%d", i)
|
|
_ = cache.Set(key, fmt.Sprintf("value%d", i), 1*time.Hour)
|
|
}
|
|
|
|
if cache.Size() > 10 {
|
|
t.Errorf("Cache size should not exceed new max size")
|
|
}
|
|
}
|
|
|
|
// TestCacheManualCleanup tests manual cleanup
|
|
func TestCacheManualCleanup(t *testing.T) {
|
|
config := DefaultConfig()
|
|
config.EnableAutoCleanup = false // Disable auto cleanup
|
|
cache := New(config)
|
|
defer cache.Close()
|
|
|
|
// Add expired items
|
|
_ = cache.Set("expired1", "value1", 1*time.Millisecond)
|
|
_ = cache.Set("expired2", "value2", 1*time.Millisecond)
|
|
_ = cache.Set("valid", "value", 1*time.Hour)
|
|
|
|
// Wait for expiration
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
// Items should still be there since auto cleanup is disabled
|
|
if cache.Size() != 3 {
|
|
t.Errorf("Expected size 3 before cleanup, got %d", cache.Size())
|
|
}
|
|
|
|
// Manual cleanup
|
|
cache.Cleanup()
|
|
|
|
// Expired items should be removed
|
|
if cache.Size() == 3 {
|
|
t.Error("Cleanup should have removed expired items")
|
|
}
|
|
|
|
// Valid item should still exist
|
|
_, exists := cache.Get("valid")
|
|
if !exists {
|
|
t.Error("Valid item should still exist after cleanup")
|
|
}
|
|
}
|
|
|
|
// TestCacheHitRate tests hit rate calculation
|
|
func TestCacheHitRate(t *testing.T) {
|
|
config := DefaultConfig()
|
|
cache := New(config)
|
|
defer cache.Close()
|
|
|
|
// Add item
|
|
_ = cache.Set("key", "value", 1*time.Hour)
|
|
|
|
// Generate hits and misses
|
|
cache.Get("key") // hit
|
|
cache.Get("key") // hit
|
|
cache.Get("nonexistent") // miss
|
|
|
|
stats := cache.GetStats()
|
|
hits := stats["hits"].(int64)
|
|
misses := stats["misses"].(int64)
|
|
|
|
if hits != 2 {
|
|
t.Errorf("Expected 2 hits, got %d", hits)
|
|
}
|
|
if misses != 1 {
|
|
t.Errorf("Expected 1 miss, got %d", misses)
|
|
}
|
|
|
|
// Check hit rate if available in stats
|
|
if hitRate, exists := stats["hit_rate"]; exists {
|
|
expectedRate := float64(hits) / float64(hits+misses)
|
|
if hitRate.(float64) != expectedRate {
|
|
t.Errorf("Expected hit rate %f, got %f", expectedRate, hitRate)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestCacheCompatibilityWrapper tests the compatibility wrapper
|
|
func TestCacheCompatibilityWrapper(t *testing.T) {
|
|
config := DefaultConfig()
|
|
cache := New(config)
|
|
defer cache.Close()
|
|
|
|
wrapper := NewCompatibilityWrapper(cache)
|
|
if wrapper == nil {
|
|
t.Error("NewCompatibilityWrapper should not return nil")
|
|
}
|
|
|
|
// Test wrapper methods
|
|
wrapper.Set("key", "value", 1*time.Hour)
|
|
|
|
value, exists := wrapper.Get("key")
|
|
if !exists {
|
|
t.Error("Expected key to exist in wrapper")
|
|
}
|
|
if value != "value" {
|
|
t.Errorf("Expected 'value', got %v", value)
|
|
}
|
|
|
|
wrapper.Delete("key")
|
|
_, exists = wrapper.Get("key")
|
|
if exists {
|
|
t.Error("Expected key to be deleted in wrapper")
|
|
}
|
|
|
|
// Test wrapper stats
|
|
stats := wrapper.GetStats()
|
|
if stats == nil {
|
|
t.Error("Wrapper GetStats should not return nil")
|
|
}
|
|
}
|
|
|
|
// TestCacheTypedCaches tests the typed cache wrappers
|
|
func TestCacheTypedCaches(t *testing.T) {
|
|
config := DefaultConfig()
|
|
baseCache := New(config)
|
|
defer baseCache.Close()
|
|
|
|
// Test JWK cache
|
|
jwkCache := NewJWKCache(baseCache)
|
|
if jwkCache == nil {
|
|
t.Error("NewJWKCache should not return nil")
|
|
}
|
|
|
|
jwkSet := &JWKSet{
|
|
Keys: []JWK{
|
|
{
|
|
Kid: "test-key",
|
|
Kty: "RSA",
|
|
Use: "sig",
|
|
N: "test-modulus",
|
|
E: "AQAB",
|
|
},
|
|
},
|
|
}
|
|
|
|
err := jwkCache.Set("test-jwk", jwkSet, 1*time.Hour)
|
|
if err != nil {
|
|
t.Errorf("JWKCache Set should not error: %v", err)
|
|
}
|
|
|
|
retrieved, exists := jwkCache.Get("test-jwk")
|
|
if !exists {
|
|
t.Error("Expected JWK to exist")
|
|
}
|
|
if retrieved == nil {
|
|
t.Error("JWK data should not be nil")
|
|
}
|
|
|
|
// Test Session cache
|
|
sessionCache := NewSessionCache(baseCache)
|
|
if sessionCache == nil {
|
|
t.Error("NewSessionCache should not return nil")
|
|
}
|
|
|
|
sessionData := SessionData{
|
|
ID: "session123",
|
|
UserID: "user123",
|
|
AccessToken: "access-token",
|
|
ExpiresAt: time.Now().Add(1 * time.Hour),
|
|
}
|
|
|
|
err = sessionCache.Set("session123", sessionData, 30*time.Minute)
|
|
if err != nil {
|
|
t.Errorf("SessionCache Set should not error: %v", err)
|
|
}
|
|
|
|
retrievedSession, exists := sessionCache.Get("session123")
|
|
if !exists {
|
|
t.Error("Expected session to exist")
|
|
}
|
|
if retrievedSession.UserID != "user123" {
|
|
t.Error("Session data should match")
|
|
}
|
|
}
|
|
|
|
// TestNoOpLogger tests the noOpLogger implementation
|
|
func TestNoOpLogger(t *testing.T) {
|
|
logger := &noOpLogger{}
|
|
|
|
// Test all logging methods - they should not panic or error
|
|
logger.Debug("debug message")
|
|
logger.Debugf("debug %s", "message")
|
|
logger.Info("info message")
|
|
logger.Infof("info %s", "message")
|
|
logger.Error("error message")
|
|
logger.Errorf("error %s", "message")
|
|
logger.Warn("warn message")
|
|
logger.Warnf("warn %s", "message")
|
|
logger.Fatal("fatal message")
|
|
logger.Fatalf("fatal %s", "message")
|
|
|
|
// Test WithField and WithFields - should return the same logger
|
|
fieldLogger := logger.WithField("key", "value")
|
|
if fieldLogger != logger {
|
|
t.Error("WithField should return the same logger instance")
|
|
}
|
|
|
|
fieldsLogger := logger.WithFields(map[string]interface{}{
|
|
"key1": "value1",
|
|
"key2": "value2",
|
|
})
|
|
if fieldsLogger != logger {
|
|
t.Error("WithFields should return the same logger instance")
|
|
}
|
|
|
|
// Test nil values don't cause issues
|
|
logger.WithField("key", nil)
|
|
logger.WithFields(nil)
|
|
logger.WithFields(map[string]interface{}{
|
|
"nil": nil,
|
|
})
|
|
}
|
|
|
|
// TestCacheEdgeCases tests various edge cases
|
|
func TestCacheEdgeCases(t *testing.T) {
|
|
config := DefaultConfig()
|
|
cache := New(config)
|
|
defer cache.Close()
|
|
|
|
// Test setting very large value
|
|
largeValue := make([]byte, 1024*1024) // 1MB
|
|
for i := range largeValue {
|
|
largeValue[i] = byte(i % 256)
|
|
}
|
|
|
|
err := cache.Set("large", largeValue, 1*time.Hour)
|
|
if err != nil {
|
|
t.Errorf("Setting large value should not error: %v", err)
|
|
}
|
|
|
|
retrieved, exists := cache.Get("large")
|
|
if !exists {
|
|
t.Error("Large value should exist")
|
|
}
|
|
if len(retrieved.([]byte)) != len(largeValue) {
|
|
t.Error("Large value should match original size")
|
|
}
|
|
|
|
// Test concurrent access to same key
|
|
var wg sync.WaitGroup
|
|
numGoroutines := 10
|
|
|
|
wg.Add(numGoroutines)
|
|
for i := 0; i < numGoroutines; i++ {
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
key := "concurrent"
|
|
value := fmt.Sprintf("value-%d", id)
|
|
cache.Set(key, value, 1*time.Hour)
|
|
cache.Get(key)
|
|
if id%2 == 0 {
|
|
cache.Delete(key)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Test setting same key multiple times
|
|
for i := 0; i < 100; i++ {
|
|
err := cache.Set("overwrite", fmt.Sprintf("value-%d", i), 1*time.Hour)
|
|
if err != nil {
|
|
t.Errorf("Overwrite should not error: %v", err)
|
|
}
|
|
}
|
|
|
|
value, exists := cache.Get("overwrite")
|
|
if !exists {
|
|
t.Error("Overwritten value should exist")
|
|
}
|
|
if !strings.HasPrefix(value.(string), "value-") {
|
|
t.Error("Value should have expected format")
|
|
}
|
|
}
|
|
|
|
// TestCompatibilityWrapperMethods tests all CompatibilityWrapper methods
|
|
func TestCompatibilityWrapperMethods(t *testing.T) {
|
|
config := DefaultConfig()
|
|
baseCache := New(config)
|
|
defer baseCache.Close()
|
|
|
|
wrapper := NewCompatibilityWrapper(baseCache)
|
|
if wrapper == nil {
|
|
t.Fatal("NewCompatibilityWrapper should not return nil")
|
|
}
|
|
|
|
// Test SetMaxSize method
|
|
wrapper.SetMaxSize(100)
|
|
if wrapper.Size() != 0 {
|
|
t.Error("Size should be 0 initially")
|
|
}
|
|
|
|
// Test Size method with data
|
|
wrapper.Set("key1", "value1", 1*time.Hour)
|
|
if wrapper.Size() != 1 {
|
|
t.Errorf("Expected size 1, got %d", wrapper.Size())
|
|
}
|
|
|
|
// Test Clear method
|
|
wrapper.Clear()
|
|
if wrapper.Size() != 0 {
|
|
t.Error("Size should be 0 after clear")
|
|
}
|
|
|
|
// Add some data for cleanup test
|
|
wrapper.Set("expired", "value", 1*time.Millisecond)
|
|
time.Sleep(5 * time.Millisecond)
|
|
|
|
// Test Cleanup method
|
|
wrapper.Cleanup()
|
|
|
|
// Test Close method (should not panic)
|
|
wrapper.Close()
|
|
}
|
|
|
|
// TestUniversalCacheCompat tests UniversalCacheCompat methods
|
|
func TestUniversalCacheCompat(t *testing.T) {
|
|
config := DefaultConfig()
|
|
compat := NewUniversalCacheCompat(config)
|
|
if compat == nil {
|
|
t.Fatal("NewUniversalCacheCompat should not return nil")
|
|
}
|
|
defer compat.Close()
|
|
|
|
// Test Set method
|
|
err := compat.Set("test-key", "test-value", 1*time.Hour)
|
|
if err != nil {
|
|
t.Errorf("UniversalCacheCompat Set should not error: %v", err)
|
|
}
|
|
|
|
// Verify the value was set
|
|
value, exists := compat.Get("test-key")
|
|
if !exists {
|
|
t.Error("Expected value to exist")
|
|
}
|
|
if value != "test-value" {
|
|
t.Errorf("Expected 'test-value', got %v", value)
|
|
}
|
|
}
|
|
|
|
// TestTokenCacheCompat tests TokenCacheCompat methods
|
|
func TestTokenCacheCompat(t *testing.T) {
|
|
compat := NewTokenCacheCompat()
|
|
if compat == nil {
|
|
t.Fatal("NewTokenCacheCompat should not return nil")
|
|
}
|
|
|
|
token := "test-token-123"
|
|
claims := map[string]interface{}{
|
|
"sub": "user123",
|
|
"iat": time.Now().Unix(),
|
|
"exp": time.Now().Add(1 * time.Hour).Unix(),
|
|
}
|
|
|
|
// Test Set method
|
|
compat.Set(token, claims, 1*time.Hour)
|
|
|
|
// Test Get method
|
|
retrievedClaims, exists := compat.Get(token)
|
|
if !exists {
|
|
t.Error("Expected token claims to exist")
|
|
}
|
|
if retrievedClaims["sub"] != "user123" {
|
|
t.Error("Claims should match what was set")
|
|
}
|
|
|
|
// Test Delete method
|
|
compat.Delete(token)
|
|
_, exists = compat.Get(token)
|
|
if exists {
|
|
t.Error("Expected token to be deleted")
|
|
}
|
|
}
|
|
|
|
// TestMetadataCacheCompat tests MetadataCacheCompat methods
|
|
func TestMetadataCacheCompat(t *testing.T) {
|
|
var wg sync.WaitGroup
|
|
compat := NewMetadataCacheCompat(&wg)
|
|
if compat == nil {
|
|
t.Fatal("NewMetadataCacheCompat should not return nil")
|
|
}
|
|
|
|
// Test with logger
|
|
logger := &noOpLogger{}
|
|
compatWithLogger := NewMetadataCacheCompatWithLogger(&wg, logger)
|
|
if compatWithLogger == nil {
|
|
t.Fatal("NewMetadataCacheCompatWithLogger should not return nil")
|
|
}
|
|
|
|
providerURL := "https://example.com/.well-known/openid_configuration"
|
|
metadata := &ProviderMetadata{
|
|
Issuer: "https://example.com",
|
|
AuthorizationEndpoint: "https://example.com/auth",
|
|
TokenEndpoint: "https://example.com/token",
|
|
JWKSUri: "https://example.com/.well-known/jwks.json",
|
|
UserInfoEndpoint: "https://example.com/userinfo",
|
|
ScopesSupported: []string{"openid", "profile", "email"},
|
|
}
|
|
|
|
// Test Set method
|
|
err := compat.Set(providerURL, metadata, 1*time.Hour)
|
|
if err != nil {
|
|
t.Errorf("MetadataCacheCompat Set should not error: %v", err)
|
|
}
|
|
|
|
// Test Get method
|
|
retrieved, exists := compat.Get(providerURL)
|
|
if !exists {
|
|
t.Error("Expected metadata to exist")
|
|
}
|
|
if retrieved.Issuer != "https://example.com" {
|
|
t.Error("Metadata should match what was set")
|
|
}
|
|
|
|
// Test GetWithGracePeriod method
|
|
ctx := context.Background()
|
|
gracePeriodRetrieved, gracePeriodExists := compat.GetWithGracePeriod(ctx, providerURL)
|
|
if !gracePeriodExists {
|
|
t.Error("Expected metadata to exist with grace period")
|
|
}
|
|
if gracePeriodRetrieved.Issuer != "https://example.com" {
|
|
t.Error("Grace period metadata should match")
|
|
}
|
|
|
|
// Test Delete method
|
|
compat.Delete(providerURL)
|
|
_, exists = compat.Get(providerURL)
|
|
if exists {
|
|
t.Error("Expected metadata to be deleted")
|
|
}
|
|
}
|
|
|
|
// TestJWKCacheCompat tests JWKCacheCompat methods
|
|
func TestJWKCacheCompat(t *testing.T) {
|
|
compat := NewJWKCacheCompat()
|
|
if compat == nil {
|
|
t.Fatal("NewJWKCacheCompat should not return nil")
|
|
}
|
|
|
|
jwksURL := "https://example.com/.well-known/jwks.json"
|
|
jwkSet := &JWKSet{
|
|
Keys: []JWK{
|
|
{
|
|
Kid: "key1",
|
|
Kty: "RSA",
|
|
Use: "sig",
|
|
N: "test-modulus",
|
|
E: "AQAB",
|
|
},
|
|
},
|
|
}
|
|
|
|
// Test Set method
|
|
err := compat.Set(jwksURL, jwkSet, 1*time.Hour)
|
|
if err != nil {
|
|
t.Errorf("JWKCacheCompat Set should not error: %v", err)
|
|
}
|
|
|
|
// Test GetJWKS method (should find cached value)
|
|
ctx := context.Background()
|
|
httpClient := &http.Client{}
|
|
retrieved, err := compat.GetJWKS(ctx, jwksURL, httpClient)
|
|
if err != nil {
|
|
t.Errorf("GetJWKS should not error: %v", err)
|
|
}
|
|
if retrieved == nil {
|
|
t.Error("Expected to retrieve cached JWKS")
|
|
return
|
|
}
|
|
if len(retrieved.Keys) != 1 || retrieved.Keys[0].Kid != "key1" {
|
|
t.Error("Retrieved JWKS should match what was set")
|
|
}
|
|
|
|
// Test GetJWKS with non-existent URL (should return nil)
|
|
nonExistent, err := compat.GetJWKS(ctx, "https://non-existent.com/jwks", httpClient)
|
|
if err != nil {
|
|
t.Errorf("GetJWKS with non-existent key should not error: %v", err)
|
|
}
|
|
if nonExistent != nil {
|
|
t.Error("Expected nil for non-existent JWKS")
|
|
}
|
|
|
|
// Test Cleanup method (should not panic)
|
|
compat.Cleanup()
|
|
|
|
// Test Close method (should not panic)
|
|
compat.Close()
|
|
}
|
|
|
|
// TestCacheManagerCompat tests CacheManagerCompat methods
|
|
func TestCacheManagerCompat(t *testing.T) {
|
|
var wg sync.WaitGroup
|
|
manager := GetGlobalCacheManagerCompat(&wg)
|
|
if manager == nil {
|
|
t.Fatal("GetGlobalCacheManagerCompat should not return nil")
|
|
}
|
|
|
|
// Test GetSharedTokenBlacklist
|
|
blacklist := manager.GetSharedTokenBlacklist()
|
|
if blacklist == nil {
|
|
t.Error("GetSharedTokenBlacklist should not return nil")
|
|
}
|
|
|
|
// Test GetSharedTokenCache
|
|
tokenCache := manager.GetSharedTokenCache()
|
|
if tokenCache == nil {
|
|
t.Error("GetSharedTokenCache should not return nil")
|
|
}
|
|
|
|
// Test GetSharedMetadataCache
|
|
metadataCache := manager.GetSharedMetadataCache()
|
|
if metadataCache == nil {
|
|
t.Error("GetSharedMetadataCache should not return nil")
|
|
}
|
|
|
|
// Test GetSharedJWKCache
|
|
jwkCache := manager.GetSharedJWKCache()
|
|
if jwkCache == nil {
|
|
t.Error("GetSharedJWKCache should not return nil")
|
|
}
|
|
|
|
// Test Close method
|
|
err := manager.Close()
|
|
if err != nil {
|
|
t.Errorf("CacheManagerCompat Close should not error: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestUniversalCacheManagerCompat tests UniversalCacheManagerCompat methods
|
|
func TestUniversalCacheManagerCompat(t *testing.T) {
|
|
logger := &noOpLogger{}
|
|
manager := GetUniversalCacheManagerCompat(logger)
|
|
if manager == nil {
|
|
t.Fatal("GetUniversalCacheManagerCompat should not return nil")
|
|
}
|
|
|
|
// Test GetTokenCache
|
|
tokenCache := manager.GetTokenCache()
|
|
if tokenCache == nil {
|
|
t.Error("GetTokenCache should not return nil")
|
|
}
|
|
|
|
// Test GetMetadataCache
|
|
metadataCache := manager.GetMetadataCache()
|
|
if metadataCache == nil {
|
|
t.Error("GetMetadataCache should not return nil")
|
|
}
|
|
|
|
// Test GetJWKCache
|
|
jwkCache := manager.GetJWKCache()
|
|
if jwkCache == nil {
|
|
t.Error("GetJWKCache should not return nil")
|
|
}
|
|
|
|
// Test GetBlacklistCache
|
|
blacklistCache := manager.GetBlacklistCache()
|
|
if blacklistCache == nil {
|
|
t.Error("GetBlacklistCache should not return nil")
|
|
}
|
|
|
|
// Test Close method
|
|
err := manager.Close()
|
|
if err != nil && err.Error() != "cache already closed" {
|
|
t.Errorf("UniversalCacheManagerCompat Close should not error (unless already closed): %v", err)
|
|
}
|
|
}
|
|
|
|
// TestTypedCacheWrapper tests TypedCache methods
|
|
func TestTypedCacheWrapper(t *testing.T) {
|
|
config := DefaultConfig()
|
|
baseCache := New(config)
|
|
defer baseCache.Close()
|
|
|
|
typedCache := NewTypedCache[string](baseCache, "test-prefix")
|
|
if typedCache == nil {
|
|
t.Fatal("NewTypedCache should not return nil")
|
|
}
|
|
|
|
// Test Set and Get
|
|
err := typedCache.Set("test-key", "test-value", 1*time.Hour)
|
|
if err != nil {
|
|
t.Errorf("TypedCache Set should not error: %v", err)
|
|
}
|
|
|
|
value, exists := typedCache.Get("test-key")
|
|
if !exists {
|
|
t.Error("Expected typed value to exist")
|
|
}
|
|
if value != "test-value" {
|
|
t.Errorf("Expected 'test-value', got '%s'", value)
|
|
}
|
|
|
|
// Test Delete method
|
|
typedCache.Delete("test-key")
|
|
_, exists = typedCache.Get("test-key")
|
|
if exists {
|
|
t.Error("Expected typed value to be deleted")
|
|
}
|
|
|
|
// Test Clear method
|
|
typedCache.Set("key1", "value1", 1*time.Hour)
|
|
typedCache.Set("key2", "value2", 1*time.Hour)
|
|
typedCache.Clear()
|
|
|
|
if typedCache.Size() != 0 {
|
|
t.Error("Expected typed cache to be empty after clear")
|
|
}
|
|
|
|
// Test Size method
|
|
if typedCache.Size() != 0 {
|
|
t.Errorf("Expected size 0, got %d", typedCache.Size())
|
|
}
|
|
|
|
// Add items to test size
|
|
typedCache.Set("size1", "value1", 1*time.Hour)
|
|
typedCache.Set("size2", "value2", 1*time.Hour)
|
|
if typedCache.Size() != 2 {
|
|
t.Errorf("Expected size 2, got %d", typedCache.Size())
|
|
}
|
|
}
|
|
|
|
// TestTokenCacheSpecificMethods tests TokenCache specific methods
|
|
func TestTokenCacheSpecificMethods(t *testing.T) {
|
|
config := DefaultConfig()
|
|
baseCache := New(config)
|
|
defer baseCache.Close()
|
|
|
|
tokenCache := NewTokenCache(baseCache)
|
|
if tokenCache == nil {
|
|
t.Fatal("NewTokenCache should not return nil")
|
|
}
|
|
|
|
token := "test-token-456"
|
|
claims := map[string]interface{}{
|
|
"sub": "user456",
|
|
"iat": time.Now().Unix(),
|
|
"exp": time.Now().Add(2 * time.Hour).Unix(),
|
|
"aud": "test-audience",
|
|
}
|
|
|
|
// Test Delete method (currently at 0% coverage)
|
|
tokenCache.Set(token, claims, 1*time.Hour)
|
|
tokenCache.Delete(token)
|
|
_, exists := tokenCache.Get(token)
|
|
if exists {
|
|
t.Error("Expected token to be deleted")
|
|
}
|
|
|
|
// Test edge case in IsBlacklisted when token doesn't exist
|
|
if tokenCache.IsBlacklisted("non-existent-token") {
|
|
t.Error("Non-existent token should not be blacklisted")
|
|
}
|
|
}
|
|
|
|
// TestMetadataCacheSpecificMethods tests MetadataCache specific methods
|
|
func TestMetadataCacheSpecificMethods(t *testing.T) {
|
|
config := DefaultConfig()
|
|
baseCache := New(config)
|
|
defer baseCache.Close()
|
|
|
|
metadataConfig := MetadataConfig{
|
|
GracePeriod: 30 * time.Minute,
|
|
}
|
|
metadataCache := NewMetadataCache(baseCache, metadataConfig)
|
|
if metadataCache == nil {
|
|
t.Fatal("NewMetadataCache should not return nil")
|
|
}
|
|
|
|
providerURL := "https://test-provider.com/.well-known/openid_configuration"
|
|
metadata := &ProviderMetadata{
|
|
Issuer: "https://test-provider.com",
|
|
AuthorizationEndpoint: "https://test-provider.com/auth",
|
|
TokenEndpoint: "https://test-provider.com/token",
|
|
JWKSUri: "https://test-provider.com/.well-known/jwks.json",
|
|
UserInfoEndpoint: "https://test-provider.com/userinfo",
|
|
ScopesSupported: []string{"openid", "profile"},
|
|
}
|
|
|
|
// Test Set method (currently at 0% coverage)
|
|
err := metadataCache.Set(providerURL, metadata, 30*time.Minute)
|
|
if err != nil {
|
|
t.Errorf("MetadataCache Set should not error: %v", err)
|
|
}
|
|
|
|
// Test Get method (currently at 0% coverage)
|
|
retrieved, exists := metadataCache.Get(providerURL)
|
|
if !exists {
|
|
t.Error("Expected metadata to exist")
|
|
}
|
|
if retrieved.Issuer != "https://test-provider.com" {
|
|
t.Error("Retrieved metadata should match what was set")
|
|
}
|
|
|
|
// Test Delete method (currently at 0% coverage)
|
|
metadataCache.Delete(providerURL)
|
|
_, exists = metadataCache.Get(providerURL)
|
|
if exists {
|
|
t.Error("Expected metadata to be deleted")
|
|
}
|
|
}
|
|
|
|
// TestJWKCacheSpecificMethods tests JWKCache specific methods
|
|
func TestJWKCacheSpecificMethods(t *testing.T) {
|
|
config := DefaultConfig()
|
|
baseCache := New(config)
|
|
defer baseCache.Close()
|
|
|
|
jwkCache := NewJWKCache(baseCache)
|
|
if jwkCache == nil {
|
|
t.Fatal("NewJWKCache should not return nil")
|
|
}
|
|
|
|
jwksURL := "https://test-jwks.com/.well-known/jwks.json"
|
|
jwkSet := &JWKSet{
|
|
Keys: []JWK{
|
|
{
|
|
Kid: "test-key-123",
|
|
Kty: "RSA",
|
|
Use: "sig",
|
|
N: "test-modulus-value",
|
|
E: "AQAB",
|
|
},
|
|
{
|
|
Kid: "test-key-456",
|
|
Kty: "EC",
|
|
Use: "sig",
|
|
N: "test-n-value",
|
|
E: "AQAB",
|
|
},
|
|
},
|
|
}
|
|
|
|
// Test Delete method (currently at 0% coverage)
|
|
jwkCache.Set(jwksURL, jwkSet, 1*time.Hour)
|
|
jwkCache.Delete(jwksURL)
|
|
_, exists := jwkCache.Get(jwksURL)
|
|
if exists {
|
|
t.Error("Expected JWK set to be deleted")
|
|
}
|
|
|
|
// Test edge case in Get method with different key types
|
|
complexJWKSet := &JWKSet{
|
|
Keys: []JWK{
|
|
{
|
|
Kid: "rsa-key",
|
|
Kty: "RSA",
|
|
Use: "sig",
|
|
N: "long-modulus-value",
|
|
E: "AQAB",
|
|
},
|
|
},
|
|
}
|
|
|
|
jwkCache.Set("complex-jwks", complexJWKSet, 2*time.Hour)
|
|
retrieved, exists := jwkCache.Get("complex-jwks")
|
|
if !exists {
|
|
t.Error("Expected complex JWK set to exist")
|
|
}
|
|
if len(retrieved.Keys) != 1 || retrieved.Keys[0].Kty != "RSA" {
|
|
t.Error("Complex JWK set should match what was set")
|
|
}
|
|
}
|
|
|
|
// TestSessionCacheSpecificMethods tests SessionCache specific methods
|
|
func TestSessionCacheSpecificMethods(t *testing.T) {
|
|
config := DefaultConfig()
|
|
baseCache := New(config)
|
|
defer baseCache.Close()
|
|
|
|
sessionCache := NewSessionCache(baseCache)
|
|
if sessionCache == nil {
|
|
t.Fatal("NewSessionCache should not return nil")
|
|
}
|
|
|
|
sessionID := "session-123-abc"
|
|
sessionData := SessionData{
|
|
ID: sessionID,
|
|
UserID: "user789",
|
|
AccessToken: "access-token-xyz",
|
|
RefreshToken: "refresh-token-abc",
|
|
ExpiresAt: time.Now().Add(1 * time.Hour),
|
|
Claims: map[string]interface{}{
|
|
"sub": "user789",
|
|
},
|
|
}
|
|
|
|
// Test Delete method (currently at 0% coverage)
|
|
sessionCache.Set(sessionID, sessionData, 45*time.Minute)
|
|
sessionCache.Delete(sessionID)
|
|
_, exists := sessionCache.Get(sessionID)
|
|
if exists {
|
|
t.Error("Expected session to be deleted")
|
|
}
|
|
|
|
// Test Exists method (currently at 0% coverage)
|
|
sessionCache.Set(sessionID, sessionData, 45*time.Minute)
|
|
if !sessionCache.Exists(sessionID) {
|
|
t.Error("Expected session to exist")
|
|
}
|
|
|
|
sessionCache.Delete(sessionID)
|
|
if sessionCache.Exists(sessionID) {
|
|
t.Error("Expected session to not exist after delete")
|
|
}
|
|
|
|
// Test Exists with non-existent session
|
|
if sessionCache.Exists("non-existent-session") {
|
|
t.Error("Non-existent session should not exist")
|
|
}
|
|
}
|
|
|
|
// TestManagerUncoveredMethods tests Manager methods currently at 0% coverage
|
|
func TestManagerUncoveredMethods(t *testing.T) {
|
|
logger := &noOpLogger{}
|
|
manager := NewManager(logger)
|
|
if manager == nil {
|
|
t.Fatal("NewManager should not return nil")
|
|
}
|
|
|
|
// Test GetGlobalManager (currently at 0% coverage)
|
|
globalManager := GetGlobalManager(logger)
|
|
if globalManager == nil {
|
|
t.Error("GetGlobalManager should not return nil")
|
|
}
|
|
|
|
// Test GetGeneralCache (currently at 0% coverage)
|
|
generalCache := manager.GetGeneralCache()
|
|
if generalCache == nil {
|
|
t.Error("GetGeneralCache should not return nil")
|
|
}
|
|
|
|
// Test GetRawTokenCache (currently at 0% coverage)
|
|
rawTokenCache := manager.GetRawTokenCache()
|
|
if rawTokenCache == nil {
|
|
t.Error("GetRawTokenCache should not return nil")
|
|
}
|
|
|
|
// Test GetRawMetadataCache (currently at 0% coverage)
|
|
rawMetadataCache := manager.GetRawMetadataCache()
|
|
if rawMetadataCache == nil {
|
|
t.Error("GetRawMetadataCache should not return nil")
|
|
}
|
|
|
|
// Test GetRawJWKCache (currently at 0% coverage)
|
|
rawJWKCache := manager.GetRawJWKCache()
|
|
if rawJWKCache == nil {
|
|
t.Error("GetRawJWKCache should not return nil")
|
|
}
|
|
|
|
// Test ClearAll (currently at 0% coverage)
|
|
// Add some data first
|
|
generalCache.Set("test-key", "test-value", 1*time.Hour)
|
|
rawTokenCache.Set("token-key", "token-value", 1*time.Hour)
|
|
|
|
manager.ClearAll()
|
|
|
|
// Verify all caches are cleared
|
|
if generalCache.Size() != 0 {
|
|
t.Error("General cache should be empty after ClearAll")
|
|
}
|
|
if rawTokenCache.Size() != 0 {
|
|
t.Error("Token cache should be empty after ClearAll")
|
|
}
|
|
|
|
// Test CleanupAll (currently at 0% coverage)
|
|
// Add some expired items
|
|
generalCache.Set("expired1", "value1", 1*time.Millisecond)
|
|
rawTokenCache.Set("expired2", "value2", 1*time.Millisecond)
|
|
time.Sleep(5 * time.Millisecond)
|
|
|
|
manager.CleanupAll()
|
|
// Note: CleanupAll may not immediately remove expired items depending on implementation
|
|
|
|
// Test SetLogger (currently at 0% coverage)
|
|
newLogger := &noOpLogger{}
|
|
manager.SetLogger(newLogger)
|
|
// Verify logger is set (we can't directly test this without exposing internal state)
|
|
|
|
// Test Close with multiple components
|
|
err := manager.Close()
|
|
if err != nil {
|
|
t.Errorf("Manager Close should not error: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestManagerCloseEdgeCases tests Manager.Close edge cases
|
|
func TestManagerCloseEdgeCases(t *testing.T) {
|
|
manager := NewManager(nil)
|
|
|
|
// Test Close when some caches might be nil
|
|
err := manager.Close()
|
|
if err != nil {
|
|
t.Errorf("Close should handle nil caches gracefully: %v", err)
|
|
}
|
|
|
|
// Test double close (should return an error for the manager's underlying caches)
|
|
err = manager.Close()
|
|
if err == nil {
|
|
t.Error("Double close should return an error")
|
|
} else if err.Error() != "cache already closed" {
|
|
t.Errorf("Expected 'cache already closed' error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestCacheRaceConditions tests concurrent access patterns with race detection
|
|
func TestCacheRaceConditions(t *testing.T) {
|
|
config := DefaultConfig()
|
|
config.MaxSize = 1000
|
|
cache := New(config)
|
|
defer cache.Close()
|
|
|
|
var wg sync.WaitGroup
|
|
numGoroutines := 50
|
|
numOperations := 100
|
|
|
|
// Test concurrent Set/Get/Delete operations
|
|
wg.Add(numGoroutines * 3)
|
|
|
|
// Concurrent Set operations
|
|
for i := 0; i < numGoroutines; i++ {
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
for j := 0; j < numOperations; j++ {
|
|
key := fmt.Sprintf("set-key-%d-%d", id, j)
|
|
value := fmt.Sprintf("value-%d-%d", id, j)
|
|
cache.Set(key, value, 1*time.Hour)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
// Concurrent Get operations
|
|
for i := 0; i < numGoroutines; i++ {
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
for j := 0; j < numOperations; j++ {
|
|
key := fmt.Sprintf("get-key-%d", j%10)
|
|
cache.Get(key)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
// Concurrent Delete operations
|
|
for i := 0; i < numGoroutines; i++ {
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
for j := 0; j < numOperations; j++ {
|
|
key := fmt.Sprintf("delete-key-%d", j%10)
|
|
cache.Delete(key)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Test concurrent cache management operations
|
|
wg.Add(4)
|
|
go func() {
|
|
defer wg.Done()
|
|
for i := 0; i < 50; i++ {
|
|
cache.Size()
|
|
time.Sleep(1 * time.Millisecond)
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
for i := 0; i < 10; i++ {
|
|
cache.GetStats()
|
|
time.Sleep(5 * time.Millisecond)
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
for i := 0; i < 5; i++ {
|
|
cache.SetMaxSize(500 + i*100)
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
for i := 0; i < 3; i++ {
|
|
cache.Cleanup()
|
|
time.Sleep(15 * time.Millisecond)
|
|
}
|
|
}()
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
// TestAdvancedEdgeCases tests complex edge cases and error scenarios
|
|
func TestAdvancedEdgeCases(t *testing.T) {
|
|
// Test with extreme configuration values
|
|
extremeConfig := Config{
|
|
Type: TypeGeneral,
|
|
MaxSize: 1, // Very small
|
|
MaxMemoryBytes: 100, // Very small memory limit
|
|
DefaultTTL: 1 * time.Nanosecond, // Very short TTL
|
|
CleanupInterval: 1 * time.Millisecond,
|
|
EnableCompression: true,
|
|
EnableMetrics: true,
|
|
EnableAutoCleanup: true,
|
|
EnableMemoryLimit: true,
|
|
}
|
|
|
|
cache := New(extremeConfig)
|
|
defer cache.Close()
|
|
|
|
// Test rapid-fire operations with extreme config
|
|
for i := 0; i < 100; i++ {
|
|
key := fmt.Sprintf("rapid-%d", i)
|
|
cache.Set(key, fmt.Sprintf("value-%d", i), 1*time.Millisecond)
|
|
cache.Get(key)
|
|
if i%10 == 0 {
|
|
cache.Delete(key)
|
|
}
|
|
}
|
|
|
|
// Test with complex nested data structures
|
|
complexData := map[string]interface{}{
|
|
"level1": map[string]interface{}{
|
|
"level2": map[string]interface{}{
|
|
"level3": []interface{}{
|
|
map[string]interface{}{
|
|
"nested": "value",
|
|
"number": 42,
|
|
"array": []int{1, 2, 3, 4, 5},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"slice": []map[string]interface{}{
|
|
{"key1": "value1"},
|
|
{"key2": "value2"},
|
|
},
|
|
}
|
|
|
|
err := cache.Set("complex", complexData, 1*time.Hour)
|
|
if err != nil {
|
|
t.Errorf("Setting complex data should not error: %v", err)
|
|
}
|
|
|
|
retrieved, exists := cache.Get("complex")
|
|
if !exists {
|
|
t.Error("Complex data should exist")
|
|
}
|
|
if retrieved == nil {
|
|
t.Error("Retrieved complex data should not be nil")
|
|
}
|
|
|
|
// Test with various data types
|
|
testCases := []struct {
|
|
key string
|
|
value interface{}
|
|
}{
|
|
{"string", "test string"},
|
|
{"int", 42},
|
|
{"float", 3.14159},
|
|
{"bool", true},
|
|
{"slice", []string{"a", "b", "c"}},
|
|
{"map", map[string]int{"one": 1, "two": 2}},
|
|
{"nil", nil},
|
|
{"empty-string", ""},
|
|
{"empty-slice", []string{}},
|
|
{"empty-map", map[string]interface{}{}},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
err := cache.Set(tc.key, tc.value, 1*time.Hour)
|
|
if err != nil {
|
|
t.Errorf("Setting %s should not error: %v", tc.key, err)
|
|
}
|
|
|
|
retrieved, exists := cache.Get(tc.key)
|
|
if !exists {
|
|
t.Errorf("Value for %s should exist", tc.key)
|
|
}
|
|
|
|
// For nil values, check that we get nil back
|
|
if tc.value == nil && retrieved != nil {
|
|
t.Errorf("Expected nil for %s, got %v", tc.key, retrieved)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestConcurrentManagerOperations tests Manager operations under concurrent access
|
|
func TestConcurrentManagerOperations(t *testing.T) {
|
|
manager := NewManager(&noOpLogger{})
|
|
defer manager.Close()
|
|
|
|
var wg sync.WaitGroup
|
|
numGoroutines := 20
|
|
|
|
// Test concurrent access to different cache types
|
|
wg.Add(numGoroutines * 5)
|
|
|
|
// Concurrent token cache operations
|
|
for i := 0; i < numGoroutines; i++ {
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
tokenCache := manager.GetTokenCache()
|
|
for j := 0; j < 20; j++ {
|
|
token := fmt.Sprintf("token-%d-%d", id, j)
|
|
claims := map[string]interface{}{
|
|
"sub": fmt.Sprintf("user-%d", id),
|
|
"exp": time.Now().Add(1 * time.Hour).Unix(),
|
|
}
|
|
tokenCache.Set(token, claims, 1*time.Hour)
|
|
tokenCache.Get(token)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
// Concurrent metadata cache operations
|
|
for i := 0; i < numGoroutines; i++ {
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
metadataCache := manager.GetMetadataCache()
|
|
for j := 0; j < 20; j++ {
|
|
url := fmt.Sprintf("https://provider-%d.com/.well-known/config-%d", id, j)
|
|
metadata := &ProviderMetadata{
|
|
Issuer: fmt.Sprintf("https://provider-%d.com", id),
|
|
}
|
|
metadataCache.Set(url, metadata, 1*time.Hour)
|
|
metadataCache.Get(url)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
// Concurrent JWK cache operations
|
|
for i := 0; i < numGoroutines; i++ {
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
jwkCache := manager.GetJWKCache()
|
|
for j := 0; j < 20; j++ {
|
|
url := fmt.Sprintf("https://jwks-%d.com/keys-%d", id, j)
|
|
jwkSet := &JWKSet{
|
|
Keys: []JWK{
|
|
{Kid: fmt.Sprintf("key-%d-%d", id, j), Kty: "RSA"},
|
|
},
|
|
}
|
|
jwkCache.Set(url, jwkSet, 1*time.Hour)
|
|
jwkCache.Get(url)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
// Concurrent session cache operations
|
|
for i := 0; i < numGoroutines; i++ {
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
sessionCache := manager.GetSessionCache()
|
|
for j := 0; j < 20; j++ {
|
|
sessionID := fmt.Sprintf("session-%d-%d", id, j)
|
|
sessionData := SessionData{
|
|
ID: sessionID,
|
|
UserID: fmt.Sprintf("user-%d", id),
|
|
ExpiresAt: time.Now().Add(30 * time.Minute),
|
|
}
|
|
sessionCache.Set(sessionID, sessionData, 30*time.Minute)
|
|
sessionCache.Get(sessionID)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
// Concurrent manager operations
|
|
for i := 0; i < numGoroutines; i++ {
|
|
go func() {
|
|
defer wg.Done()
|
|
for j := 0; j < 10; j++ {
|
|
manager.GetStats()
|
|
time.Sleep(1 * time.Millisecond)
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
// TestTTLExpirationAndCleanup tests TTL expiration and cleanup routines comprehensively
|
|
func TestTTLExpirationAndCleanup(t *testing.T) {
|
|
config := DefaultConfig()
|
|
config.CleanupInterval = 10 * time.Millisecond
|
|
config.EnableAutoCleanup = true
|
|
cache := New(config)
|
|
defer cache.Close()
|
|
|
|
// Test various TTL scenarios
|
|
testCases := []struct {
|
|
key string
|
|
ttl time.Duration
|
|
}{
|
|
{"very-short", 5 * time.Millisecond},
|
|
{"short", 25 * time.Millisecond},
|
|
{"medium", 100 * time.Millisecond},
|
|
{"long", 1 * time.Hour},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
cache.Set(tc.key, fmt.Sprintf("value-%s", tc.key), tc.ttl)
|
|
}
|
|
|
|
// Verify all items exist initially
|
|
for _, tc := range testCases {
|
|
if _, exists := cache.Get(tc.key); !exists {
|
|
t.Errorf("Item %s should exist initially", tc.key)
|
|
}
|
|
}
|
|
|
|
// Wait for very short items to expire
|
|
time.Sleep(15 * time.Millisecond)
|
|
if _, exists := cache.Get("very-short"); exists {
|
|
t.Error("Very short item should be expired")
|
|
}
|
|
|
|
// Wait for short items to expire
|
|
time.Sleep(30 * time.Millisecond)
|
|
if _, exists := cache.Get("short"); exists {
|
|
t.Error("Short item should be expired")
|
|
}
|
|
|
|
// Medium should still exist
|
|
if _, exists := cache.Get("medium"); !exists {
|
|
t.Error("Medium item should still exist")
|
|
}
|
|
|
|
// Long should definitely still exist
|
|
if _, exists := cache.Get("long"); !exists {
|
|
t.Error("Long item should still exist")
|
|
}
|
|
|
|
// Test manual cleanup
|
|
cache.Set("manual-cleanup", "value", 1*time.Millisecond)
|
|
time.Sleep(5 * time.Millisecond)
|
|
cache.Cleanup()
|
|
|
|
// Add many expired items to test bulk cleanup
|
|
for i := 0; i < 100; i++ {
|
|
key := fmt.Sprintf("bulk-%d", i)
|
|
cache.Set(key, fmt.Sprintf("value-%d", i), 1*time.Millisecond)
|
|
}
|
|
time.Sleep(5 * time.Millisecond)
|
|
|
|
sizeBefore := cache.Size()
|
|
cache.Cleanup()
|
|
sizeAfter := cache.Size()
|
|
|
|
if sizeAfter >= sizeBefore {
|
|
t.Error("Cleanup should have removed expired items")
|
|
}
|
|
}
|
|
|
|
// TestCacheStatisticsAndMetrics tests comprehensive statistics and metrics
|
|
func TestCacheStatisticsAndMetrics(t *testing.T) {
|
|
config := DefaultConfig()
|
|
config.EnableMetrics = true
|
|
cache := New(config)
|
|
defer cache.Close()
|
|
|
|
// Test initial stats
|
|
stats := cache.GetStats()
|
|
requiredFields := []string{"size", "hits", "misses", "memory", "hit_rate"}
|
|
for _, field := range requiredFields {
|
|
if _, exists := stats[field]; !exists {
|
|
t.Errorf("Stats should contain field: %s", field)
|
|
}
|
|
}
|
|
|
|
// Test stats tracking with various operations
|
|
operations := []struct {
|
|
key string
|
|
value string
|
|
exists bool
|
|
}{
|
|
{"hit1", "value1", true},
|
|
{"hit2", "value2", true},
|
|
{"miss1", "", false},
|
|
{"hit1", "value1", true}, // Repeat for hit
|
|
{"miss2", "", false},
|
|
{"hit2", "value2", true}, // Repeat for hit
|
|
}
|
|
|
|
expectedHits := 0
|
|
expectedMisses := 0
|
|
size := 0
|
|
|
|
for _, op := range operations {
|
|
if op.exists {
|
|
cache.Set(op.key, op.value, 1*time.Hour)
|
|
if size < 2 { // Only count unique keys
|
|
size++
|
|
}
|
|
}
|
|
|
|
_, exists := cache.Get(op.key)
|
|
if exists {
|
|
expectedHits++
|
|
} else {
|
|
expectedMisses++
|
|
}
|
|
}
|
|
|
|
stats = cache.GetStats()
|
|
actualHits := stats["hits"].(int64)
|
|
actualMisses := stats["misses"].(int64)
|
|
actualSize := stats["size"].(int64)
|
|
|
|
if int(actualHits) != expectedHits {
|
|
t.Errorf("Expected %d hits, got %d", expectedHits, actualHits)
|
|
}
|
|
if int(actualMisses) != expectedMisses {
|
|
t.Errorf("Expected %d misses, got %d", expectedMisses, actualMisses)
|
|
}
|
|
if int(actualSize) != size {
|
|
t.Errorf("Expected size %d, got %d", size, actualSize)
|
|
}
|
|
|
|
// Test hit rate calculation
|
|
expectedHitRate := float64(expectedHits) / float64(expectedHits+expectedMisses)
|
|
actualHitRate := stats["hit_rate"].(float64)
|
|
if actualHitRate != expectedHitRate {
|
|
t.Errorf("Expected hit rate %f, got %f", expectedHitRate, actualHitRate)
|
|
}
|
|
|
|
// Test memory usage tracking
|
|
memoryUsage := stats["memory"].(int64)
|
|
if memoryUsage <= 0 {
|
|
t.Error("Memory usage should be positive")
|
|
}
|
|
|
|
// Add larger items and verify memory increases
|
|
largeValue := string(make([]byte, 1000))
|
|
cache.Set("large", largeValue, 1*time.Hour)
|
|
|
|
newStats := cache.GetStats()
|
|
newMemoryUsage := newStats["memory"].(int64)
|
|
if newMemoryUsage <= memoryUsage {
|
|
t.Error("Memory usage should increase after adding large item")
|
|
}
|
|
}
|