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") } }