package httpapi import ( "context" "encoding/json" "io" "log/slog" "net/http" "net/http/httptest" "path/filepath" "testing" "github.com/mithral/vellum/internal/core" "github.com/mithral/vellum/migrations" ) func newTestServer(t *testing.T) *Server { t.Helper() db, err := core.OpenDB(filepath.Join(t.TempDir(), "test.db")) if err != nil { t.Fatalf("OpenDB: %v", err) } t.Cleanup(func() { _ = db.Close() }) log := slog.New(slog.NewTextHandler(io.Discard, nil)) if err := core.Migrate(context.Background(), db, migrations.FS, log); err != nil { t.Fatalf("Migrate: %v", err) } return New(db, log, "test") } func TestHealth(t *testing.T) { srv := newTestServer(t) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/healthz", nil) srv.Handler().ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want 200", rec.Code) } var body map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatalf("decode body: %v", err) } if body["status"] != "ok" { t.Errorf("status = %v, want ok", body["status"]) } if body["version"] != "test" { t.Errorf("version = %v, want test", body["version"]) } } func TestNotFoundIsJSON(t *testing.T) { srv := newTestServer(t) // API paths must never fall back to the SPA shell. rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/api/v1/nope", nil) srv.Handler().ServeHTTP(rec, req) if rec.Code != http.StatusNotFound { t.Fatalf("status = %d, want 404", rec.Code) } if ct := rec.Header().Get("Content-Type"); ct == "" { t.Error("missing Content-Type on 404") } var body map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatalf("404 body is not JSON: %v", err) } } func TestRequestIDHeaderAndLog(t *testing.T) { srv := newTestServer(t) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/healthz", nil) srv.Handler().ServeHTTP(rec, req) if rec.Header().Get("X-Request-ID") == "" { t.Error("X-Request-ID header not set") } // A caller-supplied ID must be preserved. rec2 := httptest.NewRecorder() req2 := httptest.NewRequest(http.MethodGet, "/healthz", nil) req2.Header.Set("X-Request-ID", "abc123") srv.Handler().ServeHTTP(rec2, req2) if got := rec2.Header().Get("X-Request-ID"); got != "abc123" { t.Errorf("X-Request-ID = %q, want abc123", got) } } func TestRecoveryMiddleware(t *testing.T) { srv := newTestServer(t) srv.Handle("GET /panic", http.HandlerFunc(func(http.ResponseWriter, *http.Request) { panic("boom") })) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/panic", nil) srv.Handler().ServeHTTP(rec, req) if rec.Code != http.StatusInternalServerError { t.Fatalf("status = %d, want 500", rec.Code) } }