/** * Tests for ActivateJailDialog (Task 7). * * Covers: * - "Activate" button is disabled when pre-validation returns blocking issues. * - "Activate" button is enabled when validation passes. * - Dialog stays open and shows an error when the backend returns active=false. * - `onActivated` is called and dialog closes when backend returns active=true. */ 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 { ActivateJailDialog } from "../ActivateJailDialog"; import type { InactiveJail, JailActivationResponse, JailValidationResult } from "../../../types/config"; // --------------------------------------------------------------------------- // Mocks // --------------------------------------------------------------------------- vi.mock("../../../api/config", () => ({ activateJail: vi.fn(), validateJailConfig: vi.fn(), })); import { activateJail, validateJailConfig } from "../../../api/config"; const mockActivateJail = vi.mocked(activateJail); const mockValidateJailConfig = vi.mocked(validateJailConfig); // --------------------------------------------------------------------------- // Fixtures // --------------------------------------------------------------------------- const baseJail: InactiveJail = { name: "airsonic-auth", filter: "airsonic-auth", actions: ["iptables-multiport"], port: "8080", logpath: ["/var/log/airsonic/airsonic.log"], bantime: "10m", findtime: "10m", maxretry: 5, ban_time_seconds: 600, find_time_seconds: 600, log_encoding: "auto", backend: "auto", date_pattern: null, use_dns: "warn", prefregex: "", fail_regex: ["Failed login.*from "], ignore_regex: [], bantime_escalation: null, source_file: "/config/fail2ban/jail.d/airsonic-auth.conf", enabled: false, has_local_override: false, }; /** Successful activation response. */ const successResponse: JailActivationResponse = { name: "airsonic-auth", active: true, message: "Jail activated successfully.", fail2ban_running: true, validation_warnings: [], }; /** Response when backend blocks activation (e.g. missing logpath). */ const blockedResponse: JailActivationResponse = { name: "airsonic-auth", active: false, message: "Jail 'airsonic-auth' cannot be activated: logpath does not exist.", fail2ban_running: true, validation_warnings: ["logpath: /var/log/airsonic/airsonic.log does not exist"], }; /** Validation result with a logpath issue (should block the button). */ const validationWithLogpathIssue: JailValidationResult = { jail_name: "airsonic-auth", valid: false, issues: [{ field: "logpath", message: "/var/log/airsonic/airsonic.log does not exist" }], }; /** Validation result with no issues. */ const validationPassed: JailValidationResult = { jail_name: "airsonic-auth", valid: true, issues: [], }; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- interface DialogProps { jail?: InactiveJail | null; open?: boolean; onClose?: () => void; onActivated?: () => void; } function renderDialog({ jail = baseJail, open = true, onClose = vi.fn(), onActivated = vi.fn(), }: DialogProps = {}) { return render( , ); } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe("ActivateJailDialog", () => { beforeEach(() => { vi.clearAllMocks(); }); it("disables the Activate button when pre-validation returns blocking issues", async () => { mockValidateJailConfig.mockResolvedValue(validationWithLogpathIssue); renderDialog(); // Wait for validation to complete and the error message to appear. await waitFor(() => { expect(screen.getByText(/configuration errors detected/i)).toBeInTheDocument(); }); const activateBtn = screen.getByRole("button", { name: /^activate$/i }); expect(activateBtn).toBeDisabled(); }); it("enables the Activate button when validation passes", async () => { mockValidateJailConfig.mockResolvedValue(validationPassed); renderDialog(); // Wait for validation spinner to disappear. await waitFor(() => { expect(screen.queryByText(/validating configuration/i)).not.toBeInTheDocument(); }); const activateBtn = screen.getByRole("button", { name: /^activate$/i }); expect(activateBtn).not.toBeDisabled(); }); it("keeps the dialog open and shows an error when backend returns active=false", async () => { mockValidateJailConfig.mockResolvedValue(validationPassed); mockActivateJail.mockResolvedValue(blockedResponse); const onActivated = vi.fn(); renderDialog({ onActivated }); // Wait for validation to finish. await waitFor(() => { expect(screen.queryByText(/validating configuration/i)).not.toBeInTheDocument(); }); const activateBtn = screen.getByRole("button", { name: /^activate$/i }); await userEvent.click(activateBtn); // The server's error message should appear. await waitFor(() => { expect( screen.getByText(/cannot be activated/i), ).toBeInTheDocument(); }); // onActivated must NOT have been called. expect(onActivated).not.toHaveBeenCalled(); }); it("calls onActivated when backend returns active=true", async () => { mockValidateJailConfig.mockResolvedValue(validationPassed); mockActivateJail.mockResolvedValue(successResponse); const onActivated = vi.fn(); renderDialog({ onActivated }); await waitFor(() => { expect(screen.queryByText(/validating configuration/i)).not.toBeInTheDocument(); }); const activateBtn = screen.getByRole("button", { name: /^activate$/i }); await userEvent.click(activateBtn); await waitFor(() => { expect(onActivated).toHaveBeenCalledOnce(); }); }); });