mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
341 lines
9.3 KiB
TypeScript
341 lines
9.3 KiB
TypeScript
import { render, screen } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import type { ReactNode } from "react";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import type { FilterOption, MetaDataProps, ProviderProps } from "@/types";
|
|
import type { ProvidersTableRow } from "@/types/providers-table";
|
|
import { SCAN_SCHEDULE_CAPABILITY } from "@/types/schedules";
|
|
|
|
const {
|
|
providersAccountsTableSpy,
|
|
refreshMock,
|
|
replaceMock,
|
|
searchParamsValue,
|
|
} = vi.hoisted(() => ({
|
|
providersAccountsTableSpy: vi.fn(),
|
|
refreshMock: vi.fn(),
|
|
replaceMock: vi.fn(),
|
|
searchParamsValue: { current: "" },
|
|
}));
|
|
|
|
vi.mock("next/navigation", () => ({
|
|
usePathname: () => "/providers",
|
|
useRouter: () => ({
|
|
refresh: refreshMock,
|
|
replace: replaceMock,
|
|
}),
|
|
useSearchParams: () => new URLSearchParams(searchParamsValue.current),
|
|
}));
|
|
|
|
vi.mock("@/components/providers/table", () => ({
|
|
SkeletonTableProviders: () => <div data-testid="providers-skeleton" />,
|
|
}));
|
|
|
|
vi.mock("@/components/providers/add-provider-button", () => ({
|
|
AddProviderButton: ({ onOpenWizard }: { onOpenWizard: () => void }) => (
|
|
<button type="button" onClick={onOpenWizard}>
|
|
Add Provider
|
|
</button>
|
|
),
|
|
}));
|
|
|
|
vi.mock("@/components/providers/muted-findings-config-button", () => ({
|
|
MutedFindingsConfigButton: () => (
|
|
<button type="button">Muted findings config</button>
|
|
),
|
|
}));
|
|
|
|
vi.mock("@/components/providers/providers-filters", () => ({
|
|
ProvidersFilters: ({ actions }: { actions: ReactNode }) => (
|
|
<div data-testid="providers-filters">
|
|
Filters
|
|
{actions}
|
|
</div>
|
|
),
|
|
}));
|
|
|
|
vi.mock("@/components/providers/providers-accounts-table", () => ({
|
|
ProvidersAccountsTable: (props: { scanScheduleCapability?: string }) => {
|
|
providersAccountsTableSpy(props);
|
|
return <div data-testid="providers-table">Table</div>;
|
|
},
|
|
}));
|
|
|
|
vi.mock("@/components/providers/wizard", () => ({
|
|
ProviderWizardModal: ({
|
|
open,
|
|
onOpenChange,
|
|
}: {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
}) =>
|
|
open ? (
|
|
<div role="dialog">
|
|
Provider wizard
|
|
<button type="button" onClick={() => onOpenChange(false)}>
|
|
Close
|
|
</button>
|
|
</div>
|
|
) : null,
|
|
}));
|
|
|
|
import { ProvidersAccountsView } from "./providers-accounts-view";
|
|
|
|
const filters: FilterOption[] = [];
|
|
const providers: ProviderProps[] = [];
|
|
const rows: ProvidersTableRow[] = [];
|
|
const metadata: MetaDataProps = {
|
|
pagination: { page: 1, pages: 1, count: 0, itemsPerPage: [10] },
|
|
version: "latest",
|
|
};
|
|
|
|
const disconnectedProviders: ProviderProps[] = [
|
|
{
|
|
id: "provider-1",
|
|
type: "providers",
|
|
attributes: {
|
|
provider: "aws",
|
|
uid: "123456789012",
|
|
alias: "Production",
|
|
status: "completed",
|
|
resources: 0,
|
|
connection: {
|
|
connected: false,
|
|
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("ProvidersAccountsView", () => {
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
providersAccountsTableSpy.mockClear();
|
|
searchParamsValue.current = "";
|
|
window.history.replaceState({}, "", "/");
|
|
});
|
|
|
|
it("shows a full page empty state without filters or table when there are no providers", () => {
|
|
// Given/When
|
|
render(
|
|
<ProvidersAccountsView
|
|
isCloud={false}
|
|
filters={filters}
|
|
metadata={metadata}
|
|
providers={providers}
|
|
rows={rows}
|
|
/>,
|
|
);
|
|
|
|
// Then
|
|
expect(screen.getByText("No Providers Configured")).toBeInTheDocument();
|
|
expect(
|
|
screen.getByRole("region", { name: /no providers configured/i }),
|
|
).toHaveClass("min-h-[calc(100dvh-28rem)]");
|
|
expect(screen.queryByTestId("providers-filters")).not.toBeInTheDocument();
|
|
expect(screen.queryByTestId("providers-table")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("opens the provider wizard from the no providers CTA", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
|
|
render(
|
|
<ProvidersAccountsView
|
|
isCloud={false}
|
|
filters={filters}
|
|
metadata={metadata}
|
|
providers={providers}
|
|
rows={rows}
|
|
/>,
|
|
);
|
|
|
|
// When
|
|
await user.click(
|
|
screen.getByRole("button", { name: /open add provider modal/i }),
|
|
);
|
|
|
|
// Then
|
|
expect(screen.getByRole("dialog")).toHaveTextContent("Provider wizard");
|
|
});
|
|
|
|
it("opens the provider wizard from the URL without immediately clearing the one-shot intent", () => {
|
|
// Given
|
|
searchParamsValue.current = "tab=connected&addProvider=true";
|
|
window.history.replaceState(
|
|
{},
|
|
"",
|
|
"/providers?tab=connected&addProvider=true",
|
|
);
|
|
// Spy only after the URL setup so we measure what the component does on mount.
|
|
const replaceStateSpy = vi.spyOn(window.history, "replaceState");
|
|
|
|
render(
|
|
<ProvidersAccountsView
|
|
isCloud={false}
|
|
filters={filters}
|
|
metadata={metadata}
|
|
providers={providers}
|
|
rows={rows}
|
|
/>,
|
|
);
|
|
|
|
// Then
|
|
expect(screen.getByRole("dialog")).toHaveTextContent("Provider wizard");
|
|
expect(replaceStateSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("cleans the one-shot intent from the URL without refetching when the URL-opened wizard closes", async () => {
|
|
// Given
|
|
searchParamsValue.current = "tab=connected&addProvider=true";
|
|
const replaceStateSpy = vi.spyOn(window.history, "replaceState");
|
|
const user = userEvent.setup();
|
|
|
|
render(
|
|
<ProvidersAccountsView
|
|
isCloud={false}
|
|
filters={filters}
|
|
metadata={metadata}
|
|
providers={providers}
|
|
rows={rows}
|
|
/>,
|
|
);
|
|
|
|
// When
|
|
await user.click(screen.getByRole("button", { name: /close/i }));
|
|
|
|
// Then
|
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
// The URL is cleaned via the History API (no RSC refetch). We must NOT
|
|
// refresh/replace here: re-running the /providers Server Component on close
|
|
// read as a full page reload. The provider-creation actions already
|
|
// revalidatePath("/providers"), so the table is fresh behind the modal.
|
|
expect(replaceStateSpy).toHaveBeenCalledWith(
|
|
null,
|
|
"",
|
|
"/providers?tab=connected",
|
|
);
|
|
expect(refreshMock).not.toHaveBeenCalled();
|
|
expect(replaceMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not touch the URL or refetch when a manually opened wizard closes", async () => {
|
|
// Given: no addProvider param in the URL, wizard opened via the CTA.
|
|
searchParamsValue.current = "";
|
|
const replaceStateSpy = vi.spyOn(window.history, "replaceState");
|
|
const user = userEvent.setup();
|
|
|
|
render(
|
|
<ProvidersAccountsView
|
|
isCloud={false}
|
|
filters={filters}
|
|
metadata={metadata}
|
|
providers={providers}
|
|
rows={rows}
|
|
/>,
|
|
);
|
|
|
|
// When: open the wizard from the empty-state CTA, then close it.
|
|
await user.click(
|
|
screen.getByRole("button", { name: /open add provider modal/i }),
|
|
);
|
|
await user.click(screen.getByRole("button", { name: /close/i }));
|
|
|
|
// Then: nothing to clean and no refresh — the creation actions own the
|
|
// data refresh via revalidatePath.
|
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
expect(replaceStateSpy).not.toHaveBeenCalled();
|
|
expect(refreshMock).not.toHaveBeenCalled();
|
|
expect(replaceMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("keeps filters and table visible when providers are disconnected", () => {
|
|
// Given/When
|
|
render(
|
|
<ProvidersAccountsView
|
|
isCloud={false}
|
|
filters={filters}
|
|
metadata={metadata}
|
|
providers={disconnectedProviders}
|
|
rows={rows}
|
|
/>,
|
|
);
|
|
|
|
// Then
|
|
expect(screen.getByTestId("providers-filters").parentElement).toHaveClass(
|
|
"flex",
|
|
"flex-col",
|
|
"gap-6",
|
|
);
|
|
expect(screen.getByTestId("providers-table")).toBeInTheDocument();
|
|
expect(
|
|
screen.queryByText("No Providers Configured"),
|
|
).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("passes scan schedule capability to provider row actions", () => {
|
|
// Given/When
|
|
render(
|
|
<ProvidersAccountsView
|
|
isCloud
|
|
filters={filters}
|
|
metadata={metadata}
|
|
providers={disconnectedProviders}
|
|
rows={rows}
|
|
scanScheduleCapability={SCAN_SCHEDULE_CAPABILITY.MANUAL_ONLY}
|
|
/>,
|
|
);
|
|
|
|
// Then
|
|
expect(providersAccountsTableSpy).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
scanScheduleCapability: SCAN_SCHEDULE_CAPABILITY.MANUAL_ONLY,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("opens the provider wizard from the normal Add Provider button", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
|
|
render(
|
|
<ProvidersAccountsView
|
|
isCloud={false}
|
|
filters={filters}
|
|
metadata={metadata}
|
|
providers={disconnectedProviders}
|
|
rows={rows}
|
|
/>,
|
|
);
|
|
|
|
// When
|
|
await user.click(screen.getByRole("button", { name: /add provider/i }));
|
|
|
|
// Then
|
|
expect(screen.getByRole("dialog")).toHaveTextContent("Provider wizard");
|
|
});
|
|
});
|