mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
Security/correctness fixes: - fail-open token validation preserved with lenient-audience; access token that fails verification and has no ID token to corroborate now fails closed - token_manager blacklist-expiry: fix impossible tautology (x>24h && x<24h) - constant-time OAuth state compare; clear state/nonce/code_verifier on callback token-exchange failure to prevent replay of half-failed flows - StopAllTasks gated on last-instance close - background-task panic recovery; remove task-registry counter leak - circuit breaker half-open: release probe slot; CB + per-session outcome recorded once per refresh operation, not once per waiter - Redis connection pool: handle nil receive when pool closed concurrently - FileStore: atomic temp-write + rename - errors_recovery health-check data race -> atomic Test-isolation / hardening: - reset helpers for Once-pinned global cache manager and transport pool - coverage: utils 70->93% (redirect URL security contract); fuzz targets for bearer parsers; concurrent ServeHTTP race tests for the shared request path Dead-code removal (verified orphaned, build+suite green): internal/providers (30 files, 0 importers), internal/compat, internal/features, input_validation. Verified: govulncheck, go mod tidy, yaegi-validate, cross-compile (darwin+ linux), golangci-lint (new-from-rev clean), 3x full race+shuffle green.
60 lines
2.0 KiB
Go
60 lines
2.0 KiB
Go
package recovery
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestCircuitBreaker_HalfOpenRecoversWhenSuccessThresholdExceedsMaxRequests
|
|
// guards the fix that releases the half-open probe slot when a probe
|
|
// completes below the success threshold.
|
|
//
|
|
// Repro scenario: SuccessThreshold (3) exceeds MaxRequests (1). Without
|
|
// the fix, the single allowed half-open probe stays counted in
|
|
// halfOpenRequests after it completes, so every later request is rejected
|
|
// by the MaxRequests gate and consecutiveSuccesses can never reach the
|
|
// threshold — the circuit hangs half-open forever. With the fix the slot
|
|
// is released on completion, so sequential successful probes accumulate
|
|
// to the threshold and the circuit closes.
|
|
func TestCircuitBreaker_HalfOpenRecoversWhenSuccessThresholdExceedsMaxRequests(t *testing.T) {
|
|
config := CircuitBreakerConfig{
|
|
FailureThreshold: 1,
|
|
SuccessThreshold: 3,
|
|
Timeout: 50 * time.Millisecond,
|
|
MaxRequests: 1,
|
|
}
|
|
cb := NewCircuitBreaker(config, &mockLogger{})
|
|
|
|
// Drive the circuit open with a single failure.
|
|
if err := cb.ExecuteWithContext(context.Background(), func() error {
|
|
return errors.New("fail")
|
|
}); err == nil {
|
|
t.Fatal("expected error from failed execution")
|
|
}
|
|
if cb.GetState() != CircuitBreakerOpen {
|
|
t.Fatalf("expected open after failure, got %s", cb.GetState())
|
|
}
|
|
|
|
// Wait for the open timeout so the next request transitions to half-open.
|
|
time.Sleep(70 * time.Millisecond)
|
|
|
|
// Accumulate successes one at a time (MaxRequests == 1). With the fix
|
|
// each completed probe releases its slot, so the breaker reaches the
|
|
// success threshold and closes.
|
|
for i := 0; i < config.SuccessThreshold; i++ {
|
|
err := cb.ExecuteWithContext(context.Background(), func() error {
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("successful execution %d returned error: %v (state %s)", i, err, cb.GetState())
|
|
}
|
|
}
|
|
|
|
if cb.GetState() != CircuitBreakerClosed {
|
|
t.Fatalf("expected circuit to close after %d half-open successes, got %s",
|
|
config.SuccessThreshold, cb.GetState())
|
|
}
|
|
}
|