A community based topic aggregation platform built on atproto
at main 4.7 kB view raw
1package unfurl 2 3import ( 4 "fmt" 5 "testing" 6 "time" 7) 8 9func TestCircuitBreaker_Basic(t *testing.T) { 10 cb := newCircuitBreaker() 11 12 provider := "test-provider" 13 14 // Should start closed (allow attempts) 15 canAttempt, err := cb.canAttempt(provider) 16 if !canAttempt { 17 t.Errorf("Expected circuit to be closed initially, but got error: %v", err) 18 } 19 20 // Record success 21 cb.recordSuccess(provider) 22 canAttempt, _ = cb.canAttempt(provider) 23 if !canAttempt { 24 t.Error("Expected circuit to remain closed after success") 25 } 26} 27 28func TestCircuitBreaker_OpensAfterFailures(t *testing.T) { 29 cb := newCircuitBreaker() 30 provider := "failing-provider" 31 32 // Record failures up to threshold 33 for i := 0; i < cb.failureThreshold; i++ { 34 cb.recordFailure(provider, fmt.Errorf("test error %d", i)) 35 } 36 37 // Circuit should now be open 38 canAttempt, err := cb.canAttempt(provider) 39 if canAttempt { 40 t.Error("Expected circuit to be open after threshold failures") 41 } 42 if err == nil { 43 t.Error("Expected error when circuit is open") 44 } 45} 46 47func TestCircuitBreaker_RecoveryAfterSuccess(t *testing.T) { 48 cb := newCircuitBreaker() 49 provider := "recovery-provider" 50 51 // Record some failures 52 cb.recordFailure(provider, fmt.Errorf("error 1")) 53 cb.recordFailure(provider, fmt.Errorf("error 2")) 54 55 // Record success - should reset failure count 56 cb.recordSuccess(provider) 57 58 // Should be able to attempt again 59 canAttempt, err := cb.canAttempt(provider) 60 if !canAttempt { 61 t.Errorf("Expected circuit to be closed after success, but got error: %v", err) 62 } 63 64 // Failure count should be reset 65 if count := cb.failures[provider]; count != 0 { 66 t.Errorf("Expected failure count to be reset to 0, got %d", count) 67 } 68} 69 70func TestCircuitBreaker_HalfOpenTransition(t *testing.T) { 71 cb := newCircuitBreaker() 72 cb.openDuration = 100 * time.Millisecond // Short duration for testing 73 provider := "half-open-provider" 74 75 // Open the circuit 76 for i := 0; i < cb.failureThreshold; i++ { 77 cb.recordFailure(provider, fmt.Errorf("error %d", i)) 78 } 79 80 // Should be open 81 canAttempt, _ := cb.canAttempt(provider) 82 if canAttempt { 83 t.Error("Expected circuit to be open") 84 } 85 86 // Wait for open duration 87 time.Sleep(150 * time.Millisecond) 88 89 // Should transition to half-open and allow one attempt 90 canAttempt, err := cb.canAttempt(provider) 91 if !canAttempt { 92 t.Errorf("Expected circuit to transition to half-open after duration, but got error: %v", err) 93 } 94 95 // State should be half-open 96 cb.mu.RLock() 97 state := cb.state[provider] 98 cb.mu.RUnlock() 99 100 if state != stateHalfOpen { 101 t.Errorf("Expected state to be half-open, got %v", state) 102 } 103} 104 105func TestCircuitBreaker_MultipleProviders(t *testing.T) { 106 cb := newCircuitBreaker() 107 108 // Open circuit for provider A 109 for i := 0; i < cb.failureThreshold; i++ { 110 cb.recordFailure("providerA", fmt.Errorf("error")) 111 } 112 113 // Provider A should be blocked 114 canAttemptA, _ := cb.canAttempt("providerA") 115 if canAttemptA { 116 t.Error("Expected providerA circuit to be open") 117 } 118 119 // Provider B should still be open (independent circuits) 120 canAttemptB, err := cb.canAttempt("providerB") 121 if !canAttemptB { 122 t.Errorf("Expected providerB circuit to be closed, but got error: %v", err) 123 } 124} 125 126func TestCircuitBreaker_GetStats(t *testing.T) { 127 cb := newCircuitBreaker() 128 129 // Record some activity 130 cb.recordFailure("provider1", fmt.Errorf("error 1")) 131 cb.recordFailure("provider1", fmt.Errorf("error 2")) 132 133 stats := cb.getStats() 134 135 // Should have stats for providers with failures 136 if providerStats, ok := stats["provider1"]; !ok { 137 t.Error("Expected stats for provider1") 138 } else { 139 // Check that failure count is tracked 140 statsMap := providerStats.(map[string]interface{}) 141 if failures, ok := statsMap["failures"].(int); !ok || failures != 2 { 142 t.Errorf("Expected 2 failures for provider1, got %v", statsMap["failures"]) 143 } 144 } 145 146 // Provider that succeeds is cleaned up from state 147 cb.recordSuccess("provider2") 148 _ = cb.getStats() 149 // Provider2 should not be in stats (or have state "closed" with 0 failures) 150} 151 152func TestCircuitBreaker_FailureThresholdExact(t *testing.T) { 153 cb := newCircuitBreaker() 154 provider := "exact-threshold-provider" 155 156 // Record failures just below threshold 157 for i := 0; i < cb.failureThreshold-1; i++ { 158 cb.recordFailure(provider, fmt.Errorf("error %d", i)) 159 } 160 161 // Should still be closed 162 canAttempt, err := cb.canAttempt(provider) 163 if !canAttempt { 164 t.Errorf("Expected circuit to be closed below threshold, but got error: %v", err) 165 } 166 167 // One more failure should open it 168 cb.recordFailure(provider, fmt.Errorf("final error")) 169 170 // Should now be open 171 canAttempt, _ = cb.canAttempt(provider) 172 if canAttempt { 173 t.Error("Expected circuit to be open at threshold") 174 } 175}