diff --git a/frontend/__tests__/SessionManager.test.tsx b/frontend/__tests__/SessionManager.test.tsx new file mode 100644 index 00000000..cb053a42 --- /dev/null +++ b/frontend/__tests__/SessionManager.test.tsx @@ -0,0 +1,77 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import SessionManager from "@/components/SessionManager"; +import { getSessions, revokeAllSessions, revokeSession } from "@/lib/auth"; + +jest.mock("@/lib/auth", () => ({ + getSessions: jest.fn(), + revokeSession: jest.fn(), + revokeAllSessions: jest.fn(), +})); + +jest.mock("@/lib/logger", () => ({ + logger: { error: jest.fn() }, +})); + +const mockedGetSessions = jest.mocked(getSessions); +const mockedRevokeSession = jest.mocked(revokeSession); +const mockedRevokeAllSessions = jest.mocked(revokeAllSessions); + +const sessions = [ + { + id: 1, + publicKey: "GPHONE", + deviceInfo: "Phone", + ipAddress: "127.0.0.1", + createdAt: "2026-08-25T00:00:00Z", + lastUsedAt: "2026-08-25T01:00:00Z", + expiresAt: "2026-09-25T01:00:00Z", + }, + { + id: 2, + publicKey: "GLAPTOP", + deviceInfo: "Laptop", + ipAddress: "127.0.0.2", + createdAt: "2026-08-25T00:00:00Z", + lastUsedAt: "2026-08-25T01:00:00Z", + expiresAt: "2026-09-25T01:00:00Z", + }, +]; + +describe("SessionManager", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("refreshes the sessions and removes a revoked row", async () => { + let resolveRefresh: (value: typeof sessions) => void = () => undefined; + mockedGetSessions + .mockResolvedValueOnce(sessions) + .mockImplementationOnce(() => new Promise((resolve) => { resolveRefresh = resolve; })); + mockedRevokeSession.mockResolvedValue(true); + + render(); + await screen.findByText("Phone"); + + fireEvent.click(screen.getAllByRole("button", { name: "Revoke" })[0]); + await waitFor(() => expect(mockedRevokeSession).toHaveBeenCalledWith(1)); + expect(screen.getByRole("button", { name: "Revoking..." })).toBeDisabled(); + expect(screen.getByText("Phone")).toBeInTheDocument(); + + resolveRefresh([sessions[1]]); + await waitFor(() => expect(screen.queryByText("Phone")).not.toBeInTheDocument()); + expect(mockedGetSessions).toHaveBeenCalledTimes(2); + }); + + it("refreshes after revoking all sessions", async () => { + mockedGetSessions.mockResolvedValueOnce(sessions).mockResolvedValueOnce([]); + mockedRevokeAllSessions.mockResolvedValue(true); + + render(); + await screen.findByText("Laptop"); + fireEvent.click(screen.getByRole("button", { name: "Revoke All Other Sessions" })); + + await screen.findByText("No active sessions found."); + expect(mockedRevokeAllSessions).toHaveBeenCalledTimes(1); + expect(mockedGetSessions).toHaveBeenCalledTimes(2); + }); +}); diff --git a/frontend/components/SessionManager.tsx b/frontend/components/SessionManager.tsx index 1f18b04a..9d895ff8 100644 --- a/frontend/components/SessionManager.tsx +++ b/frontend/components/SessionManager.tsx @@ -13,15 +13,15 @@ export default function SessionManager() { const [loading, setLoading] = useState(true); const [actionLoading, setActionLoading] = useState(null); - const fetchSessions = useCallback(async () => { - setLoading(true); + const fetchSessions = useCallback(async (showLoading = true) => { + if (showLoading) setLoading(true); try { const data = await getSessions(); setSessions(data); } catch (err) { logger.error("Failed to load sessions:", {}, err instanceof Error ? err : undefined); } finally { - setLoading(false); + if (showLoading) setLoading(false); } }, []); @@ -31,19 +31,21 @@ export default function SessionManager() { const handleRevokeOne = async (id: number) => { setActionLoading(id); - const success = await revokeSession(id); - setActionLoading(null); - if (success) { - setSessions((prev) => prev.filter((s) => s.id !== id)); + try { + const success = await revokeSession(id); + if (success) await fetchSessions(false); + } finally { + setActionLoading(null); } }; const handleRevokeAll = async () => { setActionLoading("all"); - const success = await revokeAllSessions(); - setActionLoading(null); - if (success) { - setSessions([]); + try { + const success = await revokeAllSessions(); + if (success) await fetchSessions(false); + } finally { + setActionLoading(null); } };