mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-20 10:24:15 +00:00
Co-authored-by: Pablo F.G <pablo.fernandez@prowler.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com> Co-authored-by: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com>
418 lines
12 KiB
TypeScript
418 lines
12 KiB
TypeScript
import { render, screen } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import { useScansStore } from "@/store";
|
|
|
|
import { ScansPageShell } from "./scans-page-shell";
|
|
|
|
const { pushMock, replaceMock, searchParamsValue } = vi.hoisted(() => ({
|
|
pushMock: vi.fn(),
|
|
replaceMock: vi.fn(),
|
|
searchParamsValue: { current: "" },
|
|
}));
|
|
|
|
const { scansFilterBarSpy } = vi.hoisted(() => ({
|
|
scansFilterBarSpy: vi.fn(),
|
|
}));
|
|
|
|
const localStorageMock = (() => {
|
|
let store: Record<string, string> = {};
|
|
|
|
return {
|
|
getItem: vi.fn((key: string) => store[key] ?? null),
|
|
setItem: vi.fn((key: string, value: string) => {
|
|
store[key] = value;
|
|
}),
|
|
removeItem: vi.fn((key: string) => {
|
|
delete store[key];
|
|
}),
|
|
clear: vi.fn(() => {
|
|
store = {};
|
|
}),
|
|
get length() {
|
|
return Object.keys(store).length;
|
|
},
|
|
key: vi.fn((index: number) => Object.keys(store)[index] ?? null),
|
|
};
|
|
})();
|
|
|
|
Object.defineProperty(window, "localStorage", {
|
|
value: localStorageMock,
|
|
writable: true,
|
|
});
|
|
|
|
vi.mock("next/navigation", () => ({
|
|
usePathname: () => "/scans",
|
|
useRouter: () => ({
|
|
push: pushMock,
|
|
replace: replaceMock,
|
|
}),
|
|
useSearchParams: () => new URLSearchParams(searchParamsValue.current),
|
|
}));
|
|
|
|
vi.mock("./scans-filter-bar", () => ({
|
|
ScansFilterBar: (props: {
|
|
showStatusFilter: boolean;
|
|
onScheduleTypeChange: (value: string) => void;
|
|
onScanStatusChange: (value: string) => void;
|
|
}) => {
|
|
scansFilterBarSpy(props);
|
|
return (
|
|
<>
|
|
<div>Shared scan filters</div>
|
|
<select
|
|
aria-label="All Types"
|
|
onChange={(event) => props.onScheduleTypeChange(event.target.value)}
|
|
>
|
|
<option value="all">All Types</option>
|
|
</select>
|
|
{props.showStatusFilter && (
|
|
<select
|
|
aria-label="All Statuses"
|
|
onChange={(event) => props.onScanStatusChange(event.target.value)}
|
|
>
|
|
<option value="all">All Statuses</option>
|
|
</select>
|
|
)}
|
|
</>
|
|
);
|
|
},
|
|
}));
|
|
|
|
vi.mock("./launch-scan-modal", () => ({
|
|
LaunchScanModal: ({
|
|
open,
|
|
onOpenChange,
|
|
}: {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
}) =>
|
|
open ? (
|
|
<div role="dialog">
|
|
Launch scan
|
|
<button type="button" onClick={() => onOpenChange(false)}>
|
|
Close
|
|
</button>
|
|
</div>
|
|
) : null,
|
|
}));
|
|
|
|
vi.mock("@/components/providers/muted-findings-config-button", () => ({
|
|
MutedFindingsConfigButton: () => <a href="/mutelist">Configure Mutelist</a>,
|
|
}));
|
|
|
|
const providers = [
|
|
{
|
|
id: "provider-1",
|
|
type: "providers" as const,
|
|
attributes: {
|
|
provider: "aws" as const,
|
|
uid: "123456789012",
|
|
alias: "Production",
|
|
status: "completed" as const,
|
|
resources: 0,
|
|
connection: {
|
|
connected: true,
|
|
last_checked_at: "2026-04-13T00:00:00Z",
|
|
},
|
|
scanner_args: {
|
|
only_logs: false,
|
|
excluded_checks: [],
|
|
aws_retries_max_attempts: 3,
|
|
},
|
|
inserted_at: "2026-04-13T00:00:00Z",
|
|
updated_at: "2026-04-13T00:00:00Z",
|
|
created_by: {
|
|
object: "user",
|
|
id: "user-1",
|
|
},
|
|
},
|
|
relationships: {
|
|
secret: {
|
|
data: null,
|
|
},
|
|
provider_groups: {
|
|
meta: {
|
|
count: 0,
|
|
},
|
|
data: [],
|
|
},
|
|
},
|
|
},
|
|
];
|
|
|
|
describe("ScansPageShell", () => {
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
vi.clearAllMocks();
|
|
localStorageMock.clear();
|
|
searchParamsValue.current = "";
|
|
useScansStore.getState().closeLaunchScanModal();
|
|
});
|
|
|
|
it("does not render an imported findings tab", () => {
|
|
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
|
|
|
render(
|
|
<ScansPageShell providers={providers} hasManageScansPermission>
|
|
<div>Scans table</div>
|
|
</ScansPageShell>,
|
|
);
|
|
|
|
expect(
|
|
screen.queryByRole("tab", { name: /imported findings/i }),
|
|
).not.toBeInTheDocument();
|
|
expect(
|
|
screen.queryByRole("button", { name: /import findings/i }),
|
|
).not.toBeInTheDocument();
|
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("uses the shared scan filter bar for scan filters", () => {
|
|
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
|
|
|
render(
|
|
<ScansPageShell providers={providers} hasManageScansPermission>
|
|
<div>Scans table</div>
|
|
</ScansPageShell>,
|
|
);
|
|
|
|
expect(screen.getByText("Shared scan filters")).toBeInTheDocument();
|
|
expect(scansFilterBarSpy).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
providers,
|
|
scheduleType: "all",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("clears the active sort when switching tabs", async () => {
|
|
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
|
searchParamsValue.current = "tab=active&sort=trigger";
|
|
const user = userEvent.setup();
|
|
|
|
render(
|
|
<ScansPageShell providers={providers} hasManageScansPermission>
|
|
<div>Scans table</div>
|
|
</ScansPageShell>,
|
|
);
|
|
|
|
await user.click(screen.getByRole("tab", { name: /completed/i }));
|
|
|
|
expect(pushMock).toHaveBeenCalled();
|
|
const calledUrl = pushMock.mock.calls.at(-1)?.[0] as string;
|
|
expect(calledUrl).toContain("tab=completed");
|
|
expect(calledUrl).not.toContain("sort=");
|
|
});
|
|
|
|
it("uses a generic type filter label in Cloud", () => {
|
|
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
|
|
|
render(
|
|
<ScansPageShell providers={providers} hasManageScansPermission>
|
|
<div>Scans table</div>
|
|
</ScansPageShell>,
|
|
);
|
|
|
|
expect(screen.getByRole("combobox", { name: /all types/i })).toBeVisible();
|
|
});
|
|
|
|
it("shows the CLI import banner in Cloud", () => {
|
|
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
|
|
|
render(
|
|
<ScansPageShell providers={providers} hasManageScansPermission>
|
|
<div>Scans table</div>
|
|
</ScansPageShell>,
|
|
);
|
|
|
|
expect(screen.getByRole("alert")).toHaveTextContent(
|
|
/import findings from prowler cli/i,
|
|
);
|
|
expect(screen.getByRole("link", { name: /learn more/i })).toHaveAttribute(
|
|
"href",
|
|
"https://docs.prowler.com/user-guide/tutorials/prowler-app-import-findings",
|
|
);
|
|
});
|
|
|
|
it("hides the CLI import banner outside Cloud", () => {
|
|
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
|
|
|
render(
|
|
<ScansPageShell providers={providers} hasManageScansPermission>
|
|
<div>Scans table</div>
|
|
</ScansPageShell>,
|
|
);
|
|
|
|
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("keeps launch scan with filters and mutelist with tabs", () => {
|
|
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
|
|
|
render(
|
|
<ScansPageShell providers={providers} hasManageScansPermission>
|
|
<div>Scans table</div>
|
|
</ScansPageShell>,
|
|
);
|
|
|
|
expect(
|
|
screen.getByRole("group", { name: /scan filters and actions/i }),
|
|
).toContainElement(screen.getByRole("button", { name: /launch scan/i }));
|
|
expect(
|
|
screen.getByRole("group", { name: /scan filters and actions/i }),
|
|
).not.toContainElement(
|
|
screen.getByRole("link", { name: /configure mutelist/i }),
|
|
);
|
|
expect(screen.getByRole("group", { name: /scan tabs/i })).toContainElement(
|
|
screen.getByRole("link", { name: /configure mutelist/i }),
|
|
);
|
|
});
|
|
|
|
it("shows the active scans count in the in progress tab", () => {
|
|
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
|
|
|
render(
|
|
<ScansPageShell
|
|
providers={providers}
|
|
hasManageScansPermission
|
|
activeScanCount={3}
|
|
>
|
|
<div>Scans table</div>
|
|
</ScansPageShell>,
|
|
);
|
|
|
|
expect(
|
|
screen.getByRole("tab", { name: /in progress \(3\)/i }),
|
|
).toBeVisible();
|
|
expect(screen.getByRole("tab", { name: /^completed$/i })).toBeVisible();
|
|
});
|
|
|
|
it("opens the launch scan modal from the URL", () => {
|
|
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
|
searchParamsValue.current = "launchScan=true";
|
|
|
|
render(
|
|
<ScansPageShell providers={providers} hasManageScansPermission>
|
|
<div>Scans table</div>
|
|
</ScansPageShell>,
|
|
);
|
|
|
|
expect(screen.getByRole("dialog")).toHaveTextContent(/launch scan/i);
|
|
});
|
|
|
|
it("strips the launchScan URL param via the History API when closing the URL-opened modal", async () => {
|
|
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
|
searchParamsValue.current = "tab=completed&launchScan=true";
|
|
const replaceStateSpy = vi.spyOn(window.history, "replaceState");
|
|
const user = userEvent.setup();
|
|
|
|
render(
|
|
<ScansPageShell providers={providers} hasManageScansPermission>
|
|
<div>Scans table</div>
|
|
</ScansPageShell>,
|
|
);
|
|
|
|
await user.click(screen.getByRole("button", { name: /close/i }));
|
|
|
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
// History API (not router.replace) so Next.js does not refetch and reload the page;
|
|
// scanOnDemand's revalidatePath already refreshes the list when a scan launches.
|
|
expect(replaceStateSpy).toHaveBeenCalledWith(
|
|
null,
|
|
"",
|
|
"/scans?tab=completed",
|
|
);
|
|
expect(replaceMock).not.toHaveBeenCalled();
|
|
expect(pushMock).not.toHaveBeenCalled();
|
|
|
|
replaceStateSpy.mockRestore();
|
|
});
|
|
|
|
it("opens and closes the launch scan modal from client state without navigation", async () => {
|
|
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
|
const user = userEvent.setup();
|
|
useScansStore.getState().openLaunchScanModal();
|
|
|
|
render(
|
|
<ScansPageShell providers={providers} hasManageScansPermission>
|
|
<div>Scans table</div>
|
|
</ScansPageShell>,
|
|
);
|
|
|
|
expect(screen.getByRole("dialog")).toHaveTextContent(/launch scan/i);
|
|
|
|
await user.click(screen.getByRole("button", { name: /close/i }));
|
|
|
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
expect(pushMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("shows the status filter only on the completed tab", () => {
|
|
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
|
searchParamsValue.current = "tab=completed";
|
|
|
|
render(
|
|
<ScansPageShell providers={providers} hasManageScansPermission>
|
|
<div>Scans table</div>
|
|
</ScansPageShell>,
|
|
);
|
|
|
|
expect(
|
|
screen.getByRole("combobox", { name: /all statuses/i }),
|
|
).toBeVisible();
|
|
});
|
|
|
|
it("hides the status filter outside of the completed tab", () => {
|
|
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
|
searchParamsValue.current = "tab=active";
|
|
|
|
render(
|
|
<ScansPageShell providers={providers} hasManageScansPermission>
|
|
<div>Scans table</div>
|
|
</ScansPageShell>,
|
|
);
|
|
|
|
expect(
|
|
screen.queryByRole("combobox", { name: /all statuses/i }),
|
|
).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("clears status filter when switching scan tabs", async () => {
|
|
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
|
searchParamsValue.current = "tab=completed&filter%5Bstate__in%5D=failed";
|
|
const user = userEvent.setup();
|
|
|
|
render(
|
|
<ScansPageShell providers={providers} hasManageScansPermission>
|
|
<div>Scans table</div>
|
|
</ScansPageShell>,
|
|
);
|
|
|
|
await user.click(screen.getByRole("tab", { name: /in progress/i }));
|
|
|
|
const calledUrl = pushMock.mock.calls.at(-1)?.[0] as string;
|
|
expect(calledUrl).toContain("tab=active");
|
|
expect(calledUrl).not.toContain("filter%5Bstate__in%5D");
|
|
});
|
|
|
|
it("clears type filter when switching to scheduled scans", async () => {
|
|
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
|
searchParamsValue.current = "tab=completed&filter%5Btrigger%5D=manual";
|
|
const user = userEvent.setup();
|
|
|
|
render(
|
|
<ScansPageShell providers={providers} hasManageScansPermission>
|
|
<div>Scans table</div>
|
|
</ScansPageShell>,
|
|
);
|
|
|
|
await user.click(screen.getByRole("tab", { name: /scheduled/i }));
|
|
|
|
const calledUrl = pushMock.mock.calls.at(-1)?.[0] as string;
|
|
expect(calledUrl).toContain("tab=scheduled");
|
|
expect(calledUrl).not.toContain("filter%5Btrigger%5D");
|
|
});
|
|
});
|