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>
442 lines
15 KiB
TypeScript
442 lines
15 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { DOCS_URLS, getProviderHelpText } from "@/lib/external-urls";
|
|
import { ORG_SETUP_PHASE, ORG_WIZARD_STEP } from "@/types/organizations";
|
|
import {
|
|
PROVIDER_WIZARD_MODE,
|
|
PROVIDER_WIZARD_STEP,
|
|
} from "@/types/provider-wizard";
|
|
import { type KnownProviderType, PROVIDER_TYPES } from "@/types/providers";
|
|
|
|
import {
|
|
getCredentialsRetryStep,
|
|
getLaunchBackStep,
|
|
getOrganizationsStepperOffset,
|
|
getProviderWizardDocsDestination,
|
|
getProviderWizardModalTitle,
|
|
getProviderWizardStepper,
|
|
} from "./provider-wizard-modal.utils";
|
|
|
|
describe("getProviderWizardStepper", () => {
|
|
const labels = (steps: { label: string }[]) => steps.map((s) => s.label);
|
|
|
|
it("lists the four generic steps until a provider is picked", () => {
|
|
const stepper = getProviderWizardStepper({
|
|
mode: PROVIDER_WIZARD_MODE.ADD,
|
|
providerType: null,
|
|
currentStep: PROVIDER_WIZARD_STEP.CONNECT,
|
|
});
|
|
|
|
expect(labels(stepper.steps)).toEqual([
|
|
"Link a Provider",
|
|
"Authenticate Credentials",
|
|
"Validate Connection",
|
|
"Launch Scan",
|
|
]);
|
|
expect(stepper.stepOffset).toBe(0);
|
|
});
|
|
|
|
it("leaves only two rows when adding an AWS account", () => {
|
|
const stepper = getProviderWizardStepper({
|
|
mode: PROVIDER_WIZARD_MODE.ADD,
|
|
providerType: "aws",
|
|
currentStep: PROVIDER_WIZARD_STEP.CONNECT,
|
|
});
|
|
|
|
expect(labels(stepper.steps)).toEqual(["Link a Provider", "Launch Scan"]);
|
|
expect(stepper.stepOffset).toBe(0);
|
|
});
|
|
|
|
it("keeps the first AWS row active if the wizard ever lands on a folded step", () => {
|
|
const stepper = getProviderWizardStepper({
|
|
mode: PROVIDER_WIZARD_MODE.ADD,
|
|
providerType: "aws",
|
|
currentStep: PROVIDER_WIZARD_STEP.CREDENTIALS,
|
|
});
|
|
|
|
expect(stepper.stepOffset).toBe(-PROVIDER_WIZARD_STEP.CREDENTIALS);
|
|
});
|
|
|
|
it("puts the AWS launch step on the second row", () => {
|
|
const stepper = getProviderWizardStepper({
|
|
mode: PROVIDER_WIZARD_MODE.ADD,
|
|
providerType: "aws",
|
|
currentStep: PROVIDER_WIZARD_STEP.LAUNCH,
|
|
});
|
|
|
|
// LAUNCH is index 3 in the wizard but the second row of the AWS stepper.
|
|
expect(stepper.stepOffset).toBe(-2);
|
|
});
|
|
|
|
it("keeps the generic rows when adding credentials to a registered AWS account", () => {
|
|
const stepper = getProviderWizardStepper({
|
|
mode: PROVIDER_WIZARD_MODE.ADD,
|
|
providerType: "aws",
|
|
currentStep: PROVIDER_WIZARD_STEP.CREDENTIALS,
|
|
isDirectCredentialsEntry: true,
|
|
});
|
|
|
|
expect(labels(stepper.steps)).toEqual([
|
|
"Link a Provider",
|
|
"Authenticate Credentials",
|
|
"Validate Connection",
|
|
"Launch Scan",
|
|
]);
|
|
expect(stepper.stepOffset).toBe(0);
|
|
});
|
|
|
|
it("keeps the generic rows for a provider that is not AWS", () => {
|
|
const stepper = getProviderWizardStepper({
|
|
mode: PROVIDER_WIZARD_MODE.ADD,
|
|
providerType: "azure",
|
|
currentStep: PROVIDER_WIZARD_STEP.TEST,
|
|
});
|
|
|
|
expect(stepper.steps).toHaveLength(4);
|
|
expect(stepper.stepOffset).toBe(0);
|
|
});
|
|
|
|
it("still shows the credentials step when updating AWS credentials", () => {
|
|
const stepper = getProviderWizardStepper({
|
|
mode: PROVIDER_WIZARD_MODE.UPDATE,
|
|
providerType: "aws",
|
|
currentStep: PROVIDER_WIZARD_STEP.CREDENTIALS,
|
|
});
|
|
|
|
expect(labels(stepper.steps)).toEqual([
|
|
"Link a Provider",
|
|
"Authenticate Credentials",
|
|
"Validate Connection",
|
|
]);
|
|
expect(stepper.stepOffset).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("getCredentialsRetryStep", () => {
|
|
it("returns an AWS account being added to its one-step form", () => {
|
|
expect(
|
|
getCredentialsRetryStep({
|
|
mode: PROVIDER_WIZARD_MODE.ADD,
|
|
providerType: "aws",
|
|
}),
|
|
).toBe(PROVIDER_WIZARD_STEP.CONNECT);
|
|
});
|
|
|
|
it("returns to the credentials step when AWS credentials were added from the list", () => {
|
|
expect(
|
|
getCredentialsRetryStep({
|
|
mode: PROVIDER_WIZARD_MODE.ADD,
|
|
providerType: "aws",
|
|
isDirectCredentialsEntry: true,
|
|
}),
|
|
).toBe(PROVIDER_WIZARD_STEP.CREDENTIALS);
|
|
});
|
|
|
|
it("returns every other provider to the credentials step", () => {
|
|
expect(
|
|
getCredentialsRetryStep({
|
|
mode: PROVIDER_WIZARD_MODE.ADD,
|
|
providerType: "azure",
|
|
}),
|
|
).toBe(PROVIDER_WIZARD_STEP.CREDENTIALS);
|
|
});
|
|
});
|
|
|
|
describe("getLaunchBackStep", () => {
|
|
it("returns an AWS account to its one-step form", () => {
|
|
expect(getLaunchBackStep({ providerType: "aws" })).toBe(
|
|
PROVIDER_WIZARD_STEP.CONNECT,
|
|
);
|
|
});
|
|
|
|
it("returns every other provider to the connection test", () => {
|
|
expect(getLaunchBackStep({ providerType: "azure" })).toBe(
|
|
PROVIDER_WIZARD_STEP.TEST,
|
|
);
|
|
});
|
|
|
|
it("returns to the connection test when AWS credentials were added from the list", () => {
|
|
expect(
|
|
getLaunchBackStep({
|
|
providerType: "aws",
|
|
isDirectCredentialsEntry: true,
|
|
}),
|
|
).toBe(PROVIDER_WIZARD_STEP.TEST);
|
|
});
|
|
});
|
|
|
|
describe("getOrganizationsStepperOffset", () => {
|
|
it("keeps step 1 active during organization details", () => {
|
|
const offset = getOrganizationsStepperOffset(
|
|
ORG_WIZARD_STEP.SETUP,
|
|
ORG_SETUP_PHASE.DETAILS,
|
|
);
|
|
|
|
expect(offset).toBe(0);
|
|
});
|
|
|
|
it("moves to step 2 during credentials phase", () => {
|
|
const offset = getOrganizationsStepperOffset(
|
|
ORG_WIZARD_STEP.SETUP,
|
|
ORG_SETUP_PHASE.ACCESS,
|
|
);
|
|
|
|
expect(offset).toBe(1);
|
|
});
|
|
|
|
it("uses step 2+ offset for later wizard steps", () => {
|
|
const offset = getOrganizationsStepperOffset(
|
|
ORG_WIZARD_STEP.VALIDATE,
|
|
ORG_SETUP_PHASE.DETAILS,
|
|
);
|
|
|
|
expect(offset).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("getProviderWizardModalTitle", () => {
|
|
it("returns add title for add mode", () => {
|
|
const title = getProviderWizardModalTitle(PROVIDER_WIZARD_MODE.ADD);
|
|
|
|
expect(title).toBe("Adding A Provider");
|
|
});
|
|
|
|
it("returns update title for update mode", () => {
|
|
const title = getProviderWizardModalTitle(PROVIDER_WIZARD_MODE.UPDATE);
|
|
|
|
expect(title).toBe("Update Provider Credentials");
|
|
});
|
|
});
|
|
|
|
describe("getProviderWizardDocsDestination", () => {
|
|
it("returns a compact provider label for short provider docs links", () => {
|
|
const destination = getProviderWizardDocsDestination(
|
|
"https://goto.prowler.com/provider-aws",
|
|
);
|
|
|
|
expect(destination).toBe("AWS");
|
|
});
|
|
|
|
it("returns a compact provider label for deep-linked getting-started URLs", () => {
|
|
const destination = getProviderWizardDocsDestination(
|
|
"https://docs.prowler.com/user-guide/providers/aws/getting-started-aws",
|
|
);
|
|
|
|
expect(destination).toBe("AWS");
|
|
});
|
|
|
|
it("returns a specific label for AWS assume role docs links", () => {
|
|
const destination = getProviderWizardDocsDestination(
|
|
"https://docs.prowler.com/user-guide/providers/aws/getting-started-aws#assume-role-recommended",
|
|
);
|
|
|
|
expect(destination).toBe("AWS Assume Role");
|
|
});
|
|
|
|
it("returns a method-specific label for every subsection deep-link", () => {
|
|
// Locks the docsSectionLabelMap keys to the URLs the frontend emits.
|
|
// Adding a new (provider, method) subsection URL requires wiring a
|
|
// matching label here or the modal header regresses to just the provider
|
|
// name.
|
|
const cases: Array<[string, string]> = [
|
|
[
|
|
"https://docs.prowler.com/user-guide/providers/aws/getting-started-aws#assume-role-recommended",
|
|
"AWS Assume Role",
|
|
],
|
|
[
|
|
"https://docs.prowler.com/user-guide/providers/aws/getting-started-aws#credentials-static-access-keys",
|
|
"AWS Credentials",
|
|
],
|
|
[
|
|
"https://docs.prowler.com/user-guide/providers/microsoft365/getting-started-m365#application-certificate-authentication-recommended",
|
|
"M365 Certificate",
|
|
],
|
|
[
|
|
"https://docs.prowler.com/user-guide/providers/microsoft365/getting-started-m365#application-client-secret-authentication",
|
|
"M365 Client Secret",
|
|
],
|
|
[
|
|
"https://docs.prowler.com/user-guide/providers/alibabacloud/getting-started-alibabacloud#ram-role-assumption-recommended",
|
|
"Alibaba Cloud RAM Role",
|
|
],
|
|
[
|
|
"https://docs.prowler.com/user-guide/providers/alibabacloud/getting-started-alibabacloud#credentials-static-access-keys",
|
|
"Alibaba Cloud Credentials",
|
|
],
|
|
[
|
|
"https://docs.prowler.com/user-guide/providers/cloudflare/getting-started-cloudflare#user-api-token-authentication-recommended",
|
|
"Cloudflare API Token",
|
|
],
|
|
[
|
|
"https://docs.prowler.com/user-guide/providers/cloudflare/getting-started-cloudflare#api-key-and-email-authentication-legacy",
|
|
"Cloudflare API Key",
|
|
],
|
|
];
|
|
|
|
for (const [url, expected] of cases) {
|
|
expect(getProviderWizardDocsDestination(url)).toBe(expected);
|
|
}
|
|
});
|
|
|
|
it("ignores a #authentication anchor when deriving the label", () => {
|
|
// The credentials step falls back to a shortlink + anchor for providers
|
|
// without a dedicated auth page (Kubernetes). The label shown in the
|
|
// modal header must stay the provider name, not shift with the anchor.
|
|
const destination = getProviderWizardDocsDestination(
|
|
"https://goto.prowler.com/provider-k8s#authentication",
|
|
);
|
|
|
|
expect(destination).toBe("Kubernetes");
|
|
});
|
|
|
|
it("derives the provider label from the dedicated authentication docs URL", () => {
|
|
// On the credentials step providers with a standalone authentication.mdx
|
|
// (all except Kubernetes) point to `/providers/<slug>/authentication`.
|
|
// The label must come from the `<slug>` segment, not from the trailing
|
|
// "authentication" page name.
|
|
const destination = getProviderWizardDocsDestination(
|
|
"https://docs.prowler.com/user-guide/providers/aws/authentication",
|
|
);
|
|
|
|
expect(destination).toBe("AWS");
|
|
});
|
|
|
|
it("maps the OCI docs slug to the Oracle Cloud label", () => {
|
|
// The provider is called `oraclecloud` in the wizard but the docs path
|
|
// uses the `oci` slug, so the parser must know both refer to the same
|
|
// provider.
|
|
const destination = getProviderWizardDocsDestination(
|
|
"https://docs.prowler.com/user-guide/providers/oci/authentication",
|
|
);
|
|
|
|
expect(destination).toBe("Oracle Cloud");
|
|
});
|
|
|
|
it("maps the microsoft365 docs slug to the Microsoft 365 label", () => {
|
|
// Same shape: the wizard's `m365` maps to the `microsoft365` folder in
|
|
// docs, so the parser must recognise the folder slug as the provider.
|
|
const destination = getProviderWizardDocsDestination(
|
|
"https://docs.prowler.com/user-guide/providers/microsoft365/authentication",
|
|
);
|
|
|
|
expect(destination).toBe("Microsoft 365");
|
|
});
|
|
|
|
it("labels the Azure organizations tutorial after the wizard flow, not its page name", () => {
|
|
// The tutorial page is named after Management Groups while the wizard flow is
|
|
// "Azure Organizations", so the label cannot come from the slug — the header
|
|
// would read "Azure Management Groups Documentation".
|
|
const destination = getProviderWizardDocsDestination(
|
|
DOCS_URLS.AZURE_ORGANIZATIONS,
|
|
);
|
|
|
|
expect(destination).toBe("Azure Organizations");
|
|
});
|
|
|
|
it("returns a compact destination label for long docs links", () => {
|
|
const destination = getProviderWizardDocsDestination(
|
|
"https://docs.prowler.com/user-guide/tutorials/prowler-cloud-aws-organizations",
|
|
);
|
|
|
|
expect(destination).toBe("AWS Organizations");
|
|
});
|
|
});
|
|
|
|
// The URL maps in `ui/lib/external-urls.ts` and the `destinationLabelMap` in
|
|
// this file share the same set of providers but use different keys (wizard
|
|
// slug vs. docs path slug — e.g. `oraclecloud` vs `oci`, `m365` vs
|
|
// `microsoft365`). Nothing structural stops a provider from being added to
|
|
// one file and forgotten in the other, which would silently render the
|
|
// title-cased URL segment (e.g. "Getting Started Aws") in the modal header
|
|
// instead of the intended label.
|
|
//
|
|
// This suite locks the end-to-end contract: for every provider in
|
|
// `PROVIDER_TYPES`, on every wizard step, the URL emitted by
|
|
// `getProviderHelpText` must round-trip through the parser into the expected
|
|
// label. `Record<KnownProviderType, ...>` also gives compile-time coverage —
|
|
// adding a provider to `PROVIDER_TYPES` requires updating this table.
|
|
const EXPECTED_MODAL_HEADER_LABEL: Record<KnownProviderType, string> = {
|
|
aws: "AWS",
|
|
azure: "Azure",
|
|
m365: "Microsoft 365",
|
|
gcp: "GCP",
|
|
kubernetes: "Kubernetes",
|
|
github: "GitHub",
|
|
iac: "IaC",
|
|
image: "Image",
|
|
oraclecloud: "Oracle Cloud",
|
|
mongodbatlas: "MongoDB Atlas",
|
|
alibabacloud: "Alibaba Cloud",
|
|
cloudflare: "Cloudflare",
|
|
openstack: "OpenStack",
|
|
googleworkspace: "Google Workspace",
|
|
vercel: "Vercel",
|
|
okta: "Okta",
|
|
};
|
|
|
|
// Method-specific labels for the credentials step. Providers with per-method
|
|
// docs subsections (AWS, M365, Alibaba Cloud, Cloudflare) must round-trip
|
|
// through `getProviderWizardDocsDestination` into a specific label —
|
|
// otherwise the URL in `PROVIDER_CREDENTIALS_METHOD_DOCS_URL` and the label
|
|
// in `docsSectionLabelMap` have drifted apart. Providers without a per-method
|
|
// deep link (GCP, GitHub, and every single-method provider) fall through to
|
|
// the generic `EXPECTED_MODAL_HEADER_LABEL` above and are not listed here.
|
|
const EXPECTED_METHOD_MODAL_HEADER_LABEL: Array<
|
|
[KnownProviderType, string, string]
|
|
> = [
|
|
["aws", "role", "AWS Assume Role"],
|
|
["aws", "credentials", "AWS Credentials"],
|
|
["m365", "app_certificate", "M365 Certificate"],
|
|
["m365", "app_client_secret", "M365 Client Secret"],
|
|
["alibabacloud", "role", "Alibaba Cloud RAM Role"],
|
|
["alibabacloud", "credentials", "Alibaba Cloud Credentials"],
|
|
["cloudflare", "api_token", "Cloudflare API Token"],
|
|
["cloudflare", "api_key", "Cloudflare API Key"],
|
|
];
|
|
|
|
describe("provider label parity", () => {
|
|
const STEPS = [
|
|
PROVIDER_WIZARD_STEP.CONNECT,
|
|
PROVIDER_WIZARD_STEP.CREDENTIALS,
|
|
PROVIDER_WIZARD_STEP.TEST,
|
|
PROVIDER_WIZARD_STEP.LAUNCH,
|
|
] as const;
|
|
|
|
it("resolves the expected modal header label for every provider on every wizard step", () => {
|
|
for (const provider of PROVIDER_TYPES) {
|
|
for (const step of STEPS) {
|
|
const { link } = getProviderHelpText(provider, step);
|
|
const label = getProviderWizardDocsDestination(link);
|
|
expect(
|
|
label,
|
|
`Modal header label drift for provider="${provider}" step=${step}: getProviderHelpText returned "${link}" which the parser resolved to "${label}" instead of "${EXPECTED_MODAL_HEADER_LABEL[provider]}". Check destinationLabelMap in provider-wizard-modal.utils.ts.`,
|
|
).toBe(EXPECTED_MODAL_HEADER_LABEL[provider]);
|
|
}
|
|
}
|
|
});
|
|
|
|
it("resolves the expected modal header label for every method-specific credentials deep link", () => {
|
|
// Round-trip check: catches drift between the URLs in
|
|
// `PROVIDER_CREDENTIALS_METHOD_DOCS_URL` (external-urls.ts) and the
|
|
// section labels in `docsSectionLabelMap` (this file). A hash typo on
|
|
// either side would otherwise slip past the individual literal-URL tests
|
|
// in both files.
|
|
for (const [
|
|
provider,
|
|
method,
|
|
expected,
|
|
] of EXPECTED_METHOD_MODAL_HEADER_LABEL) {
|
|
const { link } = getProviderHelpText(
|
|
provider,
|
|
PROVIDER_WIZARD_STEP.CREDENTIALS,
|
|
method,
|
|
);
|
|
const label = getProviderWizardDocsDestination(link);
|
|
expect(
|
|
label,
|
|
`Method-specific label drift for provider="${provider}" method="${method}": getProviderHelpText returned "${link}" which the parser resolved to "${label}" instead of "${expected}". Ensure PROVIDER_CREDENTIALS_METHOD_DOCS_URL and docsSectionLabelMap agree on the anchor.`,
|
|
).toBe(expected);
|
|
}
|
|
});
|
|
});
|