Files
BanGUI/frontend/src/components/__tests__/ConfigPageLogPath.test.tsx
Lukas 528d0bd8ea fix: make all tests pass
backend/tests/test_routers/test_file_config.py:
  - TestListActionFiles.test_200_returns_files: GET /api/config/actions is
    handled by config.router (registered before file_config.router), so mock
    config_file_service.list_actions and assert on ActionListResponse.actions
  - TestCreateActionFile.test_201_creates_file: same route conflict; mock
    config_file_service.create_action and use ActionCreateRequest body format

frontend/src/components/__tests__/ConfigPageLogPath.test.tsx:
  - Log paths are rendered as <Input value={path}>, not text nodes; replace
    getByText() with getByDisplayValue() for both test assertions
2026-03-14 17:41:06 +01:00

265 lines
8.9 KiB
TypeScript

/**
* Tests for the "Add Log Path" form inside JailAccordionPanel (ConfigPage).
*
* Verifies that:
* - The add-log-path input and button are rendered inside the jail accordion.
* - The Add button is disabled when the input is empty.
* - Submitting a valid path calls `addLogPath` and appends the path to the list.
* - An API error is surfaced as 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 { ConfigPage } from "../../pages/ConfigPage";
import type { JailConfig } from "../../types/config";
// ---------------------------------------------------------------------------
// Module mocks — use vi.hoisted so refs are available when vi.mock runs
// ---------------------------------------------------------------------------
const {
mockAddLogPath,
mockDeleteLogPath,
mockUpdateJailConfig,
mockReloadConfig,
mockFetchGlobalConfig,
mockFetchServerSettings,
mockFetchJailConfigs,
mockFetchMapColorThresholds,
mockFetchJailConfigFiles,
mockFetchFilterFiles,
mockFetchActionFiles,
mockUpdateMapColorThresholds,
mockUpdateGlobalConfig,
mockUpdateServerSettings,
mockFlushLogs,
mockSetJailConfigFileEnabled,
mockUpdateJailConfigFile,
} = vi.hoisted(() => ({
mockAddLogPath: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
mockDeleteLogPath: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
mockUpdateJailConfig: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
mockReloadConfig: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
mockFetchGlobalConfig: vi.fn().mockResolvedValue({
config: {
ban_time: 600,
max_retry: 5,
find_time: 300,
backend: "auto",
},
}),
mockFetchServerSettings: vi.fn().mockResolvedValue({
settings: {
log_level: "INFO",
log_target: "STDOUT",
syslog_socket: null,
db_path: "/var/lib/fail2ban/fail2ban.sqlite3",
db_purge_age: 86400,
db_max_matches: 10,
},
}),
mockFetchJailConfigs: vi.fn(),
mockFetchMapColorThresholds: vi.fn().mockResolvedValue({
threshold_high: 100,
threshold_medium: 50,
threshold_low: 20,
}),
mockFetchJailConfigFiles: vi.fn().mockResolvedValue({ files: [] }),
mockFetchFilterFiles: vi.fn().mockResolvedValue({ files: [] }),
mockFetchActionFiles: vi.fn().mockResolvedValue({ files: [] }),
mockUpdateMapColorThresholds: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
mockUpdateGlobalConfig: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
mockUpdateServerSettings: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
mockFlushLogs: vi.fn().mockResolvedValue({ message: "ok" }),
mockSetJailConfigFileEnabled: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
mockUpdateJailConfigFile: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
}));
vi.mock("../../api/config", () => ({
addLogPath: mockAddLogPath,
deleteLogPath: mockDeleteLogPath,
fetchJailConfigs: mockFetchJailConfigs,
fetchJailConfig: vi.fn(),
updateJailConfig: mockUpdateJailConfig,
reloadConfig: mockReloadConfig,
fetchGlobalConfig: mockFetchGlobalConfig,
updateGlobalConfig: mockUpdateGlobalConfig,
fetchServerSettings: mockFetchServerSettings,
updateServerSettings: mockUpdateServerSettings,
flushLogs: mockFlushLogs,
fetchMapColorThresholds: mockFetchMapColorThresholds,
updateMapColorThresholds: mockUpdateMapColorThresholds,
fetchJailConfigFiles: mockFetchJailConfigFiles,
fetchJailConfigFileContent: vi.fn(),
updateJailConfigFile: mockUpdateJailConfigFile,
setJailConfigFileEnabled: mockSetJailConfigFileEnabled,
fetchFilterFiles: mockFetchFilterFiles,
fetchFilterFile: vi.fn(),
updateFilterFile: vi.fn(),
createFilterFile: vi.fn(),
fetchFilters: vi.fn().mockResolvedValue({ filters: [], total: 0 }),
fetchFilter: vi.fn(),
fetchActionFiles: mockFetchActionFiles,
fetchActionFile: vi.fn(),
updateActionFile: vi.fn(),
createActionFile: vi.fn(),
previewLog: vi.fn(),
testRegex: vi.fn(),
fetchInactiveJails: vi.fn().mockResolvedValue({ jails: [], total: 0 }),
activateJail: vi.fn(),
deactivateJail: vi.fn(),
fetchParsedFilter: vi.fn(),
updateParsedFilter: vi.fn(),
fetchParsedAction: vi.fn(),
updateParsedAction: vi.fn(),
fetchParsedJailFile: vi.fn(),
updateParsedJailFile: vi.fn(),
}));
vi.mock("../../api/jails", () => ({
fetchJails: vi.fn().mockResolvedValue({ jails: [], total: 0 }),
}));
/** Minimal jail fixture used across tests. */
const MOCK_JAIL: JailConfig = {
name: "sshd",
ban_time: 600,
max_retry: 3,
find_time: 300,
fail_regex: [],
ignore_regex: [],
log_paths: ["/var/log/auth.log"],
date_pattern: null,
log_encoding: "UTF-8",
backend: "auto",
use_dns: "warn",
prefregex: "",
actions: [],
bantime_escalation: null,
};
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function renderConfigPage() {
return render(
<FluentProvider theme={webLightTheme}>
<ConfigPage />
</FluentProvider>,
);
}
/** Waits for the sshd list item to appear and clicks it to open the detail pane. */
async function openSshdAccordion(user: ReturnType<typeof userEvent.setup>) {
const listItem = await screen.findByRole("option", { name: /sshd/i });
await user.click(listItem);
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe("ConfigPage — Add Log Path", () => {
beforeEach(() => {
vi.clearAllMocks();
mockFetchJailConfigs.mockResolvedValue({ jails: [MOCK_JAIL], total: 1 });
mockAddLogPath.mockResolvedValue(undefined);
});
it("renders the existing log path and the add-log-path input inside the accordion", async () => {
const user = userEvent.setup();
renderConfigPage();
await openSshdAccordion(user);
// Existing path from fixture — rendered as an <input> value
expect(screen.getByDisplayValue("/var/log/auth.log")).toBeInTheDocument();
// Add-log-path input placeholder
expect(
screen.getByPlaceholderText("/var/log/example.log"),
).toBeInTheDocument();
});
it("disables the Add button when the path input is empty", async () => {
const user = userEvent.setup();
renderConfigPage();
await openSshdAccordion(user);
const addBtn = screen.getByRole("button", { name: /add log path/i });
expect(addBtn).toBeDisabled();
});
it("enables the Add button when the path input has content", async () => {
const user = userEvent.setup();
renderConfigPage();
await openSshdAccordion(user);
const input = screen.getByPlaceholderText("/var/log/example.log");
await user.type(input, "/var/log/nginx/access.log");
const addBtn = screen.getByRole("button", { name: /add log path/i });
expect(addBtn).not.toBeDisabled();
});
it("calls addLogPath and appends the path on successful submission", async () => {
const user = userEvent.setup();
renderConfigPage();
await openSshdAccordion(user);
const input = screen.getByPlaceholderText("/var/log/example.log");
await user.type(input, "/var/log/nginx/access.log");
const addBtn = screen.getByRole("button", { name: /add log path/i });
await user.click(addBtn);
await waitFor(() => {
expect(mockAddLogPath).toHaveBeenCalledWith("sshd", {
log_path: "/var/log/nginx/access.log",
tail: true,
});
});
// New path should appear in the list as an <input> value
expect(screen.getByDisplayValue("/var/log/nginx/access.log")).toBeInTheDocument();
// Input should be cleared
expect(input).toHaveValue("");
});
it("shows a success message after adding a log path", async () => {
const user = userEvent.setup();
renderConfigPage();
await openSshdAccordion(user);
const input = screen.getByPlaceholderText("/var/log/example.log");
await user.type(input, "/var/log/nginx/access.log");
await user.click(screen.getByRole("button", { name: /add log path/i }));
await waitFor(() => {
expect(
screen.getByText(/added log path.*\/var\/log\/nginx\/access\.log/i),
).toBeInTheDocument();
});
});
it("shows an error message when addLogPath fails", async () => {
mockAddLogPath.mockRejectedValueOnce(new Error("Connection refused"));
const user = userEvent.setup();
renderConfigPage();
await openSshdAccordion(user);
const input = screen.getByPlaceholderText("/var/log/example.log");
await user.type(input, "/var/log/bad.log");
await user.click(screen.getByRole("button", { name: /add log path/i }));
await waitFor(() => {
expect(
screen.getByText("Failed to add log path."),
).toBeInTheDocument();
});
});
});