mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-11-09 04:38:49 +00:00
470 lines
13 KiB
TypeScript
470 lines
13 KiB
TypeScript
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import {
|
|
PROVIDER_FUNNEL_EVENT,
|
|
type ProviderFunnelDetail,
|
|
} from "@/lib/provider-funnel/provider-funnel-events";
|
|
import { useOrgSetupStore } from "@/store/organizations/store";
|
|
import { useProviderWizardStore } from "@/store/provider-wizard/store";
|
|
import { ORG_WIZARD_STEP, ORGANIZATION_TYPE } from "@/types/organizations";
|
|
import {
|
|
PROVIDER_WIZARD_MODE,
|
|
PROVIDER_WIZARD_STEP,
|
|
} from "@/types/provider-wizard";
|
|
|
|
import type { ProviderWizardInitialData } from "../types";
|
|
|
|
import { useProviderWizardController } from "./use-provider-wizard-controller";
|
|
|
|
const { refreshMock } = vi.hoisted(() => ({
|
|
refreshMock: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("next/navigation", () => ({
|
|
useRouter: () => ({
|
|
refresh: refreshMock,
|
|
}),
|
|
}));
|
|
|
|
vi.mock("next-auth/react", () => ({
|
|
useSession: () => ({
|
|
data: null,
|
|
status: "unauthenticated",
|
|
}),
|
|
}));
|
|
|
|
describe("useProviderWizardController", () => {
|
|
const funnelSignals: ProviderFunnelDetail[] = [];
|
|
const recordFunnelSignal: EventListener = (event) => {
|
|
funnelSignals.push((event as CustomEvent<ProviderFunnelDetail>).detail);
|
|
};
|
|
|
|
afterEach(() => {
|
|
window.removeEventListener(PROVIDER_FUNNEL_EVENT, recordFunnelSignal);
|
|
});
|
|
|
|
beforeEach(() => {
|
|
funnelSignals.length = 0;
|
|
window.addEventListener(PROVIDER_FUNNEL_EVENT, recordFunnelSignal);
|
|
vi.useRealTimers();
|
|
vi.clearAllMocks();
|
|
sessionStorage.clear();
|
|
localStorage.clear();
|
|
useProviderWizardStore.getState().reset();
|
|
useOrgSetupStore.getState().reset();
|
|
});
|
|
|
|
it("refreshes providers data when the wizard closes", () => {
|
|
// Given
|
|
const onOpenChange = vi.fn();
|
|
const { result } = renderHook(() =>
|
|
useProviderWizardController({
|
|
open: true,
|
|
onOpenChange,
|
|
}),
|
|
);
|
|
|
|
// When
|
|
act(() => {
|
|
result.current.handleClose();
|
|
});
|
|
|
|
// Then
|
|
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
expect(refreshMock).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("signals the step where the wizard was left and that no provider was created", () => {
|
|
// Given
|
|
const { result } = renderHook(() =>
|
|
useProviderWizardController({ open: true, onOpenChange: vi.fn() }),
|
|
);
|
|
|
|
// When
|
|
act(() => {
|
|
result.current.handleClose();
|
|
});
|
|
|
|
// Then
|
|
expect(funnelSignals).toEqual([
|
|
{ step: "wizard_closed", lastStep: "connect", providerCreated: false },
|
|
]);
|
|
});
|
|
|
|
it("signals a close after the provider was created, from the step reached", () => {
|
|
// Given
|
|
const { result } = renderHook(() =>
|
|
useProviderWizardController({ open: true, onOpenChange: vi.fn() }),
|
|
);
|
|
act(() => {
|
|
useProviderWizardStore.getState().setProvider({
|
|
id: "provider-1",
|
|
type: "aws",
|
|
uid: "123456789012",
|
|
alias: null,
|
|
});
|
|
result.current.setCurrentStep(PROVIDER_WIZARD_STEP.LAUNCH);
|
|
});
|
|
|
|
// When
|
|
act(() => {
|
|
result.current.handleClose();
|
|
});
|
|
|
|
// Then
|
|
expect(funnelSignals).toEqual([
|
|
{ step: "wizard_closed", lastStep: "launch", providerCreated: true },
|
|
]);
|
|
});
|
|
|
|
it("signals the organization method when the organizations flow opens", () => {
|
|
// Given
|
|
const { result } = renderHook(() =>
|
|
useProviderWizardController({ open: true, onOpenChange: vi.fn() }),
|
|
);
|
|
|
|
// When
|
|
act(() => {
|
|
result.current.openOrganizationsFlow(ORGANIZATION_TYPE.AZURE);
|
|
});
|
|
|
|
// Then
|
|
expect(funnelSignals).toEqual([
|
|
{
|
|
step: "method_selected",
|
|
providerType: "azure",
|
|
method: "organization",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("hydrates update mode when initial data is provided", async () => {
|
|
// Given
|
|
const onOpenChange = vi.fn();
|
|
|
|
// When
|
|
const { result } = renderHook(() =>
|
|
useProviderWizardController({
|
|
open: true,
|
|
onOpenChange,
|
|
initialData: {
|
|
providerId: "provider-1",
|
|
providerType: "aws",
|
|
providerUid: "111111111111",
|
|
providerAlias: "production",
|
|
secretId: "secret-1",
|
|
mode: PROVIDER_WIZARD_MODE.UPDATE,
|
|
},
|
|
}),
|
|
);
|
|
|
|
// Then
|
|
await waitFor(() => {
|
|
expect(result.current.currentStep).toBe(PROVIDER_WIZARD_STEP.CONNECT);
|
|
});
|
|
expect(result.current.modalTitle).toBe("Update Provider Credentials");
|
|
expect(result.current.isProviderFlow).toBe(true);
|
|
// Update mode opens the connect step on its first method (IAM role here,
|
|
// no `via` stored), so the docs link lands on that method's section.
|
|
expect(result.current.docsLink).toBe(
|
|
"https://docs.prowler.com/user-guide/providers/aws/getting-started-aws#assume-role-recommended",
|
|
);
|
|
|
|
const state = useProviderWizardStore.getState();
|
|
expect(state.providerId).toBe("provider-1");
|
|
expect(state.providerType).toBe("aws");
|
|
expect(state.providerUid).toBe("111111111111");
|
|
expect(state.providerAlias).toBe("production");
|
|
expect(state.secretId).toBe("secret-1");
|
|
expect(state.mode).toBe(PROVIDER_WIZARD_MODE.UPDATE);
|
|
});
|
|
|
|
it("points the docs at the method picked on the connect step, for any provider", async () => {
|
|
// Given
|
|
const { result } = renderHook(() =>
|
|
useProviderWizardController({ open: true, onOpenChange: vi.fn() }),
|
|
);
|
|
act(() => result.current.setProviderTypeHint("m365"));
|
|
|
|
// When
|
|
act(() => {
|
|
useProviderWizardStore
|
|
.getState()
|
|
.setConnectDraft("m365", { method: "app_client_secret" });
|
|
});
|
|
|
|
// Then
|
|
expect(result.current.connectionMethod).toBe("app_client_secret");
|
|
expect(result.current.docsLink).toBe(
|
|
"https://docs.prowler.com/user-guide/providers/microsoft365/getting-started-m365#application-client-secret-authentication",
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
case: "the first listed method before any pick",
|
|
providerType: "aws",
|
|
expected: "role",
|
|
},
|
|
{
|
|
case: "no method for a provider with a single one",
|
|
providerType: "azure",
|
|
expected: null,
|
|
},
|
|
])("tells Lighthouse $case", ({ providerType, expected }) => {
|
|
// Given
|
|
const { result } = renderHook(() =>
|
|
useProviderWizardController({ open: true, onOpenChange: vi.fn() }),
|
|
);
|
|
|
|
// When
|
|
act(() => result.current.setProviderTypeHint(providerType));
|
|
|
|
// Then
|
|
expect(result.current.connectionMethod).toBe(expected);
|
|
});
|
|
|
|
it("tells Lighthouse the method an account being updated was stored with", async () => {
|
|
// When
|
|
const { result } = renderHook(() =>
|
|
useProviderWizardController({
|
|
open: true,
|
|
onOpenChange: vi.fn(),
|
|
initialData: {
|
|
providerId: "provider-1",
|
|
providerType: "gcp",
|
|
providerUid: "my-project",
|
|
providerAlias: null,
|
|
secretId: "secret-1",
|
|
via: "credentials",
|
|
},
|
|
}),
|
|
);
|
|
|
|
// Then
|
|
await waitFor(() => {
|
|
expect(result.current.connectionMethod).toBe("credentials");
|
|
});
|
|
});
|
|
|
|
it("tells Lighthouse an organization is being onboarded", () => {
|
|
// Given
|
|
const { result } = renderHook(() =>
|
|
useProviderWizardController({ open: true, onOpenChange: vi.fn() }),
|
|
);
|
|
|
|
// When
|
|
act(() => result.current.openOrganizationsFlow(ORGANIZATION_TYPE.GCP));
|
|
|
|
// Then
|
|
expect(result.current.connectionMethod).toBe("organizations");
|
|
});
|
|
|
|
it("switches into and out of organizations flow", () => {
|
|
// Given
|
|
const onOpenChange = vi.fn();
|
|
const { result } = renderHook(() =>
|
|
useProviderWizardController({
|
|
open: true,
|
|
onOpenChange,
|
|
}),
|
|
);
|
|
|
|
// When
|
|
act(() => {
|
|
result.current.openOrganizationsFlow();
|
|
});
|
|
|
|
// Then
|
|
expect(result.current.wizardVariant).toBe("organizations");
|
|
expect(result.current.isProviderFlow).toBe(false);
|
|
expect(result.current.orgCurrentStep).toBe(ORG_WIZARD_STEP.SETUP);
|
|
// The flow tags the store with the type it was opened for; AWS by default.
|
|
expect(useOrgSetupStore.getState().organizationType).toBe(
|
|
ORGANIZATION_TYPE.AWS,
|
|
);
|
|
expect(result.current.docsLink).toBe(
|
|
"https://docs.prowler.com/user-guide/tutorials/prowler-cloud-aws-organizations",
|
|
);
|
|
|
|
// When
|
|
act(() => {
|
|
result.current.backToProviderFlow();
|
|
});
|
|
|
|
// Then
|
|
expect(result.current.wizardVariant).toBe("provider");
|
|
expect(result.current.isProviderFlow).toBe(true);
|
|
expect(result.current.currentStep).toBe(PROVIDER_WIZARD_STEP.CONNECT);
|
|
// Back lands on the AWS connect step the tabs live on, not the provider picker.
|
|
expect(result.current.providerTypeHint).toBe("aws");
|
|
});
|
|
|
|
it("moves to launch step after a successful connection test in add mode", () => {
|
|
// Given
|
|
const onOpenChange = vi.fn();
|
|
const { result } = renderHook(() =>
|
|
useProviderWizardController({
|
|
open: true,
|
|
onOpenChange,
|
|
}),
|
|
);
|
|
|
|
// When
|
|
act(() => {
|
|
result.current.setCurrentStep(PROVIDER_WIZARD_STEP.LAUNCH);
|
|
result.current.handleTestSuccess();
|
|
});
|
|
|
|
// Then
|
|
expect(result.current.currentStep).toBe(PROVIDER_WIZARD_STEP.LAUNCH);
|
|
expect(onOpenChange).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("closes the wizard after a successful connection test in update mode", async () => {
|
|
// Given
|
|
const onOpenChange = vi.fn();
|
|
const { result } = renderHook(() =>
|
|
useProviderWizardController({
|
|
open: true,
|
|
onOpenChange,
|
|
initialData: {
|
|
providerId: "provider-1",
|
|
providerType: "aws",
|
|
providerUid: "111111111111",
|
|
providerAlias: "production",
|
|
secretId: "secret-1",
|
|
mode: PROVIDER_WIZARD_MODE.UPDATE,
|
|
},
|
|
}),
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.currentStep).toBe(PROVIDER_WIZARD_STEP.CONNECT);
|
|
});
|
|
|
|
// When
|
|
act(() => {
|
|
result.current.setCurrentStep(PROVIDER_WIZARD_STEP.LAUNCH);
|
|
result.current.handleTestSuccess();
|
|
});
|
|
|
|
// Credential rotation skips the launch/schedule step.
|
|
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
expect(refreshMock).toHaveBeenCalledTimes(1);
|
|
expect(result.current.currentStep).not.toBe(PROVIDER_WIZARD_STEP.LAUNCH);
|
|
});
|
|
|
|
it("does not override launch footer config in the controller", () => {
|
|
// Given
|
|
const onOpenChange = vi.fn();
|
|
const { result } = renderHook(() =>
|
|
useProviderWizardController({
|
|
open: true,
|
|
onOpenChange,
|
|
}),
|
|
);
|
|
|
|
// When
|
|
act(() => {
|
|
result.current.setCurrentStep(PROVIDER_WIZARD_STEP.LAUNCH);
|
|
});
|
|
|
|
// Then
|
|
expect(result.current.resolvedFooterConfig.showAction).toBe(false);
|
|
expect(result.current.resolvedFooterConfig.showBack).toBe(false);
|
|
expect(onOpenChange).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not reset organizations step when org store updates while modal is open", () => {
|
|
// Given
|
|
const onOpenChange = vi.fn();
|
|
const { result } = renderHook(() =>
|
|
useProviderWizardController({
|
|
open: true,
|
|
onOpenChange,
|
|
}),
|
|
);
|
|
|
|
act(() => {
|
|
result.current.openOrganizationsFlow();
|
|
result.current.setOrgCurrentStep(ORG_WIZARD_STEP.VALIDATE);
|
|
});
|
|
|
|
// When
|
|
act(() => {
|
|
useOrgSetupStore
|
|
.getState()
|
|
.setOrganization("org-1", "My Org", "o-abc123def4");
|
|
useOrgSetupStore.getState().setDiscovery("disc-1", {
|
|
orgType: ORGANIZATION_TYPE.AWS,
|
|
organization: { uid: "o-abc123def4", name: "My Org" },
|
|
nodes: [],
|
|
candidates: [],
|
|
});
|
|
});
|
|
|
|
// Then
|
|
expect(result.current.wizardVariant).toBe("organizations");
|
|
expect(result.current.orgCurrentStep).toBe(ORG_WIZARD_STEP.VALIDATE);
|
|
});
|
|
|
|
it("does not rehydrate wizard state when initial data changes while modal remains open", async () => {
|
|
// Given
|
|
const onOpenChange = vi.fn();
|
|
const { result, rerender } = renderHook(
|
|
({
|
|
open,
|
|
initialData,
|
|
}: {
|
|
open: boolean;
|
|
initialData?: ProviderWizardInitialData;
|
|
}) =>
|
|
useProviderWizardController({
|
|
open,
|
|
onOpenChange,
|
|
initialData,
|
|
}),
|
|
{
|
|
initialProps: {
|
|
open: true,
|
|
initialData: {
|
|
providerId: "provider-1",
|
|
providerType: "gcp",
|
|
providerUid: "project-123",
|
|
providerAlias: "gcp-main",
|
|
secretId: null,
|
|
mode: PROVIDER_WIZARD_MODE.ADD,
|
|
},
|
|
},
|
|
},
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.currentStep).toBe(PROVIDER_WIZARD_STEP.CONNECT);
|
|
});
|
|
|
|
act(() => {
|
|
useProviderWizardStore.getState().setVia("service-account");
|
|
result.current.setCurrentStep(PROVIDER_WIZARD_STEP.LAUNCH);
|
|
});
|
|
|
|
// Provider data refreshes while modal is still open — user progress must be kept.
|
|
rerender({
|
|
open: true,
|
|
initialData: {
|
|
providerId: "provider-1",
|
|
providerType: "gcp",
|
|
providerUid: "project-123",
|
|
providerAlias: "gcp-main",
|
|
secretId: "secret-1",
|
|
mode: PROVIDER_WIZARD_MODE.UPDATE,
|
|
},
|
|
});
|
|
|
|
expect(result.current.currentStep).toBe(PROVIDER_WIZARD_STEP.LAUNCH);
|
|
expect(useProviderWizardStore.getState().via).toBe("service-account");
|
|
});
|
|
});
|