Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { CLOUD_UPGRADE_FEATURE } from "@/types/cloud-upgrade";

import { GcpMethodSelector } from "./gcp-method-selector";

// OSS gating only: `CloudUpgradeModal` mounts outside the providers page, so the
// harness cannot see the upsell open. The Cloud case is restated by integration.
describe("GcpMethodSelector", () => {
afterEach(() => {
vi.unstubAllEnvs();
Expand Down Expand Up @@ -40,29 +42,4 @@ describe("GcpMethodSelector", () => {
CLOUD_UPGRADE_FEATURE.GCP_ORGANIZATIONS,
);
});

it("enters the GCP org flow in Cloud", async () => {
// Given
vi.stubEnv("UI_CLOUD_ENABLED", "true");
const user = userEvent.setup();
const onSelectOrganizations = vi.fn();

// When
render(
<GcpMethodSelector
onSelectSingle={vi.fn()}
onSelectOrganizations={onSelectOrganizations}
/>,
);

// Then
await user.click(
screen.getByRole("radio", {
name: /add multiple projects with gcp organization/i,
}),
);

expect(onSelectOrganizations).toHaveBeenCalledTimes(1);
expect(useCloudUpgradeStore.getState().activeFeature).toBeNull();
});
});
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { act, renderHook } from "@testing-library/react";
import { createElement, type PropsWithChildren, StrictMode } from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";

import { useOrgSetupStore } from "@/store/organizations/store";
import {
APPLY_STATUS,
DISCOVERY_STATUS,
ORG_SECRET_TYPE,
ORGANIZATION_TYPE,
Expand Down Expand Up @@ -80,10 +78,6 @@ vi.mock(
() => organizationsActionsMock,
);

function StrictModeWrapper({ children }: PropsWithChildren) {
return createElement(StrictMode, null, children);
}

/** Mocks the chain up to (and including) triggering discovery, all succeeding. */
function mockChainThroughDiscoveryTrigger() {
organizationsActionsMock.listOrganizationsByExternalId.mockResolvedValue({
Expand Down Expand Up @@ -123,116 +117,6 @@ describe("useOrgSetupSubmission", () => {
}
});

it("completes the setup chain and stores selectable candidates", async () => {
// Given
const onNext = vi.fn();
// `true` = the form owns the field and rendered the error on it.
const setFieldError = vi.fn(() => true);
const discoveryResult = {
roots: [
{ id: "r-root", arn: "arn:root", name: "Root", policy_types: [] },
],
organizational_units: [],
accounts: [
{
id: "111111111111",
name: "Account One",
arn: "arn:aws:organizations::111111111111:account/o-123/111111111111",
email: "one@example.com",
status: "ACTIVE",
joined_method: "CREATED",
joined_timestamp: "2024-01-01T00:00:00Z",
parent_id: "r-root",
registration: {
provider_exists: false,
provider_id: null,
organization_relation: "link_required",
organization_node_relation: "not_applicable",
provider_secret_state: "will_create",
apply_status: APPLY_STATUS.READY,
blocked_reasons: [],
},
},
{
id: "222222222222",
name: "Account Two",
arn: "arn:aws:organizations::222222222222:account/o-123/222222222222",
email: "two@example.com",
status: "ACTIVE",
joined_method: "CREATED",
joined_timestamp: "2024-01-01T00:00:00Z",
parent_id: "r-root",
registration: {
provider_exists: false,
provider_id: null,
organization_relation: "link_required",
organization_node_relation: "not_applicable",
provider_secret_state: "will_create",
apply_status: APPLY_STATUS.BLOCKED,
blocked_reasons: ["Already linked"],
},
},
],
};

organizationsActionsMock.listOrganizationsByExternalId.mockResolvedValue({
data: [],
});
organizationsActionsMock.createOrganization.mockResolvedValue({
data: { id: "org-1" },
});
organizationsActionsMock.listOrganizationSecretsByOrganizationId.mockResolvedValue(
{
data: [],
},
);
organizationsActionsMock.createOrganizationSecret.mockResolvedValue({
data: { id: "secret-1" },
});
organizationsActionsMock.triggerDiscovery.mockResolvedValue({
data: { id: "discovery-1" },
});
organizationsActionsMock.getDiscovery.mockResolvedValue({
data: {
attributes: {
status: DISCOVERY_STATUS.SUCCEEDED,
result: discoveryResult,
},
},
});

const { result } = renderHook(
() =>
useOrgSetupSubmission({
stackSetExternalId: "tenant-external-id",
onNext,
setFieldError,
}),
{ wrapper: StrictModeWrapper },
);

// When
await act(async () => {
await result.current.submitOrganizationSetup({
orgType: ORGANIZATION_TYPE.AWS,
organizationName: "Acme",
awsOrgId: "o-abc123def4",
roleArn: "arn:aws:iam::123456789012:role/ProwlerOrgRole",
});
});

// Then
expect(onNext).toHaveBeenCalledTimes(1);
expect(setFieldError).not.toHaveBeenCalled();

const state = useOrgSetupStore.getState();
expect(state.organizationId).toBe("org-1");
expect(state.organizationExternalId).toBe("o-abc123def4");
expect(state.discoveryId).toBe("discovery-1");
expect(state.selectedCandidateIds).toEqual(["111111111111"]);
expect(state.selectableCandidateIds).toEqual(["111111111111"]);
});

it("times out then resumes the same discovery via keep waiting", async () => {
// Given — a discovery that stays running until the client budget is spent.
vi.useFakeTimers();
Expand Down Expand Up @@ -292,6 +176,8 @@ describe("useOrgSetupSubmission", () => {
vi.useRealTimers();
});

// The MSW handler answers every trigger with the same static discovery, so
// integration can only count POSTs — not the retry clearing the failure.
it("retry triggers a fresh discovery after a failed one", async () => {
Comment thread
alejandrobailo marked this conversation as resolved.
// Given — a discovery that completes as failed.
const onNext = vi.fn();
Expand Down
162 changes: 0 additions & 162 deletions ui/components/providers/organizations/org-launch-scan.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { act, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import type { ComponentProps } from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";

Expand Down Expand Up @@ -138,47 +137,6 @@ describe("OrgLaunchScan", () => {
).toBe(`/scans?tab=${SCAN_JOBS_TAB.SCHEDULED}`);
});

it("should launch the complete organization after a partial schedule save", async () => {
Comment thread
alejandrobailo marked this conversation as resolved.
// Given
const user = userEvent.setup();
const onFooterChange = vi.fn();
updateSchedulesBulkMock.mockResolvedValue({
data: {
updated: ["provider-2"],
failed: [{ id: "provider-1", error: "Denied" }],
},
});

render(
<OrgLaunchScan
onClose={vi.fn()}
onBack={vi.fn()}
onFooterChange={onFooterChange}
capability={SCAN_SCHEDULE_CAPABILITY.ADVANCED}
/>,
);

// When
await user.click(
await screen.findByRole("checkbox", {
name: /launch an initial scan now/i,
}),
);
await act(async () => {
lastFooterConfig(onFooterChange)?.onAction?.();
});

// Then
await waitFor(() =>
expect(launchOrganizationScansMock).toHaveBeenCalledTimes(1),
);
expect(launchOrganizationScansMock).toHaveBeenCalledWith("org-1");
expect(scheduleOrganizationDailyScansMock).not.toHaveBeenCalled();
expect(
toastMock.mock.calls[0]?.[0].action.props.children.props.href,
).toBe(`/scans?tab=${SCAN_JOBS_TAB.ACTIVE}`);
});

it("should disable launch actions while schedule capability is loading", async () => {
// Given
const onFooterChange = vi.fn();
Expand Down Expand Up @@ -246,126 +204,6 @@ describe("OrgLaunchScan", () => {
expect(pushMock).not.toHaveBeenCalled();
expect(onClose).not.toHaveBeenCalled();
});

it("should treat a fully-failed bulk response as an error without navigating away", async () => {
// Given
const onClose = vi.fn();
const onFooterChange = vi.fn();
updateSchedulesBulkMock.mockResolvedValue({
data: {
updated: [],
failed: [
{ id: "provider-1", error: "Denied" },
{ id: "provider-2", error: "Denied" },
],
},
});

render(
<OrgLaunchScan
onClose={onClose}
onBack={vi.fn()}
onFooterChange={onFooterChange}
capability={SCAN_SCHEDULE_CAPABILITY.ADVANCED}
/>,
);

// When
await screen.findByText("Scan Schedule");
await act(async () => {
lastFooterConfig(onFooterChange)?.onAction?.();
});

// Then
await waitFor(() =>
expect(toastMock).toHaveBeenCalledWith(
expect.objectContaining({
variant: "destructive",
title: "Unable to save scan schedules",
description:
"The scan schedule could not be saved for 2 accounts: Denied.",
}),
),
);
expect(launchOrganizationScansMock).not.toHaveBeenCalled();
expect(pushMock).not.toHaveBeenCalled();
expect(onClose).not.toHaveBeenCalled();
});

it("should describe partial failures in the success toast", async () => {
// Given
const onFooterChange = vi.fn();
updateSchedulesBulkMock.mockResolvedValue({
data: {
updated: ["provider-2"],
failed: [{ id: "provider-1", error: "Denied" }],
},
});

render(
<OrgLaunchScan
onClose={vi.fn()}
onBack={vi.fn()}
onFooterChange={onFooterChange}
capability={SCAN_SCHEDULE_CAPABILITY.ADVANCED}
/>,
);

// When
await screen.findByText("Scan Schedule");
await act(async () => {
lastFooterConfig(onFooterChange)?.onAction?.();
});

// Then
await waitFor(() => expect(toastMock).toHaveBeenCalled());
expect(toastMock).toHaveBeenCalledWith(
expect.objectContaining({
title: "Scan schedules saved",
description:
"The schedule was saved for 1 account, but 1 account could not be updated: Denied.",
}),
);
});

it("should proceed when the response carries no result lists", async () => {
// Given — an empty 200/204 body. The endpoint commits each schedule before
// answering, so this is not a failure.
const user = userEvent.setup();
const onFooterChange = vi.fn();
updateSchedulesBulkMock.mockResolvedValue({ success: true });

render(
<OrgLaunchScan
onClose={vi.fn()}
onBack={vi.fn()}
onFooterChange={onFooterChange}
capability={SCAN_SCHEDULE_CAPABILITY.ADVANCED}
/>,
);

// When
await user.click(
await screen.findByRole("checkbox", {
name: /launch an initial scan now/i,
}),
);
await act(async () => {
lastFooterConfig(onFooterChange)?.onAction?.();
});

// Then — every created provider is treated as saved and scanned.
await waitFor(() =>
expect(launchOrganizationScansMock).toHaveBeenCalledWith("org-1"),
);
expect(toastMock).toHaveBeenCalledWith(
expect.objectContaining({
title: "Scan schedules saved and initial scans launched",
description: "The scan schedule was saved for 2 accounts.",
}),
);
expect(pushMock).toHaveBeenCalledWith("/providers");
});
});

describe("when capability is DAILY_LEGACY", () => {
Expand Down
Loading