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
265 lines
8.9 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|