Files
backend_fiesta/routes/startup_test.go
2026-09-29 22:40:36 +05:30

136 lines
4.9 KiB
Go

package routes
import (
"net/http/httptest"
"strings"
"testing"
"nearle/config"
"nearle/facade"
"github.com/gofiber/fiber/v2"
)
// Can this server be built and can its routes be reached?
//
// Everything else in this repository tests a function. This tests the thing
// that actually happens on deploy: the whole object graph is constructed and
// every route is registered. Nothing here needs a database — the repositories
// hold their handle without touching it — so it runs in CI beside the unit
// tests rather than in an environment somebody has to provision.
//
// ── Why it is worth its own file ────────────────────────────────────────────
//
// Three things in `NewFacade` PANIC rather than return an error: a tool that
// fails to register, a help corpus that will not load, and an agent naming a
// tool that does not exist. Each is a programming mistake that should stop a
// deploy, and each was previously reachable only by starting the server against
// a real database — which meant, in practice, by deploying.
//
// The agent one is not hypothetical: a typo in `agents/orders.yaml` is a file
// edit away, and it takes a working assistant down at boot.
func testFacade(t *testing.T) *facade.Facade {
t.Helper()
defer func() {
if r := recover(); r != nil {
// Rendered as a failure rather than a panicking test, because the
// message IS the point — "agent orders lists a tool that does not
// exist" is the whole diagnosis.
t.Fatalf("the server cannot start: %v", r)
}
}()
// No database, no catalogue, no embedder, no model. A deployment with none
// of those must still boot and say what it is missing, rather than failing
// somewhere the operator cannot see.
return facade.NewFacade(nil, nil, nil, nil, "", "no model in tests", nil, config.MailConfig{}, "")
}
func TestTheServerCanBeBuilt(t *testing.T) {
f := testFacade(t)
if f == nil {
t.Fatal("no facade")
}
// The two doors onto the assistant. Absent means a route registered below
// would nil-panic on its first request rather than at boot.
if f.AssistantController == nil {
t.Fatal("no assistant controller")
}
if f.MCPController == nil {
t.Fatal("no MCP controller")
}
if f.Tools == nil {
t.Fatal("no tool registry")
}
}
func TestEveryAssistantRouteIsReachable(t *testing.T) {
// Registered, not merely written down. A route added to a file that nothing
// calls is invisible until somebody reports the feature missing.
app := fiber.New()
RegisterRoutes(app, testFacade(t))
for _, route := range []struct {
method, path string
}{
{"GET", "/live/api/v1/web/assistant/status"},
{"POST", "/live/api/v1/web/assistant/ask"},
{"POST", "/live/api/v1/web/assistant/approve"},
{"POST", "/live/api/v1/web/assistant/mcp"},
} {
req := httptest.NewRequest(route.method, route.path, strings.NewReader("{}"))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req, -1)
if err != nil {
t.Fatalf("%s %s: %v", route.method, route.path, err)
}
if resp.StatusCode == fiber.StatusNotFound {
t.Fatalf("%s %s is not registered", route.method, route.path)
}
}
}
func TestHealthAnswersThroughTheRealRouteTableWithoutASession(t *testing.T) {
// Registered on `api` rather than under `/v1/web`, which is what keeps it
// outside the session guard. Asserted here rather than trusted, because the
// difference is one path segment and getting it wrong makes the endpoint
// useless for the only situation it exists for: nothing else works.
//
// It also has to survive a facade built with no database, no model and no
// embedder — the state somebody is most likely to be asking from.
app := fiber.New()
RegisterRoutes(app, testFacade(t))
resp, err := app.Test(httptest.NewRequest("GET", "/live/api/v1/health", nil), -1)
if err != nil {
t.Fatalf("calling health: %v", err)
}
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("health needs a session or is unregistered: HTTP %d", resp.StatusCode)
}
}
func TestTheAssistantSurfaceSitsBehindTheSessionGuard(t *testing.T) {
// The assistant reads the same data the console does and must read it as
// the same person. Being under `/v1/web` is what puts it behind WebAuth —
// a route registered one path segment to the left would answer anybody.
app := fiber.New()
RegisterRoutes(app, testFacade(t))
// WEB_AUTH_REQUIRED is off by default, so an untokened request reaches the
// handler; the assistant's own controller then refuses it. Either way it
// must not answer with data.
req := httptest.NewRequest("POST", "/live/api/v1/web/assistant/ask",
strings.NewReader(`{"question":"what is stuck?"}`))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req, -1)
if err != nil {
t.Fatalf("calling: %v", err)
}
if resp.StatusCode == fiber.StatusOK {
t.Fatal("an untokened question was answered")
}
}