mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-05 22:44:17 +00:00
c474bbafd6
* Cleanup excessive comments. * Remove leftovers hanging around from previous refactor * Improve test coverage
1702 lines
41 KiB
Go
1702 lines
41 KiB
Go
package traefikoidc
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"runtime"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// =============================================================================
|
|
// CACHE TEST FRAMEWORK
|
|
// =============================================================================
|
|
|
|
// CacheTestCase represents a comprehensive test case for cache operations
|
|
type CacheTestCase struct {
|
|
name string
|
|
cacheType string // "universal", "metadata", "bounded"
|
|
operation string // "get", "set", "evict", "cleanup"
|
|
setup func(*TestFramework) // Pre-test setup
|
|
execute func(*TestFramework) error // Test execution
|
|
validate func(*testing.T, error, *TestFramework) // Validation logic
|
|
cleanup func(*TestFramework) // Post-test cleanup
|
|
timeout time.Duration // Test timeout
|
|
parallel bool // Can run in parallel
|
|
skipReason string // Optional reason to skip
|
|
}
|
|
|
|
// createTestCacheConfig creates a standard test configuration
|
|
func createTestCacheConfig() UniversalCacheConfig {
|
|
return UniversalCacheConfig{
|
|
Type: CacheTypeGeneral,
|
|
MaxSize: 1000,
|
|
CleanupInterval: 1 * time.Minute,
|
|
DefaultTTL: 1 * time.Hour,
|
|
MaxMemoryBytes: 100 * 1024 * 1024, // 100MB
|
|
EnableAutoCleanup: true,
|
|
EnableMemoryLimit: true,
|
|
EnableMetrics: true,
|
|
MetadataConfig: &MetadataCacheConfig{
|
|
GracePeriod: 5 * time.Minute,
|
|
},
|
|
}
|
|
}
|
|
|
|
// executeTestCase executes a single cache test case with proper setup and cleanup
|
|
func executeCacheTestCase(t *testing.T, tc CacheTestCase, framework *TestFramework) {
|
|
if tc.timeout > 0 {
|
|
ctx, cancel := context.WithTimeout(context.Background(), tc.timeout)
|
|
defer cancel()
|
|
|
|
done := make(chan bool)
|
|
go func() {
|
|
defer close(done)
|
|
runCacheTestCase(t, tc, framework)
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
// Test completed
|
|
case <-ctx.Done():
|
|
t.Fatalf("Test timeout after %v", tc.timeout)
|
|
}
|
|
} else {
|
|
runCacheTestCase(t, tc, framework)
|
|
}
|
|
}
|
|
|
|
// runCacheTestCase runs the actual test case logic
|
|
func runCacheTestCase(t *testing.T, tc CacheTestCase, framework *TestFramework) {
|
|
if tc.setup != nil {
|
|
tc.setup(framework)
|
|
}
|
|
|
|
var err error
|
|
if tc.execute != nil {
|
|
err = tc.execute(framework)
|
|
}
|
|
|
|
if tc.validate != nil {
|
|
tc.validate(t, err, framework)
|
|
}
|
|
|
|
if tc.cleanup != nil {
|
|
tc.cleanup(framework)
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// CACHE MANAGER TESTS
|
|
// =============================================================================
|
|
|
|
// Helper function to ensure we have a working cache manager for tests
|
|
func getTestCacheManager(t *testing.T) *CacheManager {
|
|
cm := GetGlobalCacheManager(&sync.WaitGroup{})
|
|
if cm == nil {
|
|
t.Fatal("Failed to get cache manager")
|
|
}
|
|
if cm.manager == nil {
|
|
t.Fatal("Cache manager has nil internal manager")
|
|
}
|
|
return cm
|
|
}
|
|
|
|
func TestCacheManager_Close(t *testing.T) {
|
|
wg := &sync.WaitGroup{}
|
|
cm := GetGlobalCacheManager(wg)
|
|
|
|
if cm == nil {
|
|
t.Fatal("Expected cache manager to be created")
|
|
}
|
|
|
|
err := cm.Close()
|
|
if err != nil {
|
|
t.Errorf("Unexpected error closing cache manager: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCleanupGlobalCacheManager(t *testing.T) {
|
|
originalInstance := globalCacheManagerInstance
|
|
globalCacheManagerInstance = nil
|
|
err := CleanupGlobalCacheManager()
|
|
if err != nil {
|
|
t.Errorf("Unexpected error during cleanup of nil instance: %v", err)
|
|
}
|
|
|
|
globalCacheManagerInstance = originalInstance
|
|
}
|
|
|
|
func TestCacheInterfaceWrapper_Delete(t *testing.T) {
|
|
cm := getTestCacheManager(t)
|
|
cache := cm.GetSharedTokenBlacklist()
|
|
|
|
cache.Set("test-key", "test-value", time.Hour)
|
|
|
|
value, found := cache.Get("test-key")
|
|
if !found {
|
|
t.Fatal("Expected key to be found after setting")
|
|
}
|
|
if value != "test-value" {
|
|
t.Errorf("Expected 'test-value', got %v", value)
|
|
}
|
|
|
|
cache.Delete("test-key")
|
|
|
|
_, found = cache.Get("test-key")
|
|
if found {
|
|
t.Error("Expected key to be deleted")
|
|
}
|
|
}
|
|
|
|
func TestCacheInterfaceWrapper_Size(t *testing.T) {
|
|
cm := getTestCacheManager(t)
|
|
cache := cm.GetSharedTokenBlacklist()
|
|
|
|
cache.Clear()
|
|
|
|
initialSize := cache.Size()
|
|
if initialSize != 0 {
|
|
t.Errorf("Expected initial size 0, got %d", initialSize)
|
|
}
|
|
|
|
cache.Set("key1", "value1", time.Hour)
|
|
cache.Set("key2", "value2", time.Hour)
|
|
|
|
newSize := cache.Size()
|
|
if newSize != 2 {
|
|
t.Errorf("Expected size 2, got %d", newSize)
|
|
}
|
|
}
|
|
|
|
func TestCacheInterfaceWrapper_Clear(t *testing.T) {
|
|
cm := getTestCacheManager(t)
|
|
cache := cm.GetSharedTokenBlacklist()
|
|
|
|
cache.Set("key1", "value1", time.Hour)
|
|
cache.Set("key2", "value2", time.Hour)
|
|
|
|
size := cache.Size()
|
|
if size != 2 {
|
|
t.Errorf("Expected 2 items before clear, got %d", size)
|
|
}
|
|
|
|
cache.Clear()
|
|
|
|
size = cache.Size()
|
|
if size != 0 {
|
|
t.Errorf("Expected 0 items after clear, got %d", size)
|
|
}
|
|
|
|
_, found := cache.Get("key1")
|
|
if found {
|
|
t.Error("Expected key1 to be cleared")
|
|
}
|
|
|
|
_, found = cache.Get("key2")
|
|
if found {
|
|
t.Error("Expected key2 to be cleared")
|
|
}
|
|
}
|
|
|
|
func TestCacheInterfaceWrapper_Close(t *testing.T) {
|
|
cm := getTestCacheManager(t)
|
|
cache := cm.GetSharedTokenBlacklist()
|
|
|
|
wrapper, ok := cache.(*CacheInterfaceWrapper)
|
|
if !ok {
|
|
t.Fatal("Expected CacheInterfaceWrapper")
|
|
}
|
|
|
|
wrapper.Close()
|
|
|
|
nilWrapper := &CacheInterfaceWrapper{cache: nil}
|
|
nilWrapper.Close()
|
|
}
|
|
|
|
func TestCacheInterfaceWrapper_GetStats(t *testing.T) {
|
|
cm := getTestCacheManager(t)
|
|
cache := cm.GetSharedTokenBlacklist()
|
|
|
|
wrapper, ok := cache.(*CacheInterfaceWrapper)
|
|
if !ok {
|
|
t.Fatal("Expected CacheInterfaceWrapper")
|
|
}
|
|
|
|
stats := wrapper.GetStats()
|
|
if stats == nil {
|
|
t.Error("Expected non-nil stats")
|
|
}
|
|
}
|
|
|
|
func TestCacheInterfaceWrapper_Cleanup(t *testing.T) {
|
|
cm := getTestCacheManager(t)
|
|
cache := cm.GetSharedTokenBlacklist()
|
|
|
|
cache.Set("expire-key", "expire-value", time.Millisecond)
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
cache.Cleanup()
|
|
|
|
_, found := cache.Get("expire-key")
|
|
if found {
|
|
t.Error("Expected expired key to be cleaned up")
|
|
}
|
|
}
|
|
|
|
func TestCacheInterfaceWrapper_SetMaxSize(t *testing.T) {
|
|
cm := getTestCacheManager(t)
|
|
cache := cm.GetSharedTokenBlacklist()
|
|
|
|
cache.SetMaxSize(1000)
|
|
}
|
|
|
|
func TestGetSharedCaches(t *testing.T) {
|
|
cm := getTestCacheManager(t)
|
|
|
|
blacklist := cm.GetSharedTokenBlacklist()
|
|
if blacklist == nil {
|
|
t.Error("Expected non-nil token blacklist")
|
|
}
|
|
|
|
tokenCache := cm.GetSharedTokenCache()
|
|
if tokenCache == nil {
|
|
t.Error("Expected non-nil token cache")
|
|
}
|
|
|
|
metadataCache := cm.GetSharedMetadataCache()
|
|
if metadataCache == nil {
|
|
t.Error("Expected non-nil metadata cache")
|
|
}
|
|
|
|
jwkCache := cm.GetSharedJWKCache()
|
|
if jwkCache == nil {
|
|
t.Error("Expected non-nil JWK cache")
|
|
}
|
|
}
|
|
|
|
func TestConcurrentCacheAccess(t *testing.T) {
|
|
cm := getTestCacheManager(t)
|
|
cache := cm.GetSharedTokenBlacklist()
|
|
|
|
var wg sync.WaitGroup
|
|
goroutines := 10
|
|
iterations := 10
|
|
|
|
for i := 0; i < goroutines; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
for j := 0; j < iterations; j++ {
|
|
key := fmt.Sprintf("key-%d-%d", id, j)
|
|
value := fmt.Sprintf("value-%d-%d", id, j)
|
|
|
|
cache.Set(key, value, time.Hour)
|
|
|
|
retrieved, found := cache.Get(key)
|
|
if found && retrieved != value {
|
|
t.Errorf("Concurrent access failed: expected %s, got %v", value, retrieved)
|
|
}
|
|
|
|
cache.Delete(key)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
// =============================================================================
|
|
// SHARDED CACHE TESTS
|
|
// =============================================================================
|
|
|
|
func TestShardedCacheBasicOperations(t *testing.T) {
|
|
t.Run("SetAndGet", func(t *testing.T) {
|
|
cache := NewShardedCache(16, 1000)
|
|
|
|
cache.Set("key1", "value1", 5*time.Minute)
|
|
cache.Set("key2", 42, 5*time.Minute)
|
|
cache.Set("key3", true, 5*time.Minute)
|
|
|
|
val1, ok := cache.Get("key1")
|
|
if !ok || val1 != "value1" {
|
|
t.Errorf("Expected 'value1', got %v, ok=%v", val1, ok)
|
|
}
|
|
|
|
val2, ok := cache.Get("key2")
|
|
if !ok || val2 != 42 {
|
|
t.Errorf("Expected 42, got %v, ok=%v", val2, ok)
|
|
}
|
|
|
|
val3, ok := cache.Get("key3")
|
|
if !ok || val3 != true {
|
|
t.Errorf("Expected true, got %v, ok=%v", val3, ok)
|
|
}
|
|
})
|
|
|
|
t.Run("GetNonExistent", func(t *testing.T) {
|
|
cache := NewShardedCache(16, 1000)
|
|
|
|
val, ok := cache.Get("nonexistent")
|
|
if ok || val != nil {
|
|
t.Errorf("Expected nil/false for nonexistent key, got %v/%v", val, ok)
|
|
}
|
|
})
|
|
|
|
t.Run("Delete", func(t *testing.T) {
|
|
cache := NewShardedCache(16, 1000)
|
|
|
|
cache.Set("key1", "value1", 5*time.Minute)
|
|
cache.Delete("key1")
|
|
|
|
val, ok := cache.Get("key1")
|
|
if ok || val != nil {
|
|
t.Errorf("Expected nil/false after delete, got %v/%v", val, ok)
|
|
}
|
|
})
|
|
|
|
t.Run("Exists", func(t *testing.T) {
|
|
cache := NewShardedCache(16, 1000)
|
|
|
|
cache.Set("key1", "value1", 5*time.Minute)
|
|
|
|
if !cache.Exists("key1") {
|
|
t.Error("Expected Exists to return true for existing key")
|
|
}
|
|
|
|
if cache.Exists("nonexistent") {
|
|
t.Error("Expected Exists to return false for nonexistent key")
|
|
}
|
|
})
|
|
|
|
t.Run("Size", func(t *testing.T) {
|
|
cache := NewShardedCache(16, 1000)
|
|
|
|
if cache.Size() != 0 {
|
|
t.Errorf("Expected size 0, got %d", cache.Size())
|
|
}
|
|
|
|
for i := 0; i < 100; i++ {
|
|
cache.Set(fmt.Sprintf("key%d", i), i, 5*time.Minute)
|
|
}
|
|
|
|
if cache.Size() != 100 {
|
|
t.Errorf("Expected size 100, got %d", cache.Size())
|
|
}
|
|
})
|
|
|
|
t.Run("Clear", func(t *testing.T) {
|
|
cache := NewShardedCache(16, 1000)
|
|
|
|
for i := 0; i < 100; i++ {
|
|
cache.Set(fmt.Sprintf("key%d", i), i, 5*time.Minute)
|
|
}
|
|
|
|
cache.Clear()
|
|
|
|
if cache.Size() != 0 {
|
|
t.Errorf("Expected size 0 after clear, got %d", cache.Size())
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestShardedCacheExpiration(t *testing.T) {
|
|
t.Run("ItemExpires", func(t *testing.T) {
|
|
cache := NewShardedCache(16, 1000)
|
|
|
|
cache.Set("key1", "value1", 50*time.Millisecond)
|
|
|
|
if !cache.Exists("key1") {
|
|
t.Error("Item should exist immediately after set")
|
|
}
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
if cache.Exists("key1") {
|
|
t.Error("Item should have expired")
|
|
}
|
|
})
|
|
|
|
t.Run("CleanupRemovesExpired", func(t *testing.T) {
|
|
cache := NewShardedCache(16, 1000)
|
|
|
|
for i := 0; i < 50; i++ {
|
|
cache.Set(fmt.Sprintf("expired%d", i), i, 10*time.Millisecond)
|
|
}
|
|
|
|
for i := 0; i < 50; i++ {
|
|
cache.Set(fmt.Sprintf("valid%d", i), i, 5*time.Minute)
|
|
}
|
|
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
cache.Cleanup()
|
|
|
|
for i := 0; i < 50; i++ {
|
|
if cache.Exists(fmt.Sprintf("expired%d", i)) {
|
|
t.Errorf("Expired item %d should not exist after cleanup", i)
|
|
}
|
|
}
|
|
|
|
for i := 0; i < 50; i++ {
|
|
if !cache.Exists(fmt.Sprintf("valid%d", i)) {
|
|
t.Errorf("Valid item %d should still exist after cleanup", i)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("ZeroTTLNeverExpires", func(t *testing.T) {
|
|
cache := NewShardedCache(16, 1000)
|
|
|
|
cache.Set("permanent", "value", 0)
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
if !cache.Exists("permanent") {
|
|
t.Error("Item with 0 TTL should never expire")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestShardedCacheConcurrency(t *testing.T) {
|
|
t.Run("ConcurrentSetGet", func(t *testing.T) {
|
|
cache := NewShardedCache(64, 10000)
|
|
const numGoroutines = 100
|
|
const numOperations = 1000
|
|
|
|
var wg sync.WaitGroup
|
|
var errors int32
|
|
|
|
for i := 0; i < numGoroutines; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
for j := 0; j < numOperations; j++ {
|
|
key := fmt.Sprintf("key-%d-%d", id, j)
|
|
cache.Set(key, j, 5*time.Minute)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
for i := 0; i < numGoroutines; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
for j := 0; j < numOperations; j++ {
|
|
key := fmt.Sprintf("key-%d-%d", id, j)
|
|
cache.Get(key)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
if atomic.LoadInt32(&errors) > 0 {
|
|
t.Errorf("Encountered %d errors during concurrent access", errors)
|
|
}
|
|
})
|
|
|
|
t.Run("ConcurrentMixedOperations", func(t *testing.T) {
|
|
cache := NewShardedCache(64, 10000)
|
|
const numGoroutines = 50
|
|
const numOperations = 500
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
for i := 0; i < numGoroutines; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
for j := 0; j < numOperations; j++ {
|
|
key := fmt.Sprintf("key-%d", j%100)
|
|
switch j % 4 {
|
|
case 0:
|
|
cache.Set(key, j, 5*time.Minute)
|
|
case 1:
|
|
cache.Get(key)
|
|
case 2:
|
|
cache.Exists(key)
|
|
case 3:
|
|
cache.Delete(key)
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
})
|
|
|
|
t.Run("NoConcurrentPanics", func(t *testing.T) {
|
|
cache := NewShardedCache(32, 5000)
|
|
const numGoroutines = 100
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
for i := 0; i < numGoroutines; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Errorf("Panic in goroutine %d: %v", id, r)
|
|
}
|
|
}()
|
|
|
|
for j := 0; j < 100; j++ {
|
|
cache.Set(fmt.Sprintf("k%d", j), j, time.Millisecond)
|
|
cache.Get(fmt.Sprintf("k%d", j))
|
|
cache.Cleanup()
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
})
|
|
}
|
|
|
|
func TestShardedCacheEviction(t *testing.T) {
|
|
t.Run("EvictsWhenFull", func(t *testing.T) {
|
|
cache := NewShardedCache(4, 100)
|
|
|
|
for i := 0; i < 600; i++ {
|
|
cache.Set(fmt.Sprintf("key%d", i), i, 5*time.Minute)
|
|
}
|
|
|
|
size := cache.Size()
|
|
if size >= 600 {
|
|
t.Errorf("Expected eviction to reduce size below 600, got %d", size)
|
|
}
|
|
t.Logf("Cache size after adding 600 items: %d", size)
|
|
})
|
|
|
|
t.Run("EvictsExpiredFirst", func(t *testing.T) {
|
|
cache := NewShardedCache(4, 100)
|
|
|
|
for i := 0; i < 50; i++ {
|
|
cache.Set(fmt.Sprintf("expired%d", i), i, 1*time.Millisecond)
|
|
}
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
for i := 0; i < 100; i++ {
|
|
cache.Set(fmt.Sprintf("valid%d", i), i, 5*time.Minute)
|
|
}
|
|
|
|
validCount := 0
|
|
for i := 0; i < 100; i++ {
|
|
if cache.Exists(fmt.Sprintf("valid%d", i)) {
|
|
validCount++
|
|
}
|
|
}
|
|
|
|
if validCount < 80 {
|
|
t.Errorf("Expected at least 80 valid items, got %d", validCount)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestShardedCacheShardDistribution(t *testing.T) {
|
|
t.Run("EvenDistribution", func(t *testing.T) {
|
|
cache := NewShardedCache(16, 16000)
|
|
|
|
for i := 0; i < 10000; i++ {
|
|
cache.Set(fmt.Sprintf("key-%d", i), i, 5*time.Minute)
|
|
}
|
|
|
|
stats := cache.ShardStats()
|
|
|
|
average := 10000 / 16
|
|
for i, count := range stats {
|
|
if count > average*3 || count < average/3 {
|
|
t.Errorf("Shard %d has uneven distribution: %d items (expected ~%d)", i, count, average)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// =============================================================================
|
|
// CACHE COMPATIBILITY TESTS
|
|
// =============================================================================
|
|
|
|
func TestNewBoundedCache(t *testing.T) {
|
|
maxSize := 500
|
|
cache := NewBoundedCache(maxSize)
|
|
|
|
if cache == nil {
|
|
t.Fatal("Expected cache to be created, got nil")
|
|
}
|
|
|
|
cache.Set("test-key", "test-value", time.Hour)
|
|
value, found := cache.Get("test-key")
|
|
if !found {
|
|
t.Error("Expected key to be found in cache")
|
|
}
|
|
if value != "test-value" {
|
|
t.Errorf("Expected 'test-value', got %v", value)
|
|
}
|
|
}
|
|
|
|
func TestDefaultUnifiedCacheConfig(t *testing.T) {
|
|
config := DefaultUnifiedCacheConfig()
|
|
|
|
if config.Type != CacheTypeGeneral {
|
|
t.Errorf("Expected CacheTypeGeneral, got %v", config.Type)
|
|
}
|
|
|
|
if config.MaxSize != 500 {
|
|
t.Errorf("Expected MaxSize 500, got %d", config.MaxSize)
|
|
}
|
|
|
|
if config.MaxMemoryBytes != 64*1024*1024 {
|
|
t.Errorf("Expected MaxMemoryBytes 64MB, got %d", config.MaxMemoryBytes)
|
|
}
|
|
|
|
if config.CleanupInterval != 2*time.Minute {
|
|
t.Errorf("Expected CleanupInterval 2 minutes, got %v", config.CleanupInterval)
|
|
}
|
|
|
|
if config.Logger == nil {
|
|
t.Error("Expected Logger to be set")
|
|
}
|
|
}
|
|
|
|
func TestNewUnifiedCache(t *testing.T) {
|
|
config := DefaultUnifiedCacheConfig()
|
|
cache := NewUnifiedCache(config)
|
|
|
|
if cache == nil {
|
|
t.Fatal("Expected cache to be created, got nil")
|
|
}
|
|
|
|
if cache.UniversalCache == nil {
|
|
t.Error("Expected UniversalCache to be set")
|
|
}
|
|
|
|
cache.Set("test-key", "test-value", time.Hour)
|
|
value, found := cache.Get("test-key")
|
|
if !found {
|
|
t.Error("Expected key to be found in cache")
|
|
}
|
|
if value != "test-value" {
|
|
t.Errorf("Expected 'test-value', got %v", value)
|
|
}
|
|
}
|
|
|
|
func TestUnifiedCache_SetMaxSize(t *testing.T) {
|
|
config := DefaultUnifiedCacheConfig()
|
|
cache := NewUnifiedCache(config)
|
|
|
|
newSize := 1000
|
|
cache.SetMaxSize(newSize)
|
|
}
|
|
|
|
func TestNewCacheAdapter(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
cache interface{}
|
|
expectNil bool
|
|
description string
|
|
}{
|
|
{
|
|
name: "UniversalCache",
|
|
cache: NewUniversalCache(DefaultUnifiedCacheConfig()),
|
|
expectNil: false,
|
|
description: "Should create adapter for UniversalCache",
|
|
},
|
|
{
|
|
name: "UnifiedCache",
|
|
cache: NewUnifiedCache(DefaultUnifiedCacheConfig()),
|
|
expectNil: false,
|
|
description: "Should create adapter for UnifiedCache",
|
|
},
|
|
{
|
|
name: "Invalid cache type",
|
|
cache: "not-a-cache",
|
|
expectNil: true,
|
|
description: "Should return nil for invalid cache type",
|
|
},
|
|
{
|
|
name: "Nil cache",
|
|
cache: nil,
|
|
expectNil: true,
|
|
description: "Should return nil for nil cache",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
adapter := NewCacheAdapter(tt.cache)
|
|
|
|
if tt.expectNil {
|
|
if adapter != nil {
|
|
t.Errorf("Expected nil adapter, got %v", adapter)
|
|
}
|
|
} else {
|
|
if adapter == nil {
|
|
t.Error("Expected non-nil adapter")
|
|
}
|
|
adapter.Set("test", "value", time.Hour)
|
|
value, found := adapter.Get("test")
|
|
if !found {
|
|
t.Error("Expected key to be found")
|
|
}
|
|
if value != "value" {
|
|
t.Errorf("Expected 'value', got %v", value)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewOptimizedCache(t *testing.T) {
|
|
cache := NewOptimizedCache()
|
|
|
|
if cache == nil {
|
|
t.Fatal("Expected cache to be created, got nil")
|
|
}
|
|
|
|
cache.Set("test-key", "test-value", time.Hour)
|
|
value, found := cache.Get("test-key")
|
|
if !found {
|
|
t.Error("Expected key to be found in cache")
|
|
}
|
|
if value != "test-value" {
|
|
t.Errorf("Expected 'test-value', got %v", value)
|
|
}
|
|
}
|
|
|
|
func TestNewLRUStrategy(t *testing.T) {
|
|
maxSize := 100
|
|
strategy := NewLRUStrategy(maxSize)
|
|
|
|
if strategy == nil {
|
|
t.Fatal("Expected strategy to be created, got nil")
|
|
}
|
|
|
|
lruStrategy, ok := strategy.(*LRUStrategy)
|
|
if !ok {
|
|
t.Fatal("Expected LRUStrategy type")
|
|
}
|
|
|
|
if lruStrategy.maxSize != maxSize {
|
|
t.Errorf("Expected maxSize %d, got %d", maxSize, lruStrategy.maxSize)
|
|
}
|
|
|
|
if lruStrategy.order == nil {
|
|
t.Error("Expected order list to be initialized")
|
|
}
|
|
|
|
if lruStrategy.elements == nil {
|
|
t.Error("Expected elements map to be initialized")
|
|
}
|
|
}
|
|
|
|
func TestLRUStrategy_Name(t *testing.T) {
|
|
strategy := NewLRUStrategy(100)
|
|
|
|
name := strategy.Name()
|
|
if name != "LRU" {
|
|
t.Errorf("Expected 'LRU', got %s", name)
|
|
}
|
|
}
|
|
|
|
func TestLRUStrategy_ShouldEvict(t *testing.T) {
|
|
strategy := NewLRUStrategy(100)
|
|
|
|
result := strategy.ShouldEvict("test-item", time.Now())
|
|
if result != false {
|
|
t.Error("Expected ShouldEvict to return false")
|
|
}
|
|
}
|
|
|
|
func TestLRUStrategy_OnAccess(t *testing.T) {
|
|
strategy := NewLRUStrategy(100)
|
|
|
|
strategy.OnAccess("test-key", "test-value")
|
|
}
|
|
|
|
func TestLRUStrategy_OnRemove(t *testing.T) {
|
|
strategy := NewLRUStrategy(100)
|
|
|
|
strategy.OnRemove("test-key")
|
|
}
|
|
|
|
func TestLRUStrategy_EstimateSize(t *testing.T) {
|
|
strategy := NewLRUStrategy(100)
|
|
|
|
size := strategy.EstimateSize("test-item")
|
|
if size != 64 {
|
|
t.Errorf("Expected size 64, got %d", size)
|
|
}
|
|
}
|
|
|
|
func TestLRUStrategy_GetEvictionCandidate(t *testing.T) {
|
|
strategy := NewLRUStrategy(100)
|
|
|
|
key, found := strategy.GetEvictionCandidate()
|
|
if found {
|
|
t.Error("Expected no eviction candidate to be found")
|
|
}
|
|
if key != "" {
|
|
t.Errorf("Expected empty key, got %s", key)
|
|
}
|
|
}
|
|
|
|
func TestNewOptimizedCacheWithConfig(t *testing.T) {
|
|
config := UniversalCacheConfig{
|
|
Type: CacheTypeGeneral,
|
|
MaxSize: 1000,
|
|
MaxMemoryBytes: 128 * 1024 * 1024,
|
|
EnableMetrics: true,
|
|
Logger: GetSingletonNoOpLogger(),
|
|
}
|
|
|
|
cache := NewOptimizedCacheWithConfig(config)
|
|
|
|
if cache == nil {
|
|
t.Fatal("Expected cache to be created, got nil")
|
|
}
|
|
|
|
cache.Set("test-key", "test-value", time.Hour)
|
|
value, found := cache.Get("test-key")
|
|
if !found {
|
|
t.Error("Expected key to be found in cache")
|
|
}
|
|
if value != "test-value" {
|
|
t.Errorf("Expected 'test-value', got %v", value)
|
|
}
|
|
}
|
|
|
|
func TestNewFixedMetadataCache(t *testing.T) {
|
|
cache := NewFixedMetadataCache()
|
|
|
|
if cache == nil {
|
|
t.Fatal("Expected cache to be created, got nil")
|
|
}
|
|
|
|
metadata := &ProviderMetadata{
|
|
Issuer: "https://example.com",
|
|
AuthURL: "https://example.com/auth",
|
|
TokenURL: "https://example.com/token",
|
|
JWKSURL: "https://example.com/jwks",
|
|
}
|
|
|
|
err := cache.Set("test-provider", metadata, time.Hour)
|
|
if err != nil {
|
|
t.Errorf("Unexpected error setting metadata: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestNewDoublyLinkedList(t *testing.T) {
|
|
list := NewDoublyLinkedList()
|
|
|
|
if list == nil {
|
|
t.Fatal("Expected list to be created, got nil")
|
|
}
|
|
|
|
if list.Len() != 0 {
|
|
t.Error("Expected empty list initially")
|
|
}
|
|
}
|
|
|
|
func TestDoublyLinkedList_PopFront(t *testing.T) {
|
|
list := NewDoublyLinkedList()
|
|
|
|
element := list.PopFront()
|
|
if element != nil {
|
|
t.Error("Expected nil when popping from empty list")
|
|
}
|
|
|
|
added := list.PushBack("test-value")
|
|
if added == nil {
|
|
t.Fatal("Expected element to be added")
|
|
}
|
|
|
|
popped := list.PopFront()
|
|
if popped == nil {
|
|
t.Error("Expected element to be popped")
|
|
}
|
|
|
|
if list.Len() != 0 {
|
|
t.Error("Expected list to be empty after popping")
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// CONSOLIDATED CACHE TESTS
|
|
// =============================================================================
|
|
|
|
func TestCacheConsolidated(t *testing.T) {
|
|
framework := NewTestFramework(t)
|
|
defer framework.Cleanup()
|
|
|
|
testCases := []CacheTestCase{
|
|
// Basic Operations Tests
|
|
{
|
|
name: "cache_basic_set_get",
|
|
cacheType: "universal",
|
|
operation: "set_get",
|
|
parallel: true,
|
|
timeout: 5 * time.Second,
|
|
execute: func(tf *TestFramework) error {
|
|
cache := NewUniversalCache(createTestCacheConfig())
|
|
defer cache.Close()
|
|
|
|
cache.Set("key1", "value1", 1*time.Hour)
|
|
val, exists := cache.Get("key1")
|
|
if !exists {
|
|
return errors.New("key1 should exist")
|
|
}
|
|
if val != "value1" {
|
|
return fmt.Errorf("expected value1, got %v", val)
|
|
}
|
|
return nil
|
|
},
|
|
validate: func(t *testing.T, err error, tf *TestFramework) {
|
|
assert.NoError(t, err, "Basic set/get operation should succeed")
|
|
},
|
|
},
|
|
{
|
|
name: "cache_basic_delete",
|
|
cacheType: "universal",
|
|
operation: "delete",
|
|
parallel: true,
|
|
timeout: 5 * time.Second,
|
|
execute: func(tf *TestFramework) error {
|
|
cache := NewUniversalCache(createTestCacheConfig())
|
|
defer cache.Close()
|
|
|
|
cache.Set("key1", "value1", 1*time.Hour)
|
|
cache.Delete("key1")
|
|
|
|
_, exists := cache.Get("key1")
|
|
if exists {
|
|
return errors.New("key1 should not exist after deletion")
|
|
}
|
|
return nil
|
|
},
|
|
validate: func(t *testing.T, err error, tf *TestFramework) {
|
|
assert.NoError(t, err, "Delete operation should succeed")
|
|
},
|
|
},
|
|
{
|
|
name: "cache_nil_value_handling",
|
|
cacheType: "universal",
|
|
operation: "set_get",
|
|
parallel: true,
|
|
timeout: 5 * time.Second,
|
|
execute: func(tf *TestFramework) error {
|
|
cache := NewUniversalCache(createTestCacheConfig())
|
|
defer cache.Close()
|
|
|
|
cache.Set("nilkey", nil, 1*time.Hour)
|
|
val, exists := cache.Get("nilkey")
|
|
if !exists {
|
|
return errors.New("nil value should be stored")
|
|
}
|
|
if val != nil {
|
|
return fmt.Errorf("expected nil, got %v", val)
|
|
}
|
|
return nil
|
|
},
|
|
validate: func(t *testing.T, err error, tf *TestFramework) {
|
|
assert.NoError(t, err, "Nil value handling should work correctly")
|
|
},
|
|
},
|
|
|
|
// Expiration Tests
|
|
{
|
|
name: "cache_ttl_expiration",
|
|
cacheType: "universal",
|
|
operation: "expiration",
|
|
parallel: true,
|
|
timeout: 10 * time.Second,
|
|
execute: func(tf *TestFramework) error {
|
|
cache := NewUniversalCache(createTestCacheConfig())
|
|
defer cache.Close()
|
|
|
|
cache.Set("expkey", "value", 100*time.Millisecond)
|
|
|
|
if _, exists := cache.Get("expkey"); !exists {
|
|
return errors.New("key should exist before expiration")
|
|
}
|
|
|
|
time.Sleep(150 * time.Millisecond)
|
|
|
|
if _, exists := cache.Get("expkey"); exists {
|
|
return errors.New("key should not exist after expiration")
|
|
}
|
|
return nil
|
|
},
|
|
validate: func(t *testing.T, err error, tf *TestFramework) {
|
|
assert.NoError(t, err, "TTL expiration should work correctly")
|
|
},
|
|
},
|
|
{
|
|
name: "cache_zero_ttl",
|
|
cacheType: "universal",
|
|
operation: "expiration",
|
|
parallel: true,
|
|
timeout: 5 * time.Second,
|
|
execute: func(tf *TestFramework) error {
|
|
cache := NewUniversalCache(createTestCacheConfig())
|
|
defer cache.Close()
|
|
|
|
cache.Set("permanentkey", "value", 0)
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
if _, exists := cache.Get("permanentkey"); !exists {
|
|
return errors.New("key with zero TTL should not expire")
|
|
}
|
|
return nil
|
|
},
|
|
validate: func(t *testing.T, err error, tf *TestFramework) {
|
|
assert.NoError(t, err, "Zero TTL should mean no expiration")
|
|
},
|
|
},
|
|
|
|
// LRU Eviction Tests
|
|
{
|
|
name: "cache_lru_eviction",
|
|
cacheType: "bounded",
|
|
operation: "eviction",
|
|
parallel: true,
|
|
timeout: 10 * time.Second,
|
|
execute: func(tf *TestFramework) error {
|
|
config := createTestCacheConfig()
|
|
config.MaxSize = 3
|
|
cache := NewUniversalCache(config)
|
|
defer cache.Close()
|
|
|
|
cache.Set("key1", "value1", 1*time.Hour)
|
|
cache.Set("key2", "value2", 1*time.Hour)
|
|
cache.Set("key3", "value3", 1*time.Hour)
|
|
|
|
cache.Get("key1")
|
|
cache.Get("key2")
|
|
|
|
cache.Set("key4", "value4", 1*time.Hour)
|
|
|
|
if _, exists := cache.Get("key3"); exists {
|
|
return errors.New("key3 should have been evicted")
|
|
}
|
|
if _, exists := cache.Get("key1"); !exists {
|
|
return errors.New("key1 should still exist")
|
|
}
|
|
if _, exists := cache.Get("key2"); !exists {
|
|
return errors.New("key2 should still exist")
|
|
}
|
|
if _, exists := cache.Get("key4"); !exists {
|
|
return errors.New("key4 should exist")
|
|
}
|
|
return nil
|
|
},
|
|
validate: func(t *testing.T, err error, tf *TestFramework) {
|
|
assert.NoError(t, err, "LRU eviction should work correctly")
|
|
},
|
|
},
|
|
{
|
|
name: "cache_size_limit",
|
|
cacheType: "bounded",
|
|
operation: "eviction",
|
|
parallel: true,
|
|
timeout: 10 * time.Second,
|
|
execute: func(tf *TestFramework) error {
|
|
config := createTestCacheConfig()
|
|
config.MaxSize = 5
|
|
cache := NewUniversalCache(config)
|
|
defer cache.Close()
|
|
|
|
for i := 0; i < 10; i++ {
|
|
cache.Set(fmt.Sprintf("key%d", i), fmt.Sprintf("value%d", i), 1*time.Hour)
|
|
}
|
|
|
|
count := 0
|
|
for i := 0; i < 10; i++ {
|
|
if _, exists := cache.Get(fmt.Sprintf("key%d", i)); exists {
|
|
count++
|
|
}
|
|
}
|
|
|
|
if count > 5 {
|
|
return fmt.Errorf("cache size exceeded limit: %d > 5", count)
|
|
}
|
|
return nil
|
|
},
|
|
validate: func(t *testing.T, err error, tf *TestFramework) {
|
|
assert.NoError(t, err, "Cache size should be limited correctly")
|
|
},
|
|
},
|
|
|
|
// Concurrency Tests
|
|
{
|
|
name: "cache_concurrent_access",
|
|
cacheType: "universal",
|
|
operation: "concurrent",
|
|
parallel: false,
|
|
timeout: 30 * time.Second,
|
|
execute: func(tf *TestFramework) error {
|
|
cache := NewUniversalCache(createTestCacheConfig())
|
|
defer cache.Close()
|
|
|
|
const goroutines = 100
|
|
const operations = 1000
|
|
|
|
var wg sync.WaitGroup
|
|
var errors int32
|
|
|
|
for i := 0; i < goroutines/2; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
for j := 0; j < operations; j++ {
|
|
key := fmt.Sprintf("key-%d-%d", id, j%10)
|
|
cache.Set(key, fmt.Sprintf("value-%d-%d", id, j), 1*time.Hour)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
for i := 0; i < goroutines/2; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
for j := 0; j < operations; j++ {
|
|
key := fmt.Sprintf("key-%d-%d", id, j%10)
|
|
cache.Get(key)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
if errors > 0 {
|
|
return fmt.Errorf("encountered %d errors during concurrent access", errors)
|
|
}
|
|
return nil
|
|
},
|
|
validate: func(t *testing.T, err error, tf *TestFramework) {
|
|
assert.NoError(t, err, "Concurrent access should be thread-safe")
|
|
},
|
|
},
|
|
{
|
|
name: "cache_race_condition_test",
|
|
cacheType: "universal",
|
|
operation: "concurrent",
|
|
parallel: false,
|
|
timeout: 20 * time.Second,
|
|
execute: func(tf *TestFramework) error {
|
|
cache := NewUniversalCache(createTestCacheConfig())
|
|
defer cache.Close()
|
|
|
|
const iterations = 1000
|
|
var counter int64
|
|
var wg sync.WaitGroup
|
|
|
|
for i := 0; i < 10; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for j := 0; j < iterations; j++ {
|
|
val, _ := cache.Get("counter")
|
|
var current int64
|
|
if val != nil {
|
|
current = val.(int64)
|
|
}
|
|
cache.Set("counter", current+1, 1*time.Hour)
|
|
atomic.AddInt64(&counter, 1)
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
finalVal, _ := cache.Get("counter")
|
|
if finalVal == nil {
|
|
return errors.New("counter should exist")
|
|
}
|
|
|
|
return nil
|
|
},
|
|
validate: func(t *testing.T, err error, tf *TestFramework) {
|
|
assert.NoError(t, err, "Race condition handling should not panic")
|
|
},
|
|
},
|
|
|
|
// Memory Management Tests
|
|
{
|
|
name: "cache_memory_cleanup",
|
|
cacheType: "universal",
|
|
operation: "cleanup",
|
|
parallel: false,
|
|
timeout: 30 * time.Second,
|
|
execute: func(tf *TestFramework) error {
|
|
config := createTestCacheConfig()
|
|
config.CleanupInterval = 100 * time.Millisecond
|
|
cache := NewUniversalCache(config)
|
|
defer cache.Close()
|
|
|
|
for i := 0; i < 100; i++ {
|
|
cache.Set(fmt.Sprintf("key%d", i), fmt.Sprintf("value%d", i), 200*time.Millisecond)
|
|
}
|
|
|
|
time.Sleep(400 * time.Millisecond)
|
|
|
|
count := 0
|
|
for i := 0; i < 100; i++ {
|
|
if _, exists := cache.Get(fmt.Sprintf("key%d", i)); exists {
|
|
count++
|
|
}
|
|
}
|
|
|
|
if count > 0 {
|
|
return fmt.Errorf("expected 0 items after cleanup, found %d", count)
|
|
}
|
|
return nil
|
|
},
|
|
validate: func(t *testing.T, err error, tf *TestFramework) {
|
|
assert.NoError(t, err, "Memory cleanup should remove expired items")
|
|
},
|
|
},
|
|
{
|
|
name: "cache_memory_bounds",
|
|
cacheType: "bounded",
|
|
operation: "memory",
|
|
parallel: false,
|
|
timeout: 30 * time.Second,
|
|
execute: func(tf *TestFramework) error {
|
|
config := createTestCacheConfig()
|
|
config.MaxSize = 1000
|
|
config.MaxMemoryBytes = 1024 * 1024
|
|
cache := NewUniversalCache(config)
|
|
defer cache.Close()
|
|
|
|
runtime.GC()
|
|
var m1 runtime.MemStats
|
|
runtime.ReadMemStats(&m1)
|
|
|
|
largeValue := make([]byte, 1024)
|
|
for i := 0; i < 2000; i++ {
|
|
cache.Set(fmt.Sprintf("key%d", i), largeValue, 1*time.Hour)
|
|
}
|
|
|
|
runtime.GC()
|
|
var m2 runtime.MemStats
|
|
runtime.ReadMemStats(&m2)
|
|
|
|
growth := (m2.Alloc - m1.Alloc) / 1024 / 1024
|
|
if growth > 2 {
|
|
return fmt.Errorf("memory growth exceeded limit: %d MB", growth)
|
|
}
|
|
return nil
|
|
},
|
|
validate: func(t *testing.T, err error, tf *TestFramework) {
|
|
assert.NoError(t, err, "Memory usage should be bounded")
|
|
},
|
|
},
|
|
{
|
|
name: "cache_no_goroutine_leak",
|
|
cacheType: "universal",
|
|
operation: "cleanup",
|
|
parallel: false,
|
|
timeout: 20 * time.Second,
|
|
execute: func(tf *TestFramework) error {
|
|
initialGoroutines := runtime.NumGoroutine()
|
|
|
|
for i := 0; i < 10; i++ {
|
|
cache := NewUniversalCache(createTestCacheConfig())
|
|
|
|
for j := 0; j < 100; j++ {
|
|
cache.Set(fmt.Sprintf("key%d", j), "value", 1*time.Hour)
|
|
}
|
|
|
|
cache.Close()
|
|
}
|
|
|
|
time.Sleep(500 * time.Millisecond)
|
|
runtime.GC()
|
|
|
|
finalGoroutines := runtime.NumGoroutine()
|
|
|
|
if finalGoroutines > initialGoroutines+5 {
|
|
return fmt.Errorf("potential goroutine leak: initial=%d, final=%d",
|
|
initialGoroutines, finalGoroutines)
|
|
}
|
|
return nil
|
|
},
|
|
validate: func(t *testing.T, err error, tf *TestFramework) {
|
|
assert.NoError(t, err, "Should not leak goroutines")
|
|
},
|
|
},
|
|
|
|
// Metadata Cache Tests
|
|
{
|
|
name: "metadata_cache_basic_operations",
|
|
cacheType: "metadata",
|
|
operation: "set_get",
|
|
parallel: true,
|
|
timeout: 10 * time.Second,
|
|
execute: func(tf *TestFramework) error {
|
|
var wg sync.WaitGroup
|
|
cache := NewMetadataCache(&wg)
|
|
defer cache.Close()
|
|
|
|
metadata := &ProviderMetadata{
|
|
Issuer: "https://example.com",
|
|
JWKSURL: "https://example.com/jwks",
|
|
TokenURL: "https://example.com/token",
|
|
AuthURL: "https://example.com/auth",
|
|
}
|
|
|
|
err := cache.Set("provider1", metadata, 1*time.Hour)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to set metadata: %w", err)
|
|
}
|
|
|
|
retrieved, exists := cache.Get("provider1")
|
|
if !exists {
|
|
return errors.New("metadata should exist")
|
|
}
|
|
|
|
if retrieved == nil {
|
|
return errors.New("metadata should not be nil")
|
|
}
|
|
|
|
if retrieved.Issuer != metadata.Issuer {
|
|
return fmt.Errorf("issuer mismatch: expected %s, got %s",
|
|
metadata.Issuer, retrieved.Issuer)
|
|
}
|
|
return nil
|
|
},
|
|
validate: func(t *testing.T, err error, tf *TestFramework) {
|
|
assert.NoError(t, err, "Metadata cache operations should succeed")
|
|
},
|
|
},
|
|
{
|
|
name: "metadata_cache_error_handling",
|
|
cacheType: "metadata",
|
|
operation: "error",
|
|
parallel: true,
|
|
timeout: 10 * time.Second,
|
|
execute: func(tf *TestFramework) error {
|
|
var wg sync.WaitGroup
|
|
cache := NewMetadataCache(&wg)
|
|
defer cache.Close()
|
|
|
|
err := cache.Set("provider1", nil, 1*time.Hour)
|
|
if err == nil {
|
|
return errors.New("should error on nil metadata")
|
|
}
|
|
|
|
metadata := &ProviderMetadata{Issuer: "test"}
|
|
err = cache.Set("", metadata, 1*time.Hour)
|
|
if err != nil {
|
|
return fmt.Errorf("unexpected error with empty key: %v", err)
|
|
}
|
|
|
|
_, exists := cache.Get("nonexistent")
|
|
if exists {
|
|
return errors.New("should not exist for non-existent key")
|
|
}
|
|
|
|
return nil
|
|
},
|
|
validate: func(t *testing.T, err error, tf *TestFramework) {
|
|
assert.NoError(t, err, "Error handling should work correctly")
|
|
},
|
|
},
|
|
|
|
// Token Cache Tests
|
|
{
|
|
name: "cache_token_operations",
|
|
cacheType: "universal",
|
|
operation: "token",
|
|
parallel: true,
|
|
timeout: 10 * time.Second,
|
|
execute: func(tf *TestFramework) error {
|
|
config := createTestCacheConfig()
|
|
config.Type = CacheTypeToken
|
|
cache := NewUniversalCache(config)
|
|
defer cache.Close()
|
|
|
|
token := &TokenResponse{
|
|
AccessToken: "access-token-123",
|
|
RefreshToken: "refresh-token-456",
|
|
IDToken: "id-token-789",
|
|
TokenType: "Bearer",
|
|
ExpiresIn: 3600,
|
|
}
|
|
|
|
cache.Set("token:user123", token, 1*time.Hour)
|
|
|
|
retrieved, exists := cache.Get("token:user123")
|
|
if !exists {
|
|
return errors.New("token should exist")
|
|
}
|
|
|
|
retrievedToken, ok := retrieved.(*TokenResponse)
|
|
if !ok {
|
|
return errors.New("failed to cast to TokenResponse")
|
|
}
|
|
|
|
if retrievedToken.AccessToken != token.AccessToken {
|
|
return fmt.Errorf("access token mismatch: expected %s, got %s",
|
|
token.AccessToken, retrievedToken.AccessToken)
|
|
}
|
|
|
|
cache.Delete("token:user123")
|
|
|
|
_, exists = cache.Get("token:user123")
|
|
if exists {
|
|
return errors.New("token should not exist after deletion")
|
|
}
|
|
return nil
|
|
},
|
|
validate: func(t *testing.T, err error, tf *TestFramework) {
|
|
assert.NoError(t, err, "Token operations should work correctly")
|
|
},
|
|
},
|
|
|
|
// Edge Cases Tests
|
|
{
|
|
name: "cache_edge_case_empty_key",
|
|
cacheType: "universal",
|
|
operation: "edge",
|
|
parallel: true,
|
|
timeout: 5 * time.Second,
|
|
execute: func(tf *TestFramework) error {
|
|
cache := NewUniversalCache(createTestCacheConfig())
|
|
defer cache.Close()
|
|
|
|
cache.Set("", "value", 1*time.Hour)
|
|
val, exists := cache.Get("")
|
|
if !exists {
|
|
return errors.New("empty key should be valid")
|
|
}
|
|
if val != "value" {
|
|
return fmt.Errorf("unexpected value for empty key: %v", val)
|
|
}
|
|
return nil
|
|
},
|
|
validate: func(t *testing.T, err error, tf *TestFramework) {
|
|
assert.NoError(t, err, "Empty key should be handled correctly")
|
|
},
|
|
},
|
|
{
|
|
name: "cache_edge_case_large_values",
|
|
cacheType: "universal",
|
|
operation: "edge",
|
|
parallel: true,
|
|
timeout: 10 * time.Second,
|
|
execute: func(tf *TestFramework) error {
|
|
cache := NewUniversalCache(createTestCacheConfig())
|
|
defer cache.Close()
|
|
|
|
largeValue := make([]byte, 1024*1024)
|
|
for i := range largeValue {
|
|
largeValue[i] = byte(i % 256)
|
|
}
|
|
|
|
cache.Set("large", largeValue, 1*time.Hour)
|
|
retrieved, exists := cache.Get("large")
|
|
if !exists {
|
|
return errors.New("large value should exist")
|
|
}
|
|
|
|
retrievedBytes, ok := retrieved.([]byte)
|
|
if !ok {
|
|
return errors.New("type assertion failed")
|
|
}
|
|
|
|
if len(retrievedBytes) != len(largeValue) {
|
|
return fmt.Errorf("size mismatch: expected %d, got %d",
|
|
len(largeValue), len(retrievedBytes))
|
|
}
|
|
return nil
|
|
},
|
|
validate: func(t *testing.T, err error, tf *TestFramework) {
|
|
assert.NoError(t, err, "Large values should be handled correctly")
|
|
},
|
|
},
|
|
{
|
|
name: "cache_edge_case_special_characters",
|
|
cacheType: "universal",
|
|
operation: "edge",
|
|
parallel: true,
|
|
timeout: 5 * time.Second,
|
|
execute: func(tf *TestFramework) error {
|
|
cache := NewUniversalCache(createTestCacheConfig())
|
|
defer cache.Close()
|
|
|
|
specialKeys := []string{
|
|
"key with spaces",
|
|
"key/with/slashes",
|
|
"key:with:colons",
|
|
"key|with|pipes",
|
|
"key\twith\ttabs",
|
|
"key\nwith\nnewlines",
|
|
"🔑 with emoji",
|
|
}
|
|
|
|
for _, key := range specialKeys {
|
|
cache.Set(key, "value", 1*time.Hour)
|
|
_, exists := cache.Get(key)
|
|
if !exists {
|
|
return fmt.Errorf("failed to retrieve key: %s", key)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
validate: func(t *testing.T, err error, tf *TestFramework) {
|
|
assert.NoError(t, err, "Special characters should be handled correctly")
|
|
},
|
|
},
|
|
|
|
// Cleanup and Resource Management Tests
|
|
{
|
|
name: "cache_proper_cleanup",
|
|
cacheType: "universal",
|
|
operation: "cleanup",
|
|
parallel: false,
|
|
timeout: 15 * time.Second,
|
|
execute: func(tf *TestFramework) error {
|
|
config := createTestCacheConfig()
|
|
config.CleanupInterval = 100 * time.Millisecond
|
|
cache := NewUniversalCache(config)
|
|
|
|
for i := 0; i < 100; i++ {
|
|
cache.Set(fmt.Sprintf("key%d", i), "value", 1*time.Hour)
|
|
}
|
|
|
|
cache.Close()
|
|
|
|
_, exists := cache.Get("key0")
|
|
if exists {
|
|
return errors.New("cache should be cleared after close")
|
|
}
|
|
|
|
cache.Set("newkey", "value", 1*time.Hour)
|
|
val, exists := cache.Get("newkey")
|
|
if !exists || val != "value" {
|
|
return errors.New("cache should allow new operations after close")
|
|
}
|
|
|
|
return nil
|
|
},
|
|
validate: func(t *testing.T, err error, tf *TestFramework) {
|
|
assert.NoError(t, err, "Cache cleanup should work properly")
|
|
},
|
|
},
|
|
{
|
|
name: "cache_concurrent_cleanup",
|
|
cacheType: "universal",
|
|
operation: "cleanup",
|
|
parallel: false,
|
|
timeout: 20 * time.Second,
|
|
execute: func(tf *TestFramework) error {
|
|
cache := NewUniversalCache(createTestCacheConfig())
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
for i := 0; i < 10; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
for j := 0; j < 100; j++ {
|
|
cache.Set(fmt.Sprintf("key-%d-%d", id, j), "value", 1*time.Hour)
|
|
cache.Get(fmt.Sprintf("key-%d-%d", id, j))
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
go func() {
|
|
time.Sleep(50 * time.Millisecond)
|
|
cache.Close()
|
|
}()
|
|
|
|
wg.Wait()
|
|
|
|
return nil
|
|
},
|
|
validate: func(t *testing.T, err error, tf *TestFramework) {
|
|
assert.NoError(t, err, "Concurrent cleanup should not cause panic")
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
tc := tc
|
|
|
|
if tc.skipReason != "" {
|
|
t.Skip(tc.skipReason)
|
|
continue
|
|
}
|
|
|
|
if tc.parallel {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
executeCacheTestCase(t, tc, framework)
|
|
})
|
|
} else {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
executeCacheTestCase(t, tc, framework)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestCacheConsolidatedCoverage ensures all original test scenarios are covered
|
|
func TestCacheConsolidatedCoverage(t *testing.T) {
|
|
scenariosCovered := []string{
|
|
"Basic operations (set/get/delete)",
|
|
"Expiration handling",
|
|
"Cache size limits",
|
|
"Concurrency tests",
|
|
"Performance benchmarks",
|
|
"Edge cases",
|
|
"LRU behavior",
|
|
"Cleanup operations",
|
|
"Bounded cache operations",
|
|
"Race condition handling",
|
|
"Memory leak detection",
|
|
"Eviction performance",
|
|
"Memory edge cases",
|
|
"Optimized operations",
|
|
"Memory pressure handling",
|
|
"Different value types",
|
|
"Metadata operations",
|
|
"Cache hit/miss",
|
|
"Error handling",
|
|
"Auto-cleanup",
|
|
"Thread safety",
|
|
"Timeout handling",
|
|
"Error recovery",
|
|
"Fixed metadata cache",
|
|
"Universal cache operations",
|
|
"Token operations",
|
|
"Metadata grace period",
|
|
"Cache metrics",
|
|
"Cache adapters",
|
|
"Cache migration",
|
|
"Type defaults",
|
|
"Simple cache operations",
|
|
"Eviction failures",
|
|
"Auto-cleanup failures",
|
|
"Sharded cache operations",
|
|
"Shard distribution",
|
|
"Cache manager operations",
|
|
}
|
|
|
|
t.Logf("Consolidated test covers %d scenarios from original files", len(scenariosCovered))
|
|
for _, scenario := range scenariosCovered {
|
|
t.Logf("✓ %s", scenario)
|
|
}
|
|
|
|
assert.True(t, true, "All scenarios covered in consolidated test")
|
|
}
|