Files
prowler/ui/components/scans/table/scan-jobs-row-actions.test.tsx
T

524 lines
13 KiB
TypeScript

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { useCloudUpgradeStore } from "@/store/cloud-upgrade/store";
import type { ScanProps } from "@/types";
import { PAID_PLAN_UPGRADE_FEATURE } from "@/types/cloud-upgrade";
import { SCAN_SCHEDULE_CAPABILITY } from "@/types/schedules";
import { ScanJobsRowActions } from "./scan-jobs-row-actions";
const {
downloadScanZipMock,
getScheduleMock,
getTaskMock,
pushMock,
toastMock,
} = vi.hoisted(() => ({
downloadScanZipMock: vi.fn(),
getScheduleMock: vi.fn(),
getTaskMock: vi.fn(),
pushMock: vi.fn(),
toastMock: vi.fn(),
}));
vi.mock("next/navigation", () => ({
useRouter: () => ({
push: pushMock,
}),
}));
vi.mock("@/components/shadcn", async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
useToast: () => ({ toast: toastMock }),
}));
vi.mock("@/actions/task", () => ({
getTask: getTaskMock,
}));
vi.mock("@/actions/schedules", () => ({
getSchedule: getScheduleMock,
}));
vi.mock("@/lib/helper", () => ({
downloadScanZip: downloadScanZipMock,
}));
vi.mock("@/components/scans/edit-alias-modal", () => ({
EditAliasModal: ({
open,
currentAlias,
}: {
open: boolean;
currentAlias: string;
}) =>
open ? (
<div role="dialog" aria-label="Edit Alias">
Editing {currentAlias}
</div>
) : null,
}));
vi.mock("@/components/scans/schedule/edit-scan-schedule-modal", () => ({
EDIT_SCAN_SCHEDULE_STATE: {
LOADING: "loading",
LOADED: "loaded",
ERROR: "error",
},
EditScanScheduleModal: ({
open,
provider,
}: {
open: boolean;
provider?: { providerId: string };
}) =>
open ? (
<div role="dialog" aria-label="Edit Scan Schedule">
Editing schedule for {provider?.providerId}
</div>
) : null,
}));
const makeScan = (
overrides: Partial<ScanProps["attributes"]> = {},
): ScanProps => ({
type: "scans",
id: "scan-1",
attributes: {
name: "Production scan",
trigger: "scheduled",
state: "scheduled",
unique_resource_count: 0,
progress: 0,
scanner_args: null,
duration: 0,
started_at: "",
inserted_at: "",
completed_at: "",
scheduled_at: "",
next_scan_at: "",
...overrides,
},
relationships: {
provider: { data: { type: "providers", id: "provider-1" } },
task: { data: { type: "tasks", id: "task-1" } },
},
});
describe("ScanJobsRowActions", () => {
beforeEach(() => {
getScheduleMock.mockResolvedValue({
data: {
type: "schedules",
id: "provider-1",
attributes: { scan_hour: null },
relationships: {
provider: { data: { type: "providers", id: "provider-1" } },
},
},
});
});
afterEach(() => {
vi.unstubAllEnvs();
vi.clearAllMocks();
useCloudUpgradeStore.getState().closeCloudUpgrade();
});
it("opens the Edit modal seeded with the current scan name", async () => {
// Given
const user = userEvent.setup();
render(<ScanJobsRowActions scan={makeScan()} tab="scheduled" />);
// When
await user.click(
screen.getByRole("button", { name: /open actions menu/i }),
);
await user.click(
screen.getByRole("menuitem", { name: /edit scan alias/i }),
);
// Then
expect(
screen.getByRole("dialog", { name: /edit alias/i }),
).toHaveTextContent("Editing Production scan");
});
it("opens Edit Scan Schedule for Prowler Cloud subscribed scan rows", async () => {
// Given
vi.stubEnv("UI_CLOUD_ENABLED", "true");
const user = userEvent.setup();
render(<ScanJobsRowActions scan={makeScan()} tab="scheduled" />);
// When
await user.click(
screen.getByRole("button", { name: /open actions menu/i }),
);
await user.click(
screen.getByRole("menuitem", { name: /edit scan schedule/i }),
);
// Then
expect(
screen.getByRole("dialog", { name: /edit scan schedule/i }),
).toHaveTextContent("Editing schedule for provider-1");
});
it("hides Edit Scan Schedule outside Prowler Cloud (OSS)", async () => {
// Given
vi.stubEnv("UI_CLOUD_ENABLED", "false");
const user = userEvent.setup();
render(<ScanJobsRowActions scan={makeScan()} tab="scheduled" />);
// When
await user.click(
screen.getByRole("button", { name: /open actions menu/i }),
);
// Then
expect(
screen.queryByRole("menuitem", { name: /edit scan schedule/i }),
).not.toBeInTheDocument();
});
it("hides Edit Scan Schedule outside the Scheduled tab even on Cloud", async () => {
// Given - advanced capability (Cloud) but rendered in the Completed tab.
vi.stubEnv("UI_CLOUD_ENABLED", "true");
const user = userEvent.setup();
render(
<ScanJobsRowActions
scan={makeScan({ state: "completed" })}
tab="completed"
/>,
);
// When
await user.click(
screen.getByRole("button", { name: /open actions menu/i }),
);
// Then
expect(
screen.queryByRole("menuitem", { name: /edit scan schedule/i }),
).not.toBeInTheDocument();
});
it("hides Edit Scan Schedule for manual-only Cloud scan rows", async () => {
// Given
vi.stubEnv("UI_CLOUD_ENABLED", "true");
const user = userEvent.setup();
render(
<ScanJobsRowActions
scan={makeScan()}
tab="scheduled"
capability={SCAN_SCHEDULE_CAPABILITY.MANUAL_ONLY}
/>,
);
// When
await user.click(
screen.getByRole("button", { name: /open actions menu/i }),
);
// Then
expect(
screen.queryByRole("menuitem", { name: /edit scan schedule/i }),
).not.toBeInTheDocument();
});
it("hides Edit Scan Schedule for blocked Cloud scan rows", async () => {
// Given
vi.stubEnv("UI_CLOUD_ENABLED", "true");
const user = userEvent.setup();
render(
<ScanJobsRowActions
scan={makeScan()}
tab="scheduled"
capability={SCAN_SCHEDULE_CAPABILITY.BLOCKED}
/>,
);
// When
await user.click(
screen.getByRole("button", { name: /open actions menu/i }),
);
// Then
expect(
screen.queryByRole("menuitem", { name: /edit scan schedule/i }),
).not.toBeInTheDocument();
});
it("links completed scans to compliance from the actions menu", async () => {
// Given
const user = userEvent.setup();
render(
<ScanJobsRowActions
scan={makeScan({
state: "completed",
completed_at: "2026-01-01T10:05:00Z",
})}
tab="completed"
/>,
);
// When
await user.click(
screen.getByRole("button", { name: /open actions menu/i }),
);
await user.click(
screen.getByRole("menuitem", { name: /view compliance/i }),
);
// Then
expect(pushMock).toHaveBeenCalledWith(
"/compliance?tab=per-scan&scanId=scan-1",
);
});
it("renames the completed scan report download action", async () => {
// Given
const user = userEvent.setup();
render(
<ScanJobsRowActions
scan={makeScan({
state: "completed",
completed_at: "2026-01-01T10:05:00Z",
})}
tab="completed"
/>,
);
// When
await user.click(
screen.getByRole("button", { name: /open actions menu/i }),
);
// Then
expect(
screen.getByRole("menuitem", { name: /download scan reports/i }),
).toBeInTheDocument();
expect(
screen.queryByRole("menuitem", { name: /download findings/i }),
).not.toBeInTheDocument();
});
it("links completed scans to filtered findings without a browser-local date", async () => {
// Given
const user = userEvent.setup();
render(
<ScanJobsRowActions
scan={makeScan({
state: "completed",
started_at: "2026-01-01T09:50:00Z",
completed_at: "2026-01-01T10:05:00Z",
})}
tab="completed"
/>,
);
// When
await user.click(
screen.getByRole("button", { name: /open actions menu/i }),
);
await user.click(screen.getByRole("menuitem", { name: /view findings/i }));
// Then
expect(pushMock).toHaveBeenCalledWith(
"/findings?filter[scan__in]=scan-1&filter[status__in]=FAIL",
);
});
it("disables View Findings when the completed scan has no completion timestamp", async () => {
// Given
const user = userEvent.setup();
render(
<ScanJobsRowActions
scan={makeScan({ state: "completed", completed_at: "" })}
tab="completed"
/>,
);
// When
await user.click(
screen.getByRole("button", { name: /open actions menu/i }),
);
const viewFindings = screen.getByRole("menuitem", {
name: /view findings/i,
});
await user.click(viewFindings);
// Then
expect(viewFindings).toHaveAttribute("aria-disabled", "true");
expect(pushMock).not.toHaveBeenCalled();
});
it("triggers downloadScanZip with the scan id when downloading reports", async () => {
// Given
const user = userEvent.setup();
render(
<ScanJobsRowActions
scan={makeScan({
state: "completed",
completed_at: "2026-01-01T10:05:00Z",
})}
tab="completed"
/>,
);
// When
await user.click(
screen.getByRole("button", { name: /open actions menu/i }),
);
await user.click(
screen.getByRole("menuitem", { name: /download scan reports/i }),
);
// Then
expect(downloadScanZipMock).toHaveBeenCalledWith("scan-1", toastMock);
});
it("offers neither report download nor compliance for a partial scan", async () => {
// A partial scan re-checks a few resources: no report files, no compliance.
const user = userEvent.setup();
render(
<ScanJobsRowActions
scan={makeScan({
state: "completed",
completed_at: "2026-01-01T10:05:00Z",
is_partial: true,
})}
tab="completed"
/>,
);
await user.click(
screen.getByRole("button", { name: /open actions menu/i }),
);
expect(
screen.queryByRole("menuitem", { name: /download scan reports/i }),
).not.toBeInTheDocument();
expect(
screen.queryByRole("menuitem", { name: /view compliance/i }),
).not.toBeInTheDocument();
// The rest of the completed-scan actions stay available.
expect(
screen.getByRole("menuitem", { name: /view findings/i }),
).toBeInTheDocument();
});
it("opens the paid plan upgrade instead of downloading subscription-only reports", async () => {
// Given
const user = userEvent.setup();
render(
<ScanJobsRowActions
scan={makeScan({
state: "completed",
completed_at: "2026-01-01T10:05:00Z",
})}
tab="completed"
subscriptionOnly
/>,
);
// When
await user.click(
screen.getByRole("button", { name: /open actions menu/i }),
);
await user.click(
screen.getByRole("menuitem", { name: /download scan reports/i }),
);
// Then
expect(downloadScanZipMock).not.toHaveBeenCalled();
expect(useCloudUpgradeStore.getState().activeFeature).toBe(
PAID_PLAN_UPGRADE_FEATURE.REPORT_DOWNLOAD,
);
});
it("opens failed scan error details from the actions menu", async () => {
// Given
const user = userEvent.setup();
const writeText = vi.fn().mockResolvedValue(undefined);
Object.defineProperty(navigator, "clipboard", {
value: { writeText },
configurable: true,
});
getTaskMock.mockResolvedValue({
data: {
attributes: {
result: {
exc_type: "ValidationError",
exc_message: ["Missing cloud credentials", "Retry scan setup"],
},
},
},
});
render(
<ScanJobsRowActions
scan={makeScan({
state: "failed",
completed_at: "2026-01-01T10:05:00Z",
})}
tab="completed"
/>,
);
// When
await user.click(
screen.getByRole("button", { name: /open actions menu/i }),
);
await user.click(
screen.getByRole("menuitem", { name: /view error details/i }),
);
// Then
expect(getTaskMock).toHaveBeenCalledWith("task-1");
expect(
await screen.findByRole("dialog", { name: /scan error details/i }),
).toBeInTheDocument();
expect(screen.getByText("ValidationError")).toBeInTheDocument();
expect(screen.getByText(/Missing cloud credentials/)).toBeInTheDocument();
expect(screen.getByText(/Retry scan setup/)).toBeInTheDocument();
await user.click(
screen.getByRole("button", { name: /copy error details/i }),
);
expect(writeText).toHaveBeenCalledWith(
"ErrorType: ValidationError\nError: Missing cloud credentials\nRetry scan setup",
);
});
it("does not show error details for non-failed scans", async () => {
// Given
const user = userEvent.setup();
render(
<ScanJobsRowActions
scan={makeScan({ state: "completed" })}
tab="completed"
/>,
);
// When
await user.click(
screen.getByRole("button", { name: /open actions menu/i }),
);
// Then
expect(
screen.queryByRole("menuitem", { name: /view error details/i }),
).not.toBeInTheDocument();
});
});