//go:build test package mw import ( "testing" "time" "crussell/clock" ) // ============================================================ // TrustProxyHeaders — exported accessor for the init-time // TRUST_PROXY_HEADERS capture (batch-1 fix) // ============================================================ // TestTrustProxyHeaders_ReflectsPackageVar pins the accessor contract: it // reports the current value of the package-level trustProxyHeaders flag. The // flag is captured once at init from TRUST_PROXY_HEADERS (ratelimit_shared.go) // and cannot be re-read after process start, so the true/false paths are // exercised by toggling the var directly (the test lives in package mw). func TestTrustProxyHeaders_ReflectsPackageVar(t *testing.T) { saved := trustProxyHeaders t.Cleanup(func() { trustProxyHeaders = saved }) trustProxyHeaders = false if TrustProxyHeaders() { t.Error("expected TrustProxyHeaders() == false when trustProxyHeaders is false") } trustProxyHeaders = true if !TrustProxyHeaders() { t.Error("expected TrustProxyHeaders() == true when trustProxyHeaders is true") } } // TestTrustProxyHeaders_DefaultIsFalse verifies the documented default: when // the flag has never been flipped to true (the init-time no-TRUST_PROXY_HEADERS // path resolves to false), the accessor reports false — an origin-exposed // backend must not trust proxy headers by default. func TestTrustProxyHeaders_DefaultIsFalse(t *testing.T) { saved := trustProxyHeaders t.Cleanup(func() { trustProxyHeaders = saved }) trustProxyHeaders = false if TrustProxyHeaders() { t.Error("expected TrustProxyHeaders() to default to false") } } // ============================================================ // RateLimiter.Allow — fixed-window per-key semantics (batch-1 // fix regression: per-key bucketing) // ============================================================ // TestRateLimiter_Allow_RespectsLimit verifies exactly `limit` allowances per // key within the window, then refusals. func TestRateLimiter_Allow_RespectsLimit(t *testing.T) { rl := NewRateLimiter(2, time.Minute) if !rl.Allow("key-a") { t.Fatal("expected first request to be allowed") } if !rl.Allow("key-a") { t.Fatal("expected second request to be allowed") } if rl.Allow("key-a") { t.Error("expected third request to be refused once the limit is hit") } } // TestRateLimiter_Allow_PerKeyBuckets verifies keys are bucketed independently: // exhausting one key must not consume another key's allowance. func TestRateLimiter_Allow_PerKeyBuckets(t *testing.T) { rl := NewRateLimiter(2, time.Minute) for i := 0; i < 2; i++ { if !rl.Allow("busy-key") { t.Fatalf("request %d: expected busy-key to be allowed", i+1) } } if rl.Allow("busy-key") { t.Error("expected busy-key to be exhausted after its limit") } if !rl.Allow("other-key") { t.Error("expected other-key to keep its own allowance") } } // TestRateLimiter_Allow_WindowExpiryPrunesStale verifies timestamps older than // the window no longer count: an Allow after the window is free again. func TestRateLimiter_Allow_WindowExpiryPrunesStale(t *testing.T) { rl := NewRateLimiter(1, time.Minute) // Fill the bucket with a timestamp from before the window started. rl.mu.Lock() rl.requests["key-stale"] = []time.Time{clock.Now().Add(-2 * time.Minute)} rl.mu.Unlock() if !rl.Allow("key-stale") { t.Error("expected an expired entry to be pruned and the request allowed") } rl.mu.RLock() got := len(rl.requests["key-stale"]) rl.mu.RUnlock() if got != 1 { t.Errorf("expected the stale entry to be replaced by the fresh one, got %d entries", got) } } // TestRateLimiter_Allow_BoundaryExactLimit verifies the limit is an exclusive // boundary: the request that makes the count EQUAL the limit is the last one // allowed. func TestRateLimiter_Allow_BoundaryExactLimit(t *testing.T) { rl := NewRateLimiter(3, time.Minute) for i := 0; i < 3; i++ { if !rl.Allow("boundary-key") { t.Fatalf("request %d: expected allowed (count == limit is allowed)", i+1) } } if rl.Allow("boundary-key") { t.Error("expected the count-over-limit request to be refused") } }