package auth import ( "net/http" "net/http/httptest" "testing" "time" ) func TestRateLimiter_AllowUnderLimit(t *testing.T) { rl := NewRateLimiter(5, time.Minute) for i := 0; i < 5; i++ { if !rl.Allow("192.168.1.1") { t.Fatalf("request %d should be allowed", i+1) } } } func TestRateLimiter_BlockOverLimit(t *testing.T) { rl := NewRateLimiter(5, time.Minute) for i := 0; i < 5; i++ { rl.Allow("192.168.1.1") } if rl.Allow("192.168.1.1") { t.Fatal("6th request should be blocked") } } func TestRateLimiter_DifferentIPs(t *testing.T) { rl := NewRateLimiter(2, time.Minute) rl.Allow("10.0.0.1") rl.Allow("10.0.0.1") // IP 1 is exhausted. if rl.Allow("10.0.0.1") { t.Fatal("IP 10.0.0.1 should be blocked") } // IP 2 should still be allowed. if !rl.Allow("10.0.0.2") { t.Fatal("IP 10.0.0.2 should be allowed") } } func TestRateLimiter_WindowExpiry(t *testing.T) { rl := NewRateLimiter(2, 50*time.Millisecond) rl.Allow("10.0.0.1") rl.Allow("10.0.0.1") if rl.Allow("10.0.0.1") { t.Fatal("should be blocked before window expires") } time.Sleep(60 * time.Millisecond) if !rl.Allow("10.0.0.1") { t.Fatal("should be allowed after window expires") } } func TestRateLimiter_Reset(t *testing.T) { rl := NewRateLimiter(2, time.Minute) rl.Allow("10.0.0.1") rl.Allow("10.0.0.1") if rl.Allow("10.0.0.1") { t.Fatal("should be blocked") } rl.Reset("10.0.0.1") if !rl.Allow("10.0.0.1") { t.Fatal("should be allowed after reset") } } func TestRateLimitMiddleware_Returns429(t *testing.T) { rl := NewRateLimiter(1, time.Minute) handler := RateLimitMiddleware(rl, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) // First request — allowed. req1 := httptest.NewRequest("POST", "/api/auth/login", nil) req1.RemoteAddr = "192.168.1.1:12345" w1 := httptest.NewRecorder() handler(w1, req1) if w1.Code != http.StatusOK { t.Fatalf("first request: got %d, want 200", w1.Code) } // Second request — blocked. req2 := httptest.NewRequest("POST", "/api/auth/login", nil) req2.RemoteAddr = "192.168.1.1:12346" w2 := httptest.NewRecorder() handler(w2, req2) if w2.Code != http.StatusTooManyRequests { t.Fatalf("second request: got %d, want 429", w2.Code) } if w2.Header().Get("Retry-After") != "60" { t.Fatal("missing Retry-After header") } }