- Structured logging: replace log.* with log/slog JSON output across backend - Request logger middleware: logs method, path, status, duration for all non-health requests - Rate limiting: token bucket (5 req/min, burst 10) on AI endpoints (/api/ai/*) - Integration tests: full critical path test (auth -> create case -> add deadline -> dashboard) - Seed demo data: 1 tenant, 5 cases with deadlines/appointments/parties/events - docker-compose.yml: add all required env vars (DATABASE_URL, SUPABASE_*, ANTHROPIC_API_KEY) - .env.example: document all env vars including DATABASE_URL and CalDAV note
71 lines
1.8 KiB
Go
71 lines
1.8 KiB
Go
package middleware
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
func TestTokenBucket_AllowsBurst(t *testing.T) {
|
|
tb := NewTokenBucket(1.0, 5) // 1/sec, burst 5
|
|
|
|
handler := tb.LimitFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
// Should allow burst of 5 requests
|
|
for i := 0; i < 5; i++ {
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("request %d: expected 200, got %d", i+1, w.Code)
|
|
}
|
|
}
|
|
|
|
// 6th request should be rate limited
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
if w.Code != http.StatusTooManyRequests {
|
|
t.Fatalf("request 6: expected 429, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestTokenBucket_DifferentIPs(t *testing.T) {
|
|
tb := NewTokenBucket(1.0, 2) // 1/sec, burst 2
|
|
|
|
handler := tb.LimitFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
// Exhaust IP1's bucket
|
|
for i := 0; i < 2; i++ {
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
req.Header.Set("X-Forwarded-For", "1.2.3.4")
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("ip1 request %d: expected 200, got %d", i+1, w.Code)
|
|
}
|
|
}
|
|
|
|
// IP1 should now be limited
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
req.Header.Set("X-Forwarded-For", "1.2.3.4")
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
if w.Code != http.StatusTooManyRequests {
|
|
t.Fatalf("ip1 request 3: expected 429, got %d", w.Code)
|
|
}
|
|
|
|
// IP2 should still work
|
|
req = httptest.NewRequest("GET", "/test", nil)
|
|
req.Header.Set("X-Forwarded-For", "5.6.7.8")
|
|
w = httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("ip2 request 1: expected 200, got %d", w.Code)
|
|
}
|
|
}
|