mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-04 02:04:06 +00:00
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
503 lines
14 KiB
TypeScript
503 lines
14 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, requestOpenOnWizardCloseMock } = vi.hoisted(() => ({
|
|
refreshMock: vi.fn(),
|
|
requestOpenOnWizardCloseMock: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("next/navigation", () => ({
|
|
useRouter: () => ({
|
|
refresh: refreshMock,
|
|
}),
|
|
}));
|
|
|
|
vi.mock("@/store/onboarding-checkpoint", () => ({
|
|
useOnboardingCheckpointStore: {
|
|
getState: () => ({
|
|
requestOpenOnWizardClose: requestOpenOnWizardCloseMock,
|
|
}),
|
|
},
|
|
}));
|
|
|
|
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();
|
|
requestOpenOnWizardCloseMock.mockClear();
|
|
sessionStorage.clear();
|
|
localStorage.clear();
|
|
// Checkpoint is Cloud-only.
|
|
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
|
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("requests the onboarding checkpoint with providerConnected:true when a provider was created", () => {
|
|
const onOpenChange = vi.fn();
|
|
const { result } = renderHook(() =>
|
|
useProviderWizardController({
|
|
open: true,
|
|
onOpenChange,
|
|
}),
|
|
);
|
|
act(() => {
|
|
useProviderWizardStore.getState().setProvider({
|
|
id: "provider-1",
|
|
type: "aws",
|
|
uid: "111111111111",
|
|
alias: "production",
|
|
});
|
|
});
|
|
|
|
act(() => {
|
|
result.current.handleClose();
|
|
});
|
|
|
|
// providerId is read before the store reset clears it.
|
|
expect(requestOpenOnWizardCloseMock).toHaveBeenCalledWith({
|
|
providerConnected: true,
|
|
});
|
|
});
|
|
|
|
it("requests the onboarding checkpoint with providerConnected:false when no provider was created", () => {
|
|
const onOpenChange = vi.fn();
|
|
const { result } = renderHook(() =>
|
|
useProviderWizardController({
|
|
open: true,
|
|
onOpenChange,
|
|
}),
|
|
);
|
|
|
|
act(() => {
|
|
result.current.handleClose();
|
|
});
|
|
|
|
expect(requestOpenOnWizardCloseMock).toHaveBeenCalledWith({
|
|
providerConnected: false,
|
|
});
|
|
});
|
|
|
|
it("does not request the onboarding checkpoint in self-hosted (OSS) deployments", () => {
|
|
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
|
const onOpenChange = vi.fn();
|
|
const { result } = renderHook(() =>
|
|
useProviderWizardController({
|
|
open: true,
|
|
onOpenChange,
|
|
}),
|
|
);
|
|
act(() => {
|
|
useProviderWizardStore.getState().setProvider({
|
|
id: "provider-1",
|
|
type: "aws",
|
|
uid: "111111111111",
|
|
alias: "production",
|
|
});
|
|
});
|
|
|
|
act(() => {
|
|
result.current.handleClose();
|
|
});
|
|
|
|
// Checkpoint stays untouched, but the close still refreshes.
|
|
expect(requestOpenOnWizardCloseMock).not.toHaveBeenCalled();
|
|
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.TEST);
|
|
});
|
|
|
|
// When
|
|
act(() => {
|
|
result.current.handleClose();
|
|
});
|
|
|
|
// Then
|
|
expect(funnelSignals).toEqual([
|
|
{ step: "wizard_closed", lastStep: "test", 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.CREDENTIALS);
|
|
});
|
|
expect(result.current.modalTitle).toBe("Update Provider Credentials");
|
|
expect(result.current.isProviderFlow).toBe(true);
|
|
// Update mode enters at the credentials step, so the docs link scrolls
|
|
// the getting-started page to the credentials/authentication section.
|
|
expect(result.current.docsLink).toBe(
|
|
"https://docs.prowler.com/user-guide/providers/aws/getting-started-aws#step-3-set-up-aws-authentication",
|
|
);
|
|
|
|
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("updates the credentials docs link when AWS assume role is selected", async () => {
|
|
const onOpenChange = vi.fn();
|
|
const { result } = renderHook(() =>
|
|
useProviderWizardController({
|
|
open: true,
|
|
onOpenChange,
|
|
initialData: {
|
|
providerId: "provider-1",
|
|
providerType: "aws",
|
|
providerUid: "111111111111",
|
|
providerAlias: "production",
|
|
secretId: null,
|
|
mode: PROVIDER_WIZARD_MODE.ADD,
|
|
},
|
|
}),
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.currentStep).toBe(PROVIDER_WIZARD_STEP.CREDENTIALS);
|
|
});
|
|
|
|
act(() => {
|
|
useProviderWizardStore.getState().setVia("role");
|
|
});
|
|
|
|
expect(result.current.docsLink).toBe(
|
|
"https://docs.prowler.com/user-guide/providers/aws/getting-started-aws#assume-role-recommended",
|
|
);
|
|
});
|
|
|
|
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.TEST);
|
|
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.CREDENTIALS);
|
|
});
|
|
|
|
// When
|
|
act(() => {
|
|
result.current.setCurrentStep(PROVIDER_WIZARD_STEP.TEST);
|
|
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.CREDENTIALS);
|
|
});
|
|
|
|
act(() => {
|
|
useProviderWizardStore.getState().setVia("service-account");
|
|
result.current.setCurrentStep(PROVIDER_WIZARD_STEP.TEST);
|
|
});
|
|
|
|
// 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.TEST);
|
|
expect(useProviderWizardStore.getState().via).toBe("service-account");
|
|
});
|
|
});
|