Files
traefikoidc/concurrent_servehttp_test.go
T
lukaszraczylo 2ff14556d0 Audit review (rounds 1-31): fix confirmed issues, remove dead code, harden tests
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.
2026-08-12 23:08:03 +01:00

96 lines
2.8 KiB
Go

package traefikoidc
import (
"fmt"
"net/http"
"net/http/httptest"
"sync"
"testing"
)
// TestConcurrentServeHTTP_SharedInstance drives the full request path
// (bootstraps, auth-bypass evaluation, session lookup, redirect) across many
// goroutines against ONE shared plugin instance. Traefik runs the middleware
// concurrently for every request on a router, so this is the production
// concurrency shape. Holds under -race only if the request path touches
// shared mutable state via atomics/locks, never plain field writes.
func TestConcurrentServeHTTP_SharedInstance(t *testing.T) {
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
oidc := &TraefikOidc{
next: next,
logger: NewLogger("debug"),
initComplete: make(chan struct{}),
sessionManager: createTestSessionManager(t),
issuerURL: "https://provider.example.com",
redirURLPath: "/callback",
logoutURLPath: "/logout",
clientID: "test-client",
audience: "test-client",
authURL: "https://provider.example.com/auth",
firstRequestStarted: 0, // let exactly one goroutine win the bootstrap CAS
metadataRefreshStartedAtomic: 0,
}
close(oidc.initComplete)
const goroutines = 40
const perGoroutine = 5
var wg sync.WaitGroup
wg.Add(goroutines)
for g := 0; g < goroutines; g++ {
go func() {
defer wg.Done()
for i := 0; i < perGoroutine; i++ {
req := httptest.NewRequest("GET", fmt.Sprintf("/api/g%d/", g), nil)
w := httptest.NewRecorder()
oidc.ServeHTTP(w, req)
// Unauthenticated request must bounce to the IdP (3xx), not
// forward to the backend (200) or error.
if w.Code < 300 || w.Code >= 400 {
t.Errorf("expected auth redirect (3xx), got %d", w.Code)
}
}
}()
}
wg.Wait()
}
// TestConcurrentServeHTTP_ExcludedBypass drives the auth-bypass path
// concurrently (shouldBypassAuth read-path) on a shared instance.
func TestConcurrentServeHTTP_ExcludedBypass(t *testing.T) {
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
oidc := &TraefikOidc{
next: next,
logger: NewLogger("debug"),
initComplete: make(chan struct{}),
sessionManager: createTestSessionManager(t),
firstRequestStarted: 1,
metadataRefreshStartedAtomic: 1,
authURL: "https://provider.example.com/auth",
}
close(oidc.initComplete)
const goroutines = 40
const perGoroutine = 5
var wg sync.WaitGroup
wg.Add(goroutines)
for g := 0; g < goroutines; g++ {
go func() {
defer wg.Done()
for i := 0; i < perGoroutine; i++ {
req := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
oidc.ServeHTTP(w, req)
}
}()
}
wg.Wait()
}