c3d9fbd8d3
Implementa fase 2 del issue 0016: - rate_limit_middleware: limita por IP (X-Forwarded-For > X-Real-IP > RemoteAddr) - rate_limiter_by_key: middleware configurable con keyFunc custom (API key, user ID...) - Cuando se rechaza responde 429 con HTTPError JSON y headers Retry-After + X-RateLimit-* - Tests con httptest.NewRecorder cubriendo: limite, burst, IPs independientes, XFF prioritario, recarga temporal, JSON body, headers IETF, GC stop idempotente, key vacia salta limit
69 lines
1.7 KiB
Go
69 lines
1.7 KiB
Go
package infra
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestRateLimiterCleanup(t *testing.T) {
|
|
t.Run("purga entries con lastSeen mas antiguo que maxAge", func(t *testing.T) {
|
|
rl := RateLimiterNew(10, 20)
|
|
// Inyectar manualmente una entry vieja
|
|
rl.mu.Lock()
|
|
rl.clients["old"] = &rateLimiterClient{tokens: 5, lastSeen: time.Now().Add(-time.Hour)}
|
|
rl.clients["new"] = &rateLimiterClient{tokens: 5, lastSeen: time.Now()}
|
|
rl.mu.Unlock()
|
|
|
|
stop := RateLimiterCleanup(rl, 10*time.Minute, 30*time.Millisecond)
|
|
defer stop()
|
|
|
|
// Esperar al menos un tick
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
rl.mu.Lock()
|
|
_, oldExists := rl.clients["old"]
|
|
_, newExists := rl.clients["new"]
|
|
rl.mu.Unlock()
|
|
|
|
if oldExists {
|
|
t.Error("entry 'old' no fue purgada")
|
|
}
|
|
if !newExists {
|
|
t.Error("entry 'new' fue purgada incorrectamente")
|
|
}
|
|
})
|
|
|
|
t.Run("no purga entries recientes", func(t *testing.T) {
|
|
rl := RateLimiterNew(10, 20)
|
|
RateLimiterCheck(rl, "fresh")
|
|
|
|
stop := RateLimiterCleanup(rl, 10*time.Minute, 30*time.Millisecond)
|
|
defer stop()
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
rl.mu.Lock()
|
|
_, exists := rl.clients["fresh"]
|
|
rl.mu.Unlock()
|
|
if !exists {
|
|
t.Error("entry reciente fue purgada incorrectamente")
|
|
}
|
|
})
|
|
|
|
t.Run("stop detiene la goroutine sin panic", func(t *testing.T) {
|
|
rl := RateLimiterNew(10, 20)
|
|
stop := RateLimiterCleanup(rl, time.Second, 10*time.Millisecond)
|
|
stop()
|
|
// Si esto no panica, pasa
|
|
time.Sleep(50 * time.Millisecond)
|
|
})
|
|
|
|
t.Run("stop es idempotente", func(t *testing.T) {
|
|
rl := RateLimiterNew(10, 20)
|
|
stop := RateLimiterCleanup(rl, time.Second, 10*time.Millisecond)
|
|
stop()
|
|
stop() // segunda llamada no debe panic
|
|
stop() // tercera tampoco
|
|
})
|
|
}
|