113 lines
6.3 KiB
TypeScript
113 lines
6.3 KiB
TypeScript
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<typeof api>;
|
|
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(<ToastProvider><I18nProvider><Card /></I18nProvider></ToastProvider>);
|
|
}
|
|
|
|
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();
|
|
});
|