feat: isolate admin sign-in caches, retain cached probe outcomes and clear recovered mailbox issues per measured source

This commit is contained in:
Matthew Meszaros
2026-10-09 16:27:20 +00:00
committed by Devin AI
parent 2033fb8a36
commit a8ddfc65ec
9 changed files with 148 additions and 22 deletions
+81
View File
@@ -0,0 +1,81 @@
import { act, type PropsWithChildren } from "react";
import { createRoot, type Root } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { MemoryRouter } from "react-router-dom";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import LoginPage from "./LoginPage";
const state = vi.hoisted(() => ({
login: vi.fn(), confirm: vi.fn(), verify: vi.fn(), saveToken: vi.fn(),
onToken: (_value: string): Promise<void> => Promise.resolve(),
onCode: (_value: string): void => {},
}));
vi.mock("@/lib/api/client/auth", () => ({ getAuthConfig: () => Promise.resolve({ captcha: false }), login: state.login, loginConfirm: state.confirm, verifyTwoFA: state.verify }));
vi.mock("@/lib/auth/storage", () => ({ setToken: state.saveToken }));
vi.mock("sonner", () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
vi.mock("motion/react", () => ({ AnimatePresence: ({ children }: PropsWithChildren) => children, motion: { div: ({ children }: PropsWithChildren) => <div>{children}</div> } }));
vi.mock("@/components/captcha/TurnstileModal", () => ({ TurnstileModal: ({ onToken }: { onToken: typeof state.onToken }) => { state.onToken = onToken; return null; } }));
vi.mock("@/components/ui/input-otp", () => ({ InputOTP: ({ onChange }: { onChange: typeof state.onCode }) => { state.onCode = onChange; return null; }, InputOTPGroup: () => null, InputOTPSlot: () => null }));
const token = { access_token: "fixture-access", refresh_token: "fixture-refresh", access_token_expires_at: "2099-01-01T00:00:00Z", refresh_token_expires_at: "2099-01-01T00:00:00Z" };
let root: Root;
let client: QueryClient;
let clear: ReturnType<typeof vi.spyOn>;
beforeEach(async () => {
vi.clearAllMocks();
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
client.setQueryData(["me"], { id: "previous-admin", admin_permissions: 0xffff });
client.setQueryData(["system", "mail-status"], { private: "previous-session" });
clear = vi.spyOn(client, "clear");
root = createRoot(document.createElement("div"));
await act(async () => root.render(<QueryClientProvider client={client}><MemoryRouter><LoginPage /></MemoryRouter></QueryClientProvider>));
});
afterEach(async () => {
await act(async () => root.unmount());
client.clear();
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
function expectClearedBeforeToken() {
expect(client.getQueryCache().getAll()).toHaveLength(0);
expect(state.saveToken).toHaveBeenCalledWith(token);
expect(clear.mock.invocationCallOrder[0]).toBeLessThan(state.saveToken.mock.invocationCallOrder[0]);
}
describe("admin sign-in session isolation", () => {
it("clears cached identity and private data before a direct successful login", async () => {
state.login.mockResolvedValue({ code_required: false, token });
await act(async () => state.onToken("fixture-captcha"));
expectClearedBeforeToken();
});
it("keeps cache during an email-code challenge and clears it on confirmation", async () => {
state.login.mockResolvedValue({ code_required: true, session: "fixture-session" });
state.confirm.mockResolvedValue(token);
await act(async () => state.onToken("fixture-captcha"));
expect(clear).not.toHaveBeenCalled();
await act(async () => state.onCode("123456"));
expectClearedBeforeToken();
});
it("keeps cache during a second-factor challenge and clears it on verification", async () => {
state.login.mockResolvedValue({ code_required: false, two_fa_required: true, pending_token: "fixture-pending" });
state.verify.mockResolvedValue(token);
await act(async () => state.onToken("fixture-captcha"));
expect(clear).not.toHaveBeenCalled();
await act(async () => state.onCode("123456"));
expectClearedBeforeToken();
});
it("does not replace the session or clear caches on a failed login", async () => {
state.login.mockRejectedValue(new Error("fixture-login-failure"));
await act(async () => state.onToken("fixture-captcha"));
expect(clear).not.toHaveBeenCalled();
expect(state.saveToken).not.toHaveBeenCalled();
expect(client.getQueryCache().getAll()).toHaveLength(2);
});
});
+5
View File
@@ -14,6 +14,7 @@
import { useEffect, useRef, useState, type FormEvent } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import { AnimatePresence, motion, type Transition } from "motion/react";
import { useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { Eye, EyeOff, Loader2, ArrowLeft, ArrowRight } from "lucide-react";
import { Button } from "@/components/ui/button";
@@ -40,6 +41,7 @@ function errorMessage(err: unknown): string {
export default function LoginPage() {
const nav = useNavigate();
const queryClient = useQueryClient();
const loc = useLocation();
const [phase, setPhase] = useState<"credentials" | "code" | "twofa">("credentials");
const [pendingToken, setPendingToken] = useState("");
@@ -118,6 +120,7 @@ export default function LoginPage() {
return;
}
if (res.token) {
queryClient.clear();
setToken(res.token);
const dest = (loc.state as { from?: string } | null)?.from ?? "/";
nav(dest, { replace: true });
@@ -166,6 +169,7 @@ export default function LoginPage() {
return;
}
if (!res.access_token) throw new Error("The server returned an unexpected sign-in response.");
queryClient.clear();
setToken(res as LoginResponse);
const dest = (loc.state as { from?: string } | null)?.from ?? "/";
nav(dest, { replace: true });
@@ -188,6 +192,7 @@ export default function LoginPage() {
setSubmitting(true);
try {
const tok = await verifyTwoFA({ pending_token: pendingToken, code: value });
queryClient.clear();
setToken(tok);
const dest = (loc.state as { from?: string } | null)?.from ?? "/";
nav(dest, { replace: true });
@@ -74,7 +74,15 @@ describe("configuration and service coverage", () => {
state.services.data!.data = [{ name: "postgres", ok: true, latency_ms: 5 }];
state.services.data!.checked_at = new Date(Date.now() - 300_000).toISOString();
expect(render("services")).toContain("Current service coverage unknown");
expect(render("services")).toContain("Cached probe");
expect(render("services")).toContain("Cached: passed");
expect(render("services")).not.toContain("Probe passed");
});
it("retains a cached failure even when the earlier probe supplied no error details", () => {
state.services.data!.data = [{ name: "postgres", ok: true, latency_ms: 5 }, { name: "redis", ok: false, latency_ms: 20 }];
state.services.isError = true;
expect(render("services")).toContain("Cached: passed");
expect(render("services")).toContain("Cached: failed");
expect(render("services")).not.toContain("Probe passed");
});
});
@@ -162,8 +162,8 @@ function ComponentRow({ component: c, cached }: { component: SystemComponentStat
<span className="text-xs text-muted-foreground tabular-nums" title="Latency">
{c.latency_ms} ms
</span>
<StatusBadge tone={cached ? "neutral" : c.ok ? "success" : "danger"} className="w-[5.75rem] justify-center">
{cached ? "Cached probe" : c.ok ? "Probe passed" : "Probe failed"}
<StatusBadge tone={cached ? "neutral" : c.ok ? "success" : "danger"} className="min-w-[5.75rem] justify-center">
{cached ? c.ok ? "Cached: passed" : "Cached: failed" : c.ok ? "Probe passed" : "Probe failed"}
</StatusBadge>
</div>
</div>