mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-12 04:04:26 +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
139 lines
5.5 KiB
Go
139 lines
5.5 KiB
Go
package traefikoidc
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"net/http/httptest"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestRefreshCoordinator_IsUnderMemoryPressureUsesOwnThreshold guards the R122
|
|
// fix to refresh_coordinator.go: isUnderMemoryPressure consulted the GLOBAL
|
|
// memory monitor's alert level (driven by the shared 256MB "High" heap
|
|
// threshold, and only populated once real sampling runs) instead of the
|
|
// coordinator's OWN MemoryPressureThresholdMB. That meant (a) the operator's
|
|
// configured threshold was ignored entirely, and (b) in a shared process
|
|
// where real heap crosses 256MB every request was silently denied. The gate
|
|
// must compare current heap against the coordinator's own threshold.
|
|
func TestRefreshCoordinator_IsUnderMemoryPressureUsesOwnThreshold(t *testing.T) {
|
|
// Pin the global monitor's stats to a deterministic 3MB heap for the
|
|
// duration of this test (restored below). The original test assumed
|
|
// "any real heap exceeds 1MB", which broke under -shuffle when the
|
|
// real heap was below the threshold (GetCurrentStats returns the
|
|
// global monitor's cached, possibly-stale snapshot) — an intermittent
|
|
// CI failure with no production cause (R196).
|
|
mm := GetGlobalMemoryMonitor()
|
|
mm.mu.Lock()
|
|
old := mm.lastStats
|
|
mm.lastStats = &MemoryStats{HeapAllocBytes: 3 * (1 << 20)} // 3MB
|
|
mm.mu.Unlock()
|
|
t.Cleanup(func() {
|
|
mm.mu.Lock()
|
|
mm.lastStats = old
|
|
mm.mu.Unlock()
|
|
})
|
|
|
|
// Threshold 1MB: pinned heap (3MB) exceeds it -> under pressure.
|
|
cfg := DefaultRefreshCoordinatorConfig()
|
|
cfg.MemoryPressureThresholdMB = 1
|
|
rc := NewRefreshCoordinator(cfg, GetSingletonNoOpLogger())
|
|
if !rc.isUnderMemoryPressure() {
|
|
t.Fatal("coordinator with a 1MB threshold must report under memory pressure for a 3MB heap")
|
|
}
|
|
|
|
// Threshold 1TB: never under pressure.
|
|
cfg2 := DefaultRefreshCoordinatorConfig()
|
|
cfg2.MemoryPressureThresholdMB = 1 << 20 // 1,048,576 MB
|
|
rc2 := NewRefreshCoordinator(cfg2, GetSingletonNoOpLogger())
|
|
if rc2.isUnderMemoryPressure() {
|
|
t.Fatal("coordinator with a ~1TB threshold must not report under memory pressure")
|
|
}
|
|
}
|
|
|
|
// TestRegisterBackgroundTask_ReplacesNeverStartedTask guards the R122 fix to
|
|
// singleton_resources.go: RegisterBackgroundTask kept an existing task when
|
|
// stopped==0, but a task whose startOnce was consumed without ever starting
|
|
// (Start rejected by the circuit breaker: started==0, stopped==0) has
|
|
// stopped==0 too, so it was kept and StartBackgroundTask became a permanent
|
|
// no-op (Start is sync.Once-guarded), leaving the singleton task dead until
|
|
// restart. The keep-condition must require started==1.
|
|
func TestRegisterBackgroundTask_ReplacesNeverStartedTask(t *testing.T) {
|
|
rm := &ResourceManager{
|
|
tasks: map[string]*BackgroundTask{},
|
|
logger: GetSingletonNoOpLogger(),
|
|
}
|
|
const name = "replace-never-started"
|
|
|
|
// A task in the never-started state (started==0, stopped==0): its
|
|
// Start was consumed via a circuit-breaker rejection. Re-registering
|
|
// must REPLACE it so a later Start can actually run it.
|
|
dead := NewBackgroundTask(name, time.Hour, func() {}, rm.logger, &rm.wg)
|
|
atomic.StoreInt32(&dead.started, 0)
|
|
atomic.StoreInt32(&dead.stopped, 0)
|
|
rm.tasks[name] = dead
|
|
|
|
if err := rm.RegisterBackgroundTask(name, time.Hour, func() {}); err != nil {
|
|
t.Fatalf("register: %v", err)
|
|
}
|
|
if rm.tasks[name] == dead {
|
|
t.Fatal("a never-started task (started==0, stopped==0) must be REPLACED, otherwise Start is a permanent no-op")
|
|
}
|
|
|
|
// An actively running task (started==1, stopped==0) must be kept.
|
|
running := NewBackgroundTask("running", time.Hour, func() {}, rm.logger, &rm.wg)
|
|
atomic.StoreInt32(&running.started, 1)
|
|
atomic.StoreInt32(&running.stopped, 0)
|
|
rm.tasks["running"] = running
|
|
|
|
if err := rm.RegisterBackgroundTask("running", time.Hour, func() {}); err != nil {
|
|
t.Fatalf("register running: %v", err)
|
|
}
|
|
if rm.tasks["running"] != running {
|
|
t.Fatal("an actively running task must be kept (idempotent re-registration)")
|
|
}
|
|
}
|
|
|
|
// TestSetAccessToken_PreservesOldTokenOnChunkAbort guards the R122 fix to
|
|
// session.go: the chunked-write branch of SetAccessToken cleared the
|
|
// existing token (accessSession.Values["token"] and accessTokenChunks)
|
|
// BEFORE validating the new token's chunks. On any abort (here: >50
|
|
// chunks, since maxCookieSize=1400 and the token crosses 50*1400=
|
|
// 70KB with compression still < 100KB) the previous valid access token
|
|
// was left nowhere, GetAccessToken returned "", and the user was forced
|
|
// to re-authenticate. The clear now happens only after validation.
|
|
func TestSetAccessToken_PreservesOldTokenOnChunkAbort(t *testing.T) {
|
|
sm, err := NewSessionManager("test-encryption-key-32-characters", false, "", "", 0, NewLogger("error"))
|
|
if err != nil {
|
|
t.Fatalf("session manager: %v", err)
|
|
}
|
|
req := httptest.NewRequest("GET", "/", nil)
|
|
sess, err := sm.GetSession(req)
|
|
if err != nil {
|
|
t.Fatalf("get session: %v", err)
|
|
}
|
|
t.Cleanup(sess.returnToPoolSafely)
|
|
|
|
const small = "smallaccesstokenvaluevaluevalue"
|
|
sess.SetAccessToken(small)
|
|
if got := sess.GetAccessToken(); got != small {
|
|
t.Fatalf("precondition: small token must be retrievable, got %q", got)
|
|
}
|
|
|
|
// Incompressible payload large enough that compressed size lands
|
|
// between 50*maxCookieSize (70KB) and the 100KB cap, so the chunked
|
|
// branch splits into >50 chunks and aborts after too-many-chunks.
|
|
blob := make([]byte, 60*1024)
|
|
if _, err := rand.Read(blob); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bigToken := base64.StdEncoding.EncodeToString(blob)
|
|
|
|
sess.SetAccessToken(bigToken)
|
|
|
|
if got := sess.GetAccessToken(); got != small {
|
|
t.Fatalf("old access token must be preserved when storing a new oversized token fails, got %q", got)
|
|
}
|
|
}
|