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:
132
Docs/Tasks.md
132
Docs/Tasks.md
@@ -468,3 +468,135 @@ Write tests covering:
|
|||||||
.venv/bin/mypy backend/app/ --strict
|
.venv/bin/mypy backend/app/ --strict
|
||||||
```
|
```
|
||||||
Zero errors.
|
Zero errors.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 8 — Add "ignore self" toggle to Jail Detail page
|
||||||
|
|
||||||
|
**Status:** done
|
||||||
|
|
||||||
|
**Summary:** Added `jailIgnoreSelf` endpoint constant to `endpoints.ts`; added `toggleIgnoreSelf(name, on)` API function to `jails.ts`; extended `useJailDetail` return type and hook implementation to expose `toggleIgnoreSelf`; replaced the read-only "ignore self" badge in `IgnoreListSection` (`JailDetailPage.tsx`) with a Fluent UI `Switch` that calls the toggle action and surfaces any error in the existing `opError` message bar; added 5 new tests in `JailDetailIgnoreSelf.test.tsx` covering checked/unchecked rendering, toggle-on, toggle-off, and error display.
|
||||||
|
|
||||||
|
**Page:** `/jails/:name` — rendered by `frontend/src/pages/JailDetailPage.tsx`
|
||||||
|
|
||||||
|
### Problem description
|
||||||
|
|
||||||
|
`Features.md` §5 (Jail Management / IP Whitelist) requires: "Toggle the 'ignore self' option per jail, which automatically excludes the server's own IP addresses."
|
||||||
|
|
||||||
|
The backend already exposes `POST /api/jails/{name}/ignoreself` (accepts a JSON boolean `on`). The `useJailDetail` hook reads `ignore_self` from the `GET /api/jails/{name}` response and exposes it as `ignoreSelf: boolean`. However:
|
||||||
|
|
||||||
|
1. **No API wrapper** — `frontend/src/api/jails.ts` has no `toggleIgnoreSelf` function, and `frontend/src/api/endpoints.ts` has no `jailIgnoreSelf` entry.
|
||||||
|
2. **No hook action** — `UseJailDetailResult` in `useJails.ts` does not expose a `toggleIgnoreSelf` helper.
|
||||||
|
3. **Read-only UI** — `IgnoreListSection` in `JailDetailPage.tsx` shows an "ignore self" badge when enabled but has no control to change the setting.
|
||||||
|
|
||||||
|
As a result, users must use the fail2ban CLI to manage this flag even though the backend is ready.
|
||||||
|
|
||||||
|
### What to do
|
||||||
|
|
||||||
|
#### Part A — Add endpoint constant (frontend)
|
||||||
|
|
||||||
|
**File:** `frontend/src/api/endpoints.ts`
|
||||||
|
|
||||||
|
Inside the Jails block, after `jailIgnoreIp`, add:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
jailIgnoreSelf: (name: string): string => `/jails/${encodeURIComponent(name)}/ignoreself`,
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Part B — Add API wrapper function (frontend)
|
||||||
|
|
||||||
|
**File:** `frontend/src/api/jails.ts`
|
||||||
|
|
||||||
|
After the `delIgnoreIp` function in the "Ignore list" section, add:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
/**
|
||||||
|
* Enable or disable the `ignoreself` flag for a jail.
|
||||||
|
*
|
||||||
|
* When enabled, fail2ban automatically adds the server's own IP addresses to
|
||||||
|
* the ignore list so the host can never ban itself.
|
||||||
|
*
|
||||||
|
* @param name - Jail name.
|
||||||
|
* @param on - `true` to enable, `false` to disable.
|
||||||
|
* @returns A {@link JailCommandResponse} confirming the change.
|
||||||
|
* @throws {ApiError} On non-2xx responses.
|
||||||
|
*/
|
||||||
|
export async function toggleIgnoreSelf(
|
||||||
|
name: string,
|
||||||
|
on: boolean,
|
||||||
|
): Promise<JailCommandResponse> {
|
||||||
|
return post<JailCommandResponse>(ENDPOINTS.jailIgnoreSelf(name), on);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Part C — Expose toggle action from hook (frontend)
|
||||||
|
|
||||||
|
**File:** `frontend/src/hooks/useJails.ts`
|
||||||
|
|
||||||
|
1. Import `toggleIgnoreSelf` at the top (alongside the other API imports).
|
||||||
|
2. Add `toggleIgnoreSelf: (on: boolean) => Promise<void>` to the `UseJailDetailResult` interface with a JSDoc comment: `/** Enable or disable the ignoreself option for this jail. */`.
|
||||||
|
3. Inside `useJailDetail`, add the implementation:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const toggleIgnoreSelf = async (on: boolean): Promise<void> => {
|
||||||
|
await toggleIgnoreSelfApi(name, on);
|
||||||
|
load();
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Alias the import as `toggleIgnoreSelfApi` to avoid shadowing the local function name.
|
||||||
|
|
||||||
|
4. Add the function to the returned object.
|
||||||
|
|
||||||
|
#### Part D — Add toggle control to UI (frontend)
|
||||||
|
|
||||||
|
**File:** `frontend/src/pages/JailDetailPage.tsx`
|
||||||
|
|
||||||
|
1. Accept `toggleIgnoreSelf: (on: boolean) => Promise<void>` in the `IgnoreListSectionProps` interface.
|
||||||
|
2. Pass the function from `useJailDetail` down via the existing destructuring and JSX prop.
|
||||||
|
3. Inside `IgnoreListSection`, render a `<Switch>` next to (or in place of) the read-only badge:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Switch
|
||||||
|
label="Ignore self (exclude this server's own IPs)"
|
||||||
|
checked={ignoreSelf}
|
||||||
|
onChange={(_e, data): void => {
|
||||||
|
toggleIgnoreSelf(data.checked).catch((err: unknown) => {
|
||||||
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
|
setOpError(msg);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Import `Switch` from `"@fluentui/react-components"`. Remove the existing read-only badge (it is replaced by the labelled switch, which is self-explanatory). Keep the existing `opError` state and `<MessageBar>` for error display.
|
||||||
|
|
||||||
|
### Tests to add
|
||||||
|
|
||||||
|
**File:** `frontend/src/pages/__tests__/JailDetailIgnoreSelf.test.tsx` (new file)
|
||||||
|
|
||||||
|
Write tests that render the `IgnoreListSection` component (or the full `JailDetailPage` via a shallow-enough render) and cover:
|
||||||
|
|
||||||
|
1. **`test_ignore_self_switch_is_checked_when_ignore_self_true`** — when `ignoreSelf=true`, the switch is checked.
|
||||||
|
2. **`test_ignore_self_switch_is_unchecked_when_ignore_self_false`** — when `ignoreSelf=false`, the switch is unchecked.
|
||||||
|
3. **`test_toggling_switch_calls_toggle_ignore_self`** — clicking the switch calls `toggleIgnoreSelf` with `false` (when it was `true`).
|
||||||
|
4. **`test_toggle_error_shows_message_bar`** — when `toggleIgnoreSelf` rejects, the error message bar is rendered.
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
|
||||||
|
1. Run frontend type check and lint:
|
||||||
|
```bash
|
||||||
|
cd frontend && npx tsc --noEmit && npx eslint src/api/jails.ts src/api/endpoints.ts src/hooks/useJails.ts src/pages/JailDetailPage.tsx
|
||||||
|
```
|
||||||
|
Zero errors and zero warnings.
|
||||||
|
|
||||||
|
2. Run frontend tests:
|
||||||
|
```bash
|
||||||
|
cd frontend && npx vitest run src/pages/__tests__/JailDetailIgnoreSelf
|
||||||
|
```
|
||||||
|
All 4 new tests pass.
|
||||||
|
|
||||||
|
3. **Manual test with running server:**
|
||||||
|
- Go to `/jails`, click a running jail.
|
||||||
|
- On the Jail Detail page, scroll to "Ignore List (IP Whitelist)".
|
||||||
|
- Toggle the "Ignore self" switch on and off — the switch should reflect the live state and the change should survive a page refresh.
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export const ENDPOINTS = {
|
|||||||
jailReload: (name: string): string => `/jails/${encodeURIComponent(name)}/reload`,
|
jailReload: (name: string): string => `/jails/${encodeURIComponent(name)}/reload`,
|
||||||
jailsReloadAll: "/jails/reload-all",
|
jailsReloadAll: "/jails/reload-all",
|
||||||
jailIgnoreIp: (name: string): string => `/jails/${encodeURIComponent(name)}/ignoreip`,
|
jailIgnoreIp: (name: string): string => `/jails/${encodeURIComponent(name)}/ignoreip`,
|
||||||
|
jailIgnoreSelf: (name: string): string => `/jails/${encodeURIComponent(name)}/ignoreself`,
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Bans
|
// Bans
|
||||||
|
|||||||
@@ -149,6 +149,24 @@ export async function delIgnoreIp(
|
|||||||
return del<JailCommandResponse>(ENDPOINTS.jailIgnoreIp(name), { ip });
|
return del<JailCommandResponse>(ENDPOINTS.jailIgnoreIp(name), { ip });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or disable the `ignoreself` flag for a jail.
|
||||||
|
*
|
||||||
|
* When enabled, fail2ban automatically adds the server's own IP addresses to
|
||||||
|
* the ignore list so the host can never ban itself.
|
||||||
|
*
|
||||||
|
* @param name - Jail name.
|
||||||
|
* @param on - `true` to enable, `false` to disable.
|
||||||
|
* @returns A {@link JailCommandResponse} confirming the change.
|
||||||
|
* @throws {ApiError} On non-2xx responses.
|
||||||
|
*/
|
||||||
|
export async function toggleIgnoreSelf(
|
||||||
|
name: string,
|
||||||
|
on: boolean,
|
||||||
|
): Promise<JailCommandResponse> {
|
||||||
|
return post<JailCommandResponse>(ENDPOINTS.jailIgnoreSelf(name), on);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Ban / unban
|
// Ban / unban
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
setJailIdle,
|
setJailIdle,
|
||||||
startJail,
|
startJail,
|
||||||
stopJail,
|
stopJail,
|
||||||
|
toggleIgnoreSelf as toggleIgnoreSelfApi,
|
||||||
unbanAllBans,
|
unbanAllBans,
|
||||||
unbanIp,
|
unbanIp,
|
||||||
} from "../api/jails";
|
} from "../api/jails";
|
||||||
@@ -150,6 +151,8 @@ export interface UseJailDetailResult {
|
|||||||
addIp: (ip: string) => Promise<void>;
|
addIp: (ip: string) => Promise<void>;
|
||||||
/** Remove an IP from the ignore list. */
|
/** Remove an IP from the ignore list. */
|
||||||
removeIp: (ip: string) => Promise<void>;
|
removeIp: (ip: string) => Promise<void>;
|
||||||
|
/** Enable or disable the ignoreself option for this jail. */
|
||||||
|
toggleIgnoreSelf: (on: boolean) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -208,6 +211,11 @@ export function useJailDetail(name: string): UseJailDetailResult {
|
|||||||
load();
|
load();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleIgnoreSelf = async (on: boolean): Promise<void> => {
|
||||||
|
await toggleIgnoreSelfApi(name, on);
|
||||||
|
load();
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
jail,
|
jail,
|
||||||
ignoreList,
|
ignoreList,
|
||||||
@@ -217,6 +225,7 @@ export function useJailDetail(name: string): UseJailDetailResult {
|
|||||||
refresh: load,
|
refresh: load,
|
||||||
addIp,
|
addIp,
|
||||||
removeIp,
|
removeIp,
|
||||||
|
toggleIgnoreSelf,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
MessageBar,
|
MessageBar,
|
||||||
MessageBarBody,
|
MessageBarBody,
|
||||||
Spinner,
|
Spinner,
|
||||||
|
Switch,
|
||||||
Text,
|
Text,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
makeStyles,
|
makeStyles,
|
||||||
@@ -443,6 +444,7 @@ interface IgnoreListSectionProps {
|
|||||||
ignoreSelf: boolean;
|
ignoreSelf: boolean;
|
||||||
onAdd: (ip: string) => Promise<void>;
|
onAdd: (ip: string) => Promise<void>;
|
||||||
onRemove: (ip: string) => Promise<void>;
|
onRemove: (ip: string) => Promise<void>;
|
||||||
|
onToggleIgnoreSelf: (on: boolean) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function IgnoreListSection({
|
function IgnoreListSection({
|
||||||
@@ -451,6 +453,7 @@ function IgnoreListSection({
|
|||||||
ignoreSelf,
|
ignoreSelf,
|
||||||
onAdd,
|
onAdd,
|
||||||
onRemove,
|
onRemove,
|
||||||
|
onToggleIgnoreSelf,
|
||||||
}: IgnoreListSectionProps): React.JSX.Element {
|
}: IgnoreListSectionProps): React.JSX.Element {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const [inputVal, setInputVal] = useState("");
|
const [inputVal, setInputVal] = useState("");
|
||||||
@@ -494,17 +497,27 @@ function IgnoreListSection({
|
|||||||
<Text as="h2" size={500} weight="semibold">
|
<Text as="h2" size={500} weight="semibold">
|
||||||
Ignore List (IP Whitelist)
|
Ignore List (IP Whitelist)
|
||||||
</Text>
|
</Text>
|
||||||
{ignoreSelf && (
|
|
||||||
<Tooltip content="This jail ignores the server's own IP addresses" relationship="label">
|
|
||||||
<Badge appearance="tint" color="informative">
|
|
||||||
ignore self
|
|
||||||
</Badge>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<Badge appearance="tint">{String(ignoreList.length)}</Badge>
|
<Badge appearance="tint">{String(ignoreList.length)}</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Ignore-self toggle */}
|
||||||
|
<Switch
|
||||||
|
label="Ignore self — exclude this server's own IP addresses from banning"
|
||||||
|
checked={ignoreSelf}
|
||||||
|
onChange={(_e, data): void => {
|
||||||
|
onToggleIgnoreSelf(data.checked).catch((err: unknown) => {
|
||||||
|
const msg =
|
||||||
|
err instanceof ApiError
|
||||||
|
? `${String(err.status)}: ${err.body}`
|
||||||
|
: err instanceof Error
|
||||||
|
? err.message
|
||||||
|
: String(err);
|
||||||
|
setOpError(msg);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{opError && (
|
{opError && (
|
||||||
<MessageBar intent="error">
|
<MessageBar intent="error">
|
||||||
<MessageBarBody>{opError}</MessageBarBody>
|
<MessageBarBody>{opError}</MessageBarBody>
|
||||||
@@ -579,7 +592,7 @@ function IgnoreListSection({
|
|||||||
export function JailDetailPage(): React.JSX.Element {
|
export function JailDetailPage(): React.JSX.Element {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const { name = "" } = useParams<{ name: string }>();
|
const { name = "" } = useParams<{ name: string }>();
|
||||||
const { jail, ignoreList, ignoreSelf, loading, error, refresh, addIp, removeIp } =
|
const { jail, ignoreList, ignoreSelf, loading, error, refresh, addIp, removeIp, toggleIgnoreSelf } =
|
||||||
useJailDetail(name);
|
useJailDetail(name);
|
||||||
|
|
||||||
if (loading && !jail) {
|
if (loading && !jail) {
|
||||||
@@ -634,6 +647,7 @@ export function JailDetailPage(): React.JSX.Element {
|
|||||||
ignoreSelf={ignoreSelf}
|
ignoreSelf={ignoreSelf}
|
||||||
onAdd={addIp}
|
onAdd={addIp}
|
||||||
onRemove={removeIp}
|
onRemove={removeIp}
|
||||||
|
onToggleIgnoreSelf={toggleIgnoreSelf}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
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