feat(infra): rate limit middlewares HTTP por IP y por key + tests
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
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
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
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user