Files
traefikoidc/internal/cache/cache_test.go
T
lukaszraczylo 6efb78b7a8 Smarter approach to the cookies (#103)
* Smarter approach to the cookies

  - Single maxCookieSize = 1400 constant with clear documentation
  - Combined cookie storage for ~40-45% size reduction
  - Backward compatible migration from legacy cookies

* Tuneup the code.
2025-12-12 18:35:06 +00:00

2127 lines
52 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 {
value interface{}
key string
}{
{key: "string", value: "test string"},
{key: "int", value: 42},
{key: "float", value: 3.14159},
{key: "bool", value: true},
{key: "slice", value: []string{"a", "b", "c"}},
{key: "map", value: map[string]int{"one": 1, "two": 2}},
{key: "nil", value: nil},
{key: "empty-string", value: ""},
{key: "empty-slice", value: []string{}},
{key: "empty-map", value: 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 = 50 * time.Millisecond
config.EnableAutoCleanup = true
cache := New(config)
defer cache.Close()
// Test various TTL scenarios
// Note: Timing increased 5x to account for race detector overhead
testCases := []struct {
key string
ttl time.Duration
}{
{"very-short", 25 * time.Millisecond},
{"short", 125 * time.Millisecond},
{"medium", 500 * 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(75 * 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(150 * 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", 5*time.Millisecond)
time.Sleep(25 * 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), 5*time.Millisecond)
}
time.Sleep(25 * 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")
}
}
// ============================================================================
// noOpLogger Tests
// ============================================================================
// TestNoOpLogger_AllMethods tests all noOpLogger methods to ensure they don't panic
func TestNoOpLogger_AllMethods(t *testing.T) {
logger := &noOpLogger{}
// Test simple message methods
logger.Debug("test debug message")
logger.Info("test info message")
logger.Error("test error message")
logger.Warn("test warn message")
logger.Fatal("test fatal message")
// Test formatted message methods
logger.Debugf("test debug: %s", "value")
logger.Infof("test info: %s", "value")
logger.Errorf("test error: %s", "value")
logger.Warnf("test warn: %s", "value")
logger.Fatalf("test fatal: %s", "value")
// If we reach here, all methods executed without panicking
// This is expected behavior for a no-op logger
}
// TestNoOpLogger_WithField verifies WithField returns the same logger
func TestNoOpLogger_WithField(t *testing.T) {
logger := &noOpLogger{}
result := logger.WithField("key", "value")
if result != logger {
t.Error("WithField should return the same logger instance")
}
// Verify the returned logger works
result.Info("test message after WithField")
}
// TestNoOpLogger_WithFields verifies WithFields returns the same logger
func TestNoOpLogger_WithFields(t *testing.T) {
logger := &noOpLogger{}
fields := map[string]interface{}{
"key1": "value1",
"key2": 123,
"key3": true,
}
result := logger.WithFields(fields)
if result != logger {
t.Error("WithFields should return the same logger instance")
}
// Verify the returned logger works
result.Info("test message after WithFields")
}
// TestNoOpLogger_Chaining verifies method chaining works
func TestNoOpLogger_Chaining(t *testing.T) {
logger := &noOpLogger{}
// Use WithField and verify it returns a usable logger
result := logger.WithField("key1", "value1")
// Verify the result can be used for logging (Logger interface methods)
result.Info("info after WithField")
result.Infof("infof after WithField: %s", "test")
result.Debug("debug after WithField")
result.Debugf("debugf after WithField: %d", 123)
result.Error("error after WithField")
result.Errorf("errorf after WithField: %v", true)
// Use WithFields and verify it returns a usable logger
result2 := logger.WithFields(map[string]interface{}{
"key2": "value2",
"key3": 123,
})
// Verify the result can be used for logging
result2.Infof("message after WithFields: %s", "test")
}