diff --git a/backend/mw/ratelimit.go b/backend/mw/ratelimit.go index a782056..e7b5f02 100644 --- a/backend/mw/ratelimit.go +++ b/backend/mw/ratelimit.go @@ -1,5 +1,3 @@ -//go:build !dev - package mw import ( diff --git a/backend/mw/ratelimit_dev.go b/backend/mw/ratelimit_dev.go deleted file mode 100644 index 6de24ec..0000000 --- a/backend/mw/ratelimit_dev.go +++ /dev/null @@ -1,42 +0,0 @@ -//go:build dev - -package mw - -import ( - "net/http" - "time" -) - -func NewRateLimiter(limit int, window time.Duration) *RateLimiter { - rl := &RateLimiter{ - requests: make(map[string][]time.Time), - limit: limit, - window: window, - } - registerLimiter(rl) - return rl -} - -func (rl *RateLimiter) Allow(key string) bool { return true } - -func NewProgressiveRateLimiter() *ProgressiveRateLimiter { - return &ProgressiveRateLimiter{ - requests: make(map[string]*ipProgressiveState), - } -} - -func (prl *ProgressiveRateLimiter) Check(ip string) int { return 0 } - -func ProgressiveRateLimit(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - next.ServeHTTP(w, r) - }) -} - -func RateLimit(limit int, window time.Duration) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - next.ServeHTTP(w, r) - }) - } -} diff --git a/backend/mw/ratelimit_dev_test.go b/backend/mw/ratelimit_dev_test.go deleted file mode 100644 index 9327efe..0000000 --- a/backend/mw/ratelimit_dev_test.go +++ /dev/null @@ -1,231 +0,0 @@ -//go:build test && dev - -package mw - -import ( - "context" - "net/http" - "net/http/httptest" - "testing" - "time" - - "crussell/clock" - "github.com/stretchr/testify/assert" -) - -// ============================================================ -// Dev Stub Behavioral Tests -// ============================================================ - -// TestDevRateLimiter_AllowAlwaysTrue verifies the dev stub never throttles. -func TestDevRateLimiter_AllowAlwaysTrue(t *testing.T) { - rl := NewRateLimiter(10, time.Minute) - - if !rl.Allow("any-key") { - t.Error("expected Allow to return true for dev stub") - } - if !rl.Allow("another-key") { - t.Error("expected Allow to return true for dev stub (different key)") - } - // Even an empty key should pass - if !rl.Allow("") { - t.Error("expected Allow to return true for empty key in dev stub") - } -} - -// TestDevProgressiveRateLimiter_CheckAlwaysZero verifies the dev stub never delays. -func TestDevProgressiveRateLimiter_CheckAlwaysZero(t *testing.T) { - prl := NewProgressiveRateLimiter() - - for i := 0; i < 200; i++ { - delay := prl.Check("10.0.0.1") - if delay != 0 { - t.Errorf("expected 0 delay for dev stub (request %d), got %d", i+1, delay) - } - } - - // Different IP also returns 0 - if delay := prl.Check("10.0.0.2"); delay != 0 { - t.Errorf("expected 0 delay for different IP in dev stub, got %d", delay) - } -} - -func TestProgressiveRateLimiter_CleanupRemovesStaleEntries(t *testing.T) { - prl := NewProgressiveRateLimiter() - - prl.mu.Lock() - prl.requests["stale-ip"] = &ipProgressiveState{ - timestamps: []time.Time{clock.Now().Add(-120 * time.Second)}, - } - prl.mu.Unlock() - - prl.Cleanup() - - prl.mu.RLock() - _, exists := prl.requests["stale-ip"] - prl.mu.RUnlock() - if exists { - t.Error("expected stale IP to be cleaned up") - } -} - -func TestRateLimiter_Cleanup(t *testing.T) { - rl := NewRateLimiter(10, time.Minute) - - rl.mu.Lock() - rl.requests["stale-key"] = []time.Time{clock.Now().Add(-5 * time.Minute)} - rl.requests["fresh-key"] = []time.Time{clock.Now()} - rl.mu.Unlock() - - rl.Cleanup() - - rl.mu.RLock() - _, staleExists := rl.requests["stale-key"] - _, freshExists := rl.requests["fresh-key"] - rl.mu.RUnlock() - - if staleExists { - t.Error("expected stale-key to be removed") - } - if !freshExists { - t.Error("expected fresh-key to be preserved") - } -} - -func TestRateLimiter_Cleanup_EmptyMap(t *testing.T) { - rl := NewRateLimiter(10, time.Minute) - - rl.mu.Lock() - rl.requests = make(map[string][]time.Time) - rl.mu.Unlock() - - rl.Cleanup() - - rl.mu.RLock() - count := len(rl.requests) - rl.mu.RUnlock() - if count != 0 { - t.Errorf("expected empty map, got %d entries", count) - } -} - -func TestCleanupAllRateLimiters(t *testing.T) { - registeredLimitersMu.Lock() - saved := registeredLimiters - registeredLimitersMu.Unlock() - defer func() { - registeredLimitersMu.Lock() - registeredLimiters = saved - registeredLimitersMu.Unlock() - }() - - rl1 := NewRateLimiter(10, time.Minute) - rl2 := NewRateLimiter(20, time.Minute) - - rl1.mu.Lock() - rl1.requests["rl1-stale"] = []time.Time{clock.Now().Add(-5 * time.Minute)} - rl1.mu.Unlock() - - rl2.mu.Lock() - rl2.requests["rl2-stale"] = []time.Time{clock.Now().Add(-5 * time.Minute)} - rl2.mu.Unlock() - - _, err := CleanupAllRateLimiters(context.Background()) - if err != nil { - t.Errorf("expected nil error, got %v", err) - } - - rl1.mu.RLock() - _, rl1Stale := rl1.requests["rl1-stale"] - rl1.mu.RUnlock() - - rl2.mu.RLock() - _, rl2Stale := rl2.requests["rl2-stale"] - rl2.mu.RUnlock() - - if rl1Stale { - t.Error("expected rl1-stale to be removed") - } - if rl2Stale { - t.Error("expected rl2-stale to be removed") - } -} - -func TestCleanupAllRateLimiters_Empty(t *testing.T) { - registeredLimitersMu.Lock() - saved := registeredLimiters - registeredLimiters = nil - registeredLimitersMu.Unlock() - defer func() { - registeredLimitersMu.Lock() - registeredLimiters = saved - registeredLimitersMu.Unlock() - }() - - _, err := CleanupAllRateLimiters(context.Background()) - if err != nil { - t.Errorf("expected nil error, got %v", err) - } -} - -func TestCleanupProgressiveRateLimiter(t *testing.T) { - globalProgressiveLimiter.mu.Lock() - saved := globalProgressiveLimiter.requests - globalProgressiveLimiter.requests = map[string]*ipProgressiveState{ - "global-stale": {timestamps: []time.Time{clock.Now().Add(-120 * time.Second)}}, - } - globalProgressiveLimiter.mu.Unlock() - defer func() { - globalProgressiveLimiter.mu.Lock() - globalProgressiveLimiter.requests = saved - globalProgressiveLimiter.mu.Unlock() - }() - - _, err := CleanupProgressiveRateLimiter(context.Background()) - if err != nil { - t.Errorf("expected nil error, got %v", err) - } - - globalProgressiveLimiter.mu.RLock() - _, exists := globalProgressiveLimiter.requests["global-stale"] - globalProgressiveLimiter.mu.RUnlock() - if exists { - t.Error("expected global-stale to be removed") - } -} - -// ============================================================ -// Dev Stub Middleware Pass-Through Tests -// ============================================================ - -// TestProgressiveRateLimit_PassThrough verifies the dev stub middleware -// passes through to the next handler without rate limiting. -func TestProgressiveRateLimit_PassThrough(t *testing.T) { - handler := ProgressiveRateLimit(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte("ok")) - })) - - req := httptest.NewRequest("GET", "/", nil) - w := httptest.NewRecorder() - handler.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - assert.Equal(t, "ok", w.Body.String()) -} - -// TestRateLimit_PassThrough verifies the dev stub middleware -// passes through to the next handler without rate limiting. -func TestRateLimit_PassThrough(t *testing.T) { - handler := RateLimit(10, time.Minute)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte("ok")) - })) - - req := httptest.NewRequest("GET", "/", nil) - w := httptest.NewRecorder() - handler.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - assert.Equal(t, "ok", w.Body.String()) -} diff --git a/backend/mw/ratelimit_prod_test.go b/backend/mw/ratelimit_prod_test.go index f87f949..a9c480b 100644 --- a/backend/mw/ratelimit_prod_test.go +++ b/backend/mw/ratelimit_prod_test.go @@ -1,5 +1,3 @@ -//go:build test && !dev - package mw import ( diff --git a/backend/mw/ratelimit_test.go b/backend/mw/ratelimit_test.go index e5df86c..77cae5b 100644 --- a/backend/mw/ratelimit_test.go +++ b/backend/mw/ratelimit_test.go @@ -1,5 +1,3 @@ -//go:build test && !dev - package mw import (