Files
traefikoidc/review_r125_test.go
T
lukaszraczylo 6d96e8869b test: add R-round regression and security suite
- fail-on-old regression tests for every shipped fix (R-rounds)
- fuzz targets (parseJWT, bearer), concurrency-stress, shuffle-flake guards
- coverage-closing contract tests (health-check backend, TLS-posture reuse)
- r154 ID-token fixture built from parts to avoid false-positive secret scan
2026-08-14 16:22:39 +01:00

101 lines
3.8 KiB
Go

package traefikoidc
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
"time"
)
// testHeaderWriter records the response headers exactly as they are at
// WriteHeader time, so tests can verify header fields are set before the
// status line is committed (setting a header after WriteHeader has no effect
// on the wire).
type testHeaderWriter struct {
status http.Header
headerAtCode http.Header
code int
}
func (w *testHeaderWriter) Header() http.Header { return w.status }
func (w *testHeaderWriter) Write(b []byte) (int, error) { return len(b), nil }
func (w *testHeaderWriter) WriteHeader(code int) {
w.code = code
w.headerAtCode = w.status.Clone()
}
// TestWriteBearerError_SetsCacheControlBeforeWriteHeader guards the R125 fix
// to bearer_auth.go writeBearerError: the Cache-Control: no-store header
// (the R101 no-cache contract for 401/403/429/503 rejections) was set
// AFTER WriteHeader, so it was never actually sent on the wire — a cached
// 429 could be replayed after the penalty box expired. It must be set
// before the status line is committed.
func TestWriteBearerError_SetsCacheControlBeforeWriteHeader(t *testing.T) {
w := &testHeaderWriter{status: http.Header{}}
tObj := &TraefikOidc{}
req := httptest.NewRequest(http.MethodGet, "https://app.example.com/api", nil)
tObj.writeBearerError(w, req, newBearerError(bearerErrForbidden, "denied"))
if w.code != http.StatusForbidden {
t.Fatalf("status = %d, want %d", w.code, http.StatusForbidden)
}
if got := w.headerAtCode.Get("Cache-Control"); got != "no-store" {
t.Fatalf("Cache-Control at WriteHeader = %q, want \"no-store\" (header set after WriteHeader is never sent)", got)
}
}
// TestChunkManager_OpaqueTokenAbove8KReadable guards the R125 fix to
// session_chunk_manager.go validateTokenSize: the read path hard-capped
// opaque (dot-less) tokens at 8KB while the write path (session.go
// SetAccessToken) accepts and chunk-stores them up to config.MaxLength.
// An opaque token between 8KB and MaxLength was therefore written but
// permanently rejected on read ("" -> forced re-auth). The read cap must
// accept whatever the write path accepts.
func TestChunkManager_OpaqueTokenAbove8KReadable(t *testing.T) {
cm := &ChunkManager{}
// > the old 8KB opaque cap, < AccessTokenConfig.MaxLength (100KB).
token := strings.Repeat("A", 9*1024)
if err := cm.validateTokenSize(token, AccessTokenConfig); err != nil {
t.Fatalf("opaque token of %d bytes (within write MaxLength) must be readable, got: %v", len(token), err)
}
}
// TestSharedTransportPool_CleanupResetsCountAndStaysUsable guards the R125
// fix to http_client_pool.go Cleanup: Cleanup canceled the singleton
// cleanup goroutine without restarting it and left clientCount unreset. A
// reused pool then had a permanently-dead cleaner and a stale count
// (corrupting the maxClients soft cap). Cleanup must reset the pool to a
// fully-reusable state.
func TestSharedTransportPool_CleanupResetsCountAndStaysUsable(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
p := &SharedTransportPool{
ctx: ctx,
cancel: cancel,
transports: map[string]*sharedTransport{},
maxConns: 20,
maxClients: 5,
}
cfg1 := HTTPClientConfig{DialTimeout: time.Second}
cfg2 := HTTPClientConfig{DialTimeout: 2 * time.Second}
_ = p.GetOrCreateTransport(cfg1)
_ = p.GetOrCreateTransport(cfg2)
if got := atomic.LoadInt32(&p.clientCount); got != 2 {
t.Fatalf("clientCount before cleanup = %d, want 2", got)
}
p.Cleanup()
if got := atomic.LoadInt32(&p.clientCount); got != 0 {
t.Fatalf("clientCount after Cleanup = %d, want 0 (pool must be reusable at full capacity)", got)
}
// The pool must still hand out transports after Cleanup.
if tr := p.GetOrCreateTransport(cfg2); tr == nil {
t.Fatal("pool must remain usable after Cleanup")
}
}