//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 { expiresAt time.Time value []byte } 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") } }