package handlers import ( "testing" "time" ) func TestRateLimiterNotBlocked(t *testing.T) { rl := newLoginRateLimiter(5, 15*time.Minute) if rl.isBlocked("1.2.3.4") { t.Error("expected new rate limiter to not block any IP") } } func TestRateLimiterBlockedAfterMaxFailures(t *testing.T) { rl := newLoginRateLimiter(5, 15*time.Minute) ip := "10.0.0.1" for i := 0; i < 5; i++ { rl.recordFailure(ip) } if !rl.isBlocked(ip) { t.Error("expected IP to be blocked after 5 failures") } } func TestRateLimiterNotBlockedBeforeMaxFailures(t *testing.T) { rl := newLoginRateLimiter(5, 15*time.Minute) ip := "10.0.0.2" for i := 0; i < 4; i++ { rl.recordFailure(ip) } if rl.isBlocked(ip) { t.Error("expected IP to not be blocked with 4 failures (max is 5)") } } func TestRateLimiterWindowExpires(t *testing.T) { rl := newLoginRateLimiter(3, 50*time.Millisecond) ip := "10.0.0.3" for i := 0; i < 3; i++ { rl.recordFailure(ip) } if !rl.isBlocked(ip) { t.Fatal("expected IP to be blocked after 3 failures") } // Wait for the window to expire time.Sleep(70 * time.Millisecond) if rl.isBlocked(ip) { t.Error("expected IP to be unblocked after window expires") } } func TestRateLimiterDifferentIPsIndependent(t *testing.T) { rl := newLoginRateLimiter(3, 15*time.Minute) ip1 := "10.0.0.1" ip2 := "10.0.0.2" for i := 0; i < 3; i++ { rl.recordFailure(ip1) } if !rl.isBlocked(ip1) { t.Error("expected ip1 to be blocked") } if rl.isBlocked(ip2) { t.Error("expected ip2 to not be blocked") } } func TestRateLimiterWindowCleansOldEntries(t *testing.T) { rl := newLoginRateLimiter(3, 50*time.Millisecond) ip := "10.0.0.4" // Record 2 failures rl.recordFailure(ip) rl.recordFailure(ip) // Wait for window to expire time.Sleep(70 * time.Millisecond) // Record 1 more failure - old entries should be cleaned rl.recordFailure(ip) if rl.isBlocked(ip) { t.Error("expected IP to not be blocked after window cleaned old entries and 1 new failure") } } func TestRateLimiterExactMaxBoundary(t *testing.T) { rl := newLoginRateLimiter(1, 15*time.Minute) ip := "10.0.0.5" if rl.isBlocked(ip) { t.Error("expected IP to not be blocked with 0 failures") } rl.recordFailure(ip) if !rl.isBlocked(ip) { t.Error("expected IP to be blocked after exactly 1 failure (max is 1)") } } func TestRateLimiterMultipleIPsConcurrent(t *testing.T) { rl := newLoginRateLimiter(2, 15*time.Minute) ips := []string{"10.0.1.1", "10.0.1.2", "10.0.1.3"} // Fill up first two IPs for _, ip := range ips[:2] { for i := 0; i < 2; i++ { rl.recordFailure(ip) } } for i, ip := range ips { blocked := rl.isBlocked(ip) if i < 2 && !blocked { t.Errorf("expected %s to be blocked", ip) } if i == 2 && blocked { t.Errorf("expected %s to not be blocked", ip) } } } func TestRateLimiterRecordFailureAfterBlock(t *testing.T) { rl := newLoginRateLimiter(2, 50*time.Millisecond) ip := "10.0.0.6" rl.recordFailure(ip) rl.recordFailure(ip) if !rl.isBlocked(ip) { t.Fatal("expected IP to be blocked") } // Record more failures while blocked rl.recordFailure(ip) rl.recordFailure(ip) if !rl.isBlocked(ip) { t.Error("expected IP to remain blocked with additional failures") } // Wait for window, should be unblocked time.Sleep(70 * time.Millisecond) if rl.isBlocked(ip) { t.Error("expected IP to be unblocked after window expires even with many recorded failures") } }