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,90 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRateLimiterCheck(t *testing.T) {
|
||||
t.Run("primer request siempre allowed con burst-1 remaining", func(t *testing.T) {
|
||||
rl := RateLimiterNew(10, 20)
|
||||
result := RateLimiterCheck(rl, "client-1")
|
||||
if !result.Allowed {
|
||||
t.Error("primer request rechazado")
|
||||
}
|
||||
if result.Remaining != 19 {
|
||||
t.Errorf("Remaining=%d, want 19", result.Remaining)
|
||||
}
|
||||
if result.RetryAfter != 0 {
|
||||
t.Errorf("RetryAfter=%v, want 0", result.RetryAfter)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("consumir todos los tokens bloquea siguiente request", func(t *testing.T) {
|
||||
rl := RateLimiterNew(1, 5)
|
||||
// Consumir los 5 tokens del burst
|
||||
for i := 0; i < 5; i++ {
|
||||
r := RateLimiterCheck(rl, "client-2")
|
||||
if !r.Allowed {
|
||||
t.Fatalf("request %d rechazado, esperaba allowed", i)
|
||||
}
|
||||
}
|
||||
// El sexto deberia ser rechazado
|
||||
r := RateLimiterCheck(rl, "client-2")
|
||||
if r.Allowed {
|
||||
t.Error("request post-burst permitido, esperaba rechazo")
|
||||
}
|
||||
if r.Remaining != 0 {
|
||||
t.Errorf("Remaining=%d, want 0", r.Remaining)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("los tokens se recargan con el paso del tiempo", func(t *testing.T) {
|
||||
rl := RateLimiterNew(100, 2) // 100 tokens/seg = 1 token cada 10ms
|
||||
// Consumir burst
|
||||
RateLimiterCheck(rl, "client-3")
|
||||
RateLimiterCheck(rl, "client-3")
|
||||
// Tercer request rechazado
|
||||
r := RateLimiterCheck(rl, "client-3")
|
||||
if r.Allowed {
|
||||
t.Fatal("tercer request inmediato deberia estar rechazado")
|
||||
}
|
||||
// Esperar suficiente para recargar al menos 1 token
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
r2 := RateLimiterCheck(rl, "client-3")
|
||||
if !r2.Allowed {
|
||||
t.Error("request tras recarga deberia estar permitido")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("retryAfter es positivo cuando se rechaza", func(t *testing.T) {
|
||||
rl := RateLimiterNew(1, 1) // 1 token/seg, burst 1
|
||||
RateLimiterCheck(rl, "client-4")
|
||||
r := RateLimiterCheck(rl, "client-4")
|
||||
if r.Allowed {
|
||||
t.Fatal("segundo request inmediato deberia rechazarse")
|
||||
}
|
||||
if r.RetryAfter <= 0 {
|
||||
t.Errorf("RetryAfter=%v, want > 0", r.RetryAfter)
|
||||
}
|
||||
if r.RetryAfter > 1.5 {
|
||||
t.Errorf("RetryAfter=%v, esperado <= 1s aprox", r.RetryAfter)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("keys distintas tienen buckets independientes", func(t *testing.T) {
|
||||
rl := RateLimiterNew(1, 2)
|
||||
// Consumir todo de A
|
||||
RateLimiterCheck(rl, "A")
|
||||
RateLimiterCheck(rl, "A")
|
||||
ra := RateLimiterCheck(rl, "A")
|
||||
if ra.Allowed {
|
||||
t.Fatal("A deberia estar agotado")
|
||||
}
|
||||
// B sigue intacto
|
||||
rb := RateLimiterCheck(rl, "B")
|
||||
if !rb.Allowed {
|
||||
t.Error("B deberia tener tokens propios")
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user