package routes_test import ( "context" "net/http" "net/http/httptest" "strings" "testing" "time" "doormile/controllers" "doormile/internal/ai/playground" "doormile/utils" ) // The AI agent registry's gates, over real HTTP (see routes_logistics_test.go // for what this style of test does and does not prove). Every refusal below // happens in middleware, before a handler could touch the database. func tenantToken(t *testing.T, userID, roleID, tenantID int) string { t.Helper() tok, err := utils.GenerateToken(userID, "client@partner.example", roleID, tenantID, 1001, jwtSecret) if err != nil { t.Fatalf("could not mint a tenant token: %v", err) } return tok } var aiReads = []string{ "/api/v1/admin/ai/agents", "/api/v1/admin/ai/agents/EXCEPTION_AGENT", "/api/v1/admin/ai/skills", "/api/v1/admin/ai/tools", "/api/v1/admin/ai/audit", "/api/v1/admin/ai/insights", "/api/v1/admin/ai/insights?days=30", "/api/v1/admin/ai/decisions", } var aiWrites = []struct{ method, path, body string }{ {http.MethodPatch, "/api/v1/admin/ai/skills/skill_sla_guardian", `{"enabled":false}`}, {http.MethodPost, "/api/v1/admin/ai/skills", `{"agentid":"CONSOLE_OPS_AGENT","title":"x","tools":["scan_bookings"]}`}, {http.MethodPatch, "/api/v1/admin/ai/agents/EXCEPTION_AGENT", `{"autonomous":true,"confirm":"EXCEPTION_AGENT"}`}, {http.MethodPost, "/api/v1/admin/ai/playground/run", `{"agentid":"EXCEPTION_AGENT","prompt":"hi"}`}, } func TestAIRegistryRequiresALogin(t *testing.T) { app := newApp() for _, p := range aiReads { if code, _ := do(t, app, http.MethodGet, p, "", ""); code != http.StatusUnauthorized { t.Errorf("GET %s with no token = %d, want 401", p, code) } } } func TestAIRegistryRefusesNonConsoleRoles(t *testing.T) { app := newApp() for _, role := range []int{5, 6, 9} { // miler, hub staff, customer for _, p := range aiReads { if code, _ := do(t, app, http.MethodGet, p, token(t, 1, role), ""); code != http.StatusForbidden { t.Errorf("role %d GET %s = %d, want 403", role, p, code) } } } } // A partner-tenant login is refused outright — even an admin-role one — rather // than shown an empty registry. func TestAIRegistryRefusesPartnerTenantLogins(t *testing.T) { app := newApp() tok := tenantToken(t, 50, 1, 7) for _, p := range aiReads { code, body := do(t, app, http.MethodGet, p, tok, "") if code != http.StatusForbidden { t.Errorf("tenant GET %s = %d, want 403", p, code) } if code == http.StatusForbidden && !strings.Contains(body, "Doormile staff only") { t.Errorf("tenant GET %s refused with the wrong message: %s", p, body) } } for _, w := range aiWrites { if code, _ := do(t, app, w.method, w.path, tok, w.body); code != http.StatusForbidden { t.Errorf("tenant %s %s = %d, want 403", w.method, w.path, code) } } } // Managers (3) and executives (4) may read the registry but not change it. func TestAIRegistryWritesAreAdminOnly(t *testing.T) { app := newApp() for _, role := range []int{3, 4} { for _, w := range aiWrites { code, body := do(t, app, w.method, w.path, token(t, 1, role), w.body) if code != http.StatusForbidden { t.Errorf("role %d %s %s = %d, want 403", role, w.method, w.path, code) } if code == http.StatusForbidden && !strings.Contains(body, "insufficient permissions") { t.Errorf("role %d %s %s refused by the wrong gate: %s", role, w.method, w.path, body) } } } } // Doormile staff with roleid 1 get through every gate. There is no database // in this test, so the handler's first query panics and recover answers 500 — // which is the proof the route exists and nothing in front of it refused. func TestAIRegistryAdminPassesEveryGate(t *testing.T) { app := newApp() tok := token(t, 1, 1) for _, p := range aiReads { if code, _ := do(t, app, http.MethodGet, p, tok, ""); code == 401 || code == 403 || code == 404 { t.Errorf("admin GET %s = %d; a gate refused or the route is missing", p, code) } } for _, w := range aiWrites { if code, _ := do(t, app, w.method, w.path, tok, w.body); code == 401 || code == 403 || code == 404 { t.Errorf("admin %s %s = %d; a gate refused or the route is missing", w.method, w.path, code) } } // Read roles get through the read gates too. for _, role := range []int{3, 4} { if code, _ := do(t, app, http.MethodGet, "/api/v1/admin/ai/agents", token(t, 1, role), ""); code == 401 || code == 403 { t.Errorf("role %d was refused a registry read (%d)", role, code) } } } func TestInternalRegistryNeedsTheInternalKey(t *testing.T) { t.Setenv("INTERNAL_API_KEY", "engine-key-for-tests") app := newApp() for _, key := range []string{"", "wrong-key"} { req := httptest.NewRequest(http.MethodGet, "/api/v1/internal/ai/registry", nil) if key != "" { req.Header.Set("X-Internal-Key", key) } resp, err := app.Test(req, int(10*time.Second/time.Millisecond)) if err != nil { t.Fatal(err) } if resp.StatusCode != http.StatusUnauthorized { t.Errorf("internal registry with key %q = %d, want 401", key, resp.StatusCode) } } // A console JWT is not an internal key. if code, _ := do(t, app, http.MethodGet, "/api/v1/internal/ai/registry", token(t, 1, 1), ""); code != http.StatusUnauthorized { t.Errorf("internal registry with an admin JWT = %d, want 401", code) } } // The Test playground (Phase 6). With no model client wired the endpoint // says so plainly (503 with a code the console branches on) — it never // pretends to run. With one wired, bad input is refused before any database // or model call. func TestAIPlaygroundWithoutAClientIs503(t *testing.T) { app := newApp() prev := controllers.PlaygroundModel controllers.PlaygroundModel = nil defer func() { controllers.PlaygroundModel = prev }() code, body := do(t, app, http.MethodPost, "/api/v1/admin/ai/playground/run", token(t, 1, 1), `{"agentid":"EXCEPTION_AGENT","prompt":"hi"}`) if code != http.StatusServiceUnavailable || !strings.Contains(body, "PLAYGROUND_NOT_CONFIGURED") { t.Fatalf("no client: %d %s, want 503 PLAYGROUND_NOT_CONFIGURED", code, body) } } type noCallModel struct{ t *testing.T } func (m noCallModel) Next(context.Context, playground.Request) (playground.Reply, error) { m.t.Fatal("the model was called for a request that should have been refused") return playground.Reply{}, nil } func TestAIPlaygroundRefusesBadInput(t *testing.T) { app := newApp() prev := controllers.PlaygroundModel controllers.PlaygroundModel = noCallModel{t} defer func() { controllers.PlaygroundModel = prev }() for _, b := range []string{ `{"agentid":"EXCEPTION_AGENT","prompt":" "}`, `{"prompt":"hi"}`, `{"agentid":"EXCEPTION_AGENT","prompt":"` + strings.Repeat("x", 2001) + `"}`, `not json`, } { if code, body := do(t, app, http.MethodPost, "/api/v1/admin/ai/playground/run", token(t, 1, 1), b); code != http.StatusBadRequest { t.Errorf("body %.40q = %d %s, want 400", b, code, body) } } }