mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-21 04:57:48 +00:00
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
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user