349 lines
11 KiB
JavaScript
Raw Normal View History

import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals";
const stubbedGlobals = new Map();
const vi = {
fn: (...args) => jest.fn(...args),
spyOn: (...args) => jest.spyOn(...args),
resetModules: () => jest.resetModules(),
restoreAllMocks: () => jest.restoreAllMocks(),
stubGlobal: (name, value) => {
if (!stubbedGlobals.has(name)) {
stubbedGlobals.set(name, Object.getOwnPropertyDescriptor(globalThis, name));
}
Object.defineProperty(globalThis, name, {
configurable: true,
writable: true,
value,
});
},
unstubAllGlobals: () => {
for (const [name, descriptor] of stubbedGlobals.entries()) {
if (descriptor) {
Object.defineProperty(globalThis, name, descriptor);
} else {
Reflect.deleteProperty(globalThis, name);
}
}
stubbedGlobals.clear();
},
};
2026-04-11 00:02:26 -03:00
async function loadAuth() {
vi.resetModules();
return import("../../../frontend/src/auth.js");
}
describe("auth helpers", () => {
beforeEach(() => {
globalThis.__ATLAS_KEYCLOAK_FACTORY__ = null;
globalThis.__ATLAS_KEYCLOAK_CONSTRUCTOR__ = null;
});
afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
vi.resetModules();
});
it("normalizes token groups", async () => {
const { normalizeGroups } = await loadAuth();
expect(normalizeGroups(["/dev", "admin", 3, null, ""])).toEqual(["dev", "admin"]);
expect(normalizeGroups("not-an-array")).toEqual([]);
});
it("attaches a bearer token when fetching", async () => {
const authModule = await loadAuth();
authModule.auth.token = "bearer-token";
const fetchMock = vi.fn(async () => new Response("{}", { status: 200 }));
vi.stubGlobal("fetch", fetchMock);
await authModule.authFetch("/api/healthz");
expect(fetchMock).toHaveBeenCalledTimes(1);
const [, options] = fetchMock.mock.calls[0];
expect(new Headers(options.headers).get("Authorization")).toBe("Bearer bearer-token");
});
it("initializes auth state from the auth config endpoint", async () => {
const authModule = await loadAuth();
vi.spyOn(window, "setInterval").mockReturnValue(1234);
vi.stubGlobal(
"fetch",
vi.fn(async () =>
new Response(JSON.stringify({ enabled: false, login_url: "", reset_url: "" }), {
status: 200,
headers: { "content-type": "application/json" },
}),
),
);
await authModule.initAuth();
expect(authModule.auth.ready).toBe(true);
expect(authModule.auth.enabled).toBe(false);
});
it("falls back to the keycloak-js constructor when no injected factory exists", async () => {
const client = {
authenticated: true,
token: "mock-token",
tokenParsed: {
preferred_username: "bob",
email: "bob@example.dev",
groups: ["/ops"],
},
init: vi.fn(async () => true),
login: vi.fn(async () => {}),
logout: vi.fn(async () => {}),
updateToken: vi.fn(async () => true),
};
globalThis.__ATLAS_KEYCLOAK_CONSTRUCTOR__ = function MockKeycloak() {
return client;
};
const authModule = await loadAuth();
const created = authModule.createKeycloak({ url: "https://sso.example.dev" });
expect(created).toBe(client);
});
it("uses the real Keycloak constructor when no test hooks are present", async () => {
const authModule = await loadAuth();
const client = authModule.createKeycloak({
url: "https://sso.example.dev",
realm: "atlas",
clientId: "portal-client",
});
expect(client).toHaveProperty("init");
expect(client).toHaveProperty("login");
expect(client).toHaveProperty("logout");
});
it("reuses the auth initialization promise and tolerates empty token fields", async () => {
const client = {
authenticated: false,
token: "",
tokenParsed: undefined,
init: vi.fn(async () => true),
login: vi.fn(async () => {}),
logout: vi.fn(async () => {}),
updateToken: vi.fn(async () => true),
};
globalThis.__ATLAS_KEYCLOAK_FACTORY__ = () => client;
const authModule = await loadAuth();
vi.spyOn(window, "setInterval").mockReturnValue(1234);
vi.stubGlobal(
"fetch",
vi.fn(async () =>
new Response(
JSON.stringify({
enabled: true,
url: "https://sso.example.dev",
realm: "atlas",
client_id: "portal-client",
login_url: "https://sso.example.dev/login",
reset_url: "https://sso.example.dev/reset",
account_url: "https://sso.example.dev/account",
account_password_url: "https://sso.example.dev/account/#/security/signingin",
}),
{
status: 200,
headers: { "content-type": "application/json" },
},
),
),
);
const first = authModule.initAuth();
const second = authModule.initAuth();
await first;
await second;
client.onAuthSuccess();
expect(authModule.auth.authenticated).toBe(false);
expect(authModule.auth.username).toBe("");
expect(authModule.auth.email).toBe("");
expect(authModule.auth.groups).toEqual([]);
});
it("hydrates and proxies keycloak actions when enabled", async () => {
const calls = { init: 0, login: 0, logout: 0, updateToken: 0 };
const client = {
authenticated: true,
token: "mock-token",
tokenParsed: {
preferred_username: "alice",
email: "alice@example.dev",
groups: ["/dev", "/admin"],
},
init: vi.fn(async () => {
calls.init += 1;
return true;
}),
login: vi.fn(async () => {
calls.login += 1;
}),
logout: vi.fn(async () => {
calls.logout += 1;
}),
updateToken: vi.fn(async () => {
calls.updateToken += 1;
return true;
}),
};
globalThis.__ATLAS_KEYCLOAK_FACTORY__ = () => client;
const authModule = await loadAuth();
vi.spyOn(window, "setInterval").mockReturnValue(1234);
vi.stubGlobal(
"fetch",
vi.fn(async () =>
new Response(
JSON.stringify({
enabled: true,
url: "https://sso.example.dev",
realm: "atlas",
client_id: "portal-client",
login_url: "https://sso.example.dev/login",
reset_url: "https://sso.example.dev/reset",
account_url: "https://sso.example.dev/account",
account_password_url: "https://sso.example.dev/account/#/security/signingin",
}),
{
status: 200,
headers: { "content-type": "application/json" },
},
),
),
);
await authModule.initAuth();
await authModule.login("/account", " ");
await authModule.login("/account", "alice");
await authModule.logout();
await authModule.authFetch("/api/healthz");
expect(calls.init).toBe(1);
expect(calls.login).toBe(2);
expect(calls.logout).toBe(1);
expect(calls.updateToken).toBeGreaterThan(0);
expect(client.login.mock.calls[0][0]).not.toHaveProperty("loginHint");
expect(client.login.mock.calls[1][0]).toMatchObject({ loginHint: "alice" });
expect(authModule.auth.ready).toBe(true);
expect(authModule.auth.enabled).toBe(true);
expect(authModule.auth.authenticated).toBe(true);
expect(authModule.auth.username).toBe("alice");
expect(authModule.auth.email).toBe("alice@example.dev");
expect(authModule.auth.groups).toEqual(["dev", "admin"]);
expect(authModule.auth.token).toBe("mock-token");
});
it("covers the auth refresh handlers and polling branches", async () => {
const intervalCalls = [];
const client = {
authenticated: true,
token: "refresh-token",
tokenParsed: {
preferred_username: "carol",
email: "carol@example.dev",
groups: ["/ops"],
},
init: vi.fn(async () => true),
login: vi.fn(async () => {}),
logout: vi.fn(async () => {}),
updateToken: vi.fn(async () => true),
};
globalThis.__ATLAS_KEYCLOAK_FACTORY__ = () => client;
const authModule = await loadAuth();
vi.spyOn(window, "setInterval").mockImplementation((callback) => {
intervalCalls.push(callback);
return 1234;
});
vi.stubGlobal(
"fetch",
vi.fn(async () =>
new Response(
JSON.stringify({
enabled: true,
url: "https://sso.example.dev",
realm: "atlas",
client_id: "portal-client",
login_url: "https://sso.example.dev/login",
reset_url: "https://sso.example.dev/reset",
account_url: "https://sso.example.dev/account",
account_password_url: "https://sso.example.dev/account/#/security/signingin",
}),
{
status: 200,
headers: { "content-type": "application/json" },
},
),
),
);
await authModule.initAuth();
expect(intervalCalls).toHaveLength(1);
client.onAuthSuccess();
client.onAuthLogout();
client.onAuthRefreshSuccess();
client.updateToken.mockResolvedValueOnce(true);
client.onTokenExpired();
await Promise.resolve();
client.updateToken.mockRejectedValueOnce(new Error("boom"));
client.onTokenExpired();
await Promise.resolve();
client.updateToken.mockResolvedValueOnce(true);
intervalCalls[0]();
await Promise.resolve();
client.authenticated = false;
intervalCalls[0]();
await Promise.resolve();
client.authenticated = true;
client.updateToken.mockRejectedValueOnce(new Error("refresh failed"));
await authModule.authFetch("/api/healthz");
expect(authModule.auth.username).toBe("carol");
expect(authModule.auth.groups).toEqual(["ops"]);
expect(client.updateToken).toHaveBeenCalled();
});
it("leaves auth alone when login/logout are called before initialization", async () => {
const authModule = await loadAuth();
const fetchMock = vi.fn(async () => new Response("{}", { status: 200 }));
vi.stubGlobal("fetch", fetchMock);
await authModule.login("/account", "alice");
await authModule.logout();
await authModule.authFetch("/api/healthz", { headers: { "X-Test": "1" } });
expect(fetchMock).toHaveBeenCalledTimes(1);
const [, options] = fetchMock.mock.calls[0];
const headers = new Headers(options.headers);
expect(headers.get("X-Test")).toBe("1");
expect(headers.get("Authorization")).toBeNull();
});
it("recovers when the auth config endpoint fails", async () => {
const authModule = await loadAuth();
vi.stubGlobal("fetch", vi.fn(async () => new Response("boom", { status: 500 })));
await authModule.initAuth();
expect(authModule.auth.ready).toBe(true);
expect(authModule.auth.enabled).toBe(false);
expect(authModule.auth.authenticated).toBe(false);
});
});