Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
539 lines
17 KiB
Go
539 lines
17 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/descrybe/descrybe-v2/apps/api/internal/config"
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
func TestRateLimitAuthBlocksBurst(t *testing.T) {
|
|
t.Parallel()
|
|
s := &Server{Config: config.Config{}}
|
|
h := s.RateLimitAuth(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
|
|
var saw429 bool
|
|
for i := 0; i < authLoginRPM+5; i++ {
|
|
req := httptest.NewRequest(http.MethodPost, "/api/auth/login", nil)
|
|
req.RemoteAddr = "203.0.113.10:12345"
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
if rec.Code == http.StatusTooManyRequests {
|
|
saw429 = true
|
|
break
|
|
}
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Fatalf("unexpected status %d", rec.Code)
|
|
}
|
|
}
|
|
if !saw429 {
|
|
t.Fatal("expected 429 after auth burst")
|
|
}
|
|
}
|
|
|
|
func TestRateLimitAuthRegisterStricterThanLogin(t *testing.T) {
|
|
t.Parallel()
|
|
s := &Server{Config: config.Config{}}
|
|
h := s.RateLimitAuth(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
var saw429 bool
|
|
for i := 0; i < authRegisterRPM+3; i++ {
|
|
req := httptest.NewRequest(http.MethodPost, "/api/auth/register", nil)
|
|
req.RemoteAddr = "203.0.113.20:12345"
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
if rec.Code == http.StatusTooManyRequests {
|
|
saw429 = true
|
|
break
|
|
}
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Fatalf("unexpected status %d", rec.Code)
|
|
}
|
|
}
|
|
if !saw429 {
|
|
t.Fatal("expected 429 after register burst")
|
|
}
|
|
// Login budget is independent — register exhaust must not block login.
|
|
login := httptest.NewRequest(http.MethodPost, "/api/auth/login", nil)
|
|
login.RemoteAddr = "203.0.113.20:12345"
|
|
recLogin := httptest.NewRecorder()
|
|
h.ServeHTTP(recLogin, login)
|
|
if recLogin.Code != http.StatusNoContent {
|
|
t.Fatalf("login should use separate bucket, got %d", recLogin.Code)
|
|
}
|
|
}
|
|
|
|
func TestRateLimitAuthIncludesSalesContact(t *testing.T) {
|
|
t.Parallel()
|
|
s := &Server{Config: config.Config{}}
|
|
h := s.RateLimitAuth(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
var saw429 bool
|
|
for i := 0; i < authLoginRPM+3; i++ {
|
|
req := httptest.NewRequest(http.MethodPost, "/api/sales/contact", nil)
|
|
req.RemoteAddr = "203.0.113.21:12345"
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
if rec.Code == http.StatusTooManyRequests {
|
|
saw429 = true
|
|
break
|
|
}
|
|
}
|
|
if !saw429 {
|
|
t.Fatal("expected 429 after sales contact burst")
|
|
}
|
|
}
|
|
|
|
func TestRateLimitAuthSkipsSafeMethods(t *testing.T) {
|
|
t.Parallel()
|
|
s := &Server{Config: config.Config{}}
|
|
h := s.RateLimitAuth(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
for i := 0; i < 30; i++ {
|
|
req := httptest.NewRequest(http.MethodGet, "/api/auth/me", nil)
|
|
req.RemoteAddr = "203.0.113.11:12345"
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Fatalf("GET should not be rate-limited, got %d", rec.Code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRateLimitAdminPlanFeaturesBlocksBurst(t *testing.T) {
|
|
t.Parallel()
|
|
s := &Server{Config: config.Config{}}
|
|
uid := uuid.MustParse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb")
|
|
h := s.RateLimitAdminPlanFeatures(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
var saw429 bool
|
|
for i := 0; i < adminPlanFeaturesGetRPM+5; i++ {
|
|
req := httptest.NewRequest(http.MethodGet, "/api/admin/plans/1/features", nil)
|
|
req = req.WithContext(context.WithValue(req.Context(), ctxUserID, uid))
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
if rec.Code == http.StatusTooManyRequests {
|
|
saw429 = true
|
|
break
|
|
}
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Fatalf("unexpected status %d", rec.Code)
|
|
}
|
|
}
|
|
if !saw429 {
|
|
t.Fatal("expected 429 after plan-features GET burst")
|
|
}
|
|
}
|
|
|
|
func TestRateLimitAdminAnalyticsBlocksBurst(t *testing.T) {
|
|
t.Parallel()
|
|
s := &Server{Config: config.Config{}}
|
|
uid := uuid.MustParse("cccccccc-cccc-cccc-cccc-cccccccccccc")
|
|
h := s.RateLimitAdminAnalytics(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
var saw429 bool
|
|
for i := 0; i < adminAnalyticsGetRPM+5; i++ {
|
|
req := httptest.NewRequest(http.MethodGet, "/api/admin/analytics", nil)
|
|
req = req.WithContext(context.WithValue(req.Context(), ctxUserID, uid))
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
if rec.Code == http.StatusTooManyRequests {
|
|
saw429 = true
|
|
break
|
|
}
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Fatalf("unexpected status %d", rec.Code)
|
|
}
|
|
}
|
|
if !saw429 {
|
|
t.Fatal("expected 429 after analytics GET burst")
|
|
}
|
|
}
|
|
|
|
func TestIsAdminAnalyticsGet(t *testing.T) {
|
|
t.Parallel()
|
|
cases := []struct {
|
|
method string
|
|
path string
|
|
want bool
|
|
}{
|
|
{http.MethodGet, "/api/admin/analytics", true},
|
|
{http.MethodGet, "/api/admin/analytics/", true},
|
|
{http.MethodGet, "/api/admin/diagnostics", true},
|
|
{http.MethodGet, "/api/admin/diagnostics/", true},
|
|
{http.MethodPost, "/api/admin/analytics", false},
|
|
{http.MethodGet, "/api/admin/readiness", false},
|
|
{http.MethodGet, "/api/admin/jobs", false},
|
|
}
|
|
for _, tc := range cases {
|
|
req := httptest.NewRequest(tc.method, tc.path, nil)
|
|
if got := isAdminAnalyticsGet(req); got != tc.want {
|
|
t.Fatalf("%s %s: got %v want %v", tc.method, tc.path, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIsAIOrMailProbePOST(t *testing.T) {
|
|
t.Parallel()
|
|
cases := []struct {
|
|
method string
|
|
path string
|
|
want bool
|
|
}{
|
|
{http.MethodPost, "/api/integrations/ai/test", true},
|
|
{http.MethodPost, "/api/admin/settings/mail/test", true},
|
|
{http.MethodPost, "/api/admin/settings/ai-roles/support/test", true},
|
|
{http.MethodGet, "/api/integrations/ai/test", false},
|
|
{http.MethodPost, "/api/admin/settings", false},
|
|
}
|
|
for _, tc := range cases {
|
|
req := httptest.NewRequest(tc.method, tc.path, nil)
|
|
if got := isAIOrMailProbePOST(req); got != tc.want {
|
|
t.Fatalf("%s %s: got %v want %v", tc.method, tc.path, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRateLimitAIProbesBlocksBurst(t *testing.T) {
|
|
t.Parallel()
|
|
s := &Server{Config: config.Config{}}
|
|
uid := uuid.MustParse("dddddddd-dddd-dddd-dddd-dddddddddddd")
|
|
h := s.RateLimitAIProbes(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
var saw429 bool
|
|
for i := 0; i < aiProbeRPM+5; i++ {
|
|
req := httptest.NewRequest(http.MethodPost, "/api/integrations/ai/test", nil)
|
|
req = req.WithContext(context.WithValue(req.Context(), ctxUserID, uid))
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
if rec.Code == http.StatusTooManyRequests {
|
|
saw429 = true
|
|
break
|
|
}
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Fatalf("unexpected status %d", rec.Code)
|
|
}
|
|
}
|
|
if !saw429 {
|
|
t.Fatal("expected 429 after AI probe burst")
|
|
}
|
|
}
|
|
|
|
func TestIsAdminPlanFeaturesGet(t *testing.T) {
|
|
t.Parallel()
|
|
cases := []struct {
|
|
method string
|
|
path string
|
|
want bool
|
|
}{
|
|
{http.MethodGet, "/api/admin/plans/1/features", true},
|
|
{http.MethodGet, "/api/admin/plans/99/features/", true},
|
|
{http.MethodPut, "/api/admin/plans/1/features", false},
|
|
{http.MethodGet, "/api/admin/plans", false},
|
|
{http.MethodGet, "/api/admin/feature-gates", false},
|
|
{http.MethodPost, "/api/admin/plans/1/features/enable-all", false},
|
|
}
|
|
for _, tc := range cases {
|
|
req := httptest.NewRequest(tc.method, tc.path, nil)
|
|
if got := isAdminPlanFeaturesGet(req); got != tc.want {
|
|
t.Fatalf("%s %s: got %v want %v", tc.method, tc.path, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIsHeavyFeedOrProcessMutation(t *testing.T) {
|
|
t.Parallel()
|
|
cases := []struct {
|
|
method string
|
|
path string
|
|
want bool
|
|
}{
|
|
{http.MethodPost, "/api/v1/feeds/abc/sync", true},
|
|
{http.MethodPost, "/api/feeds/abc/sync", true},
|
|
{http.MethodPost, "/api/v1/feeds/abc/extract-schema", true},
|
|
{http.MethodPost, "/api/feeds/abc/extract-schema", true},
|
|
{http.MethodPost, "/api/v1/feeds/abc/sync-process-sample", true},
|
|
{http.MethodPost, "/api/v1/process", true},
|
|
{http.MethodPost, "/api/processing/jobs", true},
|
|
{http.MethodPost, "/api/processing/jobs/abc/retry", true},
|
|
{http.MethodPost, "/api/v1/process/abc/retry", true},
|
|
{http.MethodPost, "/api/export-feeds/abc/generate", true},
|
|
{http.MethodPost, "/api/v1/export-feeds/abc/generate", true},
|
|
{http.MethodPost, "/api/export-feeds/abc/export-products", true},
|
|
{http.MethodPost, "/api/v1/export-feeds/abc/export-products", true},
|
|
{http.MethodGet, "/api/v1/feeds/abc/sync", false},
|
|
{http.MethodPost, "/api/v1/feeds/abc/mappings", false},
|
|
{http.MethodPost, "/api/integrations/shopify/sync", false},
|
|
{http.MethodPost, "/api/woocommerce/sync", false},
|
|
{http.MethodPost, "/api/processing/jobs/abc/cancel", false},
|
|
{http.MethodPost, "/api/campaigns/abc/generate", false},
|
|
}
|
|
for _, tc := range cases {
|
|
req := httptest.NewRequest(tc.method, tc.path, nil)
|
|
if got := isHeavyFeedOrProcessMutation(req); got != tc.want {
|
|
t.Fatalf("%s %s: got %v want %v", tc.method, tc.path, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRateLimitV1ProcessBlocksBurst(t *testing.T) {
|
|
t.Parallel()
|
|
s := &Server{Config: config.Config{}}
|
|
h := s.RateLimitV1Process(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
var saw429 bool
|
|
for i := 0; i < heavyMutationRPM+5; i++ {
|
|
req := httptest.NewRequest(http.MethodPost, "/api/export-feeds/abc/generate", nil)
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
if rec.Code == http.StatusTooManyRequests {
|
|
saw429 = true
|
|
if rec.Header().Get("Retry-After") == "" {
|
|
t.Fatal("expected Retry-After on 429")
|
|
}
|
|
if rec.Header().Get("RateLimit") == "" {
|
|
t.Fatal("expected RateLimit on 429")
|
|
}
|
|
if rec.Header().Get("RateLimit-Policy") == "" {
|
|
t.Fatal("expected RateLimit-Policy on 429")
|
|
}
|
|
break
|
|
}
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Fatalf("unexpected status %d", rec.Code)
|
|
}
|
|
}
|
|
if !saw429 {
|
|
t.Fatal("expected 429 after heavy mutation burst")
|
|
}
|
|
}
|
|
|
|
func TestRateLimitEffectiveCap(t *testing.T) {
|
|
t.Parallel()
|
|
if got := rateLimitEffectiveCap(30, 1); got != 30 {
|
|
t.Fatalf("replicas=1 want 30 got %d", got)
|
|
}
|
|
if got := rateLimitEffectiveCap(30, 3); got != 10 {
|
|
t.Fatalf("replicas=3 want 10 got %d", got)
|
|
}
|
|
if got := rateLimitEffectiveCap(30, 7); got != 5 {
|
|
t.Fatalf("replicas=7 want ceil(30/7)=5 got %d", got)
|
|
}
|
|
if got := rateLimitEffectiveCap(0, 2); got != 1 {
|
|
t.Fatalf("base<=0 want 1 got %d", got)
|
|
}
|
|
if got := rateLimitEffectiveCap(10, 0); got != 10 {
|
|
t.Fatalf("replicas<=1 want base got %d", got)
|
|
}
|
|
}
|
|
|
|
func TestRateLimitAuthRespectsReplicas(t *testing.T) {
|
|
t.Parallel()
|
|
// ceil(authRegisterRPM/5)=1 → second register must 429
|
|
s := &Server{Config: config.Config{RateLimitReplicas: authRegisterRPM}}
|
|
h := s.RateLimitAuth(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
req1 := httptest.NewRequest(http.MethodPost, "/api/auth/register", nil)
|
|
req1.RemoteAddr = "203.0.113.50:40000"
|
|
rec1 := httptest.NewRecorder()
|
|
h.ServeHTTP(rec1, req1)
|
|
if rec1.Code != http.StatusNoContent {
|
|
t.Fatalf("first status=%d", rec1.Code)
|
|
}
|
|
req2 := httptest.NewRequest(http.MethodPost, "/api/auth/register", nil)
|
|
req2.RemoteAddr = "203.0.113.50:40000"
|
|
rec2 := httptest.NewRecorder()
|
|
h.ServeHTTP(rec2, req2)
|
|
if rec2.Code != http.StatusTooManyRequests {
|
|
t.Fatalf("second status=%d want 429", rec2.Code)
|
|
}
|
|
}
|
|
|
|
func TestRateLimitV1ProcessRespectsReplicas(t *testing.T) {
|
|
t.Parallel()
|
|
s := &Server{Config: config.Config{RateLimitReplicas: heavyMutationRPM}}
|
|
h := s.RateLimitV1Process(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
// ceil(30/30)=1 → second request must 429
|
|
req1 := httptest.NewRequest(http.MethodPost, "/api/v1/process", nil)
|
|
rec1 := httptest.NewRecorder()
|
|
h.ServeHTTP(rec1, req1)
|
|
if rec1.Code != http.StatusNoContent {
|
|
t.Fatalf("first status=%d", rec1.Code)
|
|
}
|
|
req2 := httptest.NewRequest(http.MethodPost, "/api/v1/process", nil)
|
|
rec2 := httptest.NewRecorder()
|
|
h.ServeHTTP(rec2, req2)
|
|
if rec2.Code != http.StatusTooManyRequests {
|
|
t.Fatalf("second status=%d want 429", rec2.Code)
|
|
}
|
|
}
|
|
|
|
func TestRateLimitV1ProcessSeparateCompanies(t *testing.T) {
|
|
t.Parallel()
|
|
s := &Server{Config: config.Config{}}
|
|
h := s.RateLimitV1Process(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
cidA := uuid.MustParse("11111111-1111-1111-1111-111111111111")
|
|
cidB := uuid.MustParse("22222222-2222-2222-2222-222222222222")
|
|
for i := 0; i < heavyMutationRPM; i++ {
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/process", nil)
|
|
req = req.WithContext(context.WithValue(req.Context(), ctxCompanyID, cidA))
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Fatalf("company A request %d: status %d", i, rec.Code)
|
|
}
|
|
}
|
|
blocked := httptest.NewRequest(http.MethodPost, "/api/v1/process", nil)
|
|
blocked = blocked.WithContext(context.WithValue(blocked.Context(), ctxCompanyID, cidA))
|
|
recBlocked := httptest.NewRecorder()
|
|
h.ServeHTTP(recBlocked, blocked)
|
|
if recBlocked.Code != http.StatusTooManyRequests {
|
|
t.Fatalf("company A should be limited, got %d", recBlocked.Code)
|
|
}
|
|
okB := httptest.NewRequest(http.MethodPost, "/api/export-feeds/abc/generate", nil)
|
|
okB = okB.WithContext(context.WithValue(okB.Context(), ctxCompanyID, cidB))
|
|
recB := httptest.NewRecorder()
|
|
h.ServeHTTP(recB, okB)
|
|
if recB.Code != http.StatusNoContent {
|
|
t.Fatalf("company B should not share A budget, got %d", recB.Code)
|
|
}
|
|
}
|
|
|
|
func TestRateLimitPublicBlocksBurst(t *testing.T) {
|
|
t.Parallel()
|
|
s := &Server{Config: config.Config{}}
|
|
h := s.RateLimitPublic(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
var saw429 bool
|
|
for i := 0; i < 40; i++ {
|
|
req := httptest.NewRequest(http.MethodGet, "/api/public/unsubscribe", nil)
|
|
req.RemoteAddr = "203.0.113.50:12345"
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
if rec.Code == http.StatusTooManyRequests {
|
|
saw429 = true
|
|
break
|
|
}
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Fatalf("unexpected status %d", rec.Code)
|
|
}
|
|
}
|
|
if !saw429 {
|
|
t.Fatal("expected 429 after public burst")
|
|
}
|
|
}
|
|
|
|
func TestRateLimitPublicExportRejectsBadTokenShape(t *testing.T) {
|
|
t.Parallel()
|
|
s := &Server{Config: config.Config{}}
|
|
r := chi.NewRouter()
|
|
r.With(s.RateLimitPublicExport).Get("/export-feeds/{token}.xml", func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
})
|
|
req := httptest.NewRequest(http.MethodGet, "/export-feeds/short.xml", nil)
|
|
req.RemoteAddr = "203.0.113.60:1"
|
|
rec := httptest.NewRecorder()
|
|
r.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("expected 404 for bad token shape, got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestRateLimitPublicExportBlocksIPBurst(t *testing.T) {
|
|
t.Parallel()
|
|
s := &Server{Config: config.Config{}}
|
|
r := chi.NewRouter()
|
|
r.With(s.RateLimitPublicExport).Get("/export-feeds/{token}.xml", func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
})
|
|
token := "0123456789abcdef0123456789abcdef"
|
|
var saw429 bool
|
|
for i := 0; i < 40; i++ {
|
|
req := httptest.NewRequest(http.MethodGet, "/export-feeds/"+token+".xml", nil)
|
|
req.RemoteAddr = "203.0.113.61:1"
|
|
rec := httptest.NewRecorder()
|
|
r.ServeHTTP(rec, req)
|
|
if rec.Code == http.StatusTooManyRequests {
|
|
saw429 = true
|
|
break
|
|
}
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Fatalf("unexpected status %d", rec.Code)
|
|
}
|
|
}
|
|
if !saw429 {
|
|
t.Fatal("expected 429 after public export burst")
|
|
}
|
|
}
|
|
|
|
func TestRateLimitAPIKeyAttemptsBlocksBurst(t *testing.T) {
|
|
t.Parallel()
|
|
s := &Server{Config: config.Config{}}
|
|
h := s.RateLimitAPIKeyAttempts(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
var saw429 bool
|
|
for i := 0; i < apiKeyAttemptRPM+5; i++ {
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/products", nil)
|
|
req.RemoteAddr = "203.0.113.70:12345"
|
|
req.Header.Set("X-API-Key", "dk_test_key")
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
if rec.Code == http.StatusTooManyRequests {
|
|
saw429 = true
|
|
break
|
|
}
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Fatalf("unexpected status %d", rec.Code)
|
|
}
|
|
}
|
|
if !saw429 {
|
|
t.Fatal("expected 429 after API key attempt burst")
|
|
}
|
|
}
|
|
|
|
func TestRateLimitAPIKeyCompanyBudget(t *testing.T) {
|
|
t.Parallel()
|
|
s := &Server{Config: config.Config{}}
|
|
h := s.RateLimitAPIKey(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
cid := uuid.MustParse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
|
|
var saw429 bool
|
|
for i := 0; i < apiKeyCompanyRPM+5; i++ {
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/products", nil)
|
|
req = req.WithContext(context.WithValue(req.Context(), ctxCompanyID, cid))
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
if rec.Code == http.StatusTooManyRequests {
|
|
saw429 = true
|
|
break
|
|
}
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Fatalf("unexpected status %d", rec.Code)
|
|
}
|
|
}
|
|
if !saw429 {
|
|
t.Fatal("expected 429 after API key company burst")
|
|
}
|
|
}
|