mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
Fixes #529 (opened investigating #515). Setting MFA policy to "required"/"admins only" saved regardless of whether the mfa enterprise feature was licensed, and there was no enrollment UI at all, so any instance that flipped the toggle locked every unenrolled user out with no way back in. The login page and Settings save also both collapsed the resulting error into a generic message, hiding the real reason. - Reject saving mfaPolicy to admins_only/required server-side unless mfa is licensed - Surface the specific server error on login and on a failed settings save instead of a generic fallback - Add a self-service two-factor authentication enrollment flow (QR code, manual entry, recovery codes, verify, disable) so a licensed admin can actually satisfy the policy before it's enforced - Fix a pending-enrollment dead end, silent error swallowing in verify/disable, and a silent clipboard-copy failure on the recovery codes screen - Add the integration test that actually proves the fix: a real login attempt returns 403 MFA_ENROLLMENT_REQUIRED
48 lines
1.6 KiB
TypeScript
48 lines
1.6 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import "@testing-library/jest-dom/vitest";
|
|
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const apiGet = vi.hoisted(() => vi.fn().mockResolvedValue({ settings: {} }));
|
|
const apiPut = vi.hoisted(() => vi.fn());
|
|
|
|
vi.mock("@/lib/api", async (importOriginal) => {
|
|
const actual: Record<string, unknown> = await importOriginal();
|
|
return { ...actual, apiGet, apiPut };
|
|
});
|
|
|
|
import { AdminSecuritySettings } from "@/components/settings/settings-dialog";
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
apiGet.mockClear();
|
|
apiPut.mockReset();
|
|
});
|
|
|
|
describe("AdminSecuritySettings save errors", () => {
|
|
it("shows the server's specific error message when a save is rejected", async () => {
|
|
apiPut.mockRejectedValue(new Error("MFA requires an enterprise license"));
|
|
|
|
render(<AdminSecuritySettings />);
|
|
await waitFor(() => expect(apiGet).toHaveBeenCalled());
|
|
|
|
fireEvent.click(await screen.findByRole("button", { name: /save/i }));
|
|
|
|
const message = await screen.findByText("MFA requires an enterprise license");
|
|
expect(message).toHaveClass("text-destructive");
|
|
});
|
|
|
|
it("falls back to a generic message when the save rejects with a non-Error value", async () => {
|
|
apiPut.mockRejectedValue("network exploded");
|
|
|
|
render(<AdminSecuritySettings />);
|
|
await waitFor(() => expect(apiGet).toHaveBeenCalled());
|
|
|
|
fireEvent.click(await screen.findByRole("button", { name: /save/i }));
|
|
|
|
const message = await screen.findByText("Failed to save security settings");
|
|
expect(message).toHaveClass("text-destructive");
|
|
});
|
|
});
|