mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-15 04:18:03 +00:00
fixup! fixup! ... and another all nighter.
This commit is contained in:
+59
@@ -0,0 +1,59 @@
|
||||
//go:build !yaegi
|
||||
|
||||
package backends
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestDefaultHybridConfig verifies the default hybrid configuration
|
||||
func TestDefaultHybridConfig(t *testing.T) {
|
||||
redisAddr := "localhost:6379"
|
||||
|
||||
config := DefaultHybridConfig(redisAddr)
|
||||
|
||||
require.NotNil(t, config)
|
||||
|
||||
// Verify top-level config
|
||||
assert.Equal(t, BackendTypeHybrid, config.Type)
|
||||
assert.True(t, config.AsyncWrites)
|
||||
assert.True(t, config.EnableMetrics)
|
||||
|
||||
// Verify L1 (memory) config
|
||||
require.NotNil(t, config.L1Config)
|
||||
assert.Equal(t, BackendTypeMemory, config.L1Config.Type)
|
||||
assert.Equal(t, 500, config.L1Config.MaxSize)
|
||||
assert.Equal(t, int64(10*1024*1024), config.L1Config.MaxMemoryBytes) // 10MB
|
||||
assert.Equal(t, 1*time.Minute, config.L1Config.CleanupInterval)
|
||||
|
||||
// Verify L2 (Redis) config exists
|
||||
require.NotNil(t, config.L2Config)
|
||||
assert.Equal(t, BackendTypeRedis, config.L2Config.Type)
|
||||
}
|
||||
|
||||
func TestDefaultHybridConfig_DifferentRedisAddr(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
redisAddr string
|
||||
}{
|
||||
{"localhost", "localhost:6379"},
|
||||
{"remote host", "redis.example.com:6379"},
|
||||
{"IP address", "192.168.1.100:6379"},
|
||||
{"custom port", "localhost:6380"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
config := DefaultHybridConfig(tt.redisAddr)
|
||||
|
||||
require.NotNil(t, config)
|
||||
assert.Equal(t, BackendTypeHybrid, config.Type)
|
||||
assert.NotNil(t, config.L1Config)
|
||||
assert.NotNil(t, config.L2Config)
|
||||
})
|
||||
}
|
||||
}
|
||||
+1490
File diff suppressed because it is too large
Load Diff
+23
-4
@@ -250,6 +250,10 @@ func testClear(t *testing.T, ctx context.Context, backend CacheBackend) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Give async writes time to complete before clearing
|
||||
// This prevents race conditions with async write workers
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
// Clear all
|
||||
err := backend.Clear(ctx)
|
||||
require.NoError(t, err)
|
||||
@@ -395,8 +399,23 @@ func setupRedisBackend(t *testing.T) CacheBackend {
|
||||
|
||||
func setupHybridBackend(t *testing.T) CacheBackend {
|
||||
t.Helper()
|
||||
// This will be implemented in hybrid_test.go
|
||||
// For now, return nil to allow compilation
|
||||
t.Skip("HybridBackend implementation pending")
|
||||
return nil
|
||||
|
||||
primary := newMockBackend()
|
||||
secondary := newMockBackend()
|
||||
|
||||
config := &HybridConfig{
|
||||
Primary: primary,
|
||||
Secondary: secondary,
|
||||
AsyncBufferSize: 100,
|
||||
Logger: NewTestLogger(t),
|
||||
}
|
||||
|
||||
hybrid, err := NewHybridBackend(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
hybrid.Close()
|
||||
})
|
||||
|
||||
return hybrid
|
||||
}
|
||||
|
||||
+282
@@ -499,3 +499,285 @@ func TestMemoryBackend_ValueIsolation(t *testing.T) {
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, originalValue, retrieved2, "Original value should not be modified")
|
||||
}
|
||||
|
||||
// TestMemoryBackend_Keys tests the Keys method with pattern matching
|
||||
func TestMemoryBackend_Keys(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
backend, err := NewMemoryBackend(DefaultConfig())
|
||||
require.NoError(t, err)
|
||||
defer backend.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Add test data
|
||||
testKeys := []string{"user:1", "user:2", "session:abc", "session:def", "token:xyz"}
|
||||
for _, key := range testKeys {
|
||||
err := backend.Set(ctx, key, []byte("value"), 1*time.Minute)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
t.Run("AllKeys", func(t *testing.T) {
|
||||
keys, err := backend.Keys(ctx, "*")
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, keys, 5)
|
||||
})
|
||||
|
||||
t.Run("SpecificPattern", func(t *testing.T) {
|
||||
// Simple exact match
|
||||
keys, err := backend.Keys(ctx, "user:1")
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, keys, 1)
|
||||
assert.Contains(t, keys, "user:1")
|
||||
})
|
||||
|
||||
t.Run("ExcludesExpired", func(t *testing.T) {
|
||||
// Add an expired key
|
||||
expiredKey := "expired:key"
|
||||
err := backend.Set(ctx, expiredKey, []byte("value"), 1*time.Millisecond)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for expiration
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
keys, err := backend.Keys(ctx, "*")
|
||||
require.NoError(t, err)
|
||||
assert.NotContains(t, keys, expiredKey, "Expired keys should not be returned")
|
||||
})
|
||||
|
||||
t.Run("AfterClose", func(t *testing.T) {
|
||||
closedBackend, _ := NewMemoryBackend(DefaultConfig())
|
||||
closedBackend.Close()
|
||||
|
||||
_, err := closedBackend.Keys(ctx, "*")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, ErrBackendUnavailable, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMemoryBackend_Size tests the Size method
|
||||
func TestMemoryBackend_Size(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
backend, err := NewMemoryBackend(DefaultConfig())
|
||||
require.NoError(t, err)
|
||||
defer backend.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Initially empty
|
||||
size, err := backend.Size(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(0), size)
|
||||
|
||||
// Add items
|
||||
for i := 0; i < 5; i++ {
|
||||
key := fmt.Sprintf("key-%d", i)
|
||||
err := backend.Set(ctx, key, []byte("value"), 1*time.Minute)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
size, err = backend.Size(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(5), size)
|
||||
|
||||
// Delete one
|
||||
backend.Delete(ctx, "key-0")
|
||||
|
||||
size, err = backend.Size(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(4), size)
|
||||
|
||||
// After close
|
||||
backend.Close()
|
||||
_, err = backend.Size(ctx)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, ErrBackendUnavailable, err)
|
||||
}
|
||||
|
||||
// TestMemoryBackend_TTL tests the TTL method
|
||||
func TestMemoryBackend_TTL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
backend, err := NewMemoryBackend(DefaultConfig())
|
||||
require.NoError(t, err)
|
||||
defer backend.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("ExistingKey", func(t *testing.T) {
|
||||
key := "ttl-key"
|
||||
ttl := 1 * time.Minute
|
||||
|
||||
err := backend.Set(ctx, key, []byte("value"), ttl)
|
||||
require.NoError(t, err)
|
||||
|
||||
remaining, err := backend.TTL(ctx, key)
|
||||
require.NoError(t, err)
|
||||
assert.Greater(t, remaining, 50*time.Second)
|
||||
assert.LessOrEqual(t, remaining, ttl)
|
||||
})
|
||||
|
||||
t.Run("NonExistentKey", func(t *testing.T) {
|
||||
_, err := backend.TTL(ctx, "non-existent")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, ErrCacheMiss, err)
|
||||
})
|
||||
|
||||
t.Run("NoExpiration", func(t *testing.T) {
|
||||
key := "no-expiry"
|
||||
// TTL of 0 typically means no expiration
|
||||
err := backend.Set(ctx, key, []byte("value"), 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
remaining, err := backend.TTL(ctx, key)
|
||||
require.NoError(t, err)
|
||||
// No expiration returns 0
|
||||
assert.Equal(t, time.Duration(0), remaining)
|
||||
})
|
||||
|
||||
t.Run("AfterClose", func(t *testing.T) {
|
||||
closedBackend, _ := NewMemoryBackend(DefaultConfig())
|
||||
closedBackend.Close()
|
||||
|
||||
_, err := closedBackend.TTL(ctx, "key")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, ErrBackendUnavailable, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMemoryBackend_Expire tests the Expire method
|
||||
func TestMemoryBackend_Expire(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
backend, err := NewMemoryBackend(DefaultConfig())
|
||||
require.NoError(t, err)
|
||||
defer backend.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("UpdateTTL", func(t *testing.T) {
|
||||
key := "expire-key"
|
||||
err := backend.Set(ctx, key, []byte("value"), 1*time.Minute)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Update to shorter TTL
|
||||
err = backend.Expire(ctx, key, 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check new TTL
|
||||
remaining, err := backend.TTL(ctx, key)
|
||||
require.NoError(t, err)
|
||||
assert.LessOrEqual(t, remaining, 5*time.Second)
|
||||
})
|
||||
|
||||
t.Run("NonExistentKey", func(t *testing.T) {
|
||||
err := backend.Expire(ctx, "non-existent", 1*time.Minute)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, ErrCacheMiss, err)
|
||||
})
|
||||
|
||||
t.Run("RemoveExpiration", func(t *testing.T) {
|
||||
key := "no-expire-key"
|
||||
err := backend.Set(ctx, key, []byte("value"), 1*time.Minute)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Set TTL to 0 to remove expiration
|
||||
err = backend.Expire(ctx, key, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
// TTL should now be 0
|
||||
remaining, err := backend.TTL(ctx, key)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, time.Duration(0), remaining)
|
||||
})
|
||||
|
||||
t.Run("AfterClose", func(t *testing.T) {
|
||||
closedBackend, _ := NewMemoryBackend(DefaultConfig())
|
||||
closedBackend.Close()
|
||||
|
||||
err := closedBackend.Expire(ctx, "key", 1*time.Minute)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, ErrBackendUnavailable, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMemoryBackend_IsHealthy tests the IsHealthy method
|
||||
func TestMemoryBackend_IsHealthy(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
backend, err := NewMemoryBackend(DefaultConfig())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should be healthy when open
|
||||
assert.True(t, backend.IsHealthy())
|
||||
|
||||
// Should be unhealthy after close
|
||||
backend.Close()
|
||||
assert.False(t, backend.IsHealthy())
|
||||
}
|
||||
|
||||
// TestMemoryBackend_Type tests the Type method
|
||||
func TestMemoryBackend_Type(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
backend, err := NewMemoryBackend(DefaultConfig())
|
||||
require.NoError(t, err)
|
||||
defer backend.Close()
|
||||
|
||||
backendType := backend.Type()
|
||||
assert.Equal(t, TypeMemory, backendType)
|
||||
}
|
||||
|
||||
// TestMemoryBackend_Capabilities tests the Capabilities method
|
||||
func TestMemoryBackend_Capabilities(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
backend, err := NewMemoryBackend(DefaultConfig())
|
||||
require.NoError(t, err)
|
||||
defer backend.Close()
|
||||
|
||||
caps := backend.Capabilities()
|
||||
require.NotNil(t, caps)
|
||||
|
||||
// Memory backend should not be distributed or persistent
|
||||
assert.False(t, caps.Distributed)
|
||||
assert.False(t, caps.Persistent)
|
||||
|
||||
// Should support eviction and TTL
|
||||
assert.True(t, caps.Eviction)
|
||||
assert.True(t, caps.TTL)
|
||||
assert.True(t, caps.SupportsExpire)
|
||||
assert.True(t, caps.SupportsMultiGet)
|
||||
|
||||
// Check limits
|
||||
assert.Greater(t, caps.MaxKeySize, int64(0))
|
||||
assert.Greater(t, caps.MaxValueSize, int64(0))
|
||||
}
|
||||
|
||||
// TestMatchPattern tests the matchPattern helper function
|
||||
func TestMatchPattern(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
pattern string
|
||||
key string
|
||||
matches bool
|
||||
}{
|
||||
{"*", "any-key", true},
|
||||
{"*", "another", true},
|
||||
{"user:1", "user:1", true},
|
||||
{"user:1", "user:2", false},
|
||||
{"*:suffix", "prefix:suffix", true},
|
||||
{"*suffix", "prefix-suffix", true},
|
||||
{"*abc", "xyzabc", true},
|
||||
{"*abc", "xyz", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(fmt.Sprintf("%s-%s", tt.pattern, tt.key), func(t *testing.T) {
|
||||
result := matchPattern(tt.pattern, tt.key)
|
||||
assert.Equal(t, tt.matches, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,10 @@ func (l *TestLogger) Errorf(format string, args ...interface{}) {
|
||||
l.Error(format, args...)
|
||||
}
|
||||
|
||||
func (l *TestLogger) Warnf(format string, args ...interface{}) {
|
||||
l.t.Logf("[WARN] "+format, args...)
|
||||
}
|
||||
|
||||
// MiniredisServer manages a miniredis instance for testing
|
||||
type MiniredisServer struct {
|
||||
server *miniredis.Miniredis
|
||||
|
||||
Vendored
+96
-10
@@ -1880,19 +1880,20 @@ func TestConcurrentManagerOperations(t *testing.T) {
|
||||
// TestTTLExpirationAndCleanup tests TTL expiration and cleanup routines comprehensively
|
||||
func TestTTLExpirationAndCleanup(t *testing.T) {
|
||||
config := DefaultConfig()
|
||||
config.CleanupInterval = 10 * time.Millisecond
|
||||
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", 5 * time.Millisecond},
|
||||
{"short", 25 * time.Millisecond},
|
||||
{"medium", 100 * time.Millisecond},
|
||||
{"very-short", 25 * time.Millisecond},
|
||||
{"short", 125 * time.Millisecond},
|
||||
{"medium", 500 * time.Millisecond},
|
||||
{"long", 1 * time.Hour},
|
||||
}
|
||||
|
||||
@@ -1908,13 +1909,13 @@ func TestTTLExpirationAndCleanup(t *testing.T) {
|
||||
}
|
||||
|
||||
// Wait for very short items to expire
|
||||
time.Sleep(15 * time.Millisecond)
|
||||
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(30 * time.Millisecond)
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
if _, exists := cache.Get("short"); exists {
|
||||
t.Error("Short item should be expired")
|
||||
}
|
||||
@@ -1930,16 +1931,16 @@ func TestTTLExpirationAndCleanup(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test manual cleanup
|
||||
cache.Set("manual-cleanup", "value", 1*time.Millisecond)
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
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), 1*time.Millisecond)
|
||||
cache.Set(key, fmt.Sprintf("value-%d", i), 5*time.Millisecond)
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
time.Sleep(25 * time.Millisecond)
|
||||
|
||||
sizeBefore := cache.Size()
|
||||
cache.Cleanup()
|
||||
@@ -2038,3 +2039,88 @@ func TestCacheStatisticsAndMetrics(t *testing.T) {
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,561 @@
|
||||
//go:build !yaegi
|
||||
|
||||
package resilience
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/lukaszraczylo/traefikoidc/internal/cache/backends"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// mockBackend is a simple mock implementation for testing
|
||||
type mockBackend struct {
|
||||
data map[string]mockEntry
|
||||
mu sync.RWMutex
|
||||
failSet bool
|
||||
failGet bool
|
||||
failDelete bool
|
||||
failExists bool
|
||||
failClear bool
|
||||
failPing bool
|
||||
callCount int
|
||||
}
|
||||
|
||||
type mockEntry struct {
|
||||
value []byte
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
func newMockBackend() *mockBackend {
|
||||
return &mockBackend{
|
||||
data: make(map[string]mockEntry),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockBackend) Set(ctx context.Context, key string, value []byte, ttl time.Duration) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.callCount++
|
||||
|
||||
if m.failSet {
|
||||
return errors.New("mock set error")
|
||||
}
|
||||
|
||||
expiresAt := time.Now().Add(ttl)
|
||||
if ttl == 0 {
|
||||
expiresAt = time.Now().Add(24 * time.Hour)
|
||||
}
|
||||
|
||||
m.data[key] = mockEntry{
|
||||
value: value,
|
||||
expiresAt: expiresAt,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockBackend) Get(ctx context.Context, key string) ([]byte, time.Duration, bool, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
m.callCount++
|
||||
|
||||
if m.failGet {
|
||||
return nil, 0, false, errors.New("mock get error")
|
||||
}
|
||||
|
||||
entry, exists := m.data[key]
|
||||
if !exists {
|
||||
return nil, 0, false, nil
|
||||
}
|
||||
|
||||
if time.Now().After(entry.expiresAt) {
|
||||
return nil, 0, false, nil
|
||||
}
|
||||
|
||||
ttl := time.Until(entry.expiresAt)
|
||||
return entry.value, ttl, true, nil
|
||||
}
|
||||
|
||||
func (m *mockBackend) Delete(ctx context.Context, key string) (bool, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.callCount++
|
||||
|
||||
if m.failDelete {
|
||||
return false, errors.New("mock delete error")
|
||||
}
|
||||
|
||||
_, existed := m.data[key]
|
||||
delete(m.data, key)
|
||||
return existed, nil
|
||||
}
|
||||
|
||||
func (m *mockBackend) Exists(ctx context.Context, key string) (bool, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
m.callCount++
|
||||
|
||||
if m.failExists {
|
||||
return false, errors.New("mock exists error")
|
||||
}
|
||||
|
||||
entry, exists := m.data[key]
|
||||
if !exists {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if time.Now().After(entry.expiresAt) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (m *mockBackend) Clear(ctx context.Context) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.callCount++
|
||||
|
||||
if m.failClear {
|
||||
return errors.New("mock clear error")
|
||||
}
|
||||
|
||||
m.data = make(map[string]mockEntry)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockBackend) GetStats() map[string]interface{} {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
return map[string]interface{}{
|
||||
"hits": int64(0),
|
||||
"misses": int64(0),
|
||||
"call_count": m.callCount,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockBackend) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockBackend) Ping(ctx context.Context) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.callCount++
|
||||
|
||||
if m.failPing {
|
||||
return errors.New("mock ping error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Constructor Tests
|
||||
|
||||
func TestNewCircuitBreakerBackend_WithDefaultConfig(t *testing.T) {
|
||||
mockBE := newMockBackend()
|
||||
|
||||
cb := NewCircuitBreakerBackend(mockBE, nil)
|
||||
require.NotNil(t, cb)
|
||||
|
||||
// Verify it implements the interface (compile-time check)
|
||||
var _ backends.CacheBackend = cb
|
||||
}
|
||||
|
||||
func TestNewCircuitBreakerBackend_WithCustomConfig(t *testing.T) {
|
||||
mockBE := newMockBackend()
|
||||
|
||||
config := &CircuitBreakerConfig{
|
||||
MaxFailures: 3,
|
||||
FailureThreshold: 0.5,
|
||||
Timeout: 5 * time.Second,
|
||||
HalfOpenMaxRequests: 2,
|
||||
ResetTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
cb := NewCircuitBreakerBackend(mockBE, config)
|
||||
require.NotNil(t, cb)
|
||||
}
|
||||
|
||||
// Set Operation Tests
|
||||
|
||||
func TestCircuitBreakerBackend_Set_Success(t *testing.T) {
|
||||
mockBE := newMockBackend()
|
||||
cb := NewCircuitBreakerBackend(mockBE, nil)
|
||||
|
||||
ctx := context.Background()
|
||||
err := cb.Set(ctx, "key1", []byte("value1"), 1*time.Minute)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, mockBE.callCount)
|
||||
|
||||
// Verify value was stored
|
||||
value, _, exists, _ := mockBE.Get(ctx, "key1")
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, []byte("value1"), value)
|
||||
}
|
||||
|
||||
func TestCircuitBreakerBackend_Set_Failure(t *testing.T) {
|
||||
mockBE := newMockBackend()
|
||||
mockBE.failSet = true
|
||||
|
||||
cb := NewCircuitBreakerBackend(mockBE, nil)
|
||||
|
||||
ctx := context.Background()
|
||||
err := cb.Set(ctx, "key1", []byte("value1"), 1*time.Minute)
|
||||
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestCircuitBreakerBackend_Set_CircuitOpen(t *testing.T) {
|
||||
mockBE := newMockBackend()
|
||||
mockBE.failSet = true
|
||||
|
||||
config := &CircuitBreakerConfig{
|
||||
MaxFailures: 2,
|
||||
Timeout: 100 * time.Millisecond,
|
||||
}
|
||||
cb := NewCircuitBreakerBackend(mockBE, config)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Trigger failures to open circuit
|
||||
for i := 0; i < 5; i++ {
|
||||
cb.Set(ctx, "key", []byte("value"), 1*time.Minute)
|
||||
}
|
||||
|
||||
// Circuit should be open now
|
||||
err := cb.Set(ctx, "key2", []byte("value2"), 1*time.Minute)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, backends.ErrCircuitOpen, err)
|
||||
}
|
||||
|
||||
// Get Operation Tests
|
||||
|
||||
func TestCircuitBreakerBackend_Get_Success(t *testing.T) {
|
||||
mockBE := newMockBackend()
|
||||
cb := NewCircuitBreakerBackend(mockBE, nil)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// First set a value
|
||||
mockBE.Set(ctx, "key1", []byte("value1"), 1*time.Minute)
|
||||
|
||||
// Now get it through circuit breaker
|
||||
value, _, exists, err := cb.Get(ctx, "key1")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, []byte("value1"), value)
|
||||
}
|
||||
|
||||
func TestCircuitBreakerBackend_Get_Failure(t *testing.T) {
|
||||
mockBE := newMockBackend()
|
||||
mockBE.failGet = true
|
||||
|
||||
cb := NewCircuitBreakerBackend(mockBE, nil)
|
||||
|
||||
ctx := context.Background()
|
||||
_, _, _, err := cb.Get(ctx, "key1")
|
||||
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestCircuitBreakerBackend_Get_CircuitOpen(t *testing.T) {
|
||||
mockBE := newMockBackend()
|
||||
mockBE.failGet = true
|
||||
|
||||
config := &CircuitBreakerConfig{
|
||||
MaxFailures: 2,
|
||||
Timeout: 100 * time.Millisecond,
|
||||
}
|
||||
cb := NewCircuitBreakerBackend(mockBE, config)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Trigger failures
|
||||
for i := 0; i < 5; i++ {
|
||||
cb.Get(ctx, "key")
|
||||
}
|
||||
|
||||
// Circuit should be open
|
||||
_, _, _, err := cb.Get(ctx, "key2")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, backends.ErrCircuitOpen, err)
|
||||
}
|
||||
|
||||
// Delete Operation Tests
|
||||
|
||||
func TestCircuitBreakerBackend_Delete_Success(t *testing.T) {
|
||||
mockBE := newMockBackend()
|
||||
cb := NewCircuitBreakerBackend(mockBE, nil)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Set a value first
|
||||
mockBE.Set(ctx, "key1", []byte("value1"), 1*time.Minute)
|
||||
|
||||
// Delete through circuit breaker
|
||||
deleted, err := cb.Delete(ctx, "key1")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, deleted)
|
||||
|
||||
// Verify it's deleted
|
||||
exists, _ := mockBE.Exists(ctx, "key1")
|
||||
assert.False(t, exists)
|
||||
}
|
||||
|
||||
func TestCircuitBreakerBackend_Delete_CircuitOpen(t *testing.T) {
|
||||
mockBE := newMockBackend()
|
||||
mockBE.failDelete = true
|
||||
|
||||
config := &CircuitBreakerConfig{
|
||||
MaxFailures: 2,
|
||||
Timeout: 100 * time.Millisecond,
|
||||
}
|
||||
cb := NewCircuitBreakerBackend(mockBE, config)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Trigger failures
|
||||
for i := 0; i < 5; i++ {
|
||||
cb.Delete(ctx, "key")
|
||||
}
|
||||
|
||||
// Circuit should be open
|
||||
_, err := cb.Delete(ctx, "key2")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, backends.ErrCircuitOpen, err)
|
||||
}
|
||||
|
||||
// Exists Operation Tests
|
||||
|
||||
func TestCircuitBreakerBackend_Exists_Success(t *testing.T) {
|
||||
mockBE := newMockBackend()
|
||||
cb := NewCircuitBreakerBackend(mockBE, nil)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Set a value first
|
||||
mockBE.Set(ctx, "key1", []byte("value1"), 1*time.Minute)
|
||||
|
||||
// Check existence through circuit breaker
|
||||
exists, err := cb.Exists(ctx, "key1")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exists)
|
||||
}
|
||||
|
||||
func TestCircuitBreakerBackend_Exists_CircuitOpen(t *testing.T) {
|
||||
mockBE := newMockBackend()
|
||||
mockBE.failExists = true
|
||||
|
||||
config := &CircuitBreakerConfig{
|
||||
MaxFailures: 2,
|
||||
Timeout: 100 * time.Millisecond,
|
||||
}
|
||||
cb := NewCircuitBreakerBackend(mockBE, config)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Trigger failures
|
||||
for i := 0; i < 5; i++ {
|
||||
cb.Exists(ctx, "key")
|
||||
}
|
||||
|
||||
// Circuit should be open
|
||||
_, err := cb.Exists(ctx, "key2")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, backends.ErrCircuitOpen, err)
|
||||
}
|
||||
|
||||
// Clear Operation Tests
|
||||
|
||||
func TestCircuitBreakerBackend_Clear_Success(t *testing.T) {
|
||||
mockBE := newMockBackend()
|
||||
cb := NewCircuitBreakerBackend(mockBE, nil)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Set some values
|
||||
mockBE.Set(ctx, "key1", []byte("value1"), 1*time.Minute)
|
||||
mockBE.Set(ctx, "key2", []byte("value2"), 1*time.Minute)
|
||||
|
||||
// Clear through circuit breaker
|
||||
err := cb.Clear(ctx)
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify cleared
|
||||
exists1, _ := mockBE.Exists(ctx, "key1")
|
||||
exists2, _ := mockBE.Exists(ctx, "key2")
|
||||
assert.False(t, exists1)
|
||||
assert.False(t, exists2)
|
||||
}
|
||||
|
||||
func TestCircuitBreakerBackend_Clear_CircuitOpen(t *testing.T) {
|
||||
mockBE := newMockBackend()
|
||||
mockBE.failClear = true
|
||||
|
||||
config := &CircuitBreakerConfig{
|
||||
MaxFailures: 2,
|
||||
Timeout: 100 * time.Millisecond,
|
||||
}
|
||||
cb := NewCircuitBreakerBackend(mockBE, config)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Trigger failures
|
||||
for i := 0; i < 5; i++ {
|
||||
cb.Clear(ctx)
|
||||
}
|
||||
|
||||
// Circuit should be open
|
||||
err := cb.Clear(ctx)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, backends.ErrCircuitOpen, err)
|
||||
}
|
||||
|
||||
// GetStats Tests
|
||||
|
||||
func TestCircuitBreakerBackend_GetStats(t *testing.T) {
|
||||
mockBE := newMockBackend()
|
||||
cb := NewCircuitBreakerBackend(mockBE, nil)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Perform some operations
|
||||
cb.Set(ctx, "key1", []byte("value1"), 1*time.Minute)
|
||||
cb.Get(ctx, "key1")
|
||||
|
||||
stats := cb.GetStats()
|
||||
|
||||
require.NotNil(t, stats)
|
||||
|
||||
// Should have circuit breaker stats
|
||||
assert.Contains(t, stats, "circuit_breaker")
|
||||
|
||||
cbStats, ok := stats["circuit_breaker"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
|
||||
// Verify circuit breaker stats fields
|
||||
assert.Contains(t, cbStats, "state")
|
||||
assert.Contains(t, cbStats, "consecutive_failures")
|
||||
assert.Contains(t, cbStats, "total_requests")
|
||||
assert.Contains(t, cbStats, "total_failures")
|
||||
assert.Contains(t, cbStats, "success_rate")
|
||||
}
|
||||
|
||||
func TestCircuitBreakerBackend_GetStats_NilBackendStats(t *testing.T) {
|
||||
// Create a mock backend that returns nil stats
|
||||
mockBE := &mockBackendNilStats{}
|
||||
cb := NewCircuitBreakerBackend(mockBE, nil)
|
||||
|
||||
stats := cb.GetStats()
|
||||
|
||||
require.NotNil(t, stats)
|
||||
assert.Contains(t, stats, "circuit_breaker")
|
||||
}
|
||||
|
||||
// mockBackendNilStats returns nil from GetStats
|
||||
type mockBackendNilStats struct {
|
||||
mockBackend
|
||||
}
|
||||
|
||||
func (m *mockBackendNilStats) GetStats() map[string]interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ping Tests
|
||||
|
||||
func TestCircuitBreakerBackend_Ping_Success(t *testing.T) {
|
||||
mockBE := newMockBackend()
|
||||
cb := NewCircuitBreakerBackend(mockBE, nil)
|
||||
|
||||
ctx := context.Background()
|
||||
err := cb.Ping(ctx)
|
||||
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCircuitBreakerBackend_Ping_CircuitOpen(t *testing.T) {
|
||||
mockBE := newMockBackend()
|
||||
mockBE.failPing = true
|
||||
|
||||
config := &CircuitBreakerConfig{
|
||||
MaxFailures: 2,
|
||||
Timeout: 100 * time.Millisecond,
|
||||
}
|
||||
cb := NewCircuitBreakerBackend(mockBE, config)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Trigger failures
|
||||
for i := 0; i < 5; i++ {
|
||||
cb.Ping(ctx)
|
||||
}
|
||||
|
||||
// Circuit should be open
|
||||
err := cb.Ping(ctx)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, backends.ErrCircuitOpen, err)
|
||||
}
|
||||
|
||||
// Close Tests
|
||||
|
||||
func TestCircuitBreakerBackend_Close(t *testing.T) {
|
||||
mockBE := newMockBackend()
|
||||
cb := NewCircuitBreakerBackend(mockBE, nil)
|
||||
|
||||
err := cb.Close()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// Circuit Recovery Test
|
||||
|
||||
func TestCircuitBreakerBackend_CircuitRecovery(t *testing.T) {
|
||||
mockBE := newMockBackend()
|
||||
mockBE.failSet = true
|
||||
|
||||
config := &CircuitBreakerConfig{
|
||||
MaxFailures: 2,
|
||||
Timeout: 200 * time.Millisecond,
|
||||
HalfOpenMaxRequests: 1,
|
||||
}
|
||||
cb := NewCircuitBreakerBackend(mockBE, config)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Trigger failures to open circuit
|
||||
for i := 0; i < 5; i++ {
|
||||
cb.Set(ctx, "key", []byte("value"), 1*time.Minute)
|
||||
}
|
||||
|
||||
// Verify circuit is open
|
||||
err := cb.Set(ctx, "key2", []byte("value2"), 1*time.Minute)
|
||||
assert.Equal(t, backends.ErrCircuitOpen, err)
|
||||
|
||||
// Wait for timeout
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
|
||||
// Fix the backend
|
||||
mockBE.mu.Lock()
|
||||
mockBE.failSet = false
|
||||
mockBE.mu.Unlock()
|
||||
|
||||
// Circuit should be in half-open state, allow a test request
|
||||
err = cb.Set(ctx, "key3", []byte("value3"), 1*time.Minute)
|
||||
|
||||
// After success threshold is met, circuit should close
|
||||
if err == nil {
|
||||
// Circuit recovered
|
||||
err2 := cb.Set(ctx, "key4", []byte("value4"), 1*time.Minute)
|
||||
assert.NoError(t, err2, "Circuit should be closed after recovery")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user