mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
538 lines
13 KiB
Go
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++
|
|
}
|
|
})
|
|
}
|