import { afterEach, describe, expect, it, vi } from "vitest"; import { ApiError, createApiClient, logoutSession } from "../src/api"; afterEach(() => vi.unstubAllGlobals()); describe("management API safety contracts", () => { it("uses same-origin server pagination and URL-addressable filters", async () => { const fetchMock = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ kind: "tools", generatedAt: "2026-08-01T18:00:00Z", rows: [], page: { total: 0 } }) }); vi.stubGlobal("fetch", fetchMock); await createApiClient().resource("tools", { filter: "risk:R1", limit: 25 }); expect(fetchMock).toHaveBeenCalledWith("/api/tools?q=risk%3AR1&limit=25", expect.objectContaining({ credentials: "same-origin", headers: { Accept: "application/json" } })); }); it("maps protected client actions and never invents returned secret material", async () => { const fetchMock = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ clientId: "client-1", endpoint: "https://mcp.example.test/sse", scopes: ["tools:read"] }) }); vi.stubGlobal("fetch", fetchMock); const config = await createApiClient().generatedConfig("client-1"); expect(config).toEqual({ clientId: "client-1", endpoint: "https://mcp.example.test/sse", scopes: ["tools:read"] }); expect(JSON.stringify(config)).not.toContain("secret"); expect(fetchMock.mock.calls[0]?.[1]).toEqual(expect.objectContaining({ credentials: "same-origin" })); }); it("fails closed without CSRF and sends the session-bound token on writes", async () => { const fetchMock = vi.fn() .mockResolvedValueOnce({ ok: true, json: async () => ({ authenticated: true, csrf_token: "csrf-fixture" }) }) .mockResolvedValueOnce({ ok: true, json: async () => ({ decision: "deny", matchedPolicies: [], reasons: [], traceId: "trace" }) }); vi.stubGlobal("fetch", fetchMock); const api = createApiClient(); await expect(api.simulatePolicy({ project: "p", environment: "prod", tool: "t" })).rejects.toBeInstanceOf(ApiError); await api.session(); await api.simulatePolicy({ project: "p", environment: "prod", tool: "t" }); expect(fetchMock.mock.calls[1]?.[1]).toEqual(expect.objectContaining({ headers: expect.objectContaining({ "X-CSRF-Token": "csrf-fixture" }) })); }); it("logs out through the same-origin CSRF-protected BFF route", async () => { const fetchMock = vi.fn() .mockResolvedValueOnce({ ok: true, json: async () => ({ authenticated: true, csrf_token: "csrf-logout" }) }) .mockResolvedValueOnce({ ok: true, status: 204 }); vi.stubGlobal("fetch", fetchMock); await logoutSession(); expect(fetchMock.mock.calls[1]?.[0]).toBe("/auth/logout"); expect(fetchMock.mock.calls[1]?.[1]).toEqual(expect.objectContaining({ method: "POST", headers: expect.objectContaining({ "X-CSRF-Token": "csrf-logout" }) })); }); it("keeps resource listing separate from persistent client management routes", async () => { const fetchMock = vi.fn() .mockResolvedValueOnce({ ok: true, json: async () => ({ authenticated: true, csrf_token: "csrf-1" }) }) .mockResolvedValueOnce({ ok: true, json: async () => ({ status: "revoked" }) }); vi.stubGlobal("fetch", fetchMock); const api = createApiClient(); await api.session(); expect(await api.clientAction("client-1", "revoke")).toEqual({ accepted: true, message: "Client revoke completed." }); expect(fetchMock.mock.calls[1]?.[0]).toBe("/api/client-management/client-1/revoke"); }); it("posts bounded tool readiness input to the dedicated endpoint", async () => { const fetchMock = vi.fn() .mockResolvedValueOnce({ ok: true, json: async () => ({ authenticated: true, csrf_token: "csrf-ready" }) }) .mockResolvedValueOnce({ ok: true, json: async () => ({ ready: false, checks: {} }) }); vi.stubGlobal("fetch", fetchMock); const api = createApiClient(); await api.session(); await api.testTool({ project: "mobilityops", environment: "prod", tool: "mobilityops.vehicle.get", arguments: { vehicleRef: "V-42" } }); expect(fetchMock.mock.calls[1]?.[0]).toBe("/api/tools/test"); expect(fetchMock.mock.calls[1]?.[1]).toEqual(expect.objectContaining({ method: "POST", body: JSON.stringify({ project: "mobilityops", environment: "prod", tool: "mobilityops.vehicle.get", arguments: { vehicleRef: "V-42" } }) })); }); it("invokes tools and operations checks only through CSRF-protected routes", async () => { const fetchMock = vi.fn() .mockResolvedValueOnce({ ok: true, json: async () => ({ authenticated: true, csrf_token: "csrf-live" }) }) .mockResolvedValueOnce({ ok: true, json: async () => ({ ok: true, traceId: "trace-1" }) }) .mockResolvedValueOnce({ ok: true, json: async () => ({ healthy: true, connectors: {} }) }); vi.stubGlobal("fetch", fetchMock); const api = createApiClient(); await api.session(); const payload = { project: "mobilityops", environment: "prod", tool: "mobilityops.operations.summary", arguments: {} }; await api.invokeTool(payload); await api.runOperationsCheck(); expect(fetchMock.mock.calls[1]?.[0]).toBe("/api/tools/invoke"); expect(fetchMock.mock.calls[2]?.[0]).toBe("/api/operations/check"); expect(fetchMock.mock.calls[1]?.[1]).toEqual(expect.objectContaining({ headers: expect.objectContaining({ "X-CSRF-Token": "csrf-live" }) })); expect(fetchMock.mock.calls[2]?.[1]).toEqual(expect.objectContaining({ headers: expect.objectContaining({ "X-CSRF-Token": "csrf-live" }) })); }); it("converts non-success responses to typed safe errors", async () => { vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: false, status: 403 })); await expect(createApiClient().dashboard()).rejects.toBeInstanceOf(ApiError); }); });