import "@testing-library/jest-dom"; import { fireEvent, render, screen, within } from "@testing-library/react"; import React from "react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import UserProfileSettings from "./UserProfileSettings"; const submitMock = vi.fn(); let searchParams = new URLSearchParams(); vi.mock("react-router", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, Form: ({ children, ...props }: React.FormHTMLAttributes) =>
{children}
, useLocation: () => ({ pathname: "/acme/settings/profile" }), useMatches: () => [], useNavigation: () => ({ state: "idle" }), useSearchParams: () => [searchParams], useSubmit: () => submitMock, }; }); vi.mock("react-i18next", () => ({ useTranslation: () => ({ i18n: { language: "en", exists: () => true }, t: (key: string, options?: Record) => { const translations: Record = { "settings.profile.mfa.authenticatorApp": "Authenticator app", "settings.profile.mfa.setupUri": "Authenticator setup URI", "settings.profile.mfa.confirmationCode": "Authenticator code", "settings.profile.mfa.confirmAuthenticator": "Confirm authenticator", "settings.profile.mfa.recoveryCodes": "Recovery codes", "settings.profile.mfa.recoveryCodesCopy": "Recovery codes copy value", "settings.profile.mfa.saveRecoveryCodes": "Save your recovery codes", "settings.profile.session.title": "Current session", "settings.profile.session.description": "Redacted diagnostics for this browser session only.", "settings.profile.session.lightDarkPreference": "Light/dark preference", "settings.profile.session.cookieConsentChoices": "Cookie consent choices", "settings.profile.session.impersonation": "Impersonation", "settings.profile.session.adminMfaStepUp": "Admin MFA step-up", "settings.profile.session.clearStepUpVerification": "Clear step-up verification for this session", }; let value = translations[key] ?? key; for (const [name, replacement] of Object.entries(options ?? {})) { value = value.replaceAll(`{{${name}}}`, String(replacement)); } return value; }, }), })); vi.mock("~/components/ui/input/InputSelect", () => ({ default: ({ options, setValue, value }: { options: { name: string; value: string }[]; setValue?: (value: string) => void; value?: string }) => ( ), })); vi.mock("~/components/ui/uploaders/UploadDocument", () => ({ default: () => , })); vi.mock("~/components/ui/uploaders/UploadImage", () => ({ default: () =>
Upload image dialog
, })); vi.mock("~/components/ui/modals/SuccessModal", () => ({ default: () => null, })); vi.mock("~/components/ui/modals/ErrorModal", () => ({ default: () => null, })); vi.mock("~/components/ui/modals/ConfirmModal", () => ({ default: ({ onYes, placeholder }: { onYes?: (value: string) => void; placeholder?: string }) => ( ), })); vi.mock("~/utils/data/useRootData", () => ({ useRootData: () => ({ csrf: "csrf-token" }), })); const baseUser = { email: "ada@example.com", firstName: "Ada", lastName: "Lovelace", avatar: null, admin: null, }; const currentSession = { locale: "en", lightOrDarkMode: "dark", theme: "zinc", cookieConsentCount: 2, impersonationActive: true, mfaStepUp: { present: true, verified: true, stale: false, method: "totp", } as const, }; function renderSettings(props: Partial> = {}) { return render(); } function textContent() { return document.body.textContent ?? ""; } function expectNoSecretsLeak() { const serialized = document.body.innerHTML; expect(serialized).not.toContain("encrypted-secret"); expect(serialized).not.toContain("encrypted-pending"); expect(serialized).not.toContain("totpSecretEncrypted"); expect(serialized).not.toContain("hash-1"); expect(serialized).not.toContain("hash-2"); expect(serialized).not.toContain("RJ_session"); expect(serialized).not.toContain("raw-cookie-value"); expect(serialized).not.toContain("csrf-secret"); expect(serialized).not.toContain("new-password-value"); expect(serialized).not.toContain("delete-confirm-password"); } describe("UserProfileSettings", () => { beforeEach(() => { submitMock.mockClear(); searchParams = new URLSearchParams(); }); it("renders profile, preferences, and danger controls with accessible form fields", () => { renderSettings(); expect(screen.getByText("settings.profile.profileTitle")).toBeInTheDocument(); expect(screen.getByText("account.shared.email")).toBeInTheDocument(); expect(document.querySelector('input[name="email"]')).toHaveValue("ada@example.com"); expect(document.querySelector('input[name="firstName"]')).toHaveValue("Ada"); expect(document.querySelector('input[name="lastName"]')).toHaveValue("Lovelace"); expect(screen.getByText("settings.preferences.title")).toBeInTheDocument(); expect(screen.getByText("settings.preferences.language")).toBeInTheDocument(); expect(screen.getByLabelText("settings.preferences.language")).toHaveValue("en"); expect(screen.getByRole("button", { name: "settings.danger.deleteUserAccount" })).toBeInTheDocument(); expect(document.querySelector('input[name="action"][value="profile"]')).toBeInTheDocument(); expect(document.querySelector('input[name="action"][value="password"]')).toBeInTheDocument(); expect(document.querySelector('input[name="action"][value="deleteAccount"]')).toBeInTheDocument(); expect(document.querySelectorAll('form input[name="csrf"][value="csrf-token"]').length).toBeGreaterThanOrEqual(3); expectNoSecretsLeak(); }); it("renders disabled, pending, and enabled MFA controls without secret material", () => { const { rerender } = renderSettings({ mfaStatus: { enabled: false, pending: false, enabledAt: null, pendingCreatedAt: null }, }); expect(screen.getByText("Authenticator app")).toBeInTheDocument(); expect(screen.getByRole("status")).toHaveTextContent("settings.profile.mfa.status.notEnabled"); expect(screen.getByRole("button", { name: "settings.profile.mfa.startAuthenticatorSetup" })).toBeInTheDocument(); expect(document.querySelector('input[name="action"][value="startTotpEnrollment"]')).toBeInTheDocument(); expect(document.querySelector('[data-ui-id="ui.settings.profile.mfa.panel"]')).toBeInTheDocument(); const mfaToggleForm = document.querySelector('[data-ui-id="ui.settings.profile.mfa.toggle.form"]'); expect(mfaToggleForm).toBeInTheDocument(); expect(within(mfaToggleForm as HTMLElement).getByLabelText("settings.profile.passwordCurrent")).toHaveAttribute("name", "passwordCurrent"); rerender( ); expect(screen.getByText("settings.profile.mfa.status.pending")).toBeInTheDocument(); expect(screen.getByLabelText("Authenticator setup URI")).toHaveTextContent("otpauth://totp/ToolCTRL:ada@example.com?issuer=ToolCTRL"); expect(screen.getByRole("alert")).toHaveTextContent("Enter the 6-digit authenticator code."); expect(screen.getByLabelText("Authenticator code")).toHaveAttribute("name", "code"); expect(screen.getByRole("button", { name: "Confirm authenticator" })).toBeInTheDocument(); expect(document.querySelector('input[name="action"][value="confirmTotpEnrollment"]')).toBeInTheDocument(); rerender( ); expect(screen.getByText("settings.profile.mfa.status.enabled")).toBeInTheDocument(); expect(screen.getByRole("button", { name: "settings.profile.mfa.disableAuthenticator" })).toBeInTheDocument(); expect(document.querySelector('input[name="action"][value="disableTotpEnrollment"]')).toBeInTheDocument(); expectNoSecretsLeak(); }); it("shows one-time recovery codes but not their persisted hashes", () => { renderSettings({ recoveryCodes: ["ABCD-EFGH-JKLM", "NPQR-STUV-WXYZ"], }); const recoveryRegion = screen.getByLabelText("Recovery codes"); expect(within(recoveryRegion).getByText("Save your recovery codes")).toBeInTheDocument(); expect(within(recoveryRegion).getByText("ABCD-EFGH-JKLM")).toBeInTheDocument(); expect(within(recoveryRegion).getByText("NPQR-STUV-WXYZ")).toBeInTheDocument(); expect(screen.getByLabelText("Recovery codes copy value")).toHaveValue("ABCD-EFGH-JKLM\nNPQR-STUV-WXYZ"); expect(document.body.innerHTML).not.toContain("hash-1"); expect(document.body.innerHTML).not.toContain("hash-2"); }); it("renders current-session diagnostics from redacted loader facts only", () => { renderSettings(); expect(screen.getByText("Current session")).toBeInTheDocument(); expect(screen.getByText("Redacted diagnostics for this browser session only.")).toBeInTheDocument(); expect(screen.getByText("settings.profile.session.locale")).toBeInTheDocument(); expect(screen.getByText("Light/dark preference")).toBeInTheDocument(); expect(screen.getByText("settings.profile.session.theme")).toBeInTheDocument(); expect(screen.getByText("Cookie consent choices")).toBeInTheDocument(); expect(screen.getByText("Impersonation")).toBeInTheDocument(); expect(screen.getByText("Admin MFA step-up")).toBeInTheDocument(); expect(screen.getByText("shared.dark")).toBeInTheDocument(); expect(screen.getByText("zinc")).toBeInTheDocument(); expect(screen.getByText("shared.active")).toBeInTheDocument(); expect(screen.getByText("settings.profile.session.mfaStepUpVerifiedWith")).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Clear step-up verification for this session" })).toBeInTheDocument(); expect(document.querySelector('input[name="action"][value="clearMfaStepUp"]')).toBeInTheDocument(); expectNoSecretsLeak(); }); it("submits locale and password-confirmed account deletion without rendering submitted passwords", () => { renderSettings(); fireEvent.click(screen.getByRole("button", { name: "settings.danger.deleteUserAccount" })); fireEvent.click(screen.getAllByRole("button", { name: "confirm-account-delete" }).at(-1)!); const deleteForm = submitMock.mock.calls.at(-1)?.[0] as FormData; expect(deleteForm.get("action")).toBe("deleteAccount"); expect(deleteForm.get("csrf")).toBe("csrf-token"); expect(deleteForm.get("passwordCurrent")).toBe("delete-confirm-password"); expect(textContent()).not.toContain("delete-confirm-password"); fireEvent.change(screen.getByLabelText("settings.preferences.language"), { target: { value: "de" } }); const localeForm = submitMock.mock.calls.at(-1)?.[0] as FormData; expect(localeForm.get("action")).toBe("setLocale"); expect(localeForm.get("csrf")).toBe("csrf-token"); expect(localeForm.get("lng")).toBe("de"); expect(localeForm.get("redirect")).toBe("/acme/settings/profile?lng=de"); expect(submitMock.mock.calls.at(-1)?.[1]).toMatchObject({ method: "post", action: "/preferences", preventScrollReset: true }); }); it("does not echo typed password values from security forms", () => { renderSettings(); const passwordInputs = document.querySelectorAll('input[type="password"]'); passwordInputs.forEach((input) => fireEvent.change(input, { target: { value: "new-password-value" } })); expect(textContent()).not.toContain("new-password-value"); }); });