Files
BanGUI/frontend/src/components/config/__tests__/ActivateJailDialog.test.tsx
Lukas d4d04491d2 Add Deactivate Jail button for inactive jails with local override
- Add has_local_override field to InactiveJail model
- Update _build_inactive_jail and list_inactive_jails to compute the field
- Add delete_jail_local_override() service function
- Add DELETE /api/config/jails/{name}/local router endpoint
- Surface has_local_override in frontend InactiveJail type
- Show Deactivate Jail button in JailsTab when has_local_override is true
- Add tests: TestBuildInactiveJail, TestListInactiveJails, TestDeleteJailLocalOverride
2026-03-15 13:41:00 +01:00

203 lines
6.2 KiB
TypeScript

/**
* 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 <HOST>"],
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(
<FluentProvider theme={webLightTheme}>
<ActivateJailDialog
jail={jail}
open={open}
onClose={onClose}
onActivated={onActivated}
/>
</FluentProvider>,
);
}
// ---------------------------------------------------------------------------
// 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();
});
});
});