Files
fn_registry/functions/infra/rate_limiter_check_test.go
T
egutierrez c3d9fbd8d3 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
2026-04-18 17:14:25 +02:00

91 lines
2.5 KiB
Go

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")
}
})
}