130 lines
3.8 KiB
TypeScript
130 lines
3.8 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
AUTH_INVALID_EVENT,
|
|
createLlmProviderProfile,
|
|
listDevices,
|
|
login,
|
|
registerPlugin,
|
|
} from "./api";
|
|
|
|
function response(payload: unknown, status = 200): Response {
|
|
return new Response(JSON.stringify(payload), {
|
|
status,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
describe("Cloud Console API authentication", () => {
|
|
beforeEach(() => {
|
|
document.cookie = "amcp_csrf=; Max-Age=0; path=/";
|
|
vi.stubGlobal("fetch", vi.fn());
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
it("uses credentialed account login without a bearer header", async () => {
|
|
vi.mocked(fetch).mockResolvedValueOnce(
|
|
response({
|
|
id: "user-a",
|
|
username: "admin",
|
|
display_name: "Administrator",
|
|
role: "admin",
|
|
enabled: true,
|
|
must_change_password: false,
|
|
scopes: ["*"],
|
|
created_at: "2026-01-01T00:00:00+00:00",
|
|
updated_at: "2026-01-01T00:00:00+00:00",
|
|
last_login_at: null,
|
|
}),
|
|
);
|
|
|
|
await login("admin", "correct-horse-battery-staple");
|
|
|
|
expect(fetch).toHaveBeenCalledWith(
|
|
expect.stringMatching(/\/v1\/auth\/login$/),
|
|
expect.objectContaining({
|
|
credentials: "include",
|
|
headers: expect.not.objectContaining({ Authorization: expect.any(String) }),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("uses CSRF proof for session-authenticated writes", async () => {
|
|
document.cookie = "amcp_csrf=csrf-value; path=/";
|
|
vi.mocked(fetch).mockResolvedValueOnce(
|
|
response({ name: "demo", version: "1", entry_point_kind: "tool", target: "m:t", wired: false }),
|
|
);
|
|
|
|
await registerPlugin({
|
|
name: "demo",
|
|
version: "1",
|
|
entry_point_kind: "tool",
|
|
target: "m:t",
|
|
});
|
|
|
|
expect(fetch).toHaveBeenCalledWith(
|
|
expect.stringMatching(/\/v1\/plugins$/),
|
|
expect.objectContaining({
|
|
credentials: "include",
|
|
headers: expect.objectContaining({ "X-CSRF-Token": "csrf-value" }),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("sends a Provider profile write through the CSRF-protected management API", async () => {
|
|
document.cookie = "amcp_csrf=csrf-value; path=/";
|
|
vi.mocked(fetch).mockResolvedValueOnce(
|
|
response({
|
|
id: "provider-a",
|
|
name: "OpenAI compatible",
|
|
provider_type: "openai-compatible",
|
|
model: "model-a",
|
|
base_url: "https://compat.example/v1",
|
|
timeout_seconds: 30,
|
|
enabled: true,
|
|
revision: 1,
|
|
has_api_key: true,
|
|
key_last_rotated_at: "2026-01-01T00:00:00+00:00",
|
|
created_at: "2026-01-01T00:00:00+00:00",
|
|
updated_at: "2026-01-01T00:00:00+00:00",
|
|
active: false,
|
|
}),
|
|
);
|
|
|
|
await createLlmProviderProfile({
|
|
name: "OpenAI compatible",
|
|
provider_type: "openai-compatible",
|
|
model: "model-a",
|
|
base_url: "https://compat.example/v1",
|
|
timeout_seconds: 30,
|
|
api_key: "secret-value",
|
|
});
|
|
|
|
expect(fetch).toHaveBeenCalledWith(
|
|
expect.stringMatching(/\/v1\/planner\/providers$/),
|
|
expect.objectContaining({
|
|
method: "POST",
|
|
headers: expect.objectContaining({ "X-CSRF-Token": "csrf-value" }),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("invalidates the session only on 401", async () => {
|
|
const invalidated = vi.fn();
|
|
window.addEventListener(AUTH_INVALID_EVENT, invalidated);
|
|
vi.mocked(fetch).mockResolvedValueOnce(response({ detail: "unauthorized" }, 401));
|
|
|
|
await expect(listDevices()).rejects.toMatchObject({ status: 401 });
|
|
expect(invalidated).toHaveBeenCalledTimes(1);
|
|
|
|
vi.mocked(fetch).mockResolvedValueOnce(response({ detail: "forbidden" }, 403));
|
|
await expect(listDevices()).rejects.toMatchObject({ status: 403 });
|
|
expect(invalidated).toHaveBeenCalledTimes(1);
|
|
window.removeEventListener(AUTH_INVALID_EVENT, invalidated);
|
|
});
|
|
});
|