test(ui): fix provider E2E test selectors and reliability (#10178)

This commit is contained in:
Alejandro Bailo
2026-02-27 10:12:54 +01:00
committed by GitHub
parent 79d4476713
commit ddb6c03c0e
17 changed files with 601 additions and 366 deletions

View File

@@ -123,7 +123,7 @@ export const SendInvitationForm = ({
onValueChange={field.onChange}
disabled={isSelectorDisabled}
>
<SelectTrigger>
<SelectTrigger aria-label="Select a role">
<SelectValue placeholder="Select a role" />
</SelectTrigger>
<SelectContent>

View File

@@ -95,6 +95,9 @@ describe("useProviderWizardController", () => {
expect(result.current.wizardVariant).toBe("organizations");
expect(result.current.isProviderFlow).toBe(false);
expect(result.current.orgCurrentStep).toBe(ORG_WIZARD_STEP.SETUP);
expect(result.current.docsLink).toBe(
"https://docs.prowler.com/user-guide/tutorials/prowler-cloud-aws-organizations",
);
// When
act(() => {

View File

@@ -3,7 +3,7 @@
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { getProviderHelpText } from "@/lib/external-urls";
import { DOCS_URLS, getProviderHelpText } from "@/lib/external-urls";
import { useOrgSetupStore } from "@/store/organizations/store";
import { useProviderWizardStore } from "@/store/provider-wizard/store";
import {
@@ -195,9 +195,9 @@ export function useProviderWizardController({
};
const isProviderFlow = wizardVariant === WIZARD_VARIANT.PROVIDER;
const docsLink = getProviderHelpText(
isProviderFlow ? (providerTypeHint ?? providerType ?? "") : "aws",
).link;
const docsLink = isProviderFlow
? getProviderHelpText(providerTypeHint ?? providerType ?? "").link
: DOCS_URLS.AWS_ORGANIZATIONS;
const resolvedFooterConfig: WizardFooterConfig =
isProviderFlow && currentStep === PROVIDER_WIZARD_STEP.LAUNCH
? {

View File

@@ -58,7 +58,7 @@ export function ProviderWizardModal({
initialData,
});
const scrollHintRefreshToken = `${wizardVariant}-${currentStep}-${orgCurrentStep}-${orgSetupPhase}`;
const { containerRef, showScrollHint, handleScroll } = useScrollHint({
const { containerRef, sentinelRef, showScrollHint } = useScrollHint({
enabled: open,
refreshToken: scrollHintRefreshToken,
});
@@ -106,7 +106,6 @@ export function ProviderWizardModal({
<div
ref={containerRef}
className="minimal-scrollbar h-full w-full overflow-y-scroll [scrollbar-gutter:stable] lg:ml-auto lg:max-w-[620px] xl:max-w-[700px]"
onScroll={handleScroll}
>
{isProviderFlow && currentStep === PROVIDER_WIZARD_STEP.CONNECT && (
<ConnectStep
@@ -177,6 +176,9 @@ export function ProviderWizardModal({
onFooterChange={setFooterConfig}
/>
)}
{/* Sentinel element for IntersectionObserver scroll detection */}
<div ref={sentinelRef} aria-hidden className="h-px shrink-0" />
</div>
{showScrollHint && (

View File

@@ -1,63 +1,67 @@
"use client";
import { UIEvent, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useState } from "react";
interface UseScrollHintOptions {
enabled?: boolean;
refreshToken?: string | number;
}
const SCROLL_THRESHOLD_PX = 4;
function shouldShowScrollHint(element: HTMLDivElement) {
const hasOverflow =
element.scrollHeight - element.clientHeight > SCROLL_THRESHOLD_PX;
const isAtBottom =
element.scrollTop + element.clientHeight >=
element.scrollHeight - SCROLL_THRESHOLD_PX;
return hasOverflow && !isAtBottom;
}
/**
* Detects whether a scrollable container has overflow using an
* IntersectionObserver on a sentinel element placed at the end of the content.
*
* Uses callback refs (stored in state) so the observer is set up only after
* the DOM elements actually mount — critical for Radix Dialog portals where
* useRef would be null when the first useEffect fires.
*
* When the sentinel is NOT visible inside the container → content overflows
* and the user hasn't scrolled to the bottom → show hint.
*/
export function useScrollHint({
enabled = true,
refreshToken,
}: UseScrollHintOptions = {}) {
const containerRef = useRef<HTMLDivElement | null>(null);
const [containerEl, setContainerEl] = useState<HTMLDivElement | null>(null);
const [sentinelEl, setSentinelEl] = useState<HTMLDivElement | null>(null);
const [showScrollHint, setShowScrollHint] = useState(false);
useEffect(() => {
if (!enabled) {
if (!enabled || !containerEl || !sentinelEl) {
setShowScrollHint(false);
return;
}
const element = containerRef.current;
if (!element) return;
const observer = new IntersectionObserver(
([entry]) => {
setShowScrollHint(!entry.isIntersecting);
},
{
root: containerEl,
// Small margin so the hint hides slightly before the absolute bottom
rootMargin: "0px 0px 4px 0px",
threshold: 0,
},
);
const recalculate = () => {
const el = containerRef.current;
if (!el) return;
setShowScrollHint(shouldShowScrollHint(el));
};
observer.observe(sentinelEl);
const observer = new ResizeObserver(recalculate);
observer.observe(element);
return () => observer.disconnect();
}, [enabled, refreshToken, containerEl, sentinelEl]);
recalculate();
return () => {
observer.disconnect();
};
}, [enabled, refreshToken]);
const handleScroll = (event: UIEvent<HTMLDivElement>) => {
setShowScrollHint(shouldShowScrollHint(event.currentTarget));
};
// Stable callback refs — setState setters never change identity
const containerRef = useCallback(
(node: HTMLDivElement | null) => setContainerEl(node),
[],
);
const sentinelRef = useCallback(
(node: HTMLDivElement | null) => setSentinelEl(node),
[],
);
return {
containerRef,
sentinelRef,
showScrollHint,
handleScroll,
};
}

View File

@@ -4,6 +4,8 @@ import { IntegrationType } from "../types/integrations";
export const DOCS_URLS = {
FINDINGS_ANALYSIS:
"https://docs.prowler.com/user-guide/tutorials/prowler-app#step-8:-analyze-the-findings",
AWS_ORGANIZATIONS:
"https://docs.prowler.com/user-guide/tutorials/prowler-cloud-aws-organizations",
} as const;
export const getProviderHelpText = (provider: string) => {

View File

@@ -1,4 +1,11 @@
import { defineConfig, devices } from "@playwright/test";
import fs from "fs";
import path from "path";
const localEnvPath = path.resolve(__dirname, ".env.local");
if (fs.existsSync(localEnvPath)) {
process.loadEnvFile(localEnvPath);
}
export default defineConfig({
testDir: "./tests",

View File

@@ -1,8 +1,7 @@
import { expect, test } from "@playwright/test";
import { getSessionWithoutCookies, TEST_CREDENTIALS } from "../helpers";
import { TEST_CREDENTIALS } from "../helpers";
import { ProvidersPage } from "../providers/providers-page";
import { ScansPage } from "../scans/scans-page";
import { SignInPage } from "../sign-in-base/sign-in-base-page";
import { SignUpPage } from "../sign-up/sign-up-page";
@@ -30,24 +29,46 @@ test.describe("Middleware Error Handling", () => {
test(
"should maintain protection after session error",
{ tag: ["@e2e", "@auth", "@middleware", "@AUTH-MW-E2E-002"] },
async ({ page, context }) => {
async ({ page, context, browser }) => {
const signInPage = new SignInPage(page);
const providersPage = new ProvidersPage(page);
const scansPage = new ScansPage(page);
await signInPage.loginAndVerify(TEST_CREDENTIALS.VALID);
await providersPage.goto();
await providersPage.verifyPageLoaded();
// Remove auth cookies to simulate a broken/expired session deterministically.
await context.clearCookies();
// Build an isolated context with an explicitly invalid auth token.
// This avoids races from active tabs rehydrating cookies in the original context.
const authenticatedState = await context.storageState();
const authCookies = authenticatedState.cookies.filter((cookie) =>
/(authjs|next-auth)/i.test(cookie.name),
);
expect(authCookies.length).toBeGreaterThan(0);
const expiredSession = await getSessionWithoutCookies(page);
expect(expiredSession).toBeNull();
const invalidSessionContext = await browser.newContext({
storageState: {
origins: authenticatedState.origins,
cookies: authenticatedState.cookies.map((cookie) =>
/(authjs|next-auth)/i.test(cookie.name)
? { ...cookie, value: "invalid.session.token" }
: cookie,
),
},
});
await scansPage.goto();
await signInPage.verifyOnSignInPage();
try {
// Use a fresh page to force a full navigation through proxy in Next.js 16.
const freshPage = await invalidSessionContext.newPage();
const freshSignInPage = new SignInPage(freshPage);
const cacheBuster = Date.now();
await freshPage.goto(`/scans?e2e_mw=${cacheBuster}`, {
waitUntil: "commit",
});
await freshSignInPage.verifyRedirectWithCallback("/scans");
} finally {
await invalidSessionContext.close();
}
},
);

View File

@@ -135,47 +135,30 @@ export async function deleteProviderIfExists(page: ProvidersPage, providerUID: s
await page.goto();
await expect(page.providersTable).toBeVisible({ timeout: 10000 });
// Find and use the search input to filter the table
const searchInput = page.page.getByPlaceholder(/search|filter/i);
await expect(searchInput).toBeVisible({ timeout: 5000 });
// Clear and search for the specific provider
await searchInput.clear();
await searchInput.fill(providerUID);
await searchInput.press("Enter");
// Additional wait for React table to re-render with the server-filtered data
// The filtering happens on the server, but the table component needs time
// to process the response and update the DOM after network idle
await page.page.waitForTimeout(1500);
// Get all rows from the table
const allRows = page.providersTable.locator("tbody tr");
// Helper function to check if a row is the "No results" row
const isNoResultsRow = async (row: Locator): Promise<boolean> => {
const text = await row.textContent();
return text?.includes("No results") || text?.includes("No data") || false;
};
// Helper function to find the row with the specific UID
const findProviderRow = async (): Promise<Locator | null> => {
const count = await allRows.count();
const rowByText = page.providersTable
.locator("tbody tr")
.filter({ hasText: providerUID })
.first();
if (await rowByText.isVisible().catch(() => false)) {
return rowByText;
}
const count = await allRows.count();
for (let i = 0; i < count; i++) {
const row = allRows.nth(i);
// Skip "No results" rows
if (await isNoResultsRow(row)) {
continue;
}
// Check if this row contains the UID in the UID column (column 3)
const uidCell = row.locator("td").nth(3);
const uidText = await uidCell.textContent();
if (uidText?.includes(providerUID)) {
const rowText = await row.textContent();
if (rowText?.includes(providerUID)) {
return row;
}
}
@@ -183,24 +166,6 @@ export async function deleteProviderIfExists(page: ProvidersPage, providerUID: s
return null;
};
// Wait for filtering to complete (max 0 or 1 data rows)
await expect(async () => {
await findProviderRow();
const count = await allRows.count();
// Count only real data rows (not "No results")
let dataRowCount = 0;
for (let i = 0; i < count; i++) {
if (!(await isNoResultsRow(allRows.nth(i)))) {
dataRowCount++;
}
}
// Should have 0 or 1 data row
expect(dataRowCount).toBeLessThanOrEqual(1);
}).toPass({ timeout: 20000 });
// Find the provider row
const targetRow = await findProviderRow();

View File

@@ -40,11 +40,10 @@ export class InvitationsPage extends BasePage {
// Form inputs
this.emailInput = page.getByRole("textbox", { name: "Email" });
// Form select
this.roleSelect = page
.getByRole("combobox", { name: /Role|Select a role/i })
.or(page.getByRole("button", { name: /Role|Select a role/i }))
.first();
// Form select (Radix Select renders <button role="combobox"> with aria-label)
this.roleSelect = page.getByRole("combobox", {
name: /Select a role/i,
});
// Form details
this.reviewInvitationDetailsButton = page.getByRole("button", {
@@ -96,23 +95,15 @@ export class InvitationsPage extends BasePage {
}
async selectRole(role: string): Promise<void> {
// Select the role option
// Open the role dropdown
await expect(this.roleSelect).toBeVisible({ timeout: 15000 });
await this.roleSelect.click();
// Prefer ARIA role option inside listbox
const option = this.page.getByRole("option", {
name: new RegExp(`^${role}$`, "i"),
name: new RegExp(role, "i"),
});
await expect(option.first()).toBeVisible({ timeout: 10000 });
await option.first().click();
if (await option.count()) {
await option.first().click();
} else {
throw new Error(`Role option ${role} not found`);
}
// Ensure a role value was selected in the trigger
await expect(this.roleSelect).not.toContainText(/Select a role/i);
}

View File

@@ -5,69 +5,80 @@ import { SignUpPage } from "../sign-up/sign-up-page";
import { SignInPage } from "../sign-in-base/sign-in-base-page";
import { UserProfilePage } from "../profile/profile-page";
const isCloudEnv = process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true";
test.describe("New user invitation", () => {
// Invitations page object
let invitationsPage: InvitationsPage;
// Setup before each test
test.beforeEach(async ({ page }) => {
invitationsPage = new InvitationsPage(page);
});
// Use admin authentication for invitations management
test.use({ storageState: "playwright/.auth/admin_user.json" });
test(
"should invite a new user",
"should send an invitation successfully",
{
tag: ["@critical", "@e2e", "@invitations", "@INVITATION-E2E-001"],
},
async ({ page, browser }) => {
async () => {
const suffix = makeSuffix(10);
const uniqueEmail = `e2e+${suffix}@prowler.com`;
await invitationsPage.goto();
await invitationsPage.verifyPageLoaded();
await invitationsPage.clickInviteButton();
await invitationsPage.verifyInvitePageLoaded();
await invitationsPage.fillEmail(uniqueEmail);
await invitationsPage.selectRole("admin");
await invitationsPage.clickSendInviteButton();
await invitationsPage.verifyInviteDataPageLoaded();
},
);
test(
"should invite a new user and verify signup and login",
{
tag: ["@critical", "@e2e", "@invitations", "@INVITATION-E2E-002"],
},
async ({ browser }) => {
test.skip(!isCloudEnv, "Requires email-verification flow (Cloud only)");
// Test data from environment variables
const password = process.env.E2E_NEW_USER_PASSWORD;
const organizationId = process.env.E2E_ORGANIZATION_ID;
// Validate required environment variables
if (!password || !organizationId) {
throw new Error(
"E2E_NEW_USER_PASSWORD or E2E_ORGANIZATION_ID environment variable is not set",
);
}
// Generate unique test data
const suffix = makeSuffix(10);
const uniqueEmail = `e2e+${suffix}@prowler.com`;
// Navigate to providers page
await invitationsPage.goto();
await invitationsPage.verifyPageLoaded();
// Press the invite button
await invitationsPage.clickInviteButton();
await invitationsPage.verifyInvitePageLoaded();
// Fill the email
await invitationsPage.fillEmail(uniqueEmail);
await invitationsPage.selectRole("admin");
// Select the role option
await invitationsPage.selectRole("e2e_admin");
// Press the send invitation button
await invitationsPage.clickSendInviteButton();
await invitationsPage.verifyInviteDataPageLoaded();
// Get the share url
const shareUrl = await invitationsPage.getShareUrl();
// Navigate to the share url with a new context to avoid cookies from the admin context
const inviteContext = await browser.newContext({ storageState: { cookies: [], origins: [] } });
const inviteContext = await browser.newContext({
storageState: { cookies: [], origins: [] },
});
const signUpPage = new SignUpPage(await inviteContext.newPage());
// Navigate to the share url
await signUpPage.gotoInvite(shareUrl);
// Fill and submit the sign-up form
await signUpPage.signup({
name: `E2E User ${suffix}`,
email: uniqueEmail,
@@ -76,13 +87,9 @@ test.describe("New user invitation", () => {
acceptTerms: true,
});
// Verify no errors occurred during sign-up
await signUpPage.verifyNoErrors();
// Verify redirect to login page (OSS environment)
await signUpPage.verifyRedirectToLogin();
// Verify the newly created user can log in successfully with the new context
const signInPage = new SignInPage(await inviteContext.newPage());
await signInPage.goto();
await signInPage.login({
@@ -90,15 +97,13 @@ test.describe("New user invitation", () => {
password: password,
});
await signInPage.verifySuccessfulLogin();
// Navigate to the user profile page
const userProfilePage = new UserProfilePage(await inviteContext.newPage());
await userProfilePage.goto();
// Verify if user is added to the organization
const userProfilePage = new UserProfilePage(
await inviteContext.newPage(),
);
await userProfilePage.goto();
await userProfilePage.verifyOrganizationId(organizationId);
// Close the invite context
await inviteContext.close();
},
);

View File

@@ -7,6 +7,16 @@ export interface AWSProviderData {
alias?: string;
}
export interface AWSOrganizationsProviderData {
organizationId: string;
organizationName?: string;
}
export interface AWSOrganizationsProviderCredential {
roleArn: string;
stackSetDeployed?: boolean;
}
// AZURE provider data
export interface AZUREProviderData {
subscriptionId: string;
@@ -492,13 +502,9 @@ export class ProvidersPage extends BasePage {
this.roleArnInput = page.getByRole("textbox", { name: "Role ARN" });
this.externalIdInput = page.getByRole("textbox", { name: "External ID" });
// Inputs for static credentials
this.accessKeyIdInput = page.getByRole("textbox", {
name: "Access Key ID",
});
this.secretAccessKeyInput = page.getByRole("textbox", {
name: "Secret Access Key",
});
// Inputs for static credentials (type="password" fields have no textbox role)
this.accessKeyIdInput = page.getByLabel(/Access Key ID/i).first();
this.secretAccessKeyInput = page.getByLabel(/Secret Access Key/i).first();
// Delete button in confirmation modal
this.deleteProviderConfirmationButton = page.getByRole("button", {
@@ -578,17 +584,17 @@ export class ProvidersPage extends BasePage {
}
async selectAWSSingleAccountMethod(): Promise<void> {
await this.page
.getByRole("button", {
name: "Add A Single AWS Cloud Account",
exact: true,
})
.click();
const singleAccountOption = this.page.getByRole("radio", {
name: "Add A Single AWS Cloud Account",
exact: true,
});
await expect(singleAccountOption).toBeVisible({ timeout: 10000 });
await singleAccountOption.click();
}
async selectAWSOrganizationsMethod(): Promise<void> {
await this.page
.getByRole("button", {
.getByRole("radio", {
name: "Add Multiple Accounts With AWS Organizations",
exact: true,
})
@@ -633,16 +639,8 @@ export class ProvidersPage extends BasePage {
}
async fillAWSProviderDetails(data: AWSProviderData): Promise<void> {
// Fill the AWS provider details
const singleAccountButton = this.page.getByRole("button", {
name: "Add A Single AWS Cloud Account",
exact: true,
});
if (await singleAccountButton.isVisible().catch(() => false)) {
await singleAccountButton.click();
}
await this.selectAWSSingleAccountMethod();
await expect(this.accountIdInput).toBeVisible({ timeout: 10000 });
await this.accountIdInput.fill(data.accountId);
if (data.alias) {
@@ -650,6 +648,43 @@ export class ProvidersPage extends BasePage {
}
}
async fillAWSOrganizationsProviderDetails(
data: AWSOrganizationsProviderData,
): Promise<void> {
const organizationIdInput = this.page.getByRole("textbox", {
name: "Organization ID",
exact: true,
});
await expect(organizationIdInput).toBeVisible({ timeout: 10000 });
await organizationIdInput.fill(data.organizationId.toLowerCase());
if (data.organizationName) {
await this.page
.getByRole("textbox", { name: "Name (optional)", exact: true })
.fill(data.organizationName);
}
}
async fillAWSOrganizationsCredentials(
credentials: AWSOrganizationsProviderCredential,
): Promise<void> {
const roleArnInput = this.page.getByRole("textbox", {
name: "Role ARN",
exact: true,
});
await expect(roleArnInput).toBeVisible({ timeout: 10000 });
await roleArnInput.fill(credentials.roleArn);
if (credentials.stackSetDeployed ?? true) {
const stackSetCheckbox = this.page.getByRole("checkbox", {
name: /The StackSet has been successfully deployed in AWS/i,
});
if (!(await stackSetCheckbox.isChecked())) {
await stackSetCheckbox.click();
}
}
}
async fillAZUREProviderDetails(data: AZUREProviderData): Promise<void> {
// Fill the AWS provider details
@@ -705,22 +740,13 @@ export class ProvidersPage extends BasePage {
async clickNext(): Promise<void> {
await this.verifyWizardModalOpen();
const launchScanButton = this.page.getByRole("button", {
name: "Launch scan",
exact: true,
});
if (await launchScanButton.isVisible().catch(() => false)) {
await launchScanButton.click();
await this.handleLaunchScanCompletion();
return;
}
const actionNames = [
"Go to scans",
"Authenticate",
"Next",
"Save",
"Check connection",
"Launch scan",
] as const;
for (const actionName of actionNames) {
@@ -730,6 +756,9 @@ export class ProvidersPage extends BasePage {
});
if (await button.isVisible().catch(() => false)) {
await button.click();
if (actionName === "Launch scan") {
await this.handleLaunchScanCompletion();
}
return;
}
}
@@ -740,38 +769,36 @@ export class ProvidersPage extends BasePage {
}
private async handleLaunchScanCompletion(): Promise<void> {
const errorMessage = this.page
.locator(
"div.border-border-error, div.bg-red-100, p.text-text-error-primary, p.text-danger",
)
.first();
const goToScansButton = this.page.getByRole("button", {
name: "Go to scans",
exact: true,
});
const connectionError = this.page.locator(
"div.border-border-error p.text-text-error-primary",
);
try {
await Promise.race([
this.page.waitForURL(/\/scans/, { timeout: 30000 }),
goToScansButton.waitFor({ state: "visible", timeout: 30000 }),
errorMessage.waitFor({ state: "visible", timeout: 30000 }),
this.page.waitForURL(/\/scans/, { timeout: 20000 }),
goToScansButton.waitFor({ state: "visible", timeout: 20000 }),
connectionError.waitFor({ state: "visible", timeout: 20000 }),
]);
} catch {
// Continue and inspect visible state below.
}
const isErrorVisible = await errorMessage.isVisible().catch(() => false);
if (isErrorVisible) {
const errorText = await errorMessage.textContent();
if (await connectionError.isVisible().catch(() => false)) {
const errorText = await connectionError.textContent();
throw new Error(
`Test connection failed with error: ${errorText?.trim() || "Unknown error"}`,
);
}
const isGoToScansVisible = await goToScansButton
.isVisible()
.catch(() => false);
if (isGoToScansVisible) {
if (this.page.url().includes("/scans")) {
return;
}
if (await goToScansButton.isVisible().catch(() => false)) {
await goToScansButton.click();
await this.page.waitForURL(/\/scans/, { timeout: 30000 });
}
@@ -827,19 +854,48 @@ export class ProvidersPage extends BasePage {
}
async fillRoleCredentials(credentials: AWSProviderCredential): Promise<void> {
// Fill the role credentials form
await expect(this.roleArnInput).toBeVisible({ timeout: 10000 });
const accessKeyInputInWizard = this.wizardModal.getByPlaceholder(
"Enter the AWS Access Key ID",
);
const secretKeyInputInWizard = this.wizardModal.getByPlaceholder(
"Enter the AWS Secret Access Key",
);
const accessKeyId =
credentials.accessKeyId || process.env.E2E_AWS_PROVIDER_ACCESS_KEY;
const secretAccessKey =
credentials.secretAccessKey || process.env.E2E_AWS_PROVIDER_SECRET_KEY;
if (credentials.accessKeyId) {
await this.accessKeyIdInput.fill(credentials.accessKeyId);
const shouldFillStaticKeys = Boolean(
accessKeyId || secretAccessKey,
);
if (shouldFillStaticKeys) {
const accessKeyIsVisible = await accessKeyInputInWizard
.isVisible()
.catch(() => false);
// In cloud env the default can be SDK mode, so expose Access/Secret explicitly.
if (!accessKeyIsVisible) {
await this.selectAuthenticationMethod(
AWS_CREDENTIAL_OPTIONS.AWS_ROLE_ARN,
);
}
}
if (credentials.secretAccessKey) {
await this.secretAccessKeyInput.fill(credentials.secretAccessKey);
if (accessKeyId) {
await expect(accessKeyInputInWizard).toBeVisible({ timeout: 10000 });
await accessKeyInputInWizard.fill(accessKeyId);
await expect(accessKeyInputInWizard).toHaveValue(accessKeyId);
}
if (secretAccessKey) {
await expect(secretKeyInputInWizard).toBeVisible({ timeout: 10000 });
await secretKeyInputInWizard.fill(secretAccessKey);
await expect(secretKeyInputInWizard).toHaveValue(secretAccessKey);
}
if (credentials.roleArn) {
await this.roleArnInput.fill(credentials.roleArn);
}
if (credentials.externalId) {
// External ID may be prefilled and disabled; only fill if enabled
if (await this.externalIdInput.isEnabled()) {
await this.externalIdInput.fill(credentials.externalId);
}
@@ -849,13 +905,26 @@ export class ProvidersPage extends BasePage {
async fillStaticCredentials(
credentials: AWSProviderCredential,
): Promise<void> {
// Fill the static credentials form
const accessKeyInputInWizard = this.wizardModal.getByPlaceholder(
"Enter the AWS Access Key ID",
);
const secretKeyInputInWizard = this.wizardModal.getByPlaceholder(
"Enter the AWS Secret Access Key",
);
const accessKeyId =
credentials.accessKeyId || process.env.E2E_AWS_PROVIDER_ACCESS_KEY;
const secretAccessKey =
credentials.secretAccessKey || process.env.E2E_AWS_PROVIDER_SECRET_KEY;
if (credentials.accessKeyId) {
await this.accessKeyIdInput.fill(credentials.accessKeyId);
if (accessKeyId) {
await expect(accessKeyInputInWizard).toBeVisible({ timeout: 10000 });
await accessKeyInputInWizard.fill(accessKeyId);
await expect(accessKeyInputInWizard).toHaveValue(accessKeyId);
}
if (credentials.secretAccessKey) {
await this.secretAccessKeyInput.fill(credentials.secretAccessKey);
if (secretAccessKey) {
await expect(secretKeyInputInWizard).toBeVisible({ timeout: 10000 });
await secretKeyInputInWizard.fill(secretAccessKey);
await expect(secretKeyInputInWizard).toHaveValue(secretAccessKey);
}
}
@@ -1126,11 +1195,22 @@ export class ProvidersPage extends BasePage {
}
async verifyCredentialsPageLoaded(): Promise<void> {
// Verify the credentials page is loaded
await this.verifyPageHasProwlerTitle();
await this.verifyWizardModalOpen();
await expect(this.roleCredentialsRadio).toBeVisible();
const selectorRadio = this.wizardModal.getByRole("radio", {
name: /Connect assuming IAM Role/i,
});
const selectorHint = this.wizardModal.getByText(/Using IAM Role/i);
const roleArnInForm = this.wizardModal.getByRole("textbox", {
name: "Role ARN",
});
await Promise.race([
selectorRadio.waitFor({ state: "visible", timeout: 20000 }),
selectorHint.waitFor({ state: "visible", timeout: 20000 }),
roleArnInForm.waitFor({ state: "visible", timeout: 20000 }),
]);
}
async verifyM365CredentialsPageLoaded(): Promise<void> {
@@ -1186,12 +1266,17 @@ export class ProvidersPage extends BasePage {
await this.verifyPageHasProwlerTitle();
await this.verifyWizardModalOpen();
// Verify the Launch scan button is visible
const launchScanButton = this.page
.locator("button")
.filter({ hasText: "Launch scan" });
// Some providers show "Check connection" before "Launch scan".
const launchAction = this.page
.getByRole("button", { name: "Launch scan", exact: true })
.or(
this.page.getByRole("button", {
name: "Check connection",
exact: true,
}),
);
await expect(launchScanButton).toBeVisible();
await expect(launchAction).toBeVisible();
}
async verifyLoadProviderPageAfterNewProvider(): Promise<void> {
@@ -1235,7 +1320,9 @@ export class ProvidersPage extends BasePage {
.click({ force: true });
} else if (method === AWS_CREDENTIAL_OPTIONS.AWS_SDK_DEFAULT) {
await this.page
.getByRole("option", { name: "AWS SDK Default" })
.getByRole("option", {
name: /AWS SDK Default|Prowler Cloud will assume your IAM role/i,
})
.click({ force: true });
} else {
throw new Error(`Invalid authentication method: ${method}`);
@@ -1278,9 +1365,7 @@ export class ProvidersPage extends BasePage {
}
async verifyTestConnectionPageLoaded(): Promise<void> {
// Verify the test connection page is loaded
await this.verifyPageHasProwlerTitle();
await this.verifyWizardModalOpen();
const testConnectionAction = this.page
.getByRole("button", { name: "Launch scan", exact: true })
.or(
@@ -1289,6 +1374,21 @@ export class ProvidersPage extends BasePage {
exact: true,
}),
);
// Some update flows return directly to providers list after authenticating.
try {
await Promise.race([
testConnectionAction.waitFor({ state: "visible", timeout: 20000 }),
this.providersTable.waitFor({ state: "visible", timeout: 20000 }),
]);
} catch {
// Fall through to explicit assertions below.
}
if (await this.providersTable.isVisible().catch(() => false)) {
return;
}
await expect(testConnectionAction).toBeVisible();
}
}

View File

@@ -891,3 +891,60 @@
- Requires valid Alibaba Cloud account with RAM Role configured
- RAM Role must have sufficient permissions for security scanning
- Role ARN must be properly configured and assumable
---
## Test Case: `PROVIDER-E2E-016` - Add AWS Organization Using AWS Organizations Flow
**Priority:** `critical`
**Tags:**
- type → @e2e, @serial
- feature → @providers
- provider → @aws
**Description/Objective:** Validates the complete flow of adding AWS accounts through AWS Organizations, including organization setup, authentication, account selection, and scan scheduling.
**Preconditions:**
- Admin user authentication required (admin.auth.setup setup)
- Environment variables configured: E2E_AWS_ORGANIZATION_ID, E2E_AWS_ORGANIZATION_ROLE_ARN
- Remove any existing provider with the same Organization ID before starting the test
- StackSet must be deployed in AWS Organizations and expose a valid IAM Role ARN for Prowler
- This test must be run serially and never in parallel with other tests, as it requires the Organization ID not to be already registered beforehand.
### Flow Steps:
1. Navigate to providers page
2. Click "Add Provider" button
3. Select AWS provider type
4. Select "Add Multiple Accounts With AWS Organizations"
5. Fill organization details (organization ID and optional name)
6. Continue to authentication details and provide role ARN
7. Confirm StackSet deployment checkbox and authenticate
8. Confirm organization account selection step and continue
9. Verify organization launch step, choose single scan schedule, and launch
10. Verify redirect to Scans page
### Expected Result:
- AWS Organizations flow completes successfully
- Accounts are connected and launch step is displayed
- Scan scheduling selection is applied
- User is redirected to Scans page after launch
### Key verification points:
- Connect account page displays AWS option
- Organizations method selector is available
- Authentication details step loads
- Account selection step loads
- Accounts connected launch step appears
- Successful redirect to Scans page after launching
### Notes:
- Organization ID must follow AWS format (e.g., o-abc123def4)
- Role ARN must belong to the StackSet deployment for Organizations flow
- Provider cleanup is executed before test run to avoid unique constraint conflicts

View File

@@ -2,6 +2,8 @@ import { test } from "@playwright/test";
import {
ProvidersPage,
AWSProviderData,
AWSOrganizationsProviderCredential,
AWSOrganizationsProviderData,
AWSProviderCredential,
AWS_CREDENTIAL_OPTIONS,
AZUREProviderData,
@@ -36,23 +38,18 @@ test.describe("Add Provider", () => {
let providersPage: ProvidersPage;
let scansPage: ScansPage;
// Test data from environment variables
const accountId = process.env.E2E_AWS_PROVIDER_ACCOUNT_ID;
const accessKey = process.env.E2E_AWS_PROVIDER_ACCESS_KEY;
const secretKey = process.env.E2E_AWS_PROVIDER_SECRET_KEY;
const roleArn = process.env.E2E_AWS_PROVIDER_ROLE_ARN;
// Validate required environment variables
if (!accountId) {
throw new Error(
"E2E_AWS_PROVIDER_ACCOUNT_ID environment variable is not set",
);
}
const accountId = process.env.E2E_AWS_PROVIDER_ACCOUNT_ID ?? "";
const accessKey = process.env.E2E_AWS_PROVIDER_ACCESS_KEY ?? "";
const secretKey = process.env.E2E_AWS_PROVIDER_SECRET_KEY ?? "";
const roleArn = process.env.E2E_AWS_PROVIDER_ROLE_ARN ?? "";
const organizationId = process.env.E2E_AWS_ORGANIZATION_ID ?? "";
const organizationRoleArn = process.env.E2E_AWS_ORGANIZATION_ROLE_ARN ?? "";
// Setup before each test
test.beforeEach(async ({ page }) => {
test.skip(!accountId, "E2E_AWS_PROVIDER_ACCOUNT_ID is not set");
providersPage = new ProvidersPage(page);
// Clean up existing provider to ensure clean test state
await deleteProviderIfExists(providersPage, accountId);
await deleteProviderIfExists(providersPage, accountId!);
});
// Use admin authentication for provider management
@@ -216,9 +213,8 @@ test.describe("Add Provider", () => {
],
},
async ({ page }) => {
// Validate required environment variables
if (!accountId || !roleArn) {
if (!accountId || !roleArn) {
throw new Error(
"E2E_AWS_PROVIDER_ACCOUNT_ID, and E2E_AWS_PROVIDER_ROLE_ARN environment variables are not set",
);
@@ -278,6 +274,73 @@ test.describe("Add Provider", () => {
await scansPage.verifyScheduledScanStatus(accountId);
},
);
test(
"should add multiple AWS accounts using AWS Organizations",
{
tag: [
"@critical",
"@e2e",
"@providers",
"@aws",
"@serial",
"@PROVIDER-E2E-016",
],
},
async ({ page }) => {
if (!organizationId || !organizationRoleArn) {
test.skip(
true,
"E2E_AWS_ORGANIZATION_ID and E2E_AWS_ORGANIZATION_ROLE_ARN environment variables are not set",
);
return;
}
const awsOrganizationId = organizationId;
const awsOrganizationRoleArn = organizationRoleArn;
await deleteProviderIfExists(providersPage, awsOrganizationId);
const awsOrganizationData: AWSOrganizationsProviderData = {
organizationId: awsOrganizationId,
organizationName: "Test E2E AWS Organization",
};
const organizationsCredentials: AWSOrganizationsProviderCredential = {
roleArn: awsOrganizationRoleArn,
stackSetDeployed: true,
};
await providersPage.goto();
await providersPage.verifyPageLoaded();
await providersPage.clickAddProvider();
await providersPage.verifyConnectAccountPageLoaded();
await providersPage.selectAWSProvider();
await providersPage.selectAWSOrganizationsMethod();
await providersPage.fillAWSOrganizationsProviderDetails(
awsOrganizationData,
);
await providersPage.clickNext();
await providersPage.verifyOrganizationsAuthenticationStepLoaded();
await providersPage.fillAWSOrganizationsCredentials(
organizationsCredentials,
);
await providersPage.clickNext();
await providersPage.verifyOrganizationsAccountSelectionStepLoaded();
await providersPage.clickNext();
await providersPage.verifyOrganizationsLaunchStepLoaded();
await providersPage.chooseOrganizationsScanSchedule("single");
await providersPage.clickNext();
scansPage = new ScansPage(page);
await scansPage.verifyPageLoaded();
},
);
});
test.describe.serial("Add AZURE Provider", () => {
@@ -286,23 +349,19 @@ test.describe("Add Provider", () => {
let scansPage: ScansPage;
// Test data from environment variables
const subscriptionId = process.env.E2E_AZURE_SUBSCRIPTION_ID;
const clientId = process.env.E2E_AZURE_CLIENT_ID;
const clientSecret = process.env.E2E_AZURE_SECRET_ID;
const tenantId = process.env.E2E_AZURE_TENANT_ID;
// Validate required environment variables
if (!subscriptionId || !clientId || !clientSecret || !tenantId) {
throw new Error(
"E2E_AZURE_SUBSCRIPTION_ID, E2E_AZURE_CLIENT_ID, E2E_AZURE_SECRET_ID, and E2E_AZURE_TENANT_ID environment variables are not set",
);
}
const subscriptionId = process.env.E2E_AZURE_SUBSCRIPTION_ID ?? "";
const clientId = process.env.E2E_AZURE_CLIENT_ID ?? "";
const clientSecret = process.env.E2E_AZURE_SECRET_ID ?? "";
const tenantId = process.env.E2E_AZURE_TENANT_ID ?? "";
// Setup before each test
test.beforeEach(async ({ page }) => {
test.skip(
!subscriptionId || !clientId || !clientSecret || !tenantId,
"Azure E2E env vars are not set",
);
providersPage = new ProvidersPage(page);
// Clean up existing provider to ensure clean test state
await deleteProviderIfExists(providersPage, subscriptionId);
await deleteProviderIfExists(providersPage, subscriptionId!);
});
// Use admin authentication for provider management
@@ -374,22 +433,18 @@ test.describe("Add Provider", () => {
let scansPage: ScansPage;
// Test data from environment variables
const domainId = process.env.E2E_M365_DOMAIN_ID;
const clientId = process.env.E2E_M365_CLIENT_ID;
const tenantId = process.env.E2E_M365_TENANT_ID;
// Validate required environment variables
if (!domainId || !clientId || !tenantId) {
throw new Error(
"E2E_M365_DOMAIN_ID, E2E_M365_CLIENT_ID, and E2E_M365_TENANT_ID environment variables are not set",
);
}
const domainId = process.env.E2E_M365_DOMAIN_ID ?? "";
const clientId = process.env.E2E_M365_CLIENT_ID ?? "";
const tenantId = process.env.E2E_M365_TENANT_ID ?? "";
// Setup before each test
test.beforeEach(async ({ page }) => {
test.skip(
!domainId || !clientId || !tenantId,
"M365 E2E env vars are not set",
);
providersPage = new ProvidersPage(page);
// Clean up existing provider to ensure clean test state
await deleteProviderIfExists(providersPage, domainId);
await deleteProviderIfExists(providersPage, domainId!);
});
// Use admin authentication for provider management
@@ -409,7 +464,7 @@ test.describe("Add Provider", () => {
},
async ({ page }) => {
// Validate required environment variables
const clientSecret = process.env.E2E_M365_SECRET_ID;
const clientSecret = process.env.E2E_M365_SECRET_ID ?? "";
if (!clientSecret) {
throw new Error("E2E_M365_SECRET_ID environment variable is not set");
@@ -482,7 +537,8 @@ test.describe("Add Provider", () => {
},
async ({ page }) => {
// Validate required environment variables
const certificateContent = process.env.E2E_M365_CERTIFICATE_CONTENT;
const certificateContent =
process.env.E2E_M365_CERTIFICATE_CONTENT ?? "";
if (!certificateContent) {
throw new Error(
@@ -551,22 +607,17 @@ test.describe("Add Provider", () => {
let scansPage: ScansPage;
// Test data from environment variables
const context = process.env.E2E_KUBERNETES_CONTEXT;
const kubeconfigPath = process.env.E2E_KUBERNETES_KUBECONFIG_PATH;
// Validate required environment variables
if (!context || !kubeconfigPath) {
throw new Error(
"E2E_KUBERNETES_CONTEXT and E2E_KUBERNETES_KUBECONFIG_PATH environment variables are not set",
);
}
const context = process.env.E2E_KUBERNETES_CONTEXT ?? "";
const kubeconfigPath = process.env.E2E_KUBERNETES_KUBECONFIG_PATH ?? "";
// Setup before each test
test.beforeEach(async ({ page }) => {
test.skip(
!context || !kubeconfigPath,
"Kubernetes E2E env vars are not set",
);
providersPage = new ProvidersPage(page);
// Clean up existing provider to ensure clean test state
await deleteProviderIfExists(providersPage, context);
await deleteProviderIfExists(providersPage, context!);
});
// Use admin authentication for provider management
@@ -656,18 +707,13 @@ test.describe("Add Provider", () => {
let scansPage: ScansPage;
// Test data from environment variables
const projectId = process.env.E2E_GCP_PROJECT_ID;
// Validate required environment variables
if (!projectId) {
throw new Error("E2E_GCP_PROJECT_ID environment variable is not set");
}
const projectId = process.env.E2E_GCP_PROJECT_ID ?? "";
// Setup before each test
test.beforeEach(async ({ page }) => {
test.skip(!projectId, "E2E_GCP_PROJECT_ID is not set");
providersPage = new ProvidersPage(page);
// Clean up existing provider to ensure clean test state
await deleteProviderIfExists(providersPage, projectId);
await deleteProviderIfExists(providersPage, projectId!);
});
// Use admin authentication for provider management
@@ -688,7 +734,7 @@ test.describe("Add Provider", () => {
async ({ page }) => {
// Validate required environment variables
const serviceAccountKeyB64 =
process.env.E2E_GCP_BASE64_SERVICE_ACCOUNT_KEY;
process.env.E2E_GCP_BASE64_SERVICE_ACCOUNT_KEY ?? "";
// Verify service account key is base64 encoded
if (!serviceAccountKeyB64) {
@@ -767,19 +813,13 @@ test.describe("Add Provider", () => {
let scansPage: ScansPage;
test.describe("Add GitHub provider with username", () => {
// Test data from environment variables
const username = process.env.E2E_GITHUB_USERNAME;
// Validate required environment variables
if (!username) {
throw new Error("E2E_GITHUB_USERNAME environment variable is not set");
}
const username = process.env.E2E_GITHUB_USERNAME ?? "";
// Setup before each test
test.beforeEach(async ({ page }) => {
test.skip(!username, "E2E_GITHUB_USERNAME is not set");
providersPage = new ProvidersPage(page);
// Clean up existing provider to ensure clean test state
await deleteProviderIfExists(providersPage, username);
await deleteProviderIfExists(providersPage, username!);
});
// Use admin authentication for provider management
@@ -800,7 +840,7 @@ test.describe("Add Provider", () => {
async ({ page }) => {
// Validate required environment variables
const personalAccessToken =
process.env.E2E_GITHUB_PERSONAL_ACCESS_TOKEN;
process.env.E2E_GITHUB_PERSONAL_ACCESS_TOKEN ?? "";
// Verify username and personal access token are set in environment variables
if (!personalAccessToken) {
@@ -876,10 +916,9 @@ test.describe("Add Provider", () => {
},
async ({ page }) => {
// Validate required environment variables
const githubAppId =
process.env.E2E_GITHUB_APP_ID;
const githubAppId = process.env.E2E_GITHUB_APP_ID ?? "";
const githubAppPrivateKeyB64 =
process.env.E2E_GITHUB_BASE64_APP_PRIVATE_KEY;
process.env.E2E_GITHUB_BASE64_APP_PRIVATE_KEY ?? "";
// Verify github app id and private key are set in environment variables
if (!githubAppId || !githubAppPrivateKeyB64) {
@@ -930,9 +969,7 @@ test.describe("Add Provider", () => {
await providersPage.verifyGitHubAppPageLoaded();
// Fill static github app credentials details
await providersPage.fillGitHubAppCredentials(
githubCredentials,
);
await providersPage.fillGitHubAppCredentials(githubCredentials);
await providersPage.clickNext();
// Launch scan
@@ -949,21 +986,13 @@ test.describe("Add Provider", () => {
);
});
test.describe("Add GitHub provider with organization", () => {
// Test data from environment variables
const organization = process.env.E2E_GITHUB_ORGANIZATION;
// Validate required environment variables
if (!organization) {
throw new Error(
"E2E_GITHUB_ORGANIZATION environment variable is not set",
);
}
const organization = process.env.E2E_GITHUB_ORGANIZATION ?? "";
// Setup before each test
test.beforeEach(async ({ page }) => {
test.skip(!organization, "E2E_GITHUB_ORGANIZATION is not set");
providersPage = new ProvidersPage(page);
// Clean up existing provider to ensure clean test state
await deleteProviderIfExists(providersPage, organization);
await deleteProviderIfExists(providersPage, organization!);
});
// Use admin authentication for provider management
@@ -983,7 +1012,7 @@ test.describe("Add Provider", () => {
async ({ page }) => {
// Validate required environment variables
const organizationAccessToken =
process.env.E2E_GITHUB_ORGANIZATION_ACCESS_TOKEN;
process.env.E2E_GITHUB_ORGANIZATION_ACCESS_TOKEN ?? "";
// Verify username and personal access token are set in environment variables
if (!organizationAccessToken) {
@@ -1054,24 +1083,20 @@ test.describe("Add Provider", () => {
let scansPage: ScansPage;
// Test data from environment variables
const tenancyId = process.env.E2E_OCI_TENANCY_ID;
const userId = process.env.E2E_OCI_USER_ID;
const fingerprint = process.env.E2E_OCI_FINGERPRINT;
const keyContent = process.env.E2E_OCI_KEY_CONTENT;
const region = process.env.E2E_OCI_REGION;
// Validate required environment variables
if (!tenancyId || !userId || !fingerprint || !keyContent || !region) {
throw new Error(
"E2E_OCI_TENANCY_ID, E2E_OCI_USER_ID, E2E_OCI_FINGERPRINT, E2E_OCI_KEY_CONTENT, and E2E_OCI_REGION environment variables are not set",
);
}
const tenancyId = process.env.E2E_OCI_TENANCY_ID ?? "";
const userId = process.env.E2E_OCI_USER_ID ?? "";
const fingerprint = process.env.E2E_OCI_FINGERPRINT ?? "";
const keyContent = process.env.E2E_OCI_KEY_CONTENT ?? "";
const region = process.env.E2E_OCI_REGION ?? "";
// Setup before each test
test.beforeEach(async ({ page }) => {
test.skip(
!tenancyId || !userId || !fingerprint || !keyContent || !region,
"OCI E2E env vars are not set",
);
providersPage = new ProvidersPage(page);
// Clean up existing provider to ensure clean test state
await deleteProviderIfExists(providersPage, tenancyId);
await deleteProviderIfExists(providersPage, tenancyId!);
});
// Use admin authentication for provider management
@@ -1148,23 +1173,17 @@ test.describe("Add Provider", () => {
let scansPage: ScansPage;
// Test data from environment variables
const accountId = process.env.E2E_ALIBABACLOUD_ACCOUNT_ID;
const accessKeyId = process.env.E2E_ALIBABACLOUD_ACCESS_KEY_ID;
const accessKeySecret = process.env.E2E_ALIBABACLOUD_ACCESS_KEY_SECRET;
const roleArn = process.env.E2E_ALIBABACLOUD_ROLE_ARN;
// Validate required environment variable for beforeEach
if (!accountId) {
throw new Error(
"E2E_ALIBABACLOUD_ACCOUNT_ID environment variable is not set",
);
}
const accountId = process.env.E2E_ALIBABACLOUD_ACCOUNT_ID ?? "";
const accessKeyId = process.env.E2E_ALIBABACLOUD_ACCESS_KEY_ID ?? "";
const accessKeySecret =
process.env.E2E_ALIBABACLOUD_ACCESS_KEY_SECRET ?? "";
const roleArn = process.env.E2E_ALIBABACLOUD_ROLE_ARN ?? "";
// Setup before each test
test.beforeEach(async ({ page }) => {
test.skip(!accountId, "E2E_ALIBABACLOUD_ACCOUNT_ID is not set");
providersPage = new ProvidersPage(page);
// Clean up existing provider to ensure clean test state
await deleteProviderIfExists(providersPage, accountId);
await deleteProviderIfExists(providersPage, accountId!);
});
// Use admin authentication for provider management
@@ -1232,7 +1251,9 @@ test.describe("Add Provider", () => {
await providersPage.verifyAlibabaCloudStaticCredentialsPageLoaded();
// Fill static credentials
await providersPage.fillAlibabaCloudStaticCredentials(staticCredentials);
await providersPage.fillAlibabaCloudStaticCredentials(
staticCredentials,
);
await providersPage.clickNext();
// Launch scan
@@ -1334,21 +1355,18 @@ test.describe("Update Provider Credentials", () => {
let providersPage: ProvidersPage;
// Test data from environment variables (same as add OCI provider test)
const tenancyId = process.env.E2E_OCI_TENANCY_ID;
const userId = process.env.E2E_OCI_USER_ID;
const fingerprint = process.env.E2E_OCI_FINGERPRINT;
const keyContent = process.env.E2E_OCI_KEY_CONTENT;
const region = process.env.E2E_OCI_REGION;
// Validate required environment variables
if (!tenancyId || !userId || !fingerprint || !keyContent || !region) {
throw new Error(
"E2E_OCI_TENANCY_ID, E2E_OCI_USER_ID, E2E_OCI_FINGERPRINT, E2E_OCI_KEY_CONTENT, and E2E_OCI_REGION environment variables are not set",
);
}
const tenancyId = process.env.E2E_OCI_TENANCY_ID ?? "";
const userId = process.env.E2E_OCI_USER_ID ?? "";
const fingerprint = process.env.E2E_OCI_FINGERPRINT ?? "";
const keyContent = process.env.E2E_OCI_KEY_CONTENT ?? "";
const region = process.env.E2E_OCI_REGION ?? "";
// Setup before each test
test.beforeEach(async ({ page }) => {
test.skip(
!tenancyId || !userId || !fingerprint || !keyContent || !region,
"OCI E2E env vars are not set",
);
providersPage = new ProvidersPage(page);
});
@@ -1358,13 +1376,7 @@ test.describe("Update Provider Credentials", () => {
test(
"should update OCI provider credentials successfully",
{
tag: [
"@e2e",
"@providers",
"@oci",
"@serial",
"@PROVIDER-E2E-013",
],
tag: ["@e2e", "@providers", "@oci", "@serial", "@PROVIDER-E2E-013"],
},
async () => {
// Prepare updated credentials

View File

@@ -133,9 +133,7 @@ export class SignUpPage extends BasePage {
}
async verifyRedirectToLogin(): Promise<void> {
// Verify redirect to login page
await expect(this.page).toHaveURL("/sign-in");
await expect(this.page).toHaveURL(/\/sign-in/);
}
async verifyRedirectToEmailVerification(): Promise<void> {

View File

@@ -0,0 +1,48 @@
import { describe, expect, it } from "vitest";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
import { addCredentialsRoleFormSchema } from "./formSchemas";
const BASE_AWS_ROLE_VALUES = {
[ProviderCredentialFields.PROVIDER_ID]: "provider-123",
[ProviderCredentialFields.PROVIDER_TYPE]: "aws",
[ProviderCredentialFields.ROLE_ARN]:
"arn:aws:iam::123456789012:role/ProwlerRole",
[ProviderCredentialFields.EXTERNAL_ID]: "tenant-123",
[ProviderCredentialFields.CREDENTIALS_TYPE]: "access-secret-key",
} as const;
describe("addCredentialsRoleFormSchema", () => {
it("accepts AWS role credentials when access and secret keys are present", () => {
const schema = addCredentialsRoleFormSchema("aws");
const result = schema.safeParse({
...BASE_AWS_ROLE_VALUES,
[ProviderCredentialFields.AWS_ACCESS_KEY_ID]: "AKIA1234567890EXAMPLE",
[ProviderCredentialFields.AWS_SECRET_ACCESS_KEY]:
"test/secret+access=key1234567890",
});
expect(result.success).toBe(true);
});
it("reports missing AWS secret access key on aws_secret_access_key field", () => {
const schema = addCredentialsRoleFormSchema("aws");
const result = schema.safeParse({
...BASE_AWS_ROLE_VALUES,
[ProviderCredentialFields.AWS_ACCESS_KEY_ID]: "AKIA1234567890EXAMPLE",
[ProviderCredentialFields.AWS_SECRET_ACCESS_KEY]: "",
});
expect(result.success).toBe(false);
if (result.success) return;
expect(result.error.issues).toContainEqual(
expect.objectContaining({
path: [ProviderCredentialFields.AWS_SECRET_ACCESS_KEY],
}),
);
});
});

View File

@@ -420,17 +420,37 @@ export const addCredentialsRoleFormSchema = (providerType: string) =>
[ProviderCredentialFields.ROLE_SESSION_NAME]: z.string().optional(),
[ProviderCredentialFields.CREDENTIALS_TYPE]: z.string().optional(),
})
.refine(
(data) =>
.superRefine((data, ctx) => {
if (
data[ProviderCredentialFields.CREDENTIALS_TYPE] !==
"access-secret-key" ||
(data[ProviderCredentialFields.AWS_ACCESS_KEY_ID] &&
data[ProviderCredentialFields.AWS_SECRET_ACCESS_KEY]),
{
message: "AWS Access Key ID and Secret Access Key are required.",
path: [ProviderCredentialFields.AWS_ACCESS_KEY_ID],
},
)
"access-secret-key"
) {
return;
}
const hasAccessKey =
(data[ProviderCredentialFields.AWS_ACCESS_KEY_ID] || "").trim()
.length > 0;
const hasSecretAccessKey =
(data[ProviderCredentialFields.AWS_SECRET_ACCESS_KEY] || "").trim()
.length > 0;
if (!hasAccessKey) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "AWS Access Key ID is required.",
path: [ProviderCredentialFields.AWS_ACCESS_KEY_ID],
});
}
if (!hasSecretAccessKey) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "AWS Secret Access Key is required.",
path: [ProviderCredentialFields.AWS_SECRET_ACCESS_KEY],
});
}
})
: providerType === "alibabacloud"
? z.object({
[ProviderCredentialFields.PROVIDER_ID]: z.string(),