import { act, render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { http, HttpResponse } from "msw"; import { beforeEach, describe, expect, test, vi } from "vitest"; import { AppShell } from "./AppShell"; import { clearAuthState, getAuthGeneration, getAuthState, setAuthState, useAuthGeneration, useAuthUser } from "../auth/authState"; import { server } from "../test/msw"; import { useSessionStore } from "../store/sessionStore"; function renderShell(user: { subject: string; isAdmin: boolean; roles: readonly ("user" | "admin")[]; permissions: readonly string[]; }, canLogout = true) { const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); setAuthState({ issuer: "local", ...user, csrfToken: null, session: null, }); return render(); } function KeyedAuthenticatedShell() { const user = useAuthUser(); const generation = useAuthGeneration(); return user ? : null; } beforeEach(() => { clearAuthState(); useSessionStore.getState().resetSession(); server.use( http.get("/api/sessions", () => HttpResponse.json([])), http.get("/api/settings", () => HttpResponse.json({ workspace: "default", provider: "test", model: "test", thinking: "low" })), http.get("/api/workspaces", () => HttpResponse.json([])), http.get("/api/workspace-registry/status", () => HttpResponse.json({ branch: "main", ahead: 0, behind: 0, degraded: false, })), http.get("/api/models", () => HttpResponse.json({ models: [] })), http.get("/api/health/dwh", () => HttpResponse.json({ ok: true })), ); }); describe("authenticated shell permissions", () => { test("shows only read-safe workspace chrome to a session user", async () => { renderShell({ subject: "user-1", isAdmin: false, roles: ["user"], permissions: ["session.use"] }); expect(await screen.findByRole("button", { name: "Workspace management" })).toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Pi management" })).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: "All sessions" })).not.toBeInTheDocument(); }); test("shows management and all-session chrome only for exact permissions", async () => { renderShell({ subject: "admin-1", isAdmin: true, roles: ["admin"], permissions: ["session.use", "session.read_all", "workspace.manage", "workspace.secrets.manage", "pi.manage"], }); expect(await screen.findByRole("button", { name: "Pi management" })).toBeInTheDocument(); expect(screen.getByRole("button", { name: "All sessions" })).toBeInTheDocument(); }); test("keeps identity but hides logout outside local authentication", async () => { let logoutCalls = 0; server.use(http.post("/api/auth/logout", () => { logoutCalls += 1; return new HttpResponse(null, { status: 204 }); })); renderShell({ subject: "portal-user", isAdmin: false, roles: ["user"], permissions: ["session.use"], }, false); expect(await screen.findByText("portal-user")).toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Log out" })).not.toBeInTheDocument(); expect(logoutCalls).toBe(0); }); test("logout revokes the cookie session and clears in-memory auth", async () => { const user = { issuer: "local" as const, subject: "admin-1", roles: ["admin"] as const, permissions: ["session.use", "pi.manage"], isAdmin: true, csrfToken: "c".repeat(43), session: null, }; setAuthState(user); let logoutCalls = 0; server.use(http.post("/api/auth/logout", () => { logoutCalls += 1; return new HttpResponse(null, { status: 204 }); })); renderShell(user); await userEvent.click(screen.getByRole("button", { name: "Log out" })); await vi.waitFor(() => expect(logoutCalls).toBe(1)); expect(getAuthState()).toBeNull(); }); test("a stale logout continuation cannot scrub user B after the logout response settles", async () => { const userA = { issuer: "local" as const, subject: "user-a", roles: ["user"] as const, permissions: ["session.use"] as const, isAdmin: false, csrfToken: "a".repeat(43), session: null, }; const userB = { ...userA, subject: "user-b", csrfToken: "b".repeat(43) }; let releaseLogout!: () => void; let logoutStarted!: () => void; let logoutSettled!: () => void; const logoutGate = new Promise((resolve) => { releaseLogout = resolve; }); const started = new Promise((resolve) => { logoutStarted = resolve; }); const settled = new Promise((resolve) => { logoutSettled = resolve; }); server.use(http.post("/api/auth/logout", async () => { logoutStarted(); try { await logoutGate; return new HttpResponse(null, { status: 204 }); } finally { logoutSettled(); } })); const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); setAuthState(userA); render(); await userEvent.click(screen.getByRole("button", { name: "Log out" })); await started; act(() => setAuthState(userB)); act(() => { client.setQueryData(["b-only"], { owner: "user-b" }); useSessionStore.getState().applyEvent({ type: "text_delta", text: "B transcript" }); }); releaseLogout(); await act(async () => { await settled; }); expect(getAuthState()).toMatchObject({ subject: "user-b" }); expect(client.getQueryData(["b-only"])).toEqual({ owner: "user-b" }); expect(useSessionStore.getState().transcript).toEqual([{ role: "assistant", text: "B transcript" }]); expect(screen.getByRole("button", { name: "Log out" })).toBeInTheDocument(); }); test("handles a failed shell logout without an unhandled rejection", async () => { const user = { issuer: "local" as const, subject: "user-a", roles: ["user"] as const, permissions: ["session.use"] as const, isAdmin: false, csrfToken: "a".repeat(43), session: null, }; const rejection = vi.fn(); process.on("unhandledRejection", rejection); server.use(http.post("/api/auth/logout", () => HttpResponse.json( { code: "auth_unavailable" }, { status: 503 }, ))); try { renderShell(user); await userEvent.click(screen.getByRole("button", { name: "Log out" })); await waitFor(() => expect(getAuthState()).toBeNull()); await new Promise((resolve) => setImmediate(resolve)); expect(rejection).not.toHaveBeenCalled(); } finally { process.off("unhandledRejection", rejection); } }); test("permission chrome follows current auth state after a stale admin identity disappears", async () => { renderShell({ subject: "admin-1", isAdmin: true, roles: ["admin"], permissions: ["session.use", "session.read_all", "workspace.manage", "pi.manage"], }); expect(await screen.findByRole("button", { name: "Pi management" })).toBeInTheDocument(); expect(screen.getByRole("button", { name: "All sessions" })).toBeInTheDocument(); act(() => clearAuthState()); expect(screen.queryByRole("button", { name: "Pi management" })).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: "All sessions" })).not.toBeInTheDocument(); await userEvent.click(screen.getByRole("button", { name: "Workspace management" })); expect(await screen.findByRole("heading", { name: "Workspace management" })).toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Update workspace repository" })).not.toBeInTheDocument(); }); test("remounts the real shell so user-A panel and transcript state cannot survive user-B", async () => { const userA = { issuer: "local" as const, subject: "user-a", roles: ["user"] as const, permissions: ["session.use"] as const, isAdmin: false, }; const userB = { ...userA, subject: "user-b" }; const panelSession = { id: "panel-a", status: "finalized", question: "User A governed question", summary: null, created_at: "2026-08-17T10:00:00Z", updated_at: null, author: "user-a", name: null, group: null, archived: false, }; server.use( http.get("/api/sessions", () => HttpResponse.json([panelSession])), http.get("/api/sessions/panel-a/documents", () => HttpResponse.json([ { key: "question", title: "Question", phase: "F1", format: "markdown", content: "A-private-document" }, ])), ); setAuthState({ ...userA, csrfToken: null, session: null }); const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); function renderUserShell() { return render(); } renderUserShell(); await userEvent.click(await screen.findByTestId("session-item-panel-a")); expect(await screen.findByText("A-private-document")).toBeInTheDocument(); act(() => useSessionStore.getState().applyEvent({ type: "text_delta", text: "A-private-transcript" })); expect(useSessionStore.getState().transcript).toEqual([{ role: "assistant", text: "A-private-transcript" }]); act(() => setAuthState({ ...userB, csrfToken: null, session: null })); await waitFor(() => expect(screen.queryByText("A-private-document")).not.toBeInTheDocument()); expect(screen.queryByRole("heading", { name: "User A governed question" })).not.toBeInTheDocument(); expect(useSessionStore.getState().transcript).toEqual([]); expect(getAuthGeneration()).toBeGreaterThan(0); }); });