package auth import ( "testing" "time" ) func TestRateLimiterExhaustionAndRefill(t *testing.T) { rl := NewRateLimiter(10, 15*time.Minute) now := time.Date(2026, 6, 12, 12, 0, 0, 0, time.UTC) rl.now = func() time.Time { return now } for i := 0; i < 10; i++ { if !rl.Allow("ip|a@b.c") { t.Fatalf("attempt %d should be allowed", i+1) } } if rl.Allow("ip|a@b.c") { t.Fatal("11th attempt must be blocked") } // Other keys are unaffected. if !rl.Allow("ip|other@b.c") { t.Fatal("different key must have its own bucket") } // After 1.5 minutes one token has refilled (10 per 15 min). now = now.Add(90 * time.Second) if !rl.Allow("ip|a@b.c") { t.Fatal("one attempt should have refilled") } if rl.Allow("ip|a@b.c") { t.Fatal("bucket must be empty again") } // A full window refills completely, capped at capacity. now = now.Add(time.Hour) for i := 0; i < 10; i++ { if !rl.Allow("ip|a@b.c") { t.Fatalf("post-refill attempt %d should be allowed", i+1) } } if rl.Allow("ip|a@b.c") { t.Fatal("refill must cap at capacity") } } func TestRateLimiterGC(t *testing.T) { rl := NewRateLimiter(10, 15*time.Minute) now := time.Date(2026, 6, 12, 12, 0, 0, 0, time.UTC) rl.now = func() time.Time { return now } rl.Allow("stale") now = now.Add(16 * time.Minute) rl.Allow("fresh") // triggers gc rl.mu.Lock() _, staleExists := rl.buckets["stale"] rl.mu.Unlock() if staleExists { t.Fatal("idle bucket should have been swept") } }