Add ignore-self toggle to Jail Detail page
Implements the missing UI control for POST /api/jails/{name}/ignoreself:
- Add jailIgnoreSelf endpoint constant to endpoints.ts
- Add toggleIgnoreSelf(name, on) API function to jails.ts
- Expose toggleIgnoreSelf action from useJailDetail hook
- Replace read-only 'ignore self' badge with a Fluent Switch in
IgnoreListSection to allow enabling/disabling the flag per jail
- Add 5 vitest tests for checked/unchecked state and toggle behaviour
This commit is contained in:
188
frontend/src/pages/__tests__/JailDetailIgnoreSelf.test.tsx
Normal file
188
frontend/src/pages/__tests__/JailDetailIgnoreSelf.test.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* 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<void>>(),
|
||||
mockAddIp: vi.fn<(ip: string) => Promise<void>>().mockResolvedValue(undefined),
|
||||
mockRemoveIp: vi.fn<(ip: string) => Promise<void>>().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: () => <div data-testid="banned-ips-stub" />,
|
||||
}));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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 <HOST>"],
|
||||
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(
|
||||
<MemoryRouter initialEntries={["/jails/sshd"]}>
|
||||
<FluentProvider theme={webLightTheme}>
|
||||
<Routes>
|
||||
<Route path="/jails/:name" element={<JailDetailPage />} />
|
||||
</Routes>
|
||||
</FluentProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user