mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-03 17:54:05 +00:00
524 lines
13 KiB
TypeScript
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();
|
|
});
|
|
});
|