import React from "react"; import "@testing-library/jest-dom"; import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react"; import { api } from "./api"; import { I18nProvider } from "./i18n/I18nProvider"; import { translations } from "./i18n/translations"; import { ToastProvider } from "./toast"; import AuthStatusCard from "./components/AuthStatusCard"; import SessionsSettingsCard from "./components/SessionsSettingsCard"; import TwoFactorSettingsCard from "./components/TwoFactorSettingsCard"; const mockedApi = api as jest.Mocked; const session = { id: "current", deviceLabel: "Current device", isCurrentSession: true, createdAtUtc: "2026-02-10T12:00:00Z", lastSeenAtUtc: "2026-02-10T12:00:00Z", expiresAtUtc: "2026-03-10T12:00:00Z", }; function renderCard(Card: React.ComponentType, language: "en" | "nb" = "en") { localStorage.setItem("uiLanguage", language); return render(); } beforeEach(() => { jest.clearAllMocks(); localStorage.clear(); }); afterEach(() => localStorage.clear()); test("[PRES-E02/E09] Bokmål signed-in status and section semantics use the UI language", async () => { mockedApi.get.mockResolvedValue({ data: { userName: "Demo", provider: "local" } } as any); renderCard(AuthStatusCard, "nb"); expect(await screen.findByText(/Demo/)).toHaveTextContent("Logget inn som Demo"); expect(screen.getByRole("region", { name: translations.nb.authStatusTitle })).toBeInTheDocument(); expect(screen.getByRole("heading", { level: 2, name: translations.nb.authStatusTitle })).toBeInTheDocument(); expect(mockedApi.post).not.toHaveBeenCalled(); }); test("[PRES-E02/E08] a failed identity read is unknown and retries without a logout or login write", async () => { mockedApi.get.mockRejectedValueOnce({ response: { status: 500, data: "Identity unavailable" } }) .mockResolvedValueOnce({ data: { userName: "Recovered", provider: "local" } } as any); renderCard(AuthStatusCard); const alert = await screen.findByRole("alert"); expect(alert).toHaveTextContent("Identity unavailable"); expect(screen.queryByText(translations.en.authStatusNotSignedIn)).not.toBeInTheDocument(); fireEvent.click(within(alert).getByRole("button", { name: translations.en.retry })); expect(await screen.findByText(/Signed in as Recovered/)).toBeInTheDocument(); expect(mockedApi.post).not.toHaveBeenCalled(); }); test("[PRES-E02] an authoritative unauthenticated identity read retains signed-out presentation", async () => { mockedApi.get.mockRejectedValueOnce({ response: { status: 401 } }); renderCard(AuthStatusCard); expect(await screen.findByText(translations.en.authStatusNotSignedIn)).toBeInTheDocument(); expect(screen.queryByRole("alert")).not.toBeInTheDocument(); expect(mockedApi.post).not.toHaveBeenCalled(); }); test("[PRES-E04] unknown 2FA status announces loading and cannot suggest setup", async () => { let resolveStatus!: (value: any) => void; mockedApi.get.mockImplementation((url: string) => url === "/auth/2fa/status" ? new Promise(resolve => { resolveStatus = resolve; }) : Promise.resolve({ data: [] } as any)); renderCard(TwoFactorSettingsCard); expect(screen.getByRole("status")).toHaveTextContent(translations.en.loading); expect(screen.queryByRole("button", { name: translations.en.twoFactorEnableButton })).not.toBeInTheDocument(); await act(async () => resolveStatus({ data: { enabled: false, enabledAtUtc: null } })); expect(await screen.findByRole("button", { name: translations.en.twoFactorEnableButton })).toBeEnabled(); expect(mockedApi.post).not.toHaveBeenCalled(); }); test("[PRES-E04/E08] failed 2FA status offers read-only retry before security actions", async () => { let attempts = 0; mockedApi.get.mockImplementation((url: string) => { if (url !== "/auth/2fa/status") return Promise.resolve({ data: [] } as any); if (++attempts === 1) return Promise.reject({ response: { data: "Status unavailable" } }); return Promise.resolve({ data: { enabled: true, enabledAtUtc: "2026-02-10T12:00:00Z" } } as any); }); renderCard(TwoFactorSettingsCard); const alert = await screen.findByRole("alert"); expect(alert).toHaveTextContent("Status unavailable"); expect(screen.queryByRole("button", { name: translations.en.twoFactorEnableButton })).not.toBeInTheDocument(); fireEvent.click(within(alert).getByRole("button", { name: translations.en.retry })); expect(await screen.findByRole("button", { name: translations.en.twoFactorDisableButton })).toBeEnabled(); expect(attempts).toBe(2); expect(mockedApi.post).not.toHaveBeenCalled(); }); test("[PRES-E05/E08] session-revocation failure stays inside its named confirmation for retry", async () => { mockedApi.get.mockResolvedValue({ data: [session, { ...session, id: "other", isCurrentSession: false }] } as any); mockedApi.post.mockRejectedValueOnce({ response: { data: "Revocation unavailable" } }).mockResolvedValueOnce({ data: {} } as any); renderCard(SessionsSettingsCard); fireEvent.click(await screen.findByRole("button", { name: translations.en.sessionsRevokeOthers })); const dialog = screen.getByRole("dialog", { name: translations.en.sessionsRevokeOthersConfirmTitle }); fireEvent.click(within(dialog).getByRole("button", { name: translations.en.sessionsRevokeOthers })); expect(await within(dialog).findByRole("alert")).toHaveTextContent("Revocation unavailable"); expect(mockedApi.post).toHaveBeenNthCalledWith(1, "/auth/sessions/revoke-others"); fireEvent.click(within(dialog).getByRole("button", { name: translations.en.sessionsRevokeOthers })); await waitFor(() => expect(screen.queryByRole("dialog")).not.toBeInTheDocument()); expect(mockedApi.post).toHaveBeenNthCalledWith(2, "/auth/sessions/revoke-others"); }); test("[PRES-E05/E09] sessions keep stored timestamps and render dates in Bokmål", async () => { mockedApi.get.mockResolvedValue({ data: [session] } as any); renderCard(SessionsSettingsCard, "nb"); expect(await screen.findByText(/Sist aktiv/)).toHaveTextContent( new Date(session.lastSeenAtUtc).toLocaleString("nb-NO")); expect(screen.getByText(/Sist aktiv/)).toHaveTextContent( new Date(session.expiresAtUtc).toLocaleDateString("nb-NO")); expect(screen.getByRole("region", { name: translations.nb.sessionsSectionTitle })).toBeInTheDocument(); expect(mockedApi.delete).not.toHaveBeenCalled(); });