Mining/pkg/mining/circuit_breaker_test.go
snider 87b426480b fix: Implement 6 quick wins from 109-finding code review
CONC-HIGH-1: Add mutex to wsClient.miners map to prevent race condition
P2P-CRIT-2: Add MaxMessageSize config (1MB default) to prevent memory exhaustion
P2P-CRIT-3: Track pending connections during handshake to enforce connection limits
RESIL-HIGH-1: Add recover() to 4 background goroutines to prevent service crashes
TEST-CRIT-1: Create auth_test.go with 16 tests covering Basic/Digest auth
RESIL-HIGH-3: Implement circuit breaker for GitHub API with caching fallback

Also fixed: NonceExpiry validation in auth.go to prevent panic on zero interval

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 15:03:46 +00:00

334 lines
7.3 KiB
Go

package mining
import (
"errors"
"sync"
"testing"
"time"
)
func TestCircuitBreakerDefaultConfig(t *testing.T) {
cfg := DefaultCircuitBreakerConfig()
if cfg.FailureThreshold != 3 {
t.Errorf("expected FailureThreshold 3, got %d", cfg.FailureThreshold)
}
if cfg.ResetTimeout != 30*time.Second {
t.Errorf("expected ResetTimeout 30s, got %v", cfg.ResetTimeout)
}
if cfg.SuccessThreshold != 1 {
t.Errorf("expected SuccessThreshold 1, got %d", cfg.SuccessThreshold)
}
}
func TestCircuitBreakerStateString(t *testing.T) {
tests := []struct {
state CircuitState
expected string
}{
{CircuitClosed, "closed"},
{CircuitOpen, "open"},
{CircuitHalfOpen, "half-open"},
{CircuitState(99), "unknown"},
}
for _, tt := range tests {
if got := tt.state.String(); got != tt.expected {
t.Errorf("state %d: expected %s, got %s", tt.state, tt.expected, got)
}
}
}
func TestCircuitBreakerClosed(t *testing.T) {
cb := NewCircuitBreaker("test", DefaultCircuitBreakerConfig())
if cb.State() != CircuitClosed {
t.Error("expected initial state to be closed")
}
// Successful execution
result, err := cb.Execute(func() (interface{}, error) {
return "success", nil
})
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if result != "success" {
t.Errorf("expected 'success', got %v", result)
}
if cb.State() != CircuitClosed {
t.Error("state should still be closed after success")
}
}
func TestCircuitBreakerOpensAfterFailures(t *testing.T) {
cfg := CircuitBreakerConfig{
FailureThreshold: 2,
ResetTimeout: time.Minute,
SuccessThreshold: 1,
}
cb := NewCircuitBreaker("test", cfg)
testErr := errors.New("test error")
// First failure
_, err := cb.Execute(func() (interface{}, error) {
return nil, testErr
})
if err != testErr {
t.Errorf("expected test error, got %v", err)
}
if cb.State() != CircuitClosed {
t.Error("should still be closed after 1 failure")
}
// Second failure - should open circuit
_, err = cb.Execute(func() (interface{}, error) {
return nil, testErr
})
if err != testErr {
t.Errorf("expected test error, got %v", err)
}
if cb.State() != CircuitOpen {
t.Error("should be open after 2 failures")
}
}
func TestCircuitBreakerRejectsWhenOpen(t *testing.T) {
cfg := CircuitBreakerConfig{
FailureThreshold: 1,
ResetTimeout: time.Hour, // Long timeout to keep circuit open
SuccessThreshold: 1,
}
cb := NewCircuitBreaker("test", cfg)
// Open the circuit
cb.Execute(func() (interface{}, error) {
return nil, errors.New("fail")
})
if cb.State() != CircuitOpen {
t.Fatal("circuit should be open")
}
// Next request should be rejected
called := false
_, err := cb.Execute(func() (interface{}, error) {
called = true
return "should not run", nil
})
if called {
t.Error("function should not have been called when circuit is open")
}
if err != ErrCircuitOpen {
t.Errorf("expected ErrCircuitOpen, got %v", err)
}
}
func TestCircuitBreakerTransitionsToHalfOpen(t *testing.T) {
cfg := CircuitBreakerConfig{
FailureThreshold: 1,
ResetTimeout: 50 * time.Millisecond,
SuccessThreshold: 1,
}
cb := NewCircuitBreaker("test", cfg)
// Open the circuit
cb.Execute(func() (interface{}, error) {
return nil, errors.New("fail")
})
if cb.State() != CircuitOpen {
t.Fatal("circuit should be open")
}
// Wait for reset timeout
time.Sleep(100 * time.Millisecond)
// Next request should transition to half-open and execute
result, err := cb.Execute(func() (interface{}, error) {
return "probe success", nil
})
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if result != "probe success" {
t.Errorf("expected 'probe success', got %v", result)
}
if cb.State() != CircuitClosed {
t.Error("should be closed after successful probe")
}
}
func TestCircuitBreakerHalfOpenFailureReopens(t *testing.T) {
cfg := CircuitBreakerConfig{
FailureThreshold: 1,
ResetTimeout: 50 * time.Millisecond,
SuccessThreshold: 1,
}
cb := NewCircuitBreaker("test", cfg)
// Open the circuit
cb.Execute(func() (interface{}, error) {
return nil, errors.New("fail")
})
// Wait for reset timeout
time.Sleep(100 * time.Millisecond)
// Probe fails
cb.Execute(func() (interface{}, error) {
return nil, errors.New("probe failed")
})
if cb.State() != CircuitOpen {
t.Error("should be open after probe failure")
}
}
func TestCircuitBreakerCaching(t *testing.T) {
cfg := CircuitBreakerConfig{
FailureThreshold: 1,
ResetTimeout: time.Hour,
SuccessThreshold: 1,
}
cb := NewCircuitBreaker("test", cfg)
// Successful call - caches result
result, err := cb.Execute(func() (interface{}, error) {
return "cached value", nil
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result != "cached value" {
t.Fatalf("expected 'cached value', got %v", result)
}
// Open the circuit
cb.Execute(func() (interface{}, error) {
return nil, errors.New("fail")
})
// Should return cached value when circuit is open
result, err = cb.Execute(func() (interface{}, error) {
return "should not run", nil
})
if err != nil {
t.Errorf("expected cached result, got error: %v", err)
}
if result != "cached value" {
t.Errorf("expected 'cached value', got %v", result)
}
}
func TestCircuitBreakerGetCached(t *testing.T) {
cb := NewCircuitBreaker("test", DefaultCircuitBreakerConfig())
// No cache initially
_, ok := cb.GetCached()
if ok {
t.Error("expected no cached value initially")
}
// Cache a value
cb.Execute(func() (interface{}, error) {
return "test value", nil
})
cached, ok := cb.GetCached()
if !ok {
t.Error("expected cached value")
}
if cached != "test value" {
t.Errorf("expected 'test value', got %v", cached)
}
}
func TestCircuitBreakerReset(t *testing.T) {
cfg := CircuitBreakerConfig{
FailureThreshold: 1,
ResetTimeout: time.Hour,
SuccessThreshold: 1,
}
cb := NewCircuitBreaker("test", cfg)
// Open the circuit
cb.Execute(func() (interface{}, error) {
return nil, errors.New("fail")
})
if cb.State() != CircuitOpen {
t.Fatal("circuit should be open")
}
// Manual reset
cb.Reset()
if cb.State() != CircuitClosed {
t.Error("circuit should be closed after reset")
}
}
func TestCircuitBreakerConcurrency(t *testing.T) {
cb := NewCircuitBreaker("test", DefaultCircuitBreakerConfig())
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
cb.Execute(func() (interface{}, error) {
if n%3 == 0 {
return nil, errors.New("fail")
}
return "success", nil
})
}(i)
}
wg.Wait()
// Just verify no panics occurred
_ = cb.State()
}
func TestGetGitHubCircuitBreaker(t *testing.T) {
cb1 := getGitHubCircuitBreaker()
cb2 := getGitHubCircuitBreaker()
if cb1 != cb2 {
t.Error("expected singleton circuit breaker")
}
if cb1.name != "github-api" {
t.Errorf("expected name 'github-api', got %s", cb1.name)
}
}
// Benchmark tests
func BenchmarkCircuitBreakerExecute(b *testing.B) {
cb := NewCircuitBreaker("bench", DefaultCircuitBreakerConfig())
b.ResetTimer()
for i := 0; i < b.N; i++ {
cb.Execute(func() (interface{}, error) {
return "result", nil
})
}
}
func BenchmarkCircuitBreakerConcurrent(b *testing.B) {
cb := NewCircuitBreaker("bench", DefaultCircuitBreakerConfig())
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
cb.Execute(func() (interface{}, error) {
return "result", nil
})
}
})
}