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,67 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRateLimitHeaders(t *testing.T) {
|
||||
now := time.Now()
|
||||
|
||||
t.Run("setea X-RateLimit-Limit con el burst", func(t *testing.T) {
|
||||
result := RateLimitResult{Allowed: true, Remaining: 5, ResetAt: now}
|
||||
h := RateLimitHeaders(result, 20)
|
||||
if h.Get("X-RateLimit-Limit") != "20" {
|
||||
t.Errorf("X-RateLimit-Limit=%q, want 20", h.Get("X-RateLimit-Limit"))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("setea X-RateLimit-Remaining con result.Remaining", func(t *testing.T) {
|
||||
result := RateLimitResult{Allowed: true, Remaining: 7, ResetAt: now}
|
||||
h := RateLimitHeaders(result, 10)
|
||||
if h.Get("X-RateLimit-Remaining") != "7" {
|
||||
t.Errorf("X-RateLimit-Remaining=%q, want 7", h.Get("X-RateLimit-Remaining"))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("setea X-RateLimit-Reset como timestamp unix", func(t *testing.T) {
|
||||
future := now.Add(30 * time.Second)
|
||||
result := RateLimitResult{Allowed: true, Remaining: 1, ResetAt: future}
|
||||
h := RateLimitHeaders(result, 10)
|
||||
got := h.Get("X-RateLimit-Reset")
|
||||
want := strconv.FormatInt(future.Unix(), 10)
|
||||
if got != want {
|
||||
t.Errorf("X-RateLimit-Reset=%q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("incluye Retry-After cuando no esta permitido", func(t *testing.T) {
|
||||
result := RateLimitResult{Allowed: false, Remaining: 0, ResetAt: now, RetryAfter: 2.3}
|
||||
h := RateLimitHeaders(result, 10)
|
||||
if h.Get("Retry-After") == "" {
|
||||
t.Fatal("Retry-After deberia estar seteado")
|
||||
}
|
||||
// Debe redondear hacia arriba
|
||||
ra, _ := strconv.Atoi(h.Get("Retry-After"))
|
||||
if ra != 3 {
|
||||
t.Errorf("Retry-After=%d, want 3 (ceil de 2.3)", ra)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Retry-After minimo es 1", func(t *testing.T) {
|
||||
result := RateLimitResult{Allowed: false, Remaining: 0, ResetAt: now, RetryAfter: 0.1}
|
||||
h := RateLimitHeaders(result, 10)
|
||||
if h.Get("Retry-After") != "1" {
|
||||
t.Errorf("Retry-After=%q, want 1 (minimo)", h.Get("Retry-After"))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no incluye Retry-After cuando esta permitido", func(t *testing.T) {
|
||||
result := RateLimitResult{Allowed: true, Remaining: 5, ResetAt: now}
|
||||
h := RateLimitHeaders(result, 10)
|
||||
if h.Get("Retry-After") != "" {
|
||||
t.Errorf("Retry-After=%q, want vacio", h.Get("Retry-After"))
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user