diff --git a/internal/api/rate_limit_config_test.go b/internal/api/rate_limit_config_test.go new file mode 100644 index 0000000..9dc5ff9 --- /dev/null +++ b/internal/api/rate_limit_config_test.go @@ -0,0 +1,55 @@ +package api + +import ( + "net/http" + "net/http/httptest" + "strconv" + "testing" +) + +func TestUniversalRateLimitMiddleware_HeaderFormat(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + middleware := UniversalRateLimitMiddleware(handler) + + limiter := GetRateLimiterForEndpoint("/api/login", http.MethodPost) + testIP := "203.0.113.55" + t.Cleanup(func() { + ResetRateLimitForIP(testIP) + }) + + makeRequest := func() *http.Request { + req := httptest.NewRequest(http.MethodPost, "/api/login", nil) + req.RemoteAddr = testIP + ":12345" + return req + } + + // Make requests up to the limit - all should succeed + for i := 0; i < limiter.limit; i++ { + rr := httptest.NewRecorder() + middleware.ServeHTTP(rr, makeRequest()) + if rr.Code != http.StatusOK { + t.Fatalf("expected OK while under limit, got %d on request %d", rr.Code, i+1) + } + } + + // Next request should trigger rate limit + rr := httptest.NewRecorder() + middleware.ServeHTTP(rr, makeRequest()) + if rr.Code != http.StatusTooManyRequests { + t.Fatalf("expected rate-limit status, got %d", rr.Code) + } + + // Verify X-RateLimit-Limit header is a proper decimal string + limitHeader := rr.Result().Header.Get("X-RateLimit-Limit") + expected := strconv.Itoa(limiter.limit) + if limitHeader != expected { + t.Fatalf("expected X-RateLimit-Limit %q, got %q", expected, limitHeader) + } + + // Verify the header parses as a valid integer (not a control character) + if _, err := strconv.Atoi(limitHeader); err != nil { + t.Fatalf("header %q should parse as decimal: %v", limitHeader, err) + } +} diff --git a/internal/websocket/hub_concurrency_test.go b/internal/websocket/hub_concurrency_test.go index ecc6a52..6db9570 100644 --- a/internal/websocket/hub_concurrency_test.go +++ b/internal/websocket/hub_concurrency_test.go @@ -1,6 +1,7 @@ package websocket import ( + "strconv" "sync" "testing" "time" @@ -24,7 +25,7 @@ func TestHubConcurrentClients(t *testing.T) { client := &Client{ hub: hub, send: make(chan []byte, 10), - id: "client-register-" + string(rune(i)), + id: "client-register-" + strconv.Itoa(i), } hub.register <- client time.Sleep(time.Microsecond)