/** * @file AuthGate.test.tsx * @description The login gate: local mode renders the app untouched, server * mode shows the login page until a login succeeds, a wrong password says so, * and a later 401 `EAUTH` brings the login page back. `usePermissions` and * `useWorkLanes` mirror the server's roles: an admin changes everything, an * analyst reads everything but works only in their own workspaces, a * developer only ever sees (and so changes) their own. The login page wears the * OS's light/dark; a sign-in swaps in the account's saved appearance. * @author Nguyễn Ngọc Trí Vĩ */ import { describe, it, expect, vi, beforeEach } from "vitest"; import { act, cleanup, render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import "../../i18n"; const me = vi.fn(); const login = vi.fn(); const prefs = vi.fn(); const LANES = [ { id: 1, owner_id: 5 }, { id: 2, owner_id: 6 }, ]; vi.mock("../../lib/api", () => ({ AUTH_REQUIRED_EVENT: "agenthub:auth-required", api: { auth: { me: () => me(), login: (u: string, p: string) => login(u, p), logout: vi.fn() }, lanes: { list: () => Promise.resolve({ lanes: LANES }) }, me: { prefs: () => prefs(), savePrefs: () => Promise.resolve({ prefs: {} }) }, }, })); import { AuthGate, usePermissions } from "../AuthGate"; import { useWorkLanes } from "../../hooks/useLanes"; function Probe() { const p = usePermissions(); const { lanes } = useWorkLanes(); return (

{[ `admin:${p.isAdmin}`, `all:${p.seesAll}`, `mine:${p.canEditLane({ owner_id: 5 })}`, `theirs:${p.canEditLane({ owner_id: 6 })}`, `work:${lanes.map((l) => l.id).join(",")}`, ].join(" ")}

); } const user = { id: 1, username: "mia", role: "developer" }; beforeEach(() => { me.mockReset(); login.mockReset(); prefs.mockReset().mockResolvedValue({ prefs: {} }); localStorage.clear(); }); describe("AuthGate", () => { it("renders the app in local mode", async () => { me.mockResolvedValue({ mode: "local" }); render(app); expect(await screen.findByText("app")).toBeInTheDocument(); }); it("paints at once in a browser that last saw local mode", () => { localStorage.setItem("agenthub.authMode", "local"); me.mockReturnValue(new Promise(() => {})); render(app); expect(screen.getByText("app")).toBeInTheDocument(); }); it("asks again instead of guessing when the server does not answer", async () => { vi.useFakeTimers({ shouldAdvanceTime: true }); try { me.mockRejectedValueOnce(new Error("502")); me.mockResolvedValue({ mode: "server", user: null }); render(app); expect(screen.queryByText("app")).toBeNull(); await act(async () => { await vi.advanceTimersByTimeAsync(2100); }); expect(await screen.findByLabelText(/username/i)).toBeInTheDocument(); expect(screen.queryByText("app")).toBeNull(); } finally { vi.useRealTimers(); } }); it("renders the app when an older server or a dashboard token answers instead", async () => { for (const err of [ { status: 404 }, { status: 200, code: "EBADRESPONSE" }, { status: 401, code: "EUNAUTHORIZED" }, ]) { me.mockReset(); me.mockRejectedValue(Object.assign(new Error("x"), err)); const { unmount } = render(app); expect(await screen.findByText("app")).toBeInTheDocument(); unmount(); } }); it("asks for a login in server mode and opens the app after one", async () => { me.mockResolvedValue({ mode: "server", user: null }); login.mockRejectedValueOnce(Object.assign(new Error("x"), { code: "EBADLOGIN" })); login.mockResolvedValueOnce({ user }); render(app); const name = await screen.findByLabelText(/username/i); const pass = screen.getByLabelText(/^password$/i); await userEvent.type(name, "mia"); await userEvent.type(pass, "nope"); await userEvent.click(screen.getByRole("button", { name: /sign in/i })); expect(await screen.findByRole("alert")).toBeInTheDocument(); expect(screen.queryByText("app")).toBeNull(); await userEvent.type(pass, "member-pass-1"); await userEvent.click(screen.getByRole("button", { name: /sign in/i })); expect(await screen.findByText("app")).toBeInTheDocument(); expect(login).toHaveBeenLastCalledWith("mia", "member-pass-1"); act(() => { window.dispatchEvent(new Event("agenthub:auth-required")); }); expect(await screen.findByLabelText(/username/i)).toBeInTheDocument(); }); }); describe("AuthGate theme", () => { it("wears the OS theme while signed out and the account's own once signed in", async () => { const html = document.documentElement; // The previous user's choice, left in this browser. localStorage.setItem("theme", "dark"); localStorage.setItem("agenthub.theme.dark", "darcula"); const mq = { matches: false, addEventListener: vi.fn(), removeEventListener: vi.fn() }; vi.stubGlobal("matchMedia", () => mq); // the OS is light me.mockResolvedValue({ mode: "server", user: null }); prefs.mockResolvedValue({ prefs: { theme: "dark", "agenthub.theme.dark": "orca-dark" } }); login.mockResolvedValue({ user }); render(app); await userEvent.type(await screen.findByLabelText(/username/i), "mia"); expect(html.dataset.mode).toBe("light"); expect(html.hasAttribute("data-theme")).toBe(false); await userEvent.type(screen.getByLabelText(/^password$/i), "member-pass-1"); await userEvent.click(screen.getByRole("button", { name: /sign in/i })); await waitFor(() => expect(html.dataset.theme).toBe("orca-dark")); expect(html.dataset.mode).toBe("dark"); expect(localStorage.getItem("agenthub.theme.dark")).toBe("orca-dark"); vi.unstubAllGlobals(); }); }); describe("usePermissions / useWorkLanes", () => { const as = async (state: unknown) => { cleanup(); me.mockResolvedValue(state); render( ); return (await screen.findByText(/work:\d/)).textContent; }; it("lets an admin and local mode do everything", async () => { for (const state of [ { mode: "local" }, { mode: "server", user: { id: 9, username: "boss", role: "admin" } }, ]) { expect(await as(state)).toBe("admin:true all:true mine:true theirs:true work:1,2"); } }); it("lets an analyst read everything but work only in their own workspaces", async () => { expect(await as({ mode: "server", user: { id: 5, username: "ana", role: "analyst" } })).toBe( "admin:false all:true mine:true theirs:false work:1" ); }); it("lets a developer act on everything they can see, and nothing host-wide", async () => { expect(await as({ mode: "server", user: { id: 5, username: "mia", role: "developer" } })).toBe( "admin:false all:false mine:true theirs:true work:1,2" ); }); });