import { act, render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { http, HttpResponse, delay } from "msw"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { StrictMode } from "react"; import { LoginPage } from "./LoginPage"; import { clearAuthState, setAuthState } from "./authState"; import * as authApi from "../api/auth"; import { server } from "../test/msw"; const localConfig = { mode: "local", localLogin: true, oidcLogin: false } as const; const oidcConfig = { mode: "oidc", localLogin: false, oidcLogin: true } as const; const authenticated = { issuer: "local", subject: "admin-1", roles: ["admin"] as const, permissions: ["session.use", "pi.manage"], isAdmin: true, csrfToken: "c".repeat(43), session: { method: "local" as const, remembered: true, idleExpiresAt: "2026-08-17T10:00:00.000Z", absoluteExpiresAt: "2026-09-16T10:00:00.000Z", }, }; beforeEach(() => clearAuthState()); afterEach(() => clearAuthState()); describe("LoginPage", () => { test("shows an unchecked local Remember me control and clears the password after failure", async () => { let submittedPassword = ""; server.use(http.post("/api/auth/local/login", async ({ request }) => { const body = await request.json() as { password: string; remember: boolean }; submittedPassword = body.password; expect(body.remember).toBe(false); return HttpResponse.json({ code: "invalid_credentials" }, { status: 401 }); })); render(); expect(screen.getByRole("checkbox", { name: /remember me/i })).not.toBeChecked(); await userEvent.type(screen.getByLabelText(/username/i), "alice"); await userEvent.type(screen.getByLabelText(/password/i), "not-a-real-password"); await userEvent.click(screen.getByRole("button", { name: /sign in/i })); expect(await screen.findByRole("alert")).toHaveTextContent(/invalid username or password/i); expect(submittedPassword).toBe("not-a-real-password"); expect(screen.getByLabelText(/password/i)).toHaveValue(""); }); test("submits a remembered local login once and returns the safe user", async () => { let requests = 0; server.use( http.post("/api/auth/local/login", async ({ request }) => { requests += 1; const body = await request.json() as { remember: boolean }; expect(body.remember).toBe(true); await delay(20); return HttpResponse.json({}); }), http.get("/api/me", () => HttpResponse.json(authenticated)), ); const onAuthenticated = vi.fn(); render(); await userEvent.click(screen.getByRole("checkbox", { name: /remember me/i })); await userEvent.type(screen.getByLabelText(/username/i), "admin"); await userEvent.type(screen.getByLabelText(/password/i), "correct-password"); const submit = screen.getByRole("button", { name: /sign in/i }); await Promise.all([userEvent.click(submit), userEvent.click(submit)]); expect(requests).toBe(1); await vi.waitFor(() => expect(onAuthenticated).toHaveBeenCalledWith(expect.objectContaining({ subject: "admin-1" }))); expect(screen.getByLabelText(/password/i)).toHaveValue(""); }); test("shows OIDC only when public configuration enables it", () => { const { rerender } = render(); expect(screen.queryByRole("link", { name: /single sign-on/i })).not.toBeInTheDocument(); rerender(); expect(screen.getByRole("button", { name: /single sign-on/i })).toBeEnabled(); }); test("routes every rendered SSO affordance through the held logout barrier", async () => { let releaseLogout!: () => void; let logoutStarted!: () => void; const logoutGate = new Promise((resolve) => { releaseLogout = resolve; }); const logoutRequest = new Promise((resolve) => { logoutStarted = resolve; }); server.use(http.post("/api/auth/logout", async () => { logoutStarted(); await logoutGate; return new HttpResponse(null, { status: 204 }); })); setAuthState({ ...authenticated, subject: "user-a" }); const logoutPromise = authApi.logout(); await logoutRequest; const navigate = vi.fn(); const beginOidcLogin = authApi.beginOidcLogin; const begin = vi.spyOn(authApi, "beginOidcLogin") .mockImplementation(() => beginOidcLogin(navigate)); try { render(); const buttons = screen.getAllByRole("button", { name: /single sign-on/i }); expect(screen.queryAllByRole("link", { name: /single sign-on/i })).toHaveLength(0); expect(buttons).toHaveLength(1); await userEvent.click(buttons[0]); await Promise.resolve(); expect(begin).toHaveBeenCalledOnce(); expect(navigate).not.toHaveBeenCalled(); releaseLogout(); await Promise.all([logoutPromise, vi.waitFor(() => expect(navigate).toHaveBeenCalledWith("/api/auth/oidc/login"))]); } finally { releaseLogout(); begin.mockRestore(); } }); test("does not dispatch local login until an in-flight logout response settles", async () => { let releaseLogout!: () => void; let logoutStarted!: () => void; let loginDispatched = false; const logoutGate = new Promise((resolve) => { releaseLogout = resolve; }); const logoutRequest = new Promise((resolve) => { logoutStarted = resolve; }); server.use( http.post("/api/auth/logout", async () => { logoutStarted(); await logoutGate; return new HttpResponse(null, { status: 204 }); }), http.post("/api/auth/local/login", () => { loginDispatched = true; return HttpResponse.json({}); }), http.get("/api/me", () => HttpResponse.json(authenticated)), ); setAuthState({ ...authenticated, subject: "user-a" }); const logoutPromise = authApi.logout(); await logoutRequest; const loginPromise = authApi.loginLocal("admin", "password", false); await new Promise((resolve) => setTimeout(resolve, 20)); expect(loginDispatched).toBe(false); releaseLogout(); await expect(logoutPromise).resolves.toBe(true); await loginPromise; expect(loginDispatched).toBe(true); }); test("a failed logout response does not deadlock the next local login", async () => { let logoutCalls = 0; let loginDispatched = false; server.use( http.post("/api/auth/logout", () => { logoutCalls += 1; return HttpResponse.json({ code: "auth_unavailable" }, { status: 503 }); }), http.post("/api/auth/local/login", () => { loginDispatched = true; return HttpResponse.json({}); }), http.get("/api/me", () => HttpResponse.json(authenticated)), ); setAuthState({ ...authenticated, subject: "user-a" }); await expect(authApi.logout()).rejects.toMatchObject({ status: 503 }); await authApi.loginLocal("admin", "password", false); expect(logoutCalls).toBe(1); expect(loginDispatched).toBe(true); }); test("uses explicit provider-unavailable copy with retry affordance", async () => { server.use(http.post("/api/auth/local/login", () => HttpResponse.json( { code: "auth_unavailable" }, { status: 503 }, ))); const retry = vi.fn(); render(); await userEvent.type(screen.getByLabelText(/username/i), "alice"); await userEvent.type(screen.getByLabelText(/password/i), "correct-password"); await userEvent.click(screen.getByRole("button", { name: /sign in/i })); expect(await screen.findByRole("alert")).toHaveTextContent(/temporarily unavailable/i); await userEvent.click(screen.getByRole("button", { name: /retry/i })); expect(retry).toHaveBeenCalledOnce(); }); test("never writes the password, remember choice, or returned token to browser storage", async () => { server.use( http.post("/api/auth/local/login", () => HttpResponse.json({})), http.get("/api/me", () => HttpResponse.json(authenticated)), ); const storageWrites = (["setItem", "removeItem", "clear"] as const).map((method) => vi.spyOn(Storage.prototype, method)); render(); await userEvent.type(screen.getByLabelText(/username/i), "alice"); await userEvent.type(screen.getByLabelText(/password/i), "correct-password"); await userEvent.click(screen.getByRole("button", { name: /sign in/i })); await vi.waitFor(() => expect(screen.getByLabelText(/password/i)).toHaveValue("")); for (const write of storageWrites) expect(write).not.toHaveBeenCalled(); for (const write of storageWrites) write.mockRestore(); }); test("settles a successful login on the current StrictMode mount", async () => { server.use( http.post("/api/auth/local/login", () => HttpResponse.json({})), http.get("/api/me", () => HttpResponse.json(authenticated)), ); const errors = vi.spyOn(console, "error").mockImplementation(() => undefined); const onAuthenticated = vi.fn(); try { render(); await userEvent.type(screen.getByLabelText(/username/i), "admin"); await userEvent.type(screen.getByLabelText(/password/i), "correct-password"); await userEvent.click(screen.getByRole("button", { name: /sign in/i })); await vi.waitFor(() => expect(onAuthenticated).toHaveBeenCalledOnce()); expect(screen.getByLabelText(/password/i)).toHaveValue(""); expect(screen.getByRole("button", { name: /sign in/i })).toBeEnabled(); expect(errors.mock.calls.flat().join(" ")).not.toMatch(/not wrapped in act/i); } finally { errors.mockRestore(); } }); test("settles a failed login and clears the password on the current StrictMode mount", async () => { server.use(http.post("/api/auth/local/login", () => HttpResponse.json({ code: "invalid_credentials" }, { status: 401 }))); const errors = vi.spyOn(console, "error").mockImplementation(() => undefined); try { render(); await userEvent.type(screen.getByLabelText(/username/i), "admin"); await userEvent.type(screen.getByLabelText(/password/i), "wrong-password"); await userEvent.click(screen.getByRole("button", { name: /sign in/i })); expect(await screen.findByRole("alert")).toHaveTextContent(/invalid username or password/i); expect(screen.getByLabelText(/password/i)).toHaveValue(""); expect(screen.getByRole("button", { name: /sign in/i })).toBeEnabled(); expect(errors.mock.calls.flat().join(" ")).not.toMatch(/not wrapped in act/i); } finally { errors.mockRestore(); } }); test("does not update an unmounted StrictMode login attempt", async () => { let release!: () => void; const pending = new Promise((resolve) => { release = resolve; }); let settled!: () => void; const settledAfterUnmount = new Promise((resolve) => { settled = resolve; }); const loginLocal = vi.spyOn(authApi, "loginLocal").mockImplementation(async () => { try { await pending; throw new Error("deferred invalid credentials"); } finally { settled(); } }); const errors = vi.spyOn(console, "error").mockImplementation(() => undefined); try { const view = render(); await userEvent.type(screen.getByLabelText(/username/i), "admin"); await userEvent.type(screen.getByLabelText(/password/i), "wrong-password"); await userEvent.click(screen.getByRole("button", { name: /sign in/i })); view.unmount(); release(); await act(async () => { await settledAfterUnmount; }); expect(loginLocal).toHaveBeenCalledOnce(); expect(errors.mock.calls.flat().join(" ")).not.toMatch(/can't perform a react state update|not wrapped in act/i); } finally { loginLocal.mockRestore(); errors.mockRestore(); } }); });