Files
Crussell/backend/mw/ratelimit_test.go
T
popertotsandSisyphus e4b9003439 refactor(handlers): migrate remaining backend handlers to clock.Now() and transaction patterns
Apply clock.Now() migration, transaction wrapping, and minor refactors across admin, scheduling, today, user, auth handler, notifications, webhooks, services, portfolio, ratelimit, testutils, and main.go.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-24 23:43:50 +01:00

138 lines
3.4 KiB
Go

//go:build test && !dev
// +build test,!dev
package mw
import (
"sync"
"testing"
"time"
"crussell/clock"
)
// TestProgressiveRateLimiter_SingleRequest verifies no delay for first request.
func TestProgressiveRateLimiter_SingleRequest(t *testing.T) {
prl := NewProgressiveRateLimiter()
delay := prl.Check("192.168.1.1")
if delay != 0 {
t.Errorf("expected 0 delay for first request, got %d", delay)
}
}
// TestProgressiveRateLimiter_BurstAllowsPageLoad verifies 15 requests in 5s are OK.
func TestProgressiveRateLimiter_BurstAllowsPageLoad(t *testing.T) {
prl := NewProgressiveRateLimiter()
ip := "192.168.1.1"
for i := 0; i < 15; i++ {
delay := prl.Check(ip)
if delay != 0 {
t.Errorf("expected 0 delay for request %d (within burst), got %d", i+1, delay)
}
}
}
// TestProgressiveRateLimiter_ExcessBurstDelays verifies 35+ requests in 5s get delayed.
func TestProgressiveRateLimiter_ExcessBurstDelays(t *testing.T) {
prl := NewProgressiveRateLimiter()
ip := "192.168.1.1"
delayed := false
for i := 0; i < 35; i++ {
delay := prl.Check(ip)
if delay > 0 {
delayed = true
}
}
if !delayed {
t.Error("expected at least one delay after 35 burst requests")
}
}
// TestProgressiveRateLimiter_SustainedAllowsNormal verifies 30 requests
// spread over 60 seconds are not delayed.
func TestProgressiveRateLimiter_SustainedAllowsNormal(t *testing.T) {
prl := NewProgressiveRateLimiter()
ip := "192.168.1.2"
for i := 0; i < 30; i++ {
prl.mu.Lock()
state, exists := prl.requests[ip]
if !exists {
prl.requests[ip] = &ipProgressiveState{
timestamps: []time.Time{clock.Now().Add(-time.Duration(60-i*2) * time.Second)},
}
prl.mu.Unlock()
continue
}
state.timestamps = append(state.timestamps, clock.Now().Add(-time.Duration(60-i*2)*time.Second))
prl.mu.Unlock()
}
delay := prl.Check(ip)
if delay != 0 {
t.Errorf("expected 0 delay for 30 sustained requests over 60s, got %d", delay)
}
}
// TestProgressiveRateLimiter_ExcessSustainedDelays verifies 130+ requests
// in 60s triggers delay.
func TestProgressiveRateLimiter_ExcessSustainedDelays(t *testing.T) {
prl := NewProgressiveRateLimiter()
ip := "192.168.1.3"
now := clock.Now()
prl.mu.Lock()
state := &ipProgressiveState{timestamps: make([]time.Time, 130)}
for i := 0; i < 130; i++ {
state.timestamps[i] = now.Add(-time.Duration(60-i/3) * time.Second)
}
prl.requests[ip] = state
prl.mu.Unlock()
delay := prl.Check(ip)
if delay == 0 {
t.Error("expected delay > 0 for 130 sustained requests")
}
}
// TestProgressiveRateLimiter_DifferentIPs verifies rate limiter
// tracks IPs independently.
func TestProgressiveRateLimiter_DifferentIPs(t *testing.T) {
prl := NewProgressiveRateLimiter()
for i := 0; i < 40; i++ {
prl.Check("10.0.0.1")
}
delay := prl.Check("10.0.0.2")
if delay != 0 {
t.Errorf("expected 0 delay for separate IP, got %d", delay)
}
}
// TestProgressiveRateLimiter_CleanupRemovesStaleEntries verifies that
// IPs with no activity for 60s are cleaned up.
func TestProgressiveRateLimiter_CleanupRemovesStaleEntries(t *testing.T) {
prl := NewProgressiveRateLimiter()
prl.mu.Lock()
prl.requests["stale-ip"] = &ipProgressiveState{
timestamps: []time.Time{clock.Now().Add(-120 * time.Second)},
}
prl.mu.Unlock()
prl.cleanup()
prl.mu.RLock()
_, exists := prl.requests["stale-ip"]
prl.mu.RUnlock()
if exists {
t.Error("expected stale IP to be cleaned up")
}
}
var _ = sync.Mutex{}