feat(cloud-console): add user authentication and administration
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
AUTH_INVALID_EVENT,
|
||||
clearStoredToken,
|
||||
getStoredToken,
|
||||
listDevices,
|
||||
login,
|
||||
registerPlugin,
|
||||
storeToken,
|
||||
} 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(() => {
|
||||
clearStoredToken();
|
||||
document.cookie = "amcp_csrf=; Max-Age=0; path=/";
|
||||
vi.stubGlobal("fetch", vi.fn());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
clearStoredToken();
|
||||
});
|
||||
|
||||
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("keeps the explicit compatibility bearer-token path", async () => {
|
||||
storeToken("compatibility-token");
|
||||
vi.mocked(fetch).mockResolvedValueOnce(response([]));
|
||||
|
||||
await listDevices();
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/\/v1\/devices$/),
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({ Authorization: "Bearer compatibility-token" }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("clears authentication only on 401, not on 403", async () => {
|
||||
storeToken("compatibility-token");
|
||||
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(getStoredToken()).toBeNull();
|
||||
expect(invalidated).toHaveBeenCalledTimes(1);
|
||||
|
||||
storeToken("compatibility-token");
|
||||
vi.mocked(fetch).mockResolvedValueOnce(response({ detail: "forbidden" }, 403));
|
||||
await expect(listDevices()).rejects.toMatchObject({ status: 403 });
|
||||
expect(getStoredToken()).toBe("compatibility-token");
|
||||
window.removeEventListener(AUTH_INVALID_EVENT, invalidated);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user