import { act, render, screen, waitFor, within } 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"; import { workspacePreferences } from "../workspaces/preferences"; 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(); workspacePreferences.reset(); 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("does not use the legacy installation default as a preprocessing workspace", async () => { const requestedWorkspaceIds: string[] = []; server.use( http.get("/api/settings", () => HttpResponse.json({ workspace: "local", provider: "test", model: "test", thinking: "low", })), http.get("/api/workspaces", () => HttpResponse.json([{ id: "psd-clinical", name: "psd-clinical", file: "psd-clinical/workspace.yaml", displayName: "Policlinico San Donato", configurationState: "ready", revision: { id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/tmp/psd-clinical.yaml", }, }])), http.get("/api/workspaces/:workspaceId/preprocessing", ({ params }) => { requestedWorkspaceIds.push(String(params.workspaceId)); return HttpResponse.json({ schemaVersion: 1, workspaceId: String(params.workspaceId), state: "ready", actionable: true, clearable: true, detail: "Workspace preprocessing is current.", }); }), ); const user = userEvent.setup(); renderShell({ subject: "admin-1", isAdmin: true, roles: ["admin"], permissions: ["session.use", "database.manage"], }); const administration = await screen.findByRole("button", { name: "Administration" }); await user.click(administration); await waitFor(() => expect(requestedWorkspaceIds).toContain("psd-clinical")); expect(requestedWorkspaceIds).not.toContain("local"); }); test("hides administrative navigation from a session user", async () => { renderShell({ subject: "user-1", isAdmin: false, roles: ["user"], permissions: ["session.use"] }); expect(await screen.findByRole("button", { name: "New session" })).toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Administration" })).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Database management" })).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Workspace management" })).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Pi management" })).not.toBeInTheDocument(); expect(screen.queryByRole("tab", { name: "All sessions" })).not.toBeInTheDocument(); }); test("shows the ordered administrative accordion only to an admin", async () => { const user = userEvent.setup(); renderShell({ subject: "admin-1", isAdmin: true, roles: ["admin"], permissions: ["session.use", "session.read_all", "workspace.manage", "workspace.secrets.manage", "database.manage", "pi.manage"], }); const sessionNavigation = screen.getByRole("complementary", { name: "Session navigation" }); const trigger = await within(sessionNavigation).findByRole("button", { name: "Administration" }); expect(trigger).toHaveClass("font-sans"); expect(trigger).toHaveAttribute("aria-expanded", "false"); expect(within(sessionNavigation).queryByRole("region", { name: "Administration" })).not.toBeInTheDocument(); trigger.focus(); await user.keyboard("{Enter}"); expect(trigger).toHaveAttribute("aria-expanded", "true"); const panel = within(sessionNavigation).getByRole("region", { name: "Administration" }); const database = within(panel).getByRole("button", { name: "Database management" }); const separator = within(panel).getByRole("separator"); const workspace = within(panel).getByRole("button", { name: "Workspace management" }); const pi = within(panel).getByRole("button", { name: "Pi management" }); const preprocessing = within(panel).getByRole("region", { name: "Workspace preprocessing" }); expect(panel.children[0]).toBe(database); expect(panel.children[1]).toBe(separator); expect(panel.children[2]).toBe(workspace); expect(panel.children[3]).toBe(pi); expect(panel.children[4]).toBe(preprocessing); expect(screen.getByRole("tab", { name: "All sessions" })).toBeInTheDocument(); await user.keyboard(" "); expect(trigger).toHaveAttribute("aria-expanded", "false"); expect(within(sessionNavigation).queryByRole("region", { name: "Administration" })).not.toBeInTheDocument(); expect(within(sessionNavigation).queryByRole("button", { name: "Database management" })).not.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: "Administration" })).toBeInTheDocument(); expect(screen.getByRole("tab", { name: "All sessions" })).toBeInTheDocument(); act(() => clearAuthState()); expect(screen.queryByRole("button", { name: "Administration" })).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Workspace management" })).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Pi management" })).not.toBeInTheDocument(); expect(screen.queryByRole("tab", { name: "All sessions" })).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); }); });