/** * Tests for RecoveryBanner (Task 3). */ 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 } from "react-router-dom"; import { RecoveryBanner } from "../RecoveryBanner"; import type { PendingRecovery } from "../../../types/config"; // --------------------------------------------------------------------------- // Mocks // --------------------------------------------------------------------------- vi.mock("../../../api/config", () => ({ fetchPendingRecovery: vi.fn(), rollbackJail: vi.fn(), })); import { fetchPendingRecovery, rollbackJail } from "../../../api/config"; const mockFetchPendingRecovery = vi.mocked(fetchPendingRecovery); const mockRollbackJail = vi.mocked(rollbackJail); // --------------------------------------------------------------------------- // Fixtures // --------------------------------------------------------------------------- const pendingRecord: PendingRecovery = { jail_name: "sshd", activated_at: "2024-01-01T12:00:00Z", detected_at: "2024-01-01T12:00:30Z", recovered: false, }; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function renderBanner() { return render( , ); } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe("RecoveryBanner", () => { beforeEach(() => { vi.clearAllMocks(); }); it("renders nothing when pending recovery is null", async () => { mockFetchPendingRecovery.mockResolvedValue(null); renderBanner(); await waitFor(() => { expect(mockFetchPendingRecovery).toHaveBeenCalled(); }); expect(screen.queryByRole("alert")).not.toBeInTheDocument(); }); it("renders warning when there is an unresolved pending recovery", async () => { mockFetchPendingRecovery.mockResolvedValue(pendingRecord); renderBanner(); await waitFor(() => { expect(screen.getByText(/fail2ban stopped responding after activating jail/i)).toBeInTheDocument(); }); expect(screen.getByText(/sshd/i)).toBeInTheDocument(); expect(screen.getByRole("button", { name: /disable & restart/i })).toBeInTheDocument(); expect(screen.getByRole("button", { name: /view logs/i })).toBeInTheDocument(); }); it("hides the banner when recovery is marked as recovered", async () => { const recoveredRecord: PendingRecovery = { ...pendingRecord, recovered: true }; mockFetchPendingRecovery.mockResolvedValue(recoveredRecord); renderBanner(); await waitFor(() => { expect(mockFetchPendingRecovery).toHaveBeenCalled(); }); expect(screen.queryByRole("alert")).not.toBeInTheDocument(); }); it("calls rollbackJail and hides banner on successful rollback", async () => { mockFetchPendingRecovery.mockResolvedValue(pendingRecord); mockRollbackJail.mockResolvedValue({ jail_name: "sshd", disabled: true, fail2ban_running: true, active_jails: 0, message: "Rolled back.", }); renderBanner(); await waitFor(() => { expect(screen.getByRole("button", { name: /disable & restart/i })).toBeInTheDocument(); }); await userEvent.click( screen.getByRole("button", { name: /disable & restart/i }), ); expect(mockRollbackJail).toHaveBeenCalledWith("sshd"); }); it("shows rollback error when rollbackJail fails", async () => { mockFetchPendingRecovery.mockResolvedValue(pendingRecord); mockRollbackJail.mockRejectedValue(new Error("Connection refused")); renderBanner(); await waitFor(() => { expect(screen.getByRole("button", { name: /disable & restart/i })).toBeInTheDocument(); }); await userEvent.click( screen.getByRole("button", { name: /disable & restart/i }), ); await waitFor(() => { expect(screen.getByText(/rollback failed/i)).toBeInTheDocument(); }); }); });