190 lines
5.8 KiB
TypeScript
190 lines
5.8 KiB
TypeScript
/**
|
|
* Tests for the LogTab component (Task 2).
|
|
*/
|
|
|
|
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 { LogTab } from "../LogTab";
|
|
import type { Fail2BanLogResponse, ServiceStatusResponse } from "../../../types/config";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Mocks
|
|
// ---------------------------------------------------------------------------
|
|
|
|
vi.mock("../../../api/config", () => ({
|
|
fetchFail2BanLog: vi.fn(),
|
|
fetchServiceStatus: vi.fn(),
|
|
}));
|
|
|
|
import { fetchFail2BanLog, fetchServiceStatus } from "../../../api/config";
|
|
|
|
const mockFetchLog = vi.mocked(fetchFail2BanLog);
|
|
const mockFetchStatus = vi.mocked(fetchServiceStatus);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Fixtures
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const onlineStatus: ServiceStatusResponse = {
|
|
online: true,
|
|
version: "1.0.2",
|
|
jail_count: 3,
|
|
total_bans: 12,
|
|
total_failures: 5,
|
|
log_level: "INFO",
|
|
log_target: "/var/log/fail2ban.log",
|
|
};
|
|
|
|
const offlineStatus: ServiceStatusResponse = {
|
|
online: false,
|
|
version: null,
|
|
jail_count: 0,
|
|
total_bans: 0,
|
|
total_failures: 0,
|
|
log_level: "UNKNOWN",
|
|
log_target: "UNKNOWN",
|
|
};
|
|
|
|
const logResponse: Fail2BanLogResponse = {
|
|
log_path: "/var/log/fail2ban.log",
|
|
lines: [
|
|
"2025-01-01 12:00:00 INFO sshd Found 1.2.3.4",
|
|
"2025-01-01 12:00:01 WARNING sshd Too many failures",
|
|
"2025-01-01 12:00:02 ERROR fail2ban something went wrong",
|
|
],
|
|
total_lines: 1000,
|
|
log_level: "INFO",
|
|
log_target: "/var/log/fail2ban.log",
|
|
};
|
|
|
|
const nonFileLogResponse: Fail2BanLogResponse = {
|
|
...logResponse,
|
|
log_target: "STDOUT",
|
|
lines: [],
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function renderTab() {
|
|
return render(
|
|
<FluentProvider theme={webLightTheme}>
|
|
<LogTab />
|
|
</FluentProvider>,
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("LogTab", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("shows a spinner while loading", () => {
|
|
// Never resolves during this test.
|
|
mockFetchStatus.mockReturnValue(new Promise(() => undefined));
|
|
mockFetchLog.mockReturnValue(new Promise(() => undefined));
|
|
|
|
renderTab();
|
|
|
|
expect(screen.getByText(/loading log viewer/i)).toBeInTheDocument();
|
|
});
|
|
|
|
it("renders the health panel with Running badge when online", async () => {
|
|
mockFetchStatus.mockResolvedValue(onlineStatus);
|
|
mockFetchLog.mockResolvedValue(logResponse);
|
|
|
|
renderTab();
|
|
|
|
await waitFor(() => { expect(screen.queryByText(/loading log viewer/i)).toBeNull(); });
|
|
|
|
expect(screen.getByText("Running")).toBeInTheDocument();
|
|
expect(screen.getByText("1.0.2")).toBeInTheDocument();
|
|
expect(screen.getByText("3")).toBeInTheDocument(); // active jails
|
|
expect(screen.getByText("12")).toBeInTheDocument(); // total bans
|
|
});
|
|
|
|
it("renders the Offline badge and warning when fail2ban is down", async () => {
|
|
mockFetchStatus.mockResolvedValue(offlineStatus);
|
|
mockFetchLog.mockRejectedValue(new Error("not running"));
|
|
|
|
renderTab();
|
|
|
|
await waitFor(() => { expect(screen.queryByText(/loading log viewer/i)).toBeNull(); });
|
|
|
|
expect(screen.getByText("Offline")).toBeInTheDocument();
|
|
expect(screen.getByText(/not running or unreachable/i)).toBeInTheDocument();
|
|
});
|
|
|
|
it("renders log lines in the log viewer", async () => {
|
|
mockFetchStatus.mockResolvedValue(onlineStatus);
|
|
mockFetchLog.mockResolvedValue(logResponse);
|
|
|
|
renderTab();
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/2025-01-01 12:00:00 INFO/)).toBeInTheDocument();
|
|
});
|
|
|
|
expect(screen.getByText(/2025-01-01 12:00:01 WARNING/)).toBeInTheDocument();
|
|
expect(screen.getByText(/2025-01-01 12:00:02 ERROR/)).toBeInTheDocument();
|
|
});
|
|
|
|
it("shows a non-file target info banner when log_target is STDOUT", async () => {
|
|
mockFetchStatus.mockResolvedValue(onlineStatus);
|
|
mockFetchLog.mockResolvedValue(nonFileLogResponse);
|
|
|
|
renderTab();
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/fail2ban is logging to/i)).toBeInTheDocument();
|
|
});
|
|
|
|
expect(screen.getByText(/STDOUT/)).toBeInTheDocument();
|
|
expect(screen.queryByText(/Refresh/)).toBeNull();
|
|
});
|
|
|
|
it("shows empty state when no lines match the filter", async () => {
|
|
mockFetchStatus.mockResolvedValue(onlineStatus);
|
|
mockFetchLog.mockResolvedValue({ ...logResponse, lines: [] });
|
|
|
|
renderTab();
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/no log entries found/i)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it("shows truncation notice when total_lines > lines.length", async () => {
|
|
mockFetchStatus.mockResolvedValue(onlineStatus);
|
|
mockFetchLog.mockResolvedValue({ ...logResponse, lines: logResponse.lines, total_lines: 1000 });
|
|
|
|
renderTab();
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/showing last/i)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it("calls fetchFail2BanLog again on Refresh button click", async () => {
|
|
mockFetchStatus.mockResolvedValue(onlineStatus);
|
|
mockFetchLog.mockResolvedValue(logResponse);
|
|
|
|
const user = userEvent.setup();
|
|
renderTab();
|
|
|
|
await waitFor(() => { expect(screen.getByText(/Refresh/)).toBeInTheDocument(); });
|
|
|
|
const refreshBtn = screen.getByRole("button", { name: /refresh/i });
|
|
await user.click(refreshBtn);
|
|
|
|
await waitFor(() => { expect(mockFetchLog).toHaveBeenCalledTimes(2); });
|
|
});
|
|
});
|