Files
BountyBoard/internal/http/server_test.go
T
etalon 34bf5b5ac2 feat: base UI shell with themes, auth pages, profile, and role navigation (phase 4)
- embedded Go templates + vanilla ES-module JS + hand-written CSS, no build step
- exact §10 beige light / dark design tokens, square edges (radius 2px),
  system font stack, visible focus rings
- theme toggle persisted to localStorage and the user profile
- login/register/change-password pages wired to the auth API
- profile page: avatar upload (image-sniffed, old file cleanup), bio,
  contacts, arbitrary extra key/value fields, optimistic-concurrency 409
- role-based top navigation with placeholders for later-phase areas
- GET /files/{id} with scope-based access (session) or signed token (§5.1)
- security headers incl. CSP without unsafe-inline scripts

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 18:39:38 +02:00

173 lines
4.5 KiB
Go

package httpx
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"testing"
"bountyboard/internal/config"
"bountyboard/internal/metrics"
"bountyboard/internal/ulid"
)
func newTestServer(t *testing.T) *Server {
t.Helper()
t.Setenv("SESSION_SECRET", "0123456789abcdef0123456789abcdef")
t.Setenv("CREDENTIALS_ENC_KEY", base64.StdEncoding.EncodeToString(make([]byte, 32)))
cfg, err := config.Load()
if err != nil {
t.Fatalf("config: %v", err)
}
log := slog.New(slog.NewTextHandler(io.Discard, nil))
return New(cfg, log, metrics.NewRegistry(), nil, nil)
}
func get(t *testing.T, h http.Handler, path string) *httptest.ResponseRecorder {
t.Helper()
req := httptest.NewRequest(http.MethodGet, path, nil)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
return rec
}
func TestHealthz(t *testing.T) {
s := newTestServer(t)
rec := get(t, s.Handler(), "/healthz")
if rec.Code != http.StatusOK {
t.Fatalf("status = %d", rec.Code)
}
var body map[string]string
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
t.Fatal(err)
}
if body["status"] != "ok" {
t.Errorf("status field = %q", body["status"])
}
if id := rec.Header().Get("X-Request-Id"); !ulid.IsValid(id) {
t.Errorf("X-Request-Id %q is not a ULID", id)
}
}
func TestReadyzAggregation(t *testing.T) {
tests := []struct {
name string
checks []ReadinessCheck
wantStatus string
wantCode int
}{
{
name: "no checks",
wantStatus: "ok",
wantCode: http.StatusOK,
},
{
name: "all passing",
checks: []ReadinessCheck{
{Name: "mongo", Required: true, Probe: func(context.Context) error { return nil }},
{Name: "atomizer", Probe: func(context.Context) error { return nil }},
},
wantStatus: "ok",
wantCode: http.StatusOK,
},
{
name: "optional failing is non-fatal",
checks: []ReadinessCheck{
{Name: "mongo", Required: true, Probe: func(context.Context) error { return nil }},
{Name: "atomizer", Probe: func(context.Context) error { return errors.New("down") }},
},
wantStatus: "degraded",
wantCode: http.StatusOK,
},
{
name: "required failing gates readiness",
checks: []ReadinessCheck{
{Name: "mongo", Required: true, Probe: func(context.Context) error { return errors.New("no reachable servers") }},
},
wantStatus: "unavailable",
wantCode: http.StatusServiceUnavailable,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := newTestServer(t)
for _, c := range tt.checks {
s.AddReadinessCheck(c)
}
rec := get(t, s.Handler(), "/readyz")
if rec.Code != tt.wantCode {
t.Fatalf("code = %d, want %d", rec.Code, tt.wantCode)
}
var body struct {
Status string `json:"status"`
Checks map[string]checkResult `json:"checks"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
t.Fatal(err)
}
if body.Status != tt.wantStatus {
t.Errorf("status = %q, want %q", body.Status, tt.wantStatus)
}
if len(body.Checks) != len(tt.checks) {
t.Errorf("checks = %d, want %d", len(body.Checks), len(tt.checks))
}
})
}
}
func TestMetricszCountsRequests(t *testing.T) {
s := newTestServer(t)
get(t, s.Handler(), "/healthz")
get(t, s.Handler(), "/healthz")
rec := get(t, s.Handler(), "/metricsz")
if rec.Code != http.StatusOK {
t.Fatalf("status = %d", rec.Code)
}
var body struct {
Counters map[string]int64 `json:"counters"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
t.Fatal(err)
}
if body.Counters["http_requests_total"] != 2 {
t.Errorf("http_requests_total = %d, want 2", body.Counters["http_requests_total"])
}
}
func TestPanicRecoveryEnvelope(t *testing.T) {
s := newTestServer(t)
mux := http.NewServeMux()
mux.HandleFunc("GET /boom", func(http.ResponseWriter, *http.Request) { panic("kaboom") })
h := s.withMiddleware(mux)
rec := get(t, h, "/boom")
if rec.Code != http.StatusInternalServerError {
t.Fatalf("status = %d", rec.Code)
}
var env struct {
Error struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &env); err != nil {
t.Fatal(err)
}
if env.Error.Code != "internal" {
t.Errorf("error code = %q", env.Error.Code)
}
}
func TestUnknownRouteNotFound(t *testing.T) {
s := newTestServer(t)
rec := get(t, s.Handler(), "/nope")
if rec.Code != http.StatusNotFound {
t.Fatalf("status = %d, want 404", rec.Code)
}
}