ai-code-battle/cmd/acb-api/server_test.go
jedarden 7df2fad568 feat(api): wire voteLtr rate limiter for upvote endpoint (§13.6)
Add dedicated 10/hour-per-IP rate limiter for POST /api/feedback/{id}/upvote,
separate from the 20/hour feedback submission limiter. Wired in main.go init,
server_test.go helper, and RegisterRoutes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 17:37:34 -04:00

107 lines
2.9 KiB
Go

package main
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/aicodebattle/acb/ratelimit"
)
// newTestServer creates a Server with no database or redis (for unit tests
// that don't need them).
func newTestServer() *Server {
return &Server{
cfg: Config{
WorkerAPIKey: "test-key",
BotTimeoutSecs: 5,
MaxConsecFails: 3,
},
regLimiter: ratelimit.NewLimiter(5, 5.0/3600),
feedbackLtr: ratelimit.NewLimiter(20, 20.0/3600),
predictLtr: ratelimit.NewLimiter(60, 60.0/3600),
submitLtr: ratelimit.NewLimiter(5, 5.0/86400),
voteLtr: ratelimit.NewLimiter(10, 10.0/3600),
}
}
func TestHealthEndpoint(t *testing.T) {
srv := newTestServer()
mux := http.NewServeMux()
srv.RegisterRoutes(mux)
req := httptest.NewRequest("GET", "/health", nil)
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("health status = %d, want 200", w.Code)
}
var body map[string]string
json.NewDecoder(w.Body).Decode(&body)
if body["status"] != "ok" {
t.Errorf("health body = %v, want status=ok", body)
}
}
func TestWriteJSON(t *testing.T) {
w := httptest.NewRecorder()
writeJSON(w, http.StatusCreated, map[string]string{"key": "value"})
if w.Code != http.StatusCreated {
t.Errorf("status = %d, want 201", w.Code)
}
if ct := w.Header().Get("Content-Type"); ct != "application/json" {
t.Errorf("content-type = %q, want application/json", ct)
}
var body map[string]string
json.NewDecoder(w.Body).Decode(&body)
if body["key"] != "value" {
t.Errorf("body = %v, want key=value", body)
}
}
func TestWriteError(t *testing.T) {
w := httptest.NewRecorder()
writeError(w, http.StatusBadRequest, "test error")
if w.Code != http.StatusBadRequest {
t.Errorf("status = %d, want 400", w.Code)
}
var body map[string]string
json.NewDecoder(w.Body).Decode(&body)
if body["error"] != "test error" {
t.Errorf("body = %v, want error=test error", body)
}
}
// TestFeedbackEndpointPath asserts that the community feedback endpoint is
// served at /api/feedback per plan §13.6 — not /api/ui-feedback.
func TestFeedbackEndpointPath(t *testing.T) {
srv := newTestServer()
mux := http.NewServeMux()
srv.RegisterRoutes(mux)
// POST /api/feedback should be routed (200 from handler, not 404)
req := httptest.NewRequest("POST", "/api/feedback", nil)
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
// Handler returns 400 for empty body, not 404 — proves the route is registered
if w.Code == http.StatusNotFound {
t.Fatal("POST /api/feedback returned 404 — route not registered (expected per plan §13.6)")
}
// POST /api/ui-feedback (old name) must NOT be routed
reqOld := httptest.NewRequest("POST", "/api/ui-feedback", nil)
wOld := httptest.NewRecorder()
mux.ServeHTTP(wOld, reqOld)
if wOld.Code != http.StatusNotFound {
t.Errorf("POST /api/ui-feedback returned %d, want 404 — old route name should not be registered", wOld.Code)
}
}