Files
traefikoidc/universal_cache_test.go
T

538 lines
13 KiB
Go

package traefikoidc
import (
"sync"
"testing"
"time"
)
func TestUniversalCacheBasicOperations(t *testing.T) {
t.Run("SetAndGet", func(t *testing.T) {
config := UniversalCacheConfig{
Type: CacheTypeGeneral,
MaxSize: 10,
}
cache := NewUniversalCache(config)
defer cache.Close()
// Test Set and Get
cache.Set("key1", "value1", 1*time.Hour)
value, exists := cache.Get("key1")
if !exists {
t.Error("Expected key1 to exist")
}
if value != "value1" {
t.Errorf("Expected value1, got %v", value)
}
// Test non-existent key
_, exists = cache.Get("nonexistent")
if exists {
t.Error("Expected nonexistent key to not exist")
}
})
t.Run("Delete", func(t *testing.T) {
config := UniversalCacheConfig{
Type: CacheTypeGeneral,
MaxSize: 10,
}
cache := NewUniversalCache(config)
defer cache.Close()
cache.Set("key1", "value1", 1*time.Hour)
// Delete the key
deleted := cache.Delete("key1")
if !deleted {
t.Error("Expected key1 to be deleted")
}
// Verify it's gone
_, exists := cache.Get("key1")
if exists {
t.Error("Expected key1 to not exist after deletion")
}
// Delete non-existent key
deleted = cache.Delete("nonexistent")
if deleted {
t.Error("Expected delete to return false for non-existent key")
}
})
t.Run("Clear", func(t *testing.T) {
config := UniversalCacheConfig{
Type: CacheTypeGeneral,
MaxSize: 10,
}
cache := NewUniversalCache(config)
defer cache.Close()
// Add multiple items
for i := 0; i < 5; i++ {
cache.Set(string(rune('a'+i)), i, 1*time.Hour)
}
if cache.Size() != 5 {
t.Errorf("Expected size 5, got %d", cache.Size())
}
// Clear all
cache.Clear()
if cache.Size() != 0 {
t.Errorf("Expected size 0 after clear, got %d", cache.Size())
}
})
}
func TestUniversalCacheLRUEviction(t *testing.T) {
config := UniversalCacheConfig{
Type: CacheTypeGeneral,
MaxSize: 3,
}
cache := NewUniversalCache(config)
defer cache.Close()
// Fill cache to capacity
cache.Set("a", 1, 1*time.Hour)
cache.Set("b", 2, 1*time.Hour)
cache.Set("c", 3, 1*time.Hour)
// Access 'a' to move it to front
cache.Get("a")
// Add new item, should evict 'b' (least recently used)
cache.Set("d", 4, 1*time.Hour)
// Check that 'b' was evicted
_, exists := cache.Get("b")
if exists {
t.Error("Expected 'b' to be evicted")
}
// Check that others still exist
if _, exists := cache.Get("a"); !exists {
t.Error("Expected 'a' to exist")
}
if _, exists := cache.Get("c"); !exists {
t.Error("Expected 'c' to exist")
}
if _, exists := cache.Get("d"); !exists {
t.Error("Expected 'd' to exist")
}
}
func TestUniversalCacheMemoryLimit(t *testing.T) {
config := UniversalCacheConfig{
Type: CacheTypeGeneral,
MaxSize: 100,
MaxMemoryBytes: 100, // Very small limit
}
cache := NewUniversalCache(config)
defer cache.Close()
// Add items that exceed memory limit
cache.Set("key1", "This is a very long string that takes up memory", 1*time.Hour)
cache.Set("key2", "Another long string that should trigger eviction", 1*time.Hour)
cache.Set("key3", "Yet another string", 1*time.Hour)
// Check memory usage is within limit
if cache.MemoryUsage() > config.MaxMemoryBytes {
t.Errorf("Memory usage %d exceeds limit %d", cache.MemoryUsage(), config.MaxMemoryBytes)
}
// At least one item should remain
if cache.Size() == 0 {
t.Error("Expected at least one item in cache")
}
}
func TestUniversalCacheExpiration(t *testing.T) {
config := UniversalCacheConfig{
Type: CacheTypeGeneral,
MaxSize: 10,
CleanupInterval: 100 * time.Millisecond,
}
cache := NewUniversalCache(config)
defer cache.Close()
// Add item with short TTL
cache.Set("shortlived", "value", 200*time.Millisecond)
// Verify it exists
if _, exists := cache.Get("shortlived"); !exists {
t.Error("Expected shortlived to exist initially")
}
// Wait for expiration
time.Sleep(300 * time.Millisecond)
// Should be expired now
if _, exists := cache.Get("shortlived"); exists {
t.Error("Expected shortlived to be expired")
}
}
func TestUniversalCacheTokenOperations(t *testing.T) {
config := UniversalCacheConfig{
Type: CacheTypeToken,
TokenConfig: &TokenCacheConfig{
BlacklistTTL: 1 * time.Hour,
},
}
cache := NewUniversalCache(config)
defer cache.Close()
// Test blacklist operation
err := cache.BlacklistToken("bad-token", 0)
if err != nil {
t.Errorf("Failed to blacklist token: %v", err)
}
// Check if token is blacklisted
if !cache.IsTokenBlacklisted("bad-token") {
t.Error("Expected token to be blacklisted")
}
// Check non-blacklisted token
if cache.IsTokenBlacklisted("good-token") {
t.Error("Expected good-token to not be blacklisted")
}
}
func TestUniversalCacheMetadataGracePeriod(t *testing.T) {
t.Skip("Temporarily skipping grace period test - timing issues")
config := UniversalCacheConfig{
Type: CacheTypeMetadata,
DefaultTTL: 100 * time.Millisecond,
CleanupInterval: 10 * time.Second, // Disable cleanup during test
MetadataConfig: &MetadataCacheConfig{
GracePeriod: 200 * time.Millisecond,
ExtendedGracePeriod: 400 * time.Millisecond,
MaxGracePeriod: 600 * time.Millisecond,
SecurityCriticalMaxGracePeriod: 150 * time.Millisecond, // Reduced for testing
SecurityCriticalFields: []string{"jwks_uri"},
},
}
cache := NewUniversalCache(config)
defer cache.Close()
// Add metadata that will expire
cache.SetWithMetadata("provider1", "metadata", 100*time.Millisecond, map[string]interface{}{
"field": "jwks_uri",
})
// Wait for initial TTL to expire (100ms)
time.Sleep(120 * time.Millisecond)
// Should still be accessible due to grace period (120ms < 100ms TTL + 150ms grace)
value, exists := cache.Get("provider1")
if !exists {
t.Error("Expected metadata to be accessible during grace period")
}
if value != "metadata" {
t.Errorf("Expected 'metadata', got %v", value)
}
// Wait beyond security-critical max grace period (280ms > 100ms TTL + 150ms grace)
time.Sleep(160 * time.Millisecond)
// Should now be expired
_, exists = cache.Get("provider1")
if exists {
t.Error("Expected metadata to be expired after max grace period")
}
}
func TestUniversalCacheConcurrency(t *testing.T) {
config := UniversalCacheConfig{
Type: CacheTypeGeneral,
MaxSize: 100,
}
cache := NewUniversalCache(config)
defer cache.Close()
var wg sync.WaitGroup
numGoroutines := 10
numOperations := 100
// Concurrent writes
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < numOperations; j++ {
key := string(rune('a'+id)) + string(rune('0'+j%10))
cache.Set(key, id*1000+j, 1*time.Hour)
}
}(i)
}
// Concurrent reads
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < numOperations; j++ {
key := string(rune('a'+id)) + string(rune('0'+j%10))
cache.Get(key)
}
}(i)
}
// Concurrent deletes
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < numOperations/2; j++ {
key := string(rune('a'+id)) + string(rune('0'+j%10))
cache.Delete(key)
}
}(i)
}
wg.Wait()
// Cache should still be functional
cache.Set("final", "value", 1*time.Hour)
if value, exists := cache.Get("final"); !exists || value != "value" {
t.Error("Cache not functional after concurrent operations")
}
}
func TestUniversalCacheMetrics(t *testing.T) {
config := UniversalCacheConfig{
Type: CacheTypeGeneral,
MaxSize: 10,
EnableMetrics: true,
}
cache := NewUniversalCache(config)
defer cache.Close()
// Perform operations
cache.Set("key1", "value1", 1*time.Hour)
cache.Get("key1") // Hit
cache.Get("key2") // Miss
cache.Get("key3") // Miss
metrics := cache.GetMetrics()
// Check metrics
if metrics["hits"].(int64) != 1 {
t.Errorf("Expected 1 hit, got %v", metrics["hits"])
}
if metrics["misses"].(int64) != 2 {
t.Errorf("Expected 2 misses, got %v", metrics["misses"])
}
hitRate := metrics["hit_rate"].(float64)
expectedHitRate := 1.0 / 3.0
if hitRate < expectedHitRate-0.01 || hitRate > expectedHitRate+0.01 {
t.Errorf("Expected hit rate ~%f, got %f", expectedHitRate, hitRate)
}
}
func TestUniversalCacheAdapters(t *testing.T) {
t.Run("TokenCacheAdapter", func(t *testing.T) {
config := UniversalCacheConfig{
Type: CacheTypeToken,
MaxSize: 100,
Logger: NewLogger("debug"),
}
cache := NewUniversalCache(config)
defer cache.Close()
// Test token operations
claims := map[string]interface{}{
"sub": "user123",
"exp": time.Now().Add(1 * time.Hour).Unix(),
}
cache.Set("token1", claims, 1*time.Hour)
retrievedClaims, exists := cache.Get("token1")
if !exists {
t.Error("Expected token1 to exist")
}
if claimsMap, ok := retrievedClaims.(map[string]interface{}); ok {
if claimsMap["sub"] != "user123" {
t.Errorf("Expected sub=user123, got %v", claimsMap["sub"])
}
} else {
t.Error("Expected retrieved claims to be a map")
}
// Test blacklist functionality - use Set with special marker
cache.Set("blacklist:bad-token", true, 1*time.Hour)
if _, exists := cache.Get("blacklist:bad-token"); !exists {
t.Error("Expected bad-token to be blacklisted")
}
})
t.Run("MetadataCacheAdapter", func(t *testing.T) {
config := UniversalCacheConfig{
Type: CacheTypeMetadata,
MaxSize: 50,
Logger: NewLogger("debug"),
}
cache := NewUniversalCache(config)
defer cache.Close()
metadata := &ProviderMetadata{
Issuer: "https://example.com",
AuthURL: "https://example.com/auth",
TokenURL: "https://example.com/token",
JWKSURL: "https://example.com/jwks",
}
err := cache.Set("https://example.com", metadata, 1*time.Hour)
if err != nil {
t.Errorf("Failed to set metadata: %v", err)
}
retrieved, exists := cache.Get("https://example.com")
if !exists {
t.Error("Expected metadata to exist")
}
if meta, ok := retrieved.(*ProviderMetadata); ok {
if meta.Issuer != metadata.Issuer {
t.Errorf("Expected issuer %s, got %s", metadata.Issuer, meta.Issuer)
}
} else {
t.Error("Expected retrieved value to be *ProviderMetadata")
}
})
t.Run("JWKCacheAdapter", func(t *testing.T) {
config := UniversalCacheConfig{
Type: CacheTypeJWK,
MaxSize: 20,
Logger: NewLogger("debug"),
}
cache := NewUniversalCache(config)
defer cache.Close()
// Test JWK operations
jwk := map[string]interface{}{
"kty": "RSA",
"use": "sig",
"kid": "key1",
}
err := cache.Set("key1", jwk, 1*time.Hour)
if err != nil {
t.Errorf("Failed to set JWK: %v", err)
}
retrieved, exists := cache.Get("key1")
if !exists {
t.Error("Expected key1 to exist")
}
if jwkMap, ok := retrieved.(map[string]interface{}); ok {
if jwkMap["kid"] != "key1" {
t.Errorf("Expected kid=key1, got %v", jwkMap["kid"])
}
} else {
t.Error("Expected retrieved value to be a map")
}
})
}
func TestUniversalCacheMigration(t *testing.T) {
t.Run("MigrateBoundedCache", func(t *testing.T) {
// Create cache wrapper for migration testing
oldCache := NewBoundedCache(10)
oldCache.Set("key1", "value1", 1*time.Hour)
oldCache.Set("key2", "value2", 1*time.Hour)
// Since caches are now unified through UniversalCache,
// migration is not needed - all caches use the same backend
// Just verify the cache works
if val, exists := oldCache.Get("key1"); !exists || val != "value1" {
t.Error("Expected key1 to exist with value1")
}
defer oldCache.Close()
// Verify second key exists
value, exists := oldCache.Get("key2")
if !exists || value != "value2" {
t.Error("Expected key2 to exist with value2")
}
})
}
func TestUniversalCacheTypeDefaults(t *testing.T) {
tests := []struct {
name string
cacheType CacheType
expectedMaxSize int
}{
{"TokenCache", CacheTypeToken, 5000},
{"MetadataCache", CacheTypeMetadata, 100},
{"JWKCache", CacheTypeJWK, 200},
{"SessionCache", CacheTypeSession, 10000},
{"GeneralCache", CacheTypeGeneral, 1000},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config := UniversalCacheConfig{
Type: tt.cacheType,
}
cache := NewUniversalCache(config)
defer cache.Close()
if cache.config.MaxSize != tt.expectedMaxSize {
t.Errorf("Expected MaxSize=%d for %s, got %d",
tt.expectedMaxSize, tt.cacheType, cache.config.MaxSize)
}
})
}
}
func BenchmarkUniversalCacheSet(b *testing.B) {
config := UniversalCacheConfig{
Type: CacheTypeGeneral,
MaxSize: 10000,
}
cache := NewUniversalCache(config)
defer cache.Close()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
i := 0
for pb.Next() {
cache.Set(string(rune('a'+i%26)), i, 1*time.Hour)
i++
}
})
}
func BenchmarkUniversalCacheGet(b *testing.B) {
config := UniversalCacheConfig{
Type: CacheTypeGeneral,
MaxSize: 10000,
}
cache := NewUniversalCache(config)
defer cache.Close()
// Pre-populate cache
for i := 0; i < 100; i++ {
cache.Set(string(rune('a'+i%26)), i, 1*time.Hour)
}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
i := 0
for pb.Next() {
cache.Get(string(rune('a' + i%26)))
i++
}
})
}