mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-17 04:37:48 +00:00
- 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
101 lines
3.8 KiB
Go
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")
|
|
}
|
|
}
|