/** * 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( , ); } // --------------------------------------------------------------------------- // 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); }); }); });