/** * Tests for the "Ignore self" toggle in `JailDetailPage`. * * Verifies that: * - The switch is checked when `ignoreSelf` is `true`. * - The switch is unchecked when `ignoreSelf` is `false`. * - Toggling the switch calls `toggleIgnoreSelf` with the correct boolean. * - A failed toggle shows an error message bar. */ import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { FluentProvider, webLightTheme } from "@fluentui/react-components"; import { MemoryRouter, Route, Routes } from "react-router-dom"; import { JailDetailPage } from "../JailDetailPage"; import type { Jail } from "../../types/jail"; import type { UseJailDetailResult } from "../../hooks/useJails"; // --------------------------------------------------------------------------- // Module mocks // --------------------------------------------------------------------------- /** * Stable mock function refs created before vi.mock() is hoisted. * We need `mockToggleIgnoreSelf` to be a vi.fn() that tests can inspect * and the rest to be no-ops that prevent real network calls. */ const { mockToggleIgnoreSelf, mockAddIp, mockRemoveIp, mockRefresh, } = vi.hoisted(() => ({ mockToggleIgnoreSelf: vi.fn<(on: boolean) => Promise>(), mockAddIp: vi.fn<(ip: string) => Promise>().mockResolvedValue(undefined), mockRemoveIp: vi.fn<(ip: string) => Promise>().mockResolvedValue(undefined), mockRefresh: vi.fn(), })); // Mock the jail detail hook — tests control the returned state directly. vi.mock("../../hooks/useJails", () => ({ useJailDetail: vi.fn(), })); // Mock API functions used by JailInfoSection control buttons to avoid side effects. vi.mock("../../api/jails", () => ({ startJail: vi.fn().mockResolvedValue({ message: "ok", jail: "sshd" }), stopJail: vi.fn().mockResolvedValue({ message: "ok", jail: "sshd" }), reloadJail: vi.fn().mockResolvedValue({ message: "ok", jail: "sshd" }), setJailIdle: vi.fn().mockResolvedValue({ message: "ok", jail: "sshd" }), toggleIgnoreSelf: vi.fn().mockResolvedValue({ message: "ok", jail: "sshd" }), })); // Stub BannedIpsSection to prevent its own fetchJailBannedIps calls. vi.mock("../../components/jail/BannedIpsSection", () => ({ BannedIpsSection: () =>
, })); // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- import { useJailDetail } from "../../hooks/useJails"; /** Minimal `Jail` fixture. */ function makeJail(): Jail { return { name: "sshd", running: true, idle: false, backend: "systemd", log_paths: ["/var/log/auth.log"], fail_regex: ["^Failed .+ from "], ignore_regex: [], date_pattern: "", log_encoding: "UTF-8", actions: ["iptables-multiport"], find_time: 600, ban_time: 3600, max_retry: 5, status: { currently_banned: 2, total_banned: 10, currently_failed: 0, total_failed: 50, }, bantime_escalation: null, }; } /** Wire `useJailDetail` to return the given `ignoreSelf` value. */ function mockHook(ignoreSelf: boolean): void { const result: UseJailDetailResult = { jail: makeJail(), ignoreList: ["10.0.0.0/8"], ignoreSelf, loading: false, error: null, refresh: mockRefresh, addIp: mockAddIp, removeIp: mockRemoveIp, toggleIgnoreSelf: mockToggleIgnoreSelf, }; vi.mocked(useJailDetail).mockReturnValue(result); } /** Render the JailDetailPage with a fake `/jails/sshd` route. */ function renderPage() { return render( } /> , ); } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe("JailDetailPage — ignore self toggle", () => { beforeEach(() => { vi.clearAllMocks(); mockToggleIgnoreSelf.mockResolvedValue(undefined); }); it("renders the switch checked when ignoreSelf is true", async () => { mockHook(true); renderPage(); const switchEl = await screen.findByRole("switch", { name: /ignore self/i }); expect(switchEl).toBeChecked(); }); it("renders the switch unchecked when ignoreSelf is false", async () => { mockHook(false); renderPage(); const switchEl = await screen.findByRole("switch", { name: /ignore self/i }); expect(switchEl).not.toBeChecked(); }); it("calls toggleIgnoreSelf(false) when switch is toggled off", async () => { mockHook(true); renderPage(); const user = userEvent.setup(); const switchEl = await screen.findByRole("switch", { name: /ignore self/i }); await user.click(switchEl); await waitFor(() => { expect(mockToggleIgnoreSelf).toHaveBeenCalledOnce(); expect(mockToggleIgnoreSelf).toHaveBeenCalledWith(false); }); }); it("calls toggleIgnoreSelf(true) when switch is toggled on", async () => { mockHook(false); renderPage(); const user = userEvent.setup(); const switchEl = await screen.findByRole("switch", { name: /ignore self/i }); await user.click(switchEl); await waitFor(() => { expect(mockToggleIgnoreSelf).toHaveBeenCalledOnce(); expect(mockToggleIgnoreSelf).toHaveBeenCalledWith(true); }); }); it("shows an error message bar when toggleIgnoreSelf rejects", async () => { mockHook(false); mockToggleIgnoreSelf.mockRejectedValue(new Error("Connection refused")); renderPage(); const user = userEvent.setup(); const switchEl = await screen.findByRole("switch", { name: /ignore self/i }); await user.click(switchEl); await waitFor(() => { expect(screen.getByText(/Connection refused/i)).toBeInTheDocument(); }); }); });