83 lines
5.7 KiB
TypeScript
83 lines
5.7 KiB
TypeScript
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);
|
|
});
|
|
});
|