fix: remove rate limiter dev stub, consolidate tests under all build tags
CI / Docker compose check (push) Successful in 13s
CI / Env docs check (push) Successful in 14s
CI / Nginx config check (push) Successful in 15s
CI / Frontend major deps (push) Successful in 25s
CI / Frontend deps check (push) Successful in 26s
CI / Secrets scan (push) Successful in 35s
CI / Go build (push) Successful in 42s
CI / Frontend build (push) Successful in 45s
CI / Knip (push) Successful in 23s
CI / Frontend a11y check (push) Successful in 1m13s
CI / go mod tidy (push) Successful in 37s
CI / Go vet (prod) (push) Successful in 2m5s
CI / Go vet (dev) (push) Successful in 2m16s
CI / Frontend QC (audit) (push) Successful in 45s
CI / Go vulnerabilities (push) Successful in 1m29s
CI / Staticcheck (prod) (push) Successful in 3m3s
CI / Staticcheck (dev) (push) Successful in 3m46s
CI / Frontend QC (typecheck) (push) Successful in 2m1s
CI / golangci-lint (push) Successful in 4m19s
CI / Security scan (prod) (push) Successful in 4m23s
CI / Security scan (dev) (push) Successful in 4m35s
CI / Frontend QC (lint) (push) Successful in 1m51s
CI / Svelte strict check (push) Successful in 1m21s
CI / Tests (prod) (push) Successful in 3m39s
CI / Tests (dev) (push) Failing after 3m50s
CI / Race (prod) (push) Successful in 7m4s
CI / Race (dev) (push) Failing after 7m12s
CI / Docker compose check (push) Successful in 13s
CI / Env docs check (push) Successful in 14s
CI / Nginx config check (push) Successful in 15s
CI / Frontend major deps (push) Successful in 25s
CI / Frontend deps check (push) Successful in 26s
CI / Secrets scan (push) Successful in 35s
CI / Go build (push) Successful in 42s
CI / Frontend build (push) Successful in 45s
CI / Knip (push) Successful in 23s
CI / Frontend a11y check (push) Successful in 1m13s
CI / go mod tidy (push) Successful in 37s
CI / Go vet (prod) (push) Successful in 2m5s
CI / Go vet (dev) (push) Successful in 2m16s
CI / Frontend QC (audit) (push) Successful in 45s
CI / Go vulnerabilities (push) Successful in 1m29s
CI / Staticcheck (prod) (push) Successful in 3m3s
CI / Staticcheck (dev) (push) Successful in 3m46s
CI / Frontend QC (typecheck) (push) Successful in 2m1s
CI / golangci-lint (push) Successful in 4m19s
CI / Security scan (prod) (push) Successful in 4m23s
CI / Security scan (dev) (push) Successful in 4m35s
CI / Frontend QC (lint) (push) Successful in 1m51s
CI / Svelte strict check (push) Successful in 1m21s
CI / Tests (prod) (push) Successful in 3m39s
CI / Tests (dev) (push) Failing after 3m50s
CI / Race (prod) (push) Successful in 7m4s
CI / Race (dev) (push) Failing after 7m12s
This commit is contained in:
@@ -1,5 +1,3 @@
|
|||||||
//go:build !dev
|
|
||||||
|
|
||||||
package mw
|
package mw
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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())
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
//go:build test && !dev
|
|
||||||
|
|
||||||
package mw
|
package mw
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//go:build test && !dev
|
|
||||||
|
|
||||||
package mw
|
package mw
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
Reference in New Issue
Block a user