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
91 lines
2.5 KiB
Go
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")
|
|
}
|
|
})
|
|
}
|