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
68 lines
2.1 KiB
Go
68 lines
2.1 KiB
Go
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"))
|
|
}
|
|
})
|
|
}
|