A community based topic aggregation platform built on atproto
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}