import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; 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); }); });