test(ui): Add e2e test for OCI Provider (#9347)

Co-authored-by: Alan Buscaglia <gentlemanprogramming@gmail.com>
This commit is contained in:
StylusFrost
2025-12-01 16:13:12 +01:00
committed by GitHub
parent f9e1e29631
commit 56ea498cca
18 changed files with 2441 additions and 254 deletions

View File

@@ -10,6 +10,7 @@ on:
- 'ui/**'
jobs:
e2e-tests:
if: github.repository == 'prowler-cloud/prowler'
runs-on: ubuntu-latest
@@ -33,12 +34,50 @@ jobs:
E2E_M365_SECRET_ID: ${{ secrets.E2E_M365_SECRET_ID }}
E2E_M365_TENANT_ID: ${{ secrets.E2E_M365_TENANT_ID }}
E2E_M365_CERTIFICATE_CONTENT: ${{ secrets.E2E_M365_CERTIFICATE_CONTENT }}
E2E_NEW_PASSWORD: ${{ secrets.E2E_NEW_PASSWORD }}
E2E_KUBERNETES_CONTEXT: 'kind-kind'
E2E_KUBERNETES_KUBECONFIG_PATH: /home/runner/.kube/config
E2E_GCP_BASE64_SERVICE_ACCOUNT_KEY: ${{ secrets.E2E_GCP_BASE64_SERVICE_ACCOUNT_KEY }}
E2E_GCP_PROJECT_ID: ${{ secrets.E2E_GCP_PROJECT_ID }}
E2E_GITHUB_APP_ID: ${{ secrets.E2E_GITHUB_APP_ID }}
E2E_GITHUB_BASE64_APP_PRIVATE_KEY: ${{ secrets.E2E_GITHUB_BASE64_APP_PRIVATE_KEY }}
E2E_GITHUB_USERNAME: ${{ secrets.E2E_GITHUB_USERNAME }}
E2E_GITHUB_PERSONAL_ACCESS_TOKEN: ${{ secrets.E2E_GITHUB_PERSONAL_ACCESS_TOKEN }}
E2E_GITHUB_ORGANIZATION: ${{ secrets.E2E_GITHUB_ORGANIZATION }}
E2E_GITHUB_ORGANIZATION_ACCESS_TOKEN: ${{ secrets.E2E_GITHUB_ORGANIZATION_ACCESS_TOKEN }}
E2E_ORGANIZATION_ID: ${{ secrets.E2E_ORGANIZATION_ID }}
E2E_OCI_TENANCY_ID: ${{ secrets.E2E_OCI_TENANCY_ID }}
E2E_OCI_USER_ID: ${{ secrets.E2E_OCI_USER_ID }}
E2E_OCI_FINGERPRINT: ${{ secrets.E2E_OCI_FINGERPRINT }}
E2E_OCI_KEY_CONTENT: ${{ secrets.E2E_OCI_KEY_CONTENT }}
E2E_OCI_REGION: ${{ secrets.E2E_OCI_REGION }}
E2E_NEW_USER_PASSWORD: ${{ secrets.E2E_NEW_USER_PASSWORD }}
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Create k8s Kind Cluster
uses: helm/kind-action@v1
with:
cluster_name: kind
- name: Modify kubeconfig
run: |
# Modify the kubeconfig to use the kind cluster server to https://kind-control-plane:6443
# from worker service into docker-compose.yml
kubectl config set-cluster kind-kind --server=https://kind-control-plane:6443
kubectl config view
- name: Add network kind to docker compose
run: |
# Add the network kind to the docker compose to interconnect to kind cluster
yq -i '.networks.kind.external = true' docker-compose.yml
# Add network kind to worker service and default network too
yq -i '.services.worker.networks = ["kind","default"]' docker-compose.yml
- name: Fix API data directory permissions
run: docker run --rm -v $(pwd)/_data/api:/data alpine chown -R 1000:1000 /data
- name: Add AWS credentials for testing AWS SDK Default Adding Provider
run: |
echo "Adding AWS credentials for testing AWS SDK Default Adding Provider..."
echo "AWS_ACCESS_KEY_ID=${{ secrets.E2E_AWS_PROVIDER_ACCESS_KEY }}" >> .env
echo "AWS_SECRET_ACCESS_KEY=${{ secrets.E2E_AWS_PROVIDER_SECRET_KEY }}" >> .env
- name: Start API services
run: |
# Override docker-compose image tag to use latest instead of stable

View File

@@ -15,10 +15,10 @@
"format:check": "./node_modules/.bin/prettier --check ./app",
"format:write": "./node_modules/.bin/prettier --config .prettierrc.json --write ./app",
"prepare": "husky",
"test:e2e": "playwright test --project=chromium --project=sign-up --project=providers",
"test:e2e:ui": "playwright test --project=chromium --project=sign-up --project=providers --ui",
"test:e2e:debug": "playwright test --project=chromium --project=sign-up --project=providers --debug",
"test:e2e:headed": "playwright test --project=chromium --project=sign-up --project=providers --headed",
"test:e2e": "playwright test --project=chromium --project=sign-up --project=providers --project=invitations --project=scans",
"test:e2e:ui": "playwright test --project=chromium --project=sign-up --project=providers --project=invitations --project=scans --ui",
"test:e2e:debug": "playwright test --project=chromium --project=sign-up --project=providers --project=invitations --project=scans --debug",
"test:e2e:headed": "playwright test --project=chromium --project=sign-up --project=providers --project=invitations --project=scans --headed",
"test:e2e:report": "playwright show-report",
"test:e2e:install": "playwright install"
},

View File

@@ -98,12 +98,24 @@ export default defineConfig({
name: "sign-up",
testMatch: "sign-up.spec.ts",
},
// This project runs the scans test suite
{
name: "scans",
testMatch: "scans.spec.ts",
dependencies: ["admin.auth.setup"],
},
// This project runs the providers test suite
{
name: "providers",
testMatch: "providers.spec.ts",
dependencies: ["admin.auth.setup"],
},
// This project runs the invitations test suite
{
name: "invitations",
testMatch: "invitations.spec.ts",
dependencies: ["admin.auth.setup"],
},
],
webServer: {

View File

@@ -24,21 +24,14 @@ export abstract class BasePage {
// Common navigation methods
async goto(url: string): Promise<void> {
await this.page.goto(url);
await this.waitForPageLoad();
}
async waitForPageLoad(): Promise<void> {
await this.page.waitForLoadState("networkidle");
}
async refresh(): Promise<void> {
await this.page.reload();
await this.waitForPageLoad();
}
async goBack(): Promise<void> {
await this.page.goBack();
await this.waitForPageLoad();
}
// Common verification methods
@@ -137,23 +130,28 @@ export abstract class BasePage {
// Common wait methods
async waitForElement(element: Locator, timeout: number = 5000): Promise<void> {
await element.waitFor({ timeout });
}
async waitForElementToDisappear(element: Locator, timeout: number = 5000): Promise<void> {
await element.waitFor({ state: "hidden", timeout });
}
async waitForUrl(expectedUrl: string | RegExp, timeout: number = 5000): Promise<void> {
await this.page.waitForURL(expectedUrl, { timeout });
}
// Common screenshot methods
async takeScreenshot(name: string): Promise<void> {
await this.page.screenshot({ path: `screenshots/${name}.png` });
}
async takeElementScreenshot(element: Locator, name: string): Promise<void> {
await element.screenshot({ path: `screenshots/${name}.png` });
}
}

View File

@@ -1,5 +1,7 @@
import { Page, expect } from "@playwright/test";
import { Locator, Page, expect } from "@playwright/test";
import { SignInPage, SignInCredentials } from "./sign-in/sign-in-page";
import { AWSProviderCredential, AWSProviderData, AWS_CREDENTIAL_OPTIONS, ProvidersPage } from "./providers/providers-page";
import { ScansPage } from "./scans/scans-page";
export const ERROR_MESSAGES = {
INVALID_CREDENTIALS: "Invalid email or password",
@@ -191,3 +193,187 @@ export async function verifySessionValid(page: Page) {
expect(session.refreshToken).toBeTruthy();
return session;
}
export async function addAWSProvider(
page: Page,
accountId: string,
accessKey: string,
secretKey: string,
): Promise<void> {
// Prepare test data for AWS provider
const awsProviderData: AWSProviderData = {
accountId: accountId,
alias: "Test E2E AWS Account - Credentials",
};
// Prepare static credentials
const staticCredentials: AWSProviderCredential = {
type: AWS_CREDENTIAL_OPTIONS.AWS_CREDENTIALS,
accessKeyId: accessKey,
secretAccessKey: secretKey,
};
// Create providers page object
const providersPage = new ProvidersPage(page);
// Navigate to providers page
await providersPage.goto();
await providersPage.verifyPageLoaded();
// Start adding new provider
await providersPage.clickAddProvider();
await providersPage.verifyConnectAccountPageLoaded();
// Select AWS provider
await providersPage.selectAWSProvider();
// Fill provider details
await providersPage.fillAWSProviderDetails(awsProviderData);
await providersPage.clickNext();
// Verify credentials page is loaded
await providersPage.verifyCredentialsPageLoaded();
// Select static credentials type
await providersPage.selectCredentialsType(
AWS_CREDENTIAL_OPTIONS.AWS_CREDENTIALS,
);
// Fill static credentials
await providersPage.fillStaticCredentials(staticCredentials);
await providersPage.clickNext();
// Launch scan
await providersPage.verifyLaunchScanPageLoaded();
await providersPage.clickNext();
// Wait for redirect to provider page
const scansPage = new ScansPage(page);
await scansPage.verifyPageLoaded();
}
export async function deleteProviderIfExists(page: ProvidersPage, providerUID: string): Promise<void> {
// Delete the provider if it exists
// Navigate to providers page
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();
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)) {
return row;
}
}
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();
if (!targetRow) {
// Provider not found, nothing to delete
// Navigate back to providers page to ensure clean state
await page.goto();
await expect(page.providersTable).toBeVisible({ timeout: 10000 });
return;
}
// Find and click the action button (last cell = actions column)
const actionButton = targetRow
.locator("td")
.last()
.locator("button")
.first();
// Ensure the button is in view before clicking (handles horizontal scroll)
await actionButton.scrollIntoViewIfNeeded();
// Verify the button is visible
await expect(actionButton).toBeVisible({ timeout: 5000 });
await actionButton.click();
// Wait for dropdown menu to appear and find delete option
const deleteMenuItem = page.page.getByRole("menuitem", {
name: /delete.*provider/i,
});
await expect(deleteMenuItem).toBeVisible({ timeout: 5000 });
await deleteMenuItem.click();
// Wait for confirmation modal to appear
const modal = page.page
.locator('[role="dialog"], .modal, [data-testid*="modal"]')
.first();
await expect(modal).toBeVisible({ timeout: 10000 });
// Find and click the delete confirmation button
await expect(page.deleteProviderConfirmationButton).toBeVisible({
timeout: 5000,
});
await page.deleteProviderConfirmationButton.click();
// Wait for modal to close (this indicates deletion was initiated)
await expect(modal).not.toBeVisible({ timeout: 10000 });
// Navigate back to providers page to ensure clean state
await page.goto();
await expect(page.providersTable).toBeVisible({ timeout: 10000 });
}

View File

@@ -0,0 +1,117 @@
import { Page, Locator, expect } from "@playwright/test";
import { BasePage } from "../base-page";
export class InvitationsPage extends BasePage {
// Page heading
readonly pageHeadingSendInvitation: Locator;
readonly pageHeadingInvitations: Locator;
// UI elements
readonly sendInviteButton: Locator;
readonly inviteButton: Locator;
readonly emailInput: Locator;
readonly roleSelect: Locator;
// Invitation details
readonly reviewInvitationDetailsButton: Locator;
readonly shareUrl: Locator;
constructor(page: Page) {
super(page);
// Page heading
this.pageHeadingInvitations = page.getByRole("heading", { name: "Invitations" });
this.pageHeadingSendInvitation = page.getByRole("heading", { name: "Send Invitation" });
// Button to invite a new user
this.inviteButton = page.getByRole("link", { name: "Send Invitation", exact: true });
this.sendInviteButton = page.getByRole("button", { name: "Send Invitation", exact: true });
// Form inputs
this.emailInput = page.getByRole("textbox", { name: "Email" });
// Form select
this.roleSelect = page.getByRole("button", { name: /Role|Select a role/i });
// Form details
this.reviewInvitationDetailsButton = page.getByRole('button', { name: /Review Invitation Details/i });
// Multiple strategies to find the share URL
this.shareUrl = page.locator('a[href*="/sign-up?invitation_token="], [data-testid="share-url"], .share-url, code, pre').first();
}
async goto(): Promise<void> {
// Navigate to the invitations page
await super.goto("/invitations");
}
async clickSendInviteButton(): Promise<void> {
// Click the send invitation button
await this.sendInviteButton.click();
}
async clickInviteButton(): Promise<void> {
// Click the invitation button
await this.inviteButton.click();
}
async verifyPageLoaded(): Promise<void> {
// Verify the invitations page is loaded
await expect(this.pageHeadingInvitations).toBeVisible();
}
async verifyInvitePageLoaded(): Promise<void> {
// Verify the invite page is loaded
await expect(this.emailInput).toBeVisible();
await expect(this.sendInviteButton).toBeVisible();
}
async fillEmail(email: string): Promise<void> {
// Fill the email input
await this.emailInput.fill(email);
}
async selectRole(role: string): Promise<void> {
// Select the role option
// Open the role dropdown
await this.roleSelect.click();
// Prefer ARIA role option inside listbox
const option = this.page.getByRole("option", { name: new RegExp(`^${role}$`, "i") });
if (await option.count()) {
await option.first().click();
} else {
throw new Error(`Role option ${role} not found`);
}
// Ensure the combobox now shows the chosen value
await expect(this.roleSelect).toContainText(new RegExp(role, "i"));
}
async verifyInviteDataPageLoaded(): Promise<void> {
// Verify the invite data page is loaded
await expect(this.reviewInvitationDetailsButton).toBeVisible();
}
async getShareUrl(): Promise<string> {
// Get the share url
// Get the share url text content
const text = await this.shareUrl.textContent();
if (!text) {
throw new Error("Share url not found");
}
return text;
}
}

View File

@@ -0,0 +1,66 @@
### E2E Tests: Invitations Management
**Suite ID:** `INVITATION-E2E`
**Feature:** User Invitations.
---
## Test Case: `INVITATION-E2E-001` - Invite New User and Complete Sign-Up
**Priority:** `critical`
**Tags:**
- type → @e2e
- feature → @invitations
- id → @INVITATION-E2E-001
**Description/Objective:** Validates the full flow to invite a new user from the admin session, consume the invitation link, sign up as the invited user, authenticate, and verify the user is associated to the expected organization.
**Preconditions:**
- Admin authentication state available: `playwright/.auth/admin_user.json` (admin.auth.setup)
- Environment variables configured:
- `E2E_NEW_USER_PASSWORD` (password for the invited user)
- `E2E_ORGANIZATION_ID` (expected organization for membership verification)
- Application running with accessible UI/API endpoints
### Flow Steps:
1. Navigate to invitations page
2. Click "Send Invitation" button
3. Fill unique email address for the invite
4. Select role `e2e_admin`
5. Click "Send Invitation" to generate invitation
6. Read the generated share URL from the invitation details
7. Open a new browser context (no admin cookies) and navigate to the share URL
8. Complete sign-up with provided password and accept terms
9. Verify sign-up success (no errors) and redirect to login page
10. Log in with the newly created credentials in the new context
11. Verify successful login
12. Navigate to user profile and verify `organizationId` matches `E2E_ORGANIZATION_ID`
### Expected Result:
- Invitation is created and a valid share URL is provided
- Invited user can sign up successfully using the invitation link
- User is redirected to the login page after sign-up (OSS flow)
- Login succeeds with the new credentials
- User profile shows membership in the expected organization
### Key verification points:
- Invitations page loads and displays the heading
- Send Invitation form is visible (email + role select)
- Invitation details page shows share URL
- Sign-up page loads from invitation link and submits without errors
- Post sign-up, redirect to login is performed
- Login with the new account succeeds
- Profile page shows the expected organization id
### Notes:
- Test uses a fresh browser context for the invitee to avoid admin session leakage
- Email should be unique per run (the test uses a random suffix)
- Ensure `E2E_NEW_USER_PASSWORD` and `E2E_ORGANIZATION_ID` are set before execution
- The role `e2e_admin` must be available in the environment

View File

@@ -0,0 +1,105 @@
import { test } from "@playwright/test";
import { InvitationsPage } from "./invitations-page";
import { makeSuffix } from "../helpers";
import { SignUpPage } from "../sign-up/sign-up-page";
import { SignInPage } from "../sign-in/sign-in-page";
import { UserProfilePage } from "../profile/profile-page";
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",
{
tag: ["@critical", "@e2e", "@invitations", "@INVITATION-E2E-001"],
},
async ({ page, browser }) => {
// 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);
// 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 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,
password: password,
confirmPassword: password,
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({
email: uniqueEmail,
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
await userProfilePage.verifyOrganizationId(organizationId);
// Close the invite context
await inviteContext.close();
},
);
});

View File

@@ -0,0 +1,28 @@
import { Page, Locator, expect } from "@playwright/test";
import { BasePage } from "../base-page";
export class UserProfilePage extends BasePage {
// Page heading
readonly pageHeadingUserProfile: Locator;
constructor(page: Page) {
super(page);
// Page heading
this.pageHeadingUserProfile = page.getByRole("heading", {
name: "User Profile",
});
}
async goto(): Promise<void> {
// Navigate to the user profile page
await super.goto("/profile");
}
async verifyOrganizationId(organizationId: string): Promise<void> {
// Verify the organization ID is visible
await expect(this.page.getByText(organizationId)).toBeVisible();
}
}

View File

@@ -5,10 +5,6 @@ import { BasePage } from "../base-page";
export interface AWSProviderData {
accountId: string;
alias?: string;
roleArn?: string;
externalId?: string;
accessKeyId?: string;
secretAccessKey?: string;
}
// AZURE provider data
@@ -23,10 +19,35 @@ export interface M365ProviderData {
alias?: string;
}
// Kubernetes provider data
export interface KubernetesProviderData {
context: string;
alias?: string;
}
// GCP provider data
export interface GCPProviderData {
projectId: string;
alias?: string;
}
// GitHub provider data
export interface GitHubProviderData {
username: string;
alias?: string;
}
// OCI provider data
export interface OCIProviderData {
tenancyId: string;
alias?: string;
}
// AWS credential options
export const AWS_CREDENTIAL_OPTIONS = {
AWS_ROLE_ARN: "role",
AWS_CREDENTIALS: "credentials",
AWS_SDK_DEFAULT: "aws-sdk-default",
} as const;
// AWS credential type
@@ -78,8 +99,79 @@ export interface M365ProviderCredential {
certificateContent?: string;
}
// Kubernetes credential options
export const KUBERNETES_CREDENTIAL_OPTIONS = {
KUBECONFIG_CONTENT: "kubeconfig",
} as const;
// Kubernetes credential type
type KubernetesCredentialType =
(typeof KUBERNETES_CREDENTIAL_OPTIONS)[keyof typeof KUBERNETES_CREDENTIAL_OPTIONS];
// Kubernetes provider credential
export interface KubernetesProviderCredential {
type: KubernetesCredentialType;
kubeconfigContent: string;
}
// GCP credential options
export const GCP_CREDENTIAL_OPTIONS = {
GCP_SERVICE_ACCOUNT: "service_account",
} as const;
// GCP credential type
type GCPCredentialType =
(typeof GCP_CREDENTIAL_OPTIONS)[keyof typeof GCP_CREDENTIAL_OPTIONS];
// GCP provider credential
export interface GCPProviderCredential {
type: GCPCredentialType;
serviceAccountKey: string;
}
// GitHub credential options
export const GITHUB_CREDENTIAL_OPTIONS = {
GITHUB_PERSONAL_ACCESS_TOKEN: "personal_access_token",
GITHUB_ORGANIZATION_ACCESS_TOKEN: "organization_access_token",
GITHUB_APP: "github_app",
} as const;
// GitHub credential type
type GitHubCredentialType =
(typeof GITHUB_CREDENTIAL_OPTIONS)[keyof typeof GITHUB_CREDENTIAL_OPTIONS];
// GitHub provider personal access token credential
export interface GitHubProviderCredential {
type: GitHubCredentialType;
personalAccessToken?: string;
githubAppId?: string;
githubAppPrivateKey?: string;
}
// OCI credential options
export const OCI_CREDENTIAL_OPTIONS = {
OCI_API_KEY: "api_key",
} as const;
// OCI credential type
type OCICredentialType =
(typeof OCI_CREDENTIAL_OPTIONS)[keyof typeof OCI_CREDENTIAL_OPTIONS];
// OCI provider credential
export interface OCIProviderCredential {
type: OCICredentialType;
tenancyId: string;
userId?: string;
fingerprint?: string;
keyContent?: string;
region?: string;
}
// Providers page
export class ProvidersPage extends BasePage {
// Alias input
readonly aliasInput: Locator;
// Button to add a new cloud provider
readonly addProviderButton: Locator;
readonly providersTable: Locator;
@@ -91,10 +183,10 @@ export class ProvidersPage extends BasePage {
readonly m365ProviderRadio: Locator;
readonly kubernetesProviderRadio: Locator;
readonly githubProviderRadio: Locator;
readonly ociProviderRadio: Locator;
// AWS provider form elements
readonly accountIdInput: Locator;
readonly aliasInput: Locator;
readonly nextButton: Locator;
readonly backButton: Locator;
readonly saveButton: Locator;
@@ -108,6 +200,10 @@ export class ProvidersPage extends BasePage {
readonly m365StaticCredentialsRadio: Locator;
readonly m365CertificateCredentialsRadio: Locator;
// GitHub credentials type selection
readonly githubPersonalAccessTokenRadio: Locator;
readonly githubAppCredentialsRadio: Locator;
// AWS role credentials form
readonly roleArnInput: Locator;
readonly externalIdInput: Locator;
@@ -129,14 +225,38 @@ export class ProvidersPage extends BasePage {
readonly m365TenantIdInput: Locator;
readonly m365CertificateContentInput: Locator;
// Kubernetes provider form elements
readonly kubernetesContextInput: Locator;
readonly kubernetesKubeconfigContentInput: Locator;
// GCP provider form elements
readonly gcpProjectIdInput: Locator;
readonly gcpServiceAccountKeyInput: Locator;
readonly gcpServiceAccountRadio: Locator;
// GitHub provider form elements
readonly githubUsernameInput: Locator;
readonly githubAppIdInput: Locator;
readonly githubAppPrivateKeyInput: Locator;
readonly githubPersonalAccessTokenInput: Locator;
// OCI provider form elements
readonly ociTenancyIdInput: Locator;
readonly ociUserIdInput: Locator;
readonly ociFingerprintInput: Locator;
readonly ociKeyContentInput: Locator;
readonly ociRegionInput: Locator;
// Delete button
readonly deleteProviderConfirmationButton: Locator;
constructor(page: Page) {
super(page);
// Button to add a new cloud provider
this.addProviderButton = page.getByRole("link", {
name: "Add Cloud Provider",
exact: true,
});
// Table displaying existing providers
@@ -146,19 +266,30 @@ export class ProvidersPage extends BasePage {
this.awsProviderRadio = page.getByRole("radio", {
name: /Amazon Web Services/i,
});
// Google Cloud Platform
this.gcpProviderRadio = page.getByRole("radio", {
name: /Google Cloud Platform/i,
});
// Microsoft Azure
this.azureProviderRadio = page.getByRole("radio", {
name: /Microsoft Azure/i,
});
// Microsoft 365
this.m365ProviderRadio = page.getByRole("radio", {
name: /Microsoft 365/i,
});
// Kubernetes
this.kubernetesProviderRadio = page.getByRole("radio", {
name: /Kubernetes/i,
});
this.githubProviderRadio = page.getByRole("radio", { name: /GitHub/i });
// GitHub
this.githubProviderRadio = page.getByRole("radio", {
name: /GitHub/i,
});
// Oracle Cloud Infrastructure
this.ociProviderRadio = page.getByRole("radio", {
name: /Oracle Cloud Infrastructure/i,
});
// AWS provider form inputs
this.accountIdInput = page.getByRole("textbox", { name: "Account ID" });
@@ -184,6 +315,45 @@ export class ProvidersPage extends BasePage {
name: "Certificate Content",
});
// Kubernetes provider form inputs
this.kubernetesContextInput = page.getByRole("textbox", {
name: "Context",
});
this.kubernetesKubeconfigContentInput = page.getByRole("textbox", {
name: "Kubeconfig Content",
});
// GCP provider form inputs
this.gcpProjectIdInput = page.getByRole("textbox", { name: "Project ID" });
this.gcpServiceAccountKeyInput = page.getByRole("textbox", {
name: "Service Account Key",
});
// GitHub provider form inputs
this.githubUsernameInput = page.getByRole("textbox", { name: "Username" });
this.githubPersonalAccessTokenInput = page.getByRole("textbox", {
name: "Personal Access Token",
});
this.githubAppIdInput = page.getByRole("textbox", {
name: "GitHub App ID",
});
this.githubAppPrivateKeyInput = page.getByRole("textbox", {
name: "GitHub App Private Key",
});
// OCI provider form inputs
this.ociTenancyIdInput = page.getByRole("textbox", {
name: /Tenancy OCID/i,
});
this.ociUserIdInput = page.getByRole("textbox", { name: /User OCID/i });
this.ociFingerprintInput = page.getByRole("textbox", {
name: /Fingerprint/i,
});
this.ociKeyContentInput = page.getByRole("textbox", {
name: /Private Key Content/i,
});
this.ociRegionInput = page.getByRole("textbox", { name: /Region/i });
// Alias input
this.aliasInput = page.getByRole("textbox", {
name: "Provider alias (optional)",
@@ -220,6 +390,19 @@ export class ProvidersPage extends BasePage {
name: /App Certificate Credentials/i,
});
// Radios for selecting GCP credentials method
this.gcpServiceAccountRadio = page.getByRole("radio", {
name: /Service Account Key/i,
});
// Radios for selecting GitHub credentials method
this.githubPersonalAccessTokenRadio = page.getByRole("radio", {
name: /Personal Access Token/i,
});
this.githubAppCredentialsRadio = page.getByRole("radio", {
name: /GitHub App/i,
});
// Inputs for IAM Role credentials
this.roleArnInput = page.getByRole("textbox", { name: "Role ARN" });
this.externalIdInput = page.getByRole("textbox", { name: "External ID" });
@@ -245,30 +428,43 @@ export class ProvidersPage extends BasePage {
await super.goto("/providers");
}
private async verifyPageHasProwlerTitle(): Promise<void> {
await expect(this.page).toHaveTitle(/Prowler/);
}
async clickAddProvider(): Promise<void> {
// Click the add provider button
await this.addProviderButton.click();
await this.waitForPageLoad();
}
private async selectProviderRadio(radio: Locator): Promise<void> {
// Force click to handle overlay intercepts
await radio.click({ force: true });
}
async selectAWSProvider(): Promise<void> {
// Prefer label-based click for radios, force if overlay intercepts
await this.awsProviderRadio.click({ force: true });
await this.waitForPageLoad();
await this.selectProviderRadio(this.awsProviderRadio);
}
async selectAZUREProvider(): Promise<void> {
// Prefer label-based click for radios, force if overlay intercepts
await this.azureProviderRadio.click({ force: true });
await this.waitForPageLoad();
await this.selectProviderRadio(this.azureProviderRadio);
}
async selectM365Provider(): Promise<void> {
// Select the M365 provider
await this.selectProviderRadio(this.m365ProviderRadio);
}
await this.m365ProviderRadio.click({ force: true });
await this.waitForPageLoad();
async selectKubernetesProvider(): Promise<void> {
await this.selectProviderRadio(this.kubernetesProviderRadio);
}
async selectGCPProvider(): Promise<void> {
await this.selectProviderRadio(this.gcpProviderRadio);
}
async selectGitHubProvider(): Promise<void> {
await this.selectProviderRadio(this.githubProviderRadio);
}
async fillAWSProviderDetails(data: AWSProviderData): Promise<void> {
@@ -301,6 +497,38 @@ export class ProvidersPage extends BasePage {
}
}
async fillKubernetesProviderDetails(
data: KubernetesProviderData,
): Promise<void> {
// Fill the Kubernetes provider details
await this.kubernetesContextInput.fill(data.context);
if (data.alias) {
await this.aliasInput.fill(data.alias);
}
}
async fillGCPProviderDetails(data: GCPProviderData): Promise<void> {
// Fill the GCP provider details
await this.gcpProjectIdInput.fill(data.projectId);
if (data.alias) {
await this.aliasInput.fill(data.alias);
}
}
async fillGitHubProviderDetails(data: GitHubProviderData): Promise<void> {
// Fill the GitHub provider details
await this.githubUsernameInput.fill(data.username);
if (data.alias) {
await this.aliasInput.fill(data.alias);
}
}
async clickNext(): Promise<void> {
// The wizard interface may use different labels for its primary action button on each step.
// This function determines which button to click depending on the current URL and page content.
@@ -311,23 +539,20 @@ export class ProvidersPage extends BasePage {
// If on the "connect-account" step, click the "Next" button
if (/\/providers\/connect-account/.test(url)) {
await this.nextButton.click();
await this.waitForPageLoad();
return;
}
// If on the "add-credentials" step, check for "Save" and "Next" buttons
if (/\/providers\/add-credentials/.test(url)) {
// Some UI implementations use "Save" instead of "Next" for primary action
const saveBtn = this.saveButton;
if (await saveBtn.count()) {
await saveBtn.click();
await this.waitForPageLoad();
if (await this.saveButton.count()) {
await this.saveButton.click();
return;
}
// If "Save" is not present, try clicking the "Next" button
if (await this.nextButton.count()) {
await this.nextButton.click();
await this.waitForPageLoad();
return;
}
}
@@ -339,45 +564,42 @@ export class ProvidersPage extends BasePage {
.filter({ hasText: "Launch scan" });
await buttonByText.click();
await this.waitForPageLoad();
// Wait for either success (redirect to scans) or error message to appear
// The error container has multiple p.text-text-error elements, we want the first one with the technical error
const errorMessage = this.page.locator("p.text-text-error").first();
const errorMessage = this.page
.locator(
"div.border-border-error, div.bg-red-100, p.text-text-error-primary, p.text-danger",
)
.first();
// Helper to check and throw error if visible
const checkAndThrowError = async (): Promise<void> => {
const isErrorVisible = await errorMessage
.isVisible()
.catch(() => false);
if (isErrorVisible) {
const errorText = await errorMessage.textContent();
throw new Error(
`Test connection failed with error: ${errorText?.trim() || "Unknown error"}`,
);
}
};
try {
// Wait up to 15 seconds for either the error message or redirect
await Promise.race([
// Wait for error message to appear
errorMessage.waitFor({ state: "visible", timeout: 15000 }),
// Wait for redirect to scans page (success case)
this.page.waitForURL(/\/scans/, { timeout: 15000 }),
]);
// If we're still on test-connection page, check for error
if (/\/providers\/test-connection/.test(this.page.url())) {
const isErrorVisible = await errorMessage
.isVisible()
.catch(() => false);
if (isErrorVisible) {
const errorText = await errorMessage.textContent();
throw new Error(
`Test connection failed with error: ${errorText?.trim() || "Unknown error"}`,
);
}
await checkAndThrowError();
}
} catch (error) {
// If timeout or other error, check if error message is present
const isErrorVisible = await errorMessage
.isVisible()
.catch(() => false);
if (isErrorVisible) {
const errorText = await errorMessage.textContent();
throw new Error(
`Test connection failed with error: ${errorText?.trim() || "Unknown error"}`,
);
}
// Re-throw original error if no error message found
await checkAndThrowError();
throw error;
}
@@ -385,23 +607,21 @@ export class ProvidersPage extends BasePage {
}
// Fallback logic: try finding any common primary action buttons in expected order
const candidates = [
const candidates: Array<{ name: string | RegExp }> = [
{ name: "Next" }, // Try the "Next" button
{ name: "Save" }, // Try the "Save" button
{ name: "Launch scan" }, // Try the "Launch scan" button
{ name: /Continue|Proceed/i }, // Try "Continue" or "Proceed" (case-insensitive)
] as const;
];
// Try each candidate name and click it if found
for (const candidate of candidates) {
// Try each candidate name and click it if found
const btn = this.page.getByRole("button", {
name: candidate.name as any,
name: candidate.name,
});
if (await btn.count()) {
await btn.click();
await this.waitForPageLoad();
return;
}
}
@@ -416,6 +636,7 @@ export class ProvidersPage extends BasePage {
// Ensure we are on the add-credentials page where the selector exists
await expect(this.page).toHaveURL(/\/providers\/add-credentials/);
if (type === AWS_CREDENTIAL_OPTIONS.AWS_ROLE_ARN) {
await this.roleCredentialsRadio.click({ force: true });
} else if (type === AWS_CREDENTIAL_OPTIONS.AWS_CREDENTIALS) {
@@ -423,14 +644,13 @@ export class ProvidersPage extends BasePage {
} else {
throw new Error(`Invalid AWS credential type: ${type}`);
}
// Wait for the page to load
await this.waitForPageLoad();
}
async selectM365CredentialsType(type: M365CredentialType): Promise<void> {
// Ensure we are on the add-credentials page where the selector exists
await expect(this.page).toHaveURL(/\/providers\/add-credentials/);
if (type === M365_CREDENTIAL_OPTIONS.M365_CREDENTIALS) {
await this.m365StaticCredentialsRadio.click({ force: true });
} else if (type === M365_CREDENTIAL_OPTIONS.M365_CERTIFICATE_CREDENTIALS) {
@@ -438,8 +658,31 @@ export class ProvidersPage extends BasePage {
} else {
throw new Error(`Invalid M365 credential type: ${type}`);
}
// Wait for the page to load
await this.waitForPageLoad();
}
async selectGCPCredentialsType(type: GCPCredentialType): Promise<void> {
// Ensure we are on the add-credentials page where the selector exists
await expect(this.page).toHaveURL(/\/providers\/add-credentials/);
if (type === GCP_CREDENTIAL_OPTIONS.GCP_SERVICE_ACCOUNT) {
await this.gcpServiceAccountRadio.click({ force: true });
} else {
throw new Error(`Invalid GCP credential type: ${type}`);
}
}
async selectGitHubCredentialsType(type: GitHubCredentialType): Promise<void> {
// Ensure we are on the add-credentials page where the selector exists
await expect(this.page).toHaveURL(/\/providers\/add-credentials/);
if (type === GITHUB_CREDENTIAL_OPTIONS.GITHUB_PERSONAL_ACCESS_TOKEN) {
await this.githubPersonalAccessTokenRadio.click({ force: true });
} else if (type === GITHUB_CREDENTIAL_OPTIONS.GITHUB_APP) {
await this.githubAppCredentialsRadio.click({ force: true });
} else {
throw new Error(`Invalid GitHub credential type: ${type}`);
}
}
async fillRoleCredentials(credentials: AWSProviderCredential): Promise<void> {
@@ -525,32 +768,126 @@ export class ProvidersPage extends BasePage {
}
}
async fillKubernetesCredentials(
credentials: KubernetesProviderCredential,
): Promise<void> {
// Fill the Kubernetes credentials form
if (credentials.kubeconfigContent) {
await this.kubernetesKubeconfigContentInput.fill(
credentials.kubeconfigContent,
);
}
}
async fillGCPServiceAccountKeyCredentials(
credentials: GCPProviderCredential,
): Promise<void> {
// Fill the GCP credentials form
if (credentials.serviceAccountKey) {
await this.gcpServiceAccountKeyInput.fill(credentials.serviceAccountKey);
}
}
async fillGitHubPersonalAccessTokenCredentials(
credentials: GitHubProviderCredential,
): Promise<void> {
// Fill the GitHub personal access token credentials form
if (credentials.personalAccessToken) {
await this.githubPersonalAccessTokenInput.fill(
credentials.personalAccessToken,
);
}
}
async fillGitHubAppCredentials(
credentials: GitHubProviderCredential,
): Promise<void> {
// Fill the GitHub app credentials form
if (credentials.githubAppId) {
await this.githubAppIdInput.fill(credentials.githubAppId);
}
if (credentials.githubAppPrivateKey) {
await this.githubAppPrivateKeyInput.fill(credentials.githubAppPrivateKey);
}
}
async selectOCIProvider(): Promise<void> {
await this.selectProviderRadio(this.ociProviderRadio);
}
async fillOCIProviderDetails(data: OCIProviderData): Promise<void> {
// Fill the OCI provider details
await this.ociTenancyIdInput.fill(data.tenancyId);
if (data.alias) {
await this.aliasInput.fill(data.alias);
}
}
async fillOCICredentials(credentials: OCIProviderCredential): Promise<void> {
// Fill the OCI credentials form
if (credentials.userId) {
await this.ociUserIdInput.fill(credentials.userId);
}
if (credentials.fingerprint) {
await this.ociFingerprintInput.fill(credentials.fingerprint);
}
if (credentials.keyContent) {
await this.ociKeyContentInput.fill(credentials.keyContent);
}
if (credentials.region) {
await this.ociRegionInput.fill(credentials.region);
}
}
async verifyOCICredentialsPageLoaded(): Promise<void> {
// Verify the OCI credentials page is loaded
await this.verifyPageHasProwlerTitle();
await expect(this.ociTenancyIdInput).toBeVisible();
await expect(this.ociUserIdInput).toBeVisible();
await expect(this.ociFingerprintInput).toBeVisible();
await expect(this.ociKeyContentInput).toBeVisible();
await expect(this.ociRegionInput).toBeVisible();
}
async verifyPageLoaded(): Promise<void> {
// Verify the providers page is loaded
await expect(this.page).toHaveTitle(/Prowler/);
await this.verifyPageHasProwlerTitle();
await expect(this.addProviderButton).toBeVisible();
await this.page.waitForLoadState("networkidle");
}
async verifyConnectAccountPageLoaded(): Promise<void> {
// Verify the connect account page is loaded
await expect(this.page).toHaveTitle(/Prowler/);
await this.verifyPageHasProwlerTitle();
await expect(this.awsProviderRadio).toBeVisible();
await expect(this.ociProviderRadio).toBeVisible();
await expect(this.gcpProviderRadio).toBeVisible();
await expect(this.azureProviderRadio).toBeVisible();
await expect(this.m365ProviderRadio).toBeVisible();
await expect(this.kubernetesProviderRadio).toBeVisible();
await expect(this.githubProviderRadio).toBeVisible();
}
async verifyCredentialsPageLoaded(): Promise<void> {
// Verify the credentials page is loaded
await expect(this.page).toHaveTitle(/Prowler/);
await this.verifyPageHasProwlerTitle();
await expect(this.roleCredentialsRadio).toBeVisible();
}
async verifyM365CredentialsPageLoaded(): Promise<void> {
// Verify the M365 credentials page is loaded
await expect(this.page).toHaveTitle(/Prowler/);
await this.verifyPageHasProwlerTitle();
await expect(this.m365ClientIdInput).toBeVisible();
await expect(this.m365ClientSecretInput).toBeVisible();
await expect(this.m365TenantIdInput).toBeVisible();
@@ -559,30 +896,59 @@ export class ProvidersPage extends BasePage {
async verifyM365CertificateCredentialsPageLoaded(): Promise<void> {
// Verify the M365 certificate credentials page is loaded
await expect(this.page).toHaveTitle(/Prowler/);
await this.verifyPageHasProwlerTitle();
await expect(this.m365ClientIdInput).toBeVisible();
await expect(this.m365TenantIdInput).toBeVisible();
await expect(this.m365CertificateContentInput).toBeVisible();
}
async verifyKubernetesCredentialsPageLoaded(): Promise<void> {
// Verify the Kubernetes credentials page is loaded
await this.verifyPageHasProwlerTitle();
await expect(this.kubernetesContextInput).toBeVisible();
}
async verifyGCPServiceAccountPageLoaded(): Promise<void> {
// Verify the GCP service account page is loaded
await this.verifyPageHasProwlerTitle();
await expect(this.gcpServiceAccountKeyInput).toBeVisible();
}
async verifyGitHubPersonalAccessTokenPageLoaded(): Promise<void> {
// Verify the GitHub personal access token page is loaded
await this.verifyPageHasProwlerTitle();
await expect(this.githubPersonalAccessTokenInput).toBeVisible();
}
async verifyGitHubAppPageLoaded(): Promise<void> {
// Verify the GitHub app page is loaded
await this.verifyPageHasProwlerTitle();
await expect(this.githubAppIdInput).toBeVisible();
await expect(this.githubAppPrivateKeyInput).toBeVisible();
}
async verifyLaunchScanPageLoaded(): Promise<void> {
// Verify the launch scan page is loaded
await expect(this.page).toHaveTitle(/Prowler/);
await this.verifyPageHasProwlerTitle();
await expect(this.page).toHaveURL(/\/providers\/test-connection/);
// Verify the Launch scan button is visible
const launchScanButton = this.page
.locator("button")
.filter({ hasText: "Launch scan" });
await expect(launchScanButton).toBeVisible();
}
async verifyLoadProviderPageAfterNewProvider(): Promise<void> {
// Verify the provider page is loaded
await this.waitForPageLoad();
await expect(this.page).toHaveTitle(/Prowler/);
await this.verifyPageHasProwlerTitle();
await expect(this.providersTable).toBeVisible();
}
@@ -602,149 +968,29 @@ export class ProvidersPage extends BasePage {
return true;
}
async deleteProviderIfExists(providerUID: string): Promise<void> {
// Delete the provider if it exists
// Navigate to providers page
await this.goto();
await expect(this.providersTable).toBeVisible({ timeout: 10000 });
// Find and use the search input to filter the table
const searchInput = this.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");
// Wait for the table to finish loading/filtering
await this.waitForPageLoad();
// 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 this.page.waitForTimeout(1500);
// Get all rows from the table
const allRows = this.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();
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)) {
return row;
}
}
return null;
};
// Wait for filtering to complete (max 0 or 1 data rows)
await expect(async () => {
const targetRow = 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();
if (!targetRow) {
// Provider not found, nothing to delete
// Navigate back to providers page to ensure clean state
await this.goto();
await expect(this.providersTable).toBeVisible({ timeout: 10000 });
return;
}
// Find and click the action button (last cell = actions column)
const actionButton = targetRow
.locator("td")
.last()
.locator("button")
.first();
await expect(actionButton).toBeVisible({ timeout: 5000 });
await actionButton.click();
// Wait for dropdown menu to appear and find delete option
const deleteMenuItem = this.page.getByRole("menuitem", {
name: /delete.*provider/i,
});
await expect(deleteMenuItem).toBeVisible({ timeout: 5000 });
await deleteMenuItem.click();
// Wait for confirmation modal to appear
const modal = this.page
.locator('[role="dialog"], .modal, [data-testid*="modal"]')
.first();
await expect(modal).toBeVisible({ timeout: 10000 });
// Find and click the delete confirmation button
await expect(this.deleteProviderConfirmationButton).toBeVisible({
timeout: 5000,
});
await this.deleteProviderConfirmationButton.click();
// Wait for modal to close (this indicates deletion was initiated)
await expect(modal).not.toBeVisible({ timeout: 10000 });
// Wait for page to reload
await this.waitForPageLoad();
// Navigate back to providers page to ensure clean state
await this.goto();
await expect(this.providersTable).toBeVisible({ timeout: 10000 });
}
async selectAuthenticationMethod(method: AWSCredentialType): Promise<void> {
// Select the authentication method
// Search botton that contains text AWS SDK Default or Prowler Cloud will assume or Access & Secret Key
const button = this.page.locator("button").filter({
hasText: /AWS SDK Default|Prowler Cloud will assume|Access & Secret Key/i,
});
await button.click();
if (method === AWS_CREDENTIAL_OPTIONS.AWS_ROLE_ARN) {
const modal = this.page
.locator('[role="dialog"], .modal, [data-testid*="modal"]')
.first();
await expect(modal).toBeVisible({ timeout: 10000 });
// Select the role credentials
this.page
if (method === AWS_CREDENTIAL_OPTIONS.AWS_ROLE_ARN) {
await this.page
.getByRole("option", { name: "Access & Secret Key" })
.click({ force: true });
} else if (method === AWS_CREDENTIAL_OPTIONS.AWS_SDK_DEFAULT) {
await this.page
.getByRole("option", { name: "AWS SDK Default" })
.click({ force: true });
} else {
throw new Error(`Invalid authentication method: ${method}`);
}

View File

@@ -1,7 +1,7 @@
### E2E Tests: AWS Provider Management
**Suite ID:** `PROVIDER-E2E`
**Feature:** AWS Provider Management - Add and configure AWS cloud providers with different authentication methods
**Feature:** AWS Provider Management.
---
@@ -33,13 +33,15 @@
5. Select "credentials" authentication type
6. Fill static credentials (access key and secret key)
7. Launch initial scan
8. Verify redirect to provider management page
8. Verify redirect to Scans page
9. Verify scheduled scan status in Scans table (provider exists and scan name is "scheduled scan")
### Expected Result:
- AWS provider successfully added with static credentials
- Initial scan launched successfully
- User redirected to provider details page
- User redirected to Scans page
- Scheduled scan appears in Scans table with correct provider and scan name
### Key verification points:
@@ -47,7 +49,9 @@
- Connect account page displays AWS option
- Credentials form accepts static credentials
- Launch scan page appears
- Successful redirect to provider page after scan launch
- Successful redirect to Scans page after scan launch
- Provider exists in Scans table (verified by account ID)
- Scan name field contains "scheduled scan"
### Notes:
@@ -85,13 +89,15 @@
5. Select "role" authentication type
6. Fill role credentials (access key, secret key, and role ARN)
7. Launch initial scan
8. Verify redirect to provider management page
8. Verify redirect to Scans page
9. Verify scheduled scan status in Scans table (provider exists and scan name is "scheduled scan")
### Expected Result:
- AWS provider successfully added with role credentials
- Initial scan launched successfully
- User redirected to provider details page
- User redirected to Scans page
- Scheduled scan appears in Scans table with correct provider and scan name
### Key verification points:
@@ -99,7 +105,9 @@
- Connect account page displays AWS option
- Role credentials form accepts all required fields
- Launch scan page appears
- Successful redirect to provider page after scan launch
- Successful redirect to Scans page after scan launch
- Provider exists in Scans table (verified by account ID)
- Scan name field contains "scheduled scan"
### Notes:
@@ -137,13 +145,15 @@
4. Fill provider details (subscription ID and alias)
5. Fill Azure credentials (client ID, client secret, tenant ID)
6. Launch initial scan
7. Verify redirect to provider management page
7. Verify redirect to Scans page
8. Verify scheduled scan status in Scans table (provider exists and scan name is "scheduled scan")
### Expected Result:
- Azure provider successfully added with static credentials
- Initial scan launched successfully
- User redirected to provider details page
- User redirected to Scans page
- Scheduled scan appears in Scans table with correct provider and scan name
### Key verification points:
@@ -151,7 +161,9 @@
- Connect account page displays Azure option
- Azure credentials form accepts all required fields
- Launch scan page appears
- Successful redirect to provider page after scan launch
- Successful redirect to Scans page after scan launch
- Provider exists in Scans table (verified by subscription ID)
- Scan name field contains "scheduled scan"
### Notes:
@@ -190,13 +202,15 @@
5. Select static credentials type
6. Fill M365 credentials (client ID, client secret, tenant ID)
7. Launch initial scan
8. Verify redirect to provider management page
8. Verify redirect to Scans page
9. Verify scheduled scan status in Scans table (provider exists and scan name is "scheduled scan")
### Expected Result:
- M365 provider successfully added with static credentials
- Initial scan launched successfully
- User redirected to provider details page
- User redirected to Scans page
- Scheduled scan appears in Scans table with correct provider and scan name
### Key verification points:
@@ -204,7 +218,9 @@
- Connect account page displays M365 option
- M365 credentials form accepts all required fields
- Launch scan page appears
- Successful redirect to provider page after scan launch
- Successful redirect to Scans page after scan launch
- Provider exists in Scans table (verified by domain ID)
- Scan name field contains "scheduled scan"
### Notes:
@@ -243,13 +259,15 @@
5. Select certificate credentials type
6. Fill M365 certificate credentials (client ID, tenant ID, certificate content)
7. Launch initial scan
8. Verify redirect to provider management page
8. Verify redirect to Scans page
9. Verify scheduled scan status in Scans table (provider exists and scan name is "scheduled scan")
### Expected Result:
- M365 provider successfully added with certificate credentials
- Initial scan launched successfully
- User redirected to provider details page
- User redirected to Scans page
- Scheduled scan appears in Scans table with correct provider and scan name
### Key verification points:
@@ -257,7 +275,9 @@
- Connect account page displays M365 option
- Certificate credentials form accepts all required fields
- Launch scan page appears
- Successful redirect to provider page after scan launch
- Successful redirect to Scans page after scan launch
- Provider exists in Scans table (verified by domain ID)
- Scan name field contains "scheduled scan"
### Notes:
@@ -265,3 +285,426 @@
- Provider cleanup performed before each test to ensure clean state
- Requires valid Microsoft 365 tenant with certificate-based authentication
- Certificate must be properly configured and have sufficient permissions for security scanning
---
## Test Case: `PROVIDER-E2E-006` - Add Kubernetes Provider with Kubeconfig Content
**Priority:** `critical`
**Tags:**
- type → @e2e, @serial
- feature → @providers
- provider → @kubernetes
**Description/Objective:** Validates the complete flow of adding a new Kubernetes provider using kubeconfig content authentication
**Preconditions:**
- Admin user authentication required (admin.auth.setup setup)
- Environment variables configured: E2E_KUBERNETES_CONTEXT, E2E_KUBERNETES_KUBECONFIG_PATH
- Kubeconfig file must exist at the specified path
- Remove any existing provider with the same Context before starting the test
- This test must be run serially and never in parallel with other tests, as it requires the Context not to be already registered beforehand.
### Flow Steps:
1. Navigate to providers page
2. Click "Add Provider" button
3. Select Kubernetes provider type
4. Fill provider details (context and alias)
5. Verify credentials page is loaded
6. Fill Kubernetes credentials (kubeconfig content)
7. Launch initial scan
8. Verify redirect to Scans page
9. Verify scheduled scan status in Scans table (provider exists and scan name is "scheduled scan")
### Expected Result:
- Kubernetes provider successfully added with kubeconfig content
- Initial scan launched successfully
- User redirected to Scans page
- Scheduled scan appears in Scans table with correct provider and scan name
### Key verification points:
- Provider page loads correctly
- Connect account page displays Kubernetes option
- Provider details form accepts context and alias
- Credentials page loads with kubeconfig content field
- Kubeconfig content is properly filled in the correct field
- Launch scan page appears
- Successful redirect to Scans page after scan launch
- Provider exists in Scans table (verified by context)
- Scan name field contains "scheduled scan"
### Notes:
- Test uses environment variables for Kubernetes context and kubeconfig file path
- Kubeconfig content is read from file and used for authentication
- Provider cleanup performed before each test to ensure clean state
- Requires valid Kubernetes cluster with accessible kubeconfig
- Kubeconfig must have sufficient permissions for security scanning
- Test validates that kubeconfig content goes to the correct field (not the context field)
---
## Test Case: `PROVIDER-E2E-007` - Add GCP Provider with Service Account Key
**Priority:** `critical`
**Tags:**
- type → @e2e, @serial
- feature → @providers
- provider → @gcp
**Description/Objective:** Validates the complete flow of adding a new GCP provider using service account key authentication
**Preconditions:**
- Admin user authentication required (admin.auth.setup setup)
- Environment variables configured: E2E_GCP_PROJECT_ID, E2E_GCP_BASE64_SERVICE_ACCOUNT_KEY
- Remove any existing provider with the same Project ID before starting the test
- This test must be run serially and never in parallel with other tests, as it requires the Project ID not to be already registered beforehand.
### Flow Steps:
1. Navigate to providers page
2. Click "Add Provider" button
3. Select GCP provider type
4. Fill provider details (project ID and alias)
5. Select service account credentials type
6. Fill GCP service account key credentials
7. Launch initial scan
8. Verify redirect to Scans page
9. Verify scheduled scan status in Scans table (provider exists and scan name is "scheduled scan")
### Expected Result:
- GCP provider successfully added with service account key
- Initial scan launched successfully
- User redirected to Scans page
- Scheduled scan appears in Scans table with correct provider and scan name
### Key verification points:
- Provider page loads correctly
- Connect account page displays GCP option
- Provider details form accepts project ID and alias
- Service account credentials page loads with service account key field
- Service account key is properly filled in the correct field
- Launch scan page appears
- Successful redirect to Scans page after scan launch
- Provider exists in Scans table (verified by project ID)
- Scan name field contains "scheduled scan"
### Notes:
- Test uses environment variables for GCP project ID and service account key
- Service account key is provided as base64 encoded JSON content
- Provider cleanup performed before each test to ensure clean state
- Requires valid GCP project with service account having appropriate permissions
- Service account must have sufficient permissions for security scanning
- Test validates that service account key goes to the correct field
- Test uses base64 encoded environment variables for GCP service account key
---
## Test Case: `PROVIDER-E2E-008` - Add GitHub Provider with Personal Access Token
**Priority:** `critical`
**Tags:**
- type → @e2e, @serial
- feature → @providers
- provider → @github
**Description/Objective:** Validates the complete flow of adding a new GitHub provider using personal access token authentication for a user account
**Preconditions:**
- Admin user authentication required (admin.auth.setup setup)
- Environment variables configured: E2E_GITHUB_USERNAME, E2E_GITHUB_PERSONAL_ACCESS_TOKEN
- Remove any existing provider with the same Username before starting the test
- This test must be run serially and never in parallel with other tests, as it requires the Username not to be already registered beforehand.
### Flow Steps:
1. Navigate to providers page
2. Click "Add Provider" button
3. Select GitHub provider type
4. Fill provider details (username and alias)
5. Select personal access token credentials type
6. Fill GitHub personal access token credentials
7. Launch initial scan
8. Verify redirect to Scans page
9. Verify scheduled scan status in Scans table (provider exists and scan name is "scheduled scan")
### Expected Result:
- GitHub provider successfully added with personal access token
- Initial scan launched successfully
- User redirected to Scans page
- Scheduled scan appears in Scans table with correct provider and scan name
### Key verification points:
- Provider page loads correctly
- Connect account page displays GitHub option
- Provider details form accepts username and alias
- Personal access token credentials page loads with token field
- Personal access token is properly filled in the correct field
- Launch scan page appears
- Successful redirect to Scans page after scan launch
- Provider exists in Scans table (verified by username)
- Scan name field contains "scheduled scan"
### Notes:
- Test uses environment variables for GitHub username and personal access token
- Provider cleanup performed before each test to ensure clean state
- Requires valid GitHub account with personal access token
- Personal access token must have sufficient permissions for security scanning
- Test validates that personal access token goes to the correct field
---
## Test Case: `PROVIDER-E2E-009` - Add GitHub Provider with GitHub App
**Priority:** `critical`
**Tags:**
- type → @e2e, @serial
- feature → @providers
- provider → @github
**Description/Objective:** Validates the complete flow of adding a new GitHub provider using GitHub App authentication for a user account
**Preconditions:**
- Admin user authentication required (admin.auth.setup setup)
- Environment variables configured: E2E_GITHUB_USERNAME, E2E_GITHUB_APP_ID, E2E_GITHUB_BASE64_APP_PRIVATE_KEY
- Remove any existing provider with the same Username before starting the test
- This test must be run serially and never in parallel with other tests, as it requires the Username not to be already registered beforehand.
### Flow Steps:
1. Navigate to providers page
2. Click "Add Provider" button
3. Select GitHub provider type
4. Fill provider details (username and alias)
5. Select GitHub App credentials type
6. Fill GitHub App credentials (App ID and private key)
7. Launch initial scan
8. Verify redirect to Scans page
9. Verify scheduled scan status in Scans table (provider exists and scan name is "scheduled scan")
### Expected Result:
- GitHub provider successfully added with GitHub App credentials
- Initial scan launched successfully
- User redirected to Scans page
- Scheduled scan appears in Scans table with correct provider and scan name
### Key verification points:
- Provider page loads correctly
- Connect account page displays GitHub option
- Provider details form accepts username and alias
- GitHub App credentials page loads with App ID and private key fields
- GitHub App credentials are properly filled in the correct fields
- Launch scan page appears
- Successful redirect to Scans page after scan launch
- Provider exists in Scans table (verified by username)
- Scan name field contains "scheduled scan"
### Notes:
- Test uses environment variables for GitHub username, App ID, and base64 encoded private key
- Private key is base64 encoded and must be decoded before use
- Provider cleanup performed before each test to ensure clean state
- Requires valid GitHub App with App ID and private key
- GitHub App must have sufficient permissions for security scanning
- Test validates that GitHub App credentials go to the correct fields
---
## Test Case: `PROVIDER-E2E-010` - Add GitHub Provider with Organization Personal Access Token
**Priority:** `critical`
**Tags:**
- type → @e2e, @serial
- feature → @providers
- provider → @github
**Description/Objective:** Validates the complete flow of adding a new GitHub provider using organization personal access token authentication
**Preconditions:**
- Admin user authentication required (admin.auth.setup setup)
- Environment variables configured: E2E_GITHUB_ORGANIZATION, E2E_GITHUB_ORGANIZATION_ACCESS_TOKEN
- Remove any existing provider with the same Organization name before starting the test
- This test must be run serially and never in parallel with other tests, as it requires the Organization name not to be already registered beforehand.
### Flow Steps:
1. Navigate to providers page
2. Click "Add Provider" button
3. Select GitHub provider type
4. Fill provider details (organization name and alias)
5. Select personal access token credentials type
6. Fill GitHub organization personal access token credentials
7. Launch initial scan
8. Verify redirect to Scans page
9. Verify scheduled scan status in Scans table (provider exists and scan name is "scheduled scan")
### Expected Result:
- GitHub provider successfully added with organization personal access token
- Initial scan launched successfully
- User redirected to Scans page
- Scheduled scan appears in Scans table with correct provider and scan name
### Key verification points:
- Provider page loads correctly
- Connect account page displays GitHub option
- Provider details form accepts organization name and alias
- Personal access token credentials page loads with token field
- Organization personal access token is properly filled in the correct field
- Launch scan page appears
- Successful redirect to Scans page after scan launch
- Provider exists in Scans table (verified by organization name)
- Scan name field contains "scheduled scan"
### Notes:
- Test uses environment variables for GitHub organization name and organization access token
- Provider cleanup performed before each test to ensure clean state
- Requires valid GitHub organization with organization access token
- Organization access token must have sufficient permissions for security scanning
- Test validates that organization personal access token goes to the correct field
---
## Test Case: `PROVIDER-E2E-011` - Add AWS Provider with Assume Role via AWS SDK Defaults
**Priority:** `critical`
**Tags:**
- type → @e2e, @serial
- feature → @providers
- provider → @aws
**Description/Objective:** Validates adding an AWS provider assuming a role while sourcing credentials from the AWS SDK default chain.
**Preconditions:**
- Admin user authentication required (admin.auth.setup setup)
- Environment variables configured: E2E_AWS_PROVIDER_ROLE_ARN
- Remove any existing provider with the same Account ID before starting the test
- This test must be run serially and never in parallel with other tests, as it requires the Account 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. Fill provider details (account ID and alias)
5. Select "role" authentication type
6. Switch authentication method to "Use AWS SDK default credentials"
7. Fill role ARN using AWS SDK credential inputs
8. Launch initial scan
9. Verify redirect to Scans page
10. Verify scheduled scan status in Scans table (provider exists and scan name is "scheduled scan")
### Expected Result:
- AWS provider successfully added using AWS SDK default credentials to assume the role
- Initial scan launched successfully
- User redirected to Scans page
- Scheduled scan appears in Scans table with correct provider and scan name
### Key verification points:
- Provider page loads correctly
- Connect account page displays AWS option
- Credentials form exposes AWS SDK default authentication method
- Role ARN field accepts provided value when SDK method is selected
- Launch scan page appears
- Successful redirect to Scans page after scan launch
- Provider exists in Scans table (verified by account ID)
- Scan name field contains "scheduled scan"
### Notes:
- Test leverages AWS SDK default credential chain (environment-configured keys) for Access Key and Secret Key
- Environment variable `E2E_AWS_PROVIDER_ROLE_ARN` must reference a valid assumable role
- Provider cleanup performed before each test to ensure clean state
- Requires valid AWS account with permissions to assume the target role
---
## Test Case: `PROVIDER-E2E-012` - Add OCI Provider with API Key Credentials
**Priority:** `critical`
**Tags:**
- type → @e2e, @serial
- feature → @providers
- provider → @oci
**Description/Objective:** Validates the complete flow of adding a new OCI provider using API Key credentials.
**Preconditions:**
- Admin user authentication required (admin.auth.setup setup)
- Environment variables configured: E2E_OCI_TENANCY_ID, E2E_OCI_USER_ID, E2E_OCI_FINGERPRINT, E2E_OCI_KEY_CONTENT, E2E_OCI_REGION
- Remove any existing provider with the same Tenancy ID before starting the test
- This test must be run serially and never in parallel with other tests, as it requires the Tenancy ID not to be already registered beforehand.
### Flow Steps:
1. Navigate to providers page
2. Click "Add Provider" button
3. Select OCI provider type
4. Fill provider details (tenancy ID and alias)
5. Verify OCI credentials page is loaded
6. Fill OCI credentials (user ID, fingerprint, key content, region)
7. Launch initial scan
8. Verify redirect to Scans page
9. Verify scheduled scan status in Scans table (provider exists and scan name is "scheduled scan")
### Expected Result:
- OCI provider successfully added with API Key credentials
- Initial scan launched successfully
- User redirected to Scans page
- Scheduled scan appears in Scans table with correct provider and scan name
### Key verification points:
- Provider page loads correctly
- Connect account page displays OCI option
- Provider details form accepts tenancy ID and alias
- OCI credentials page loads
- Credentials form accepts all required fields (user ID, fingerprint, key content, region)
- Launch scan page appears
- Successful redirect to Scans page after scan launch
- Provider exists in Scans table (verified by tenancy ID)
- Scan name field contains "scheduled scan"
### Notes:
- Test uses environment variables for OCI credentials
- Provider cleanup performed before each test to ensure clean state
- Requires valid OCI account with API Key set up
- API Key credential type is automatically used for OCI providers

View File

@@ -10,8 +10,22 @@ import {
M365ProviderData,
M365ProviderCredential,
M365_CREDENTIAL_OPTIONS,
KubernetesProviderData,
KubernetesProviderCredential,
KUBERNETES_CREDENTIAL_OPTIONS,
GCPProviderData,
GCPProviderCredential,
GCP_CREDENTIAL_OPTIONS,
GitHubProviderData,
GitHubProviderCredential,
GITHUB_CREDENTIAL_OPTIONS,
OCIProviderData,
OCIProviderCredential,
OCI_CREDENTIAL_OPTIONS,
} from "./providers-page";
import { ScansPage } from "../scans/scans-page";
import fs from "fs";
import { deleteProviderIfExists } from "../helpers";
test.describe("Add Provider", () => {
test.describe.serial("Add AWS Provider", () => {
@@ -35,7 +49,7 @@ test.describe("Add Provider", () => {
test.beforeEach(async ({ page }) => {
providersPage = new ProvidersPage(page);
// Clean up existing provider to ensure clean test state
await providersPage.deleteProviderIfExists(accountId);
await deleteProviderIfExists(providersPage, accountId);
});
// Use admin authentication for provider management
@@ -89,11 +103,12 @@ test.describe("Add Provider", () => {
await providersPage.fillAWSProviderDetails(awsProviderData);
await providersPage.clickNext();
await providersPage.verifyCredentialsPageLoaded();
// Select static credentials type
await providersPage.selectCredentialsType(
AWS_CREDENTIAL_OPTIONS.AWS_CREDENTIALS,
);
await providersPage.verifyCredentialsPageLoaded();
// Fill static credentials
await providersPage.fillStaticCredentials(staticCredentials);
@@ -106,6 +121,9 @@ test.describe("Add Provider", () => {
// Wait for redirect to provider page
scansPage = new ScansPage(page);
await scansPage.verifyPageLoaded();
// Verify scan status is "Scheduled scan"
await scansPage.verifyScheduledScanStatus(accountId);
},
);
@@ -123,9 +141,9 @@ test.describe("Add Provider", () => {
},
async ({ page }) => {
// Validate required environment variables
if (!accountId || !accessKey || !secretKey || !roleArn) {
if (!roleArn) {
throw new Error(
"E2E_AWS_PROVIDER_ACCOUNT_ID, E2E_AWS_PROVIDER_ACCESS_KEY, E2E_AWS_PROVIDER_SECRET_KEY, and E2E_AWS_PROVIDER_ROLE_ARN environment variables are not set",
"E2E_AWS_PROVIDER_ROLE_ARN environment variable is not set",
);
}
@@ -158,14 +176,10 @@ test.describe("Add Provider", () => {
await providersPage.fillAWSProviderDetails(awsProviderData);
await providersPage.clickNext();
// Select role credentials type
await providersPage.selectCredentialsType(
AWS_CREDENTIAL_OPTIONS.AWS_ROLE_ARN,
);
await providersPage.verifyCredentialsPageLoaded();
// Select Authentication Method
await providersPage.selectAuthenticationMethod(
// Select role credentials type
await providersPage.selectCredentialsType(
AWS_CREDENTIAL_OPTIONS.AWS_ROLE_ARN,
);
@@ -180,6 +194,85 @@ test.describe("Add Provider", () => {
// Wait for redirect to provider page
scansPage = new ScansPage(page);
await scansPage.verifyPageLoaded();
// Verify scan status is "Scheduled scan"
await scansPage.verifyScheduledScanStatus(accountId);
},
);
test(
"should add a new AWS provider with assume role credentials using AWS SDK",
{
tag: [
"@critical",
"@e2e",
"@providers",
"@aws",
"@serial",
"@PROVIDER-E2E-011",
],
},
async ({ page }) => {
// Validate required environment variables
if (!accountId || !roleArn) {
throw new Error(
"E2E_AWS_PROVIDER_ACCOUNT_ID, and E2E_AWS_PROVIDER_ROLE_ARN environment variables are not set",
);
}
// Prepare test data for AWS provider
const awsProviderData: AWSProviderData = {
accountId: accountId,
alias: "Test E2E AWS Account - Credentials",
};
// Prepare role-based credentials
const roleCredentials: AWSProviderCredential = {
type: AWS_CREDENTIAL_OPTIONS.AWS_ROLE_ARN,
roleArn: roleArn,
};
// Navigate to providers page
await providersPage.goto();
await providersPage.verifyPageLoaded();
// Start adding new provider
await providersPage.clickAddProvider();
await providersPage.verifyConnectAccountPageLoaded();
// Select AWS provider
await providersPage.selectAWSProvider();
// Fill provider details
await providersPage.fillAWSProviderDetails(awsProviderData);
await providersPage.clickNext();
// Select role credentials type
await providersPage.selectCredentialsType(
AWS_CREDENTIAL_OPTIONS.AWS_ROLE_ARN,
);
await providersPage.verifyCredentialsPageLoaded();
// Select Authentication Method
await providersPage.selectAuthenticationMethod(
AWS_CREDENTIAL_OPTIONS.AWS_SDK_DEFAULT,
);
// Fill role credentials
await providersPage.fillRoleCredentials(roleCredentials);
await providersPage.clickNext();
// Launch scan
await providersPage.verifyLaunchScanPageLoaded();
await providersPage.clickNext();
// Wait for redirect to provider page
scansPage = new ScansPage(page);
await scansPage.verifyPageLoaded();
// Verify scan status is "Scheduled scan"
await scansPage.verifyScheduledScanStatus(accountId);
},
);
});
@@ -206,7 +299,7 @@ test.describe("Add Provider", () => {
test.beforeEach(async ({ page }) => {
providersPage = new ProvidersPage(page);
// Clean up existing provider to ensure clean test state
await providersPage.deleteProviderIfExists(subscriptionId);
await deleteProviderIfExists(providersPage, subscriptionId);
});
// Use admin authentication for provider management
@@ -265,6 +358,9 @@ test.describe("Add Provider", () => {
// Wait for redirect to scan page
scansPage = new ScansPage(page);
await scansPage.verifyPageLoaded();
// Verify scan status is "Scheduled scan"
await scansPage.verifyScheduledScanStatus(subscriptionId);
},
);
});
@@ -290,7 +386,7 @@ test.describe("Add Provider", () => {
test.beforeEach(async ({ page }) => {
providersPage = new ProvidersPage(page);
// Clean up existing provider to ensure clean test state
await providersPage.deleteProviderIfExists(domainId);
await deleteProviderIfExists(providersPage, domainId);
});
// Use admin authentication for provider management
@@ -363,6 +459,9 @@ test.describe("Add Provider", () => {
// Wait for redirect to scan page
scansPage = new ScansPage(page);
await scansPage.verifyPageLoaded();
// Verify scan status is "Scheduled scan"
await scansPage.verifyScheduledScanStatus(domainId);
},
);
@@ -436,8 +535,607 @@ test.describe("Add Provider", () => {
// Wait for redirect to scan page
scansPage = new ScansPage(page);
await scansPage.verifyPageLoaded();
// Verify scan status is "Scheduled scan"
await scansPage.verifyScheduledScanStatus(domainId);
},
);
});
test.describe.serial("Add Kubernetes Provider", () => {
// Providers page object
let providersPage: ProvidersPage;
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",
);
}
// Setup before each test
test.beforeEach(async ({ page }) => {
providersPage = new ProvidersPage(page);
// Clean up existing provider to ensure clean test state
await deleteProviderIfExists(providersPage, context);
});
// Use admin authentication for provider management
test.use({ storageState: "playwright/.auth/admin_user.json" });
test(
"should add a new Kubernetes provider with kubeconfig context",
{
tag: [
"@critical",
"@e2e",
"@providers",
"@kubernetes",
"@serial",
"@PROVIDER-E2E-006",
],
},
async ({ page }) => {
// Verify kubeconfig file exists
if (!fs.existsSync(kubeconfigPath)) {
throw new Error(`Kubeconfig file not found at ${kubeconfigPath}`);
}
// Read kubeconfig content from file
let kubeconfigContent: string;
try {
kubeconfigContent = fs.readFileSync(kubeconfigPath, "utf8");
} catch (error) {
throw new Error(
`Failed to read kubeconfig file at ${kubeconfigPath}: ${error}`,
);
}
// Prepare test data for Kubernetes provider
const kubernetesProviderData: KubernetesProviderData = {
context: context,
alias: "Test E2E Kubernetes Account - Kubeconfig Context",
};
// Prepare static credentials
const kubernetesCredentials: KubernetesProviderCredential = {
type: KUBERNETES_CREDENTIAL_OPTIONS.KUBECONFIG_CONTENT,
kubeconfigContent: kubeconfigContent,
};
// Navigate to providers page
await providersPage.goto();
await providersPage.verifyPageLoaded();
// Start adding new provider
await providersPage.clickAddProvider();
await providersPage.verifyConnectAccountPageLoaded();
// Select Kubernetes provider
await providersPage.selectKubernetesProvider();
// Fill provider details
await providersPage.fillKubernetesProviderDetails(
kubernetesProviderData,
);
await providersPage.clickNext();
// Verify credentials page is loaded
await providersPage.verifyKubernetesCredentialsPageLoaded();
// Fill static credentials details
await providersPage.fillKubernetesCredentials(kubernetesCredentials);
await providersPage.clickNext();
// Launch scan
await providersPage.verifyLaunchScanPageLoaded();
await providersPage.clickNext();
// Wait for redirect to provider page
scansPage = new ScansPage(page);
await scansPage.verifyPageLoaded();
// Verify scan status is "Scheduled scan"
await scansPage.verifyScheduledScanStatus(context);
},
);
});
test.describe.serial("Add GCP Provider", () => {
// Providers page object
let providersPage: ProvidersPage;
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");
}
// Setup before each test
test.beforeEach(async ({ page }) => {
providersPage = new ProvidersPage(page);
// Clean up existing provider to ensure clean test state
await deleteProviderIfExists(providersPage, projectId);
});
// Use admin authentication for provider management
test.use({ storageState: "playwright/.auth/admin_user.json" });
test(
"should add a new GCP provider with service account key",
{
tag: [
"@critical",
"@e2e",
"@providers",
"@gcp",
"@serial",
"@PROVIDER-E2E-007",
],
},
async ({ page }) => {
// Validate required environment variables
const serviceAccountKeyB64 =
process.env.E2E_GCP_BASE64_SERVICE_ACCOUNT_KEY;
// Verify service account key is base64 encoded
if (!serviceAccountKeyB64) {
throw new Error(
"E2E_GCP_BASE64_SERVICE_ACCOUNT_KEY environment variable is not set",
);
}
// Decode service account key from base64
const serviceAccountKey = Buffer.from(
serviceAccountKeyB64,
"base64",
).toString("utf8");
// Verify service account key is valid JSON
if (!JSON.parse(serviceAccountKey)) {
throw new Error("Invalid service account key format");
}
// Prepare test data for GCP provider
const gcpProviderData: GCPProviderData = {
projectId: projectId,
alias: "Test E2E GCP Account - Service Account Key",
};
// Prepare static credentials
const gcpCredentials: GCPProviderCredential = {
type: GCP_CREDENTIAL_OPTIONS.GCP_SERVICE_ACCOUNT,
serviceAccountKey: serviceAccountKey,
};
// Navigate to providers page
await providersPage.goto();
await providersPage.verifyPageLoaded();
// Start adding new provider
await providersPage.clickAddProvider();
await providersPage.verifyConnectAccountPageLoaded();
// Select M365 provider
await providersPage.selectGCPProvider();
// Fill provider details
await providersPage.fillGCPProviderDetails(gcpProviderData);
await providersPage.clickNext();
// Select static credentials type
await providersPage.selectGCPCredentialsType(
GCP_CREDENTIAL_OPTIONS.GCP_SERVICE_ACCOUNT,
);
// Verify GCP service account page is loaded
await providersPage.verifyGCPServiceAccountPageLoaded();
// Fill static service account key details
await providersPage.fillGCPServiceAccountKeyCredentials(gcpCredentials);
await providersPage.clickNext();
// Launch scan
await providersPage.verifyLaunchScanPageLoaded();
await providersPage.clickNext();
// Wait for redirect to scan page
scansPage = new ScansPage(page);
await scansPage.verifyPageLoaded();
// Verify scan status is "Scheduled scan"
await scansPage.verifyScheduledScanStatus(projectId);
},
);
});
test.describe.serial("Add GitHub Provider", () => {
// Providers page object
let providersPage: ProvidersPage;
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");
}
// Setup before each test
test.beforeEach(async ({ page }) => {
providersPage = new ProvidersPage(page);
// Clean up existing provider to ensure clean test state
await deleteProviderIfExists(providersPage, username);
});
// Use admin authentication for provider management
test.use({ storageState: "playwright/.auth/admin_user.json" });
test(
"should add a new GitHub provider with personal access token",
{
tag: [
"@critical",
"@e2e",
"@providers",
"@github",
"@serial",
"@PROVIDER-E2E-008",
],
},
async ({ page }) => {
// Validate required environment variables
const personalAccessToken =
process.env.E2E_GITHUB_PERSONAL_ACCESS_TOKEN;
// Verify username and personal access token are set in environment variables
if (!personalAccessToken) {
throw new Error(
"E2E_GITHUB_PERSONAL_ACCESS_TOKEN environment variables are not set",
);
}
// Prepare test data for GitHub provider
const githubProviderData: GitHubProviderData = {
username: username,
alias: "Test E2E GitHub Account - Personal Access Token",
};
// Prepare personal access token credentials
const githubCredentials: GitHubProviderCredential = {
type: GITHUB_CREDENTIAL_OPTIONS.GITHUB_PERSONAL_ACCESS_TOKEN,
personalAccessToken: personalAccessToken,
};
// Navigate to providers page
await providersPage.goto();
await providersPage.verifyPageLoaded();
// Start adding new provider
await providersPage.clickAddProvider();
await providersPage.verifyConnectAccountPageLoaded();
// Select GitHub provider
await providersPage.selectGitHubProvider();
// Fill provider details
await providersPage.fillGitHubProviderDetails(githubProviderData);
await providersPage.clickNext();
// Select GitHub personal access token credentials type
await providersPage.selectGitHubCredentialsType(
GITHUB_CREDENTIAL_OPTIONS.GITHUB_PERSONAL_ACCESS_TOKEN,
);
// Verify GitHub personal access token page is loaded
await providersPage.verifyGitHubPersonalAccessTokenPageLoaded();
// Fill static personal access token details
await providersPage.fillGitHubPersonalAccessTokenCredentials(
githubCredentials,
);
await providersPage.clickNext();
// Launch scan
await providersPage.verifyLaunchScanPageLoaded();
await providersPage.clickNext();
// Wait for redirect to scan page
scansPage = new ScansPage(page);
await scansPage.verifyPageLoaded();
// Verify scan status is "Scheduled scan"
await scansPage.verifyScheduledScanStatus(username);
},
);
test(
"should add a new GitHub provider with github app",
{
tag: [
"@critical",
"@e2e",
"@providers",
"@github",
"@serial",
"@PROVIDER-E2E-009",
],
},
async ({ page }) => {
// Validate required environment variables
const githubAppId =
process.env.E2E_GITHUB_APP_ID;
const githubAppPrivateKeyB64 =
process.env.E2E_GITHUB_BASE64_APP_PRIVATE_KEY;
// Verify github app id and private key are set in environment variables
if (!githubAppId || !githubAppPrivateKeyB64) {
throw new Error(
"E2E_GITHUB_APP_ID and E2E_GITHUB_APP_PRIVATE_KEY environment variables are not set",
);
}
// Decode github app private key from base64
const githubAppPrivateKey = Buffer.from(
githubAppPrivateKeyB64,
"base64",
).toString("utf8");
// Prepare test data for GitHub provider
const githubProviderData: GitHubProviderData = {
username: username,
alias: "Test E2E GitHub Account - GitHub App",
};
// Prepare github app credentials
const githubCredentials: GitHubProviderCredential = {
type: GITHUB_CREDENTIAL_OPTIONS.GITHUB_APP,
githubAppId: githubAppId,
githubAppPrivateKey: githubAppPrivateKey,
};
// Navigate to providers page
await providersPage.goto();
await providersPage.verifyPageLoaded();
// Start adding new provider
await providersPage.clickAddProvider();
await providersPage.verifyConnectAccountPageLoaded();
// Select GitHub provider
await providersPage.selectGitHubProvider();
// Fill provider details
await providersPage.fillGitHubProviderDetails(githubProviderData);
await providersPage.clickNext();
// Select static github app credentials type
await providersPage.selectGitHubCredentialsType(
GITHUB_CREDENTIAL_OPTIONS.GITHUB_APP,
);
// Verify GitHub github app page is loaded
await providersPage.verifyGitHubAppPageLoaded();
// Fill static github app credentials details
await providersPage.fillGitHubAppCredentials(
githubCredentials,
);
await providersPage.clickNext();
// Launch scan
await providersPage.verifyLaunchScanPageLoaded();
await providersPage.clickNext();
// Wait for redirect to scan page
scansPage = new ScansPage(page);
await scansPage.verifyPageLoaded();
// Verify scan status is "Scheduled scan"
await scansPage.verifyScheduledScanStatus(username);
},
);
});
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",
);
}
// Setup before each test
test.beforeEach(async ({ page }) => {
providersPage = new ProvidersPage(page);
// Clean up existing provider to ensure clean test state
await deleteProviderIfExists(providersPage, organization);
});
// Use admin authentication for provider management
test.use({ storageState: "playwright/.auth/admin_user.json" });
test(
"should add a new GitHub provider with organization personal access token",
{
tag: [
"@critical",
"@e2e",
"@providers",
"@github",
"@serial",
"@PROVIDER-E2E-010",
],
},
async ({ page }) => {
// Validate required environment variables
const organizationAccessToken =
process.env.E2E_GITHUB_ORGANIZATION_ACCESS_TOKEN;
// Verify username and personal access token are set in environment variables
if (!organizationAccessToken) {
throw new Error(
"E2E_GITHUB_ORGANIZATION_ACCESS_TOKEN environment variables are not set",
);
}
// Prepare test data for GitHub provider
const githubProviderData: GitHubProviderData = {
username: organization,
alias: "Test E2E GitHub Account - Organization Access Token",
};
// Prepare personal access token credentials
const githubCredentials: GitHubProviderCredential = {
type: GITHUB_CREDENTIAL_OPTIONS.GITHUB_PERSONAL_ACCESS_TOKEN,
personalAccessToken: organizationAccessToken,
};
// Navigate to providers page
await providersPage.goto();
await providersPage.verifyPageLoaded();
// Start adding new provider
await providersPage.clickAddProvider();
await providersPage.verifyConnectAccountPageLoaded();
// Select GitHub provider
await providersPage.selectGitHubProvider();
// Fill provider details
await providersPage.fillGitHubProviderDetails(githubProviderData);
await providersPage.clickNext();
// Select GitHub organization personal access token credentials type
await providersPage.selectGitHubCredentialsType(
GITHUB_CREDENTIAL_OPTIONS.GITHUB_PERSONAL_ACCESS_TOKEN,
);
// Verify GitHub personal access token page is loaded
await providersPage.verifyGitHubPersonalAccessTokenPageLoaded();
// Fill static personal access token details
await providersPage.fillGitHubPersonalAccessTokenCredentials(
githubCredentials,
);
await providersPage.clickNext();
// Launch scan
await providersPage.verifyLaunchScanPageLoaded();
await providersPage.clickNext();
// Wait for redirect to scan page
scansPage = new ScansPage(page);
await scansPage.verifyPageLoaded();
// Verify scan status is "Scheduled scan"
await scansPage.verifyScheduledScanStatus(organization);
},
);
});
});
test.describe.serial("Add OCI Provider", () => {
// Providers page object
let providersPage: ProvidersPage;
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",
);
}
// Setup before each test
test.beforeEach(async ({ page }) => {
providersPage = new ProvidersPage(page);
// Clean up existing provider to ensure clean test state
await deleteProviderIfExists(providersPage, tenancyId);
});
// Use admin authentication for provider management
test.use({ storageState: "playwright/.auth/admin_user.json" });
test(
"should add a new OCI provider with API key credentials",
{
tag: [
"@critical",
"@e2e",
"@providers",
"@oci",
"@serial",
"@PROVIDER-E2E-012",
],
},
async ({ page }) => {
// Prepare test data for OCI provider
const ociProviderData: OCIProviderData = {
tenancyId: tenancyId,
alias: "Test E2E OCI Account - API Key",
};
// Prepare static credentials
const ociCredentials: OCIProviderCredential = {
type: OCI_CREDENTIAL_OPTIONS.OCI_API_KEY,
tenancyId: tenancyId,
userId: userId,
fingerprint: fingerprint,
keyContent: keyContent,
region: region,
};
// Navigate to providers page
await providersPage.goto();
// Start adding new provider
await providersPage.clickAddProvider();
await providersPage.verifyConnectAccountPageLoaded();
// Select OCI provider
await providersPage.selectOCIProvider();
// Fill provider details
await providersPage.fillOCIProviderDetails(ociProviderData);
await providersPage.clickNext();
// Verify OCI credentials page is loaded
await providersPage.verifyOCICredentialsPageLoaded();
// Fill static credentials details
await providersPage.fillOCICredentials(ociCredentials);
await providersPage.clickNext();
// Launch scan
await providersPage.verifyLaunchScanPageLoaded();
await providersPage.clickNext();
// Wait for redirect to scan page
scansPage = new ScansPage(page);
await scansPage.verifyPageLoaded();
// Verify scan status is "Scheduled scan"
await scansPage.verifyScheduledScanStatus(tenancyId);
},
);
});
});

View File

@@ -6,12 +6,29 @@ export class ScansPage extends BasePage {
// Main content elements
readonly scanTable: Locator;
// Scan provider selection elements
readonly scanProviderSelect: Locator;
readonly scanAliasInput: Locator;
readonly startNowButton: Locator;
// Scan state elements
readonly successToast: Locator;
constructor(page: Page) {
super(page);
// Scan provider selection elements
this.scanProviderSelect = page.getByRole('combobox').filter({ hasText: 'Choose a cloud provider' })
this.scanAliasInput = page.getByRole("textbox", { name: "Scan label (optional)" });
this.startNowButton = page.getByRole("button", { name: /Start now|Start scan now/i });
// Scan state elements
this.successToast = page.getByRole("alert", { name: /The scan was launched successfully\.?/i });
// Main content elements
this.scanTable = page.locator("table");
}
// Navigation methods
@@ -19,10 +36,88 @@ export class ScansPage extends BasePage {
await super.goto("/scans");
}
// Verification methods
async verifyPageLoaded(): Promise<void> {
// Verify the scans page is loaded
await expect(this.page).toHaveTitle(/Prowler/);
await expect(this.scanTable).toBeVisible();
await this.waitForPageLoad();
}
async selectProviderByUID(uid: string): Promise<void> {
// Select the provider by UID
await this.scanProviderSelect.click();
await this.page.getByRole("option", { name: new RegExp(uid) }).click();
}
async fillScanAlias(alias: string): Promise<void> {
// Fill the scan alias
await this.scanAliasInput.fill(alias);
}
async clickStartNowButton(): Promise<void> {
// Click the start now button
await expect(this.startNowButton).toBeVisible();
await this.startNowButton.click();
}
async verifyScanLaunched(alias: string): Promise<void> {
// Verify the scan was launched
// Verify the success toast is visible
await this.successToast.waitFor({ state: "visible", timeout: 5000 }).catch(() => {});
// Wait for the scans table to be visible
await expect(this.scanTable).toBeVisible();
// Find a row that contains the scan alias
const rowWithAlias = this.scanTable
.locator("tbody tr")
.filter({ hasText: alias })
.first();
// Verify the row with the scan alias is visible
await expect(rowWithAlias).toBeVisible();
// Basic state/assertion hint: queued/available/executing (non-blocking if not present)
await rowWithAlias.textContent().then((text) => {
if (!text) return;
const hasExpectedState = /executing|available|queued/i.test(text);
if (!hasExpectedState) {
// Fall back to just ensuring alias is present in the row
// The expectation above already ensures visibility
}
});
}
async verifyScheduledScanStatus(accountId: string): Promise<void> {
// Verifies that:
// 1. The provider exists in the table (by account ID/UID)
// 2. The scan name field contains "scheduled scan"
// Scan Table exists
await expect(this.scanTable).toBeVisible();
// Find a row that contains the account ID (provider UID in Cloud Provider column)
const rowWithAccountId = this.scanTable
.locator("tbody tr")
.filter({ hasText: accountId })
.first();
// Verify the row with the account ID is visible (provider exists)
await expect(rowWithAccountId).toBeVisible();
// Verify the row contains "scheduled scan" in the Scan name column
// The scan name "Daily scheduled scan" is displayed as "scheduled scan" in the table
await expect(rowWithAccountId).toContainText("scheduled scan", {
ignoreCase: true,
});
}
}

55
ui/tests/scans/scans.md Normal file
View File

@@ -0,0 +1,55 @@
### E2E Tests: Scans - On Demand
**Suite ID:** `SCANS-E2E`
**Feature:** On-demand Scans.
---
## Test Case: `SCANS-E2E-001` - Execute On-Demand Scan
**Priority:** `critical`
**Tags:**
- type → @e2e, @serial
- feature → @scans
**Description/Objective:** Validates the complete flow to execute an on-demand scan selecting a provider by UID and confirming success on the Scans page.
**Preconditions:**
- Admin user authentication required (admin.auth.setup setup)
- Environment variables configured for : E2E_AWS_PROVIDER_ACCOUNT_ID,E2E_AWS_PROVIDER_ACCESS_KEY and E2E_AWS_PROVIDER_SECRET_KEY
- Remove any existing AWS provider with the same Account ID before starting the test
- This test must be run serially and never in parallel with other tests, as it requires the Account ID Provider to be already registered.
### Flow Steps:
1. Navigate to Scans page
2. Open provider selector and choose the entry whose text contains E2E_AWS_PROVIDER_ACCOUNT_ID
3. Optionally fill scan label (alias)
4. Click "Start now" to launch the scan
5. Verify the success toast appears
6. Verify a row in the Scans table contains the provided scan label (or shows the new scan entry)
### Expected Result:
- Scan is launched successfully
- Success toast is displayed to the user
- Scans table displays the new scan entry (including the alias when provided)
### Key verification points:
- Scans page loads correctly
- Provider select is available and lists the configured provider UID
- "Start now" button is rendered and enabled when form is valid
- Success toast message: "The scan was launched successfully."
- Table contains a row with the scan label or new scan state (queued/available/executing)
### Notes:
- The table may take a short time to reflect the new scan; assertions look for a row containing the alias.
- Provider cleanup performed before each test to ensure clean state
- Tests should run serially to avoid state conflicts.

View File

@@ -0,0 +1,71 @@
import { test } from "@playwright/test";
import { ScansPage } from "./scans-page";
import { ProvidersPage } from "../providers/providers-page";
import { deleteProviderIfExists, addAWSProvider } from "../helpers";
// Scans E2E suite scaffold
test.describe("Scans", () => {
test.describe.serial("Execute Scans", () => {
// Scans page object
let scansPage: ScansPage;
// Use scans-specific authenticated user
test.use({ storageState: "playwright/.auth/admin_user.json" });
// Before each scans test, ensure an AWS provider exists using admin context
test.beforeEach(async ({ page }) => {
// Create scans page object
const providersPage = new ProvidersPage(page);
// 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;
if (!accountId || !accessKey || !secretKey) {
throw new Error(
"E2E_AWS_PROVIDER_ACCOUNT_ID, E2E_AWS_PROVIDER_ACCESS_KEY, and E2E_AWS_PROVIDER_SECRET_KEY environment variables are not set",
);
}
// Clean up existing provider to ensure clean test state
await deleteProviderIfExists(providersPage, accountId);
// Add AWS provider
await addAWSProvider(providersPage.page, accountId, accessKey, secretKey);
});
test(
"should execute on demand scan",
{
tag: ["@e2e", "@scans", "@critical", "@serial", "@SCAN-E2E-001"],
},
async ({ page }) => {
const accountId = process.env.E2E_AWS_PROVIDER_ACCOUNT_ID;
if (!accountId) {
throw new Error(
"E2E_AWS_PROVIDER_ACCOUNT_ID environment variable is not set",
);
}
scansPage = new ScansPage(page);
await scansPage.goto();
// Select provider by UID (accountId)
await scansPage.selectProviderByUID(accountId);
// Complete scan alias
await scansPage.fillScanAlias("E2E Test Scan - On Demand");
// Press start now button
await scansPage.clickStartNowButton();
// Verify the scan was launched
await scansPage.verifyScanLaunched("E2E Test Scan - On Demand");
},
);
});
});

View File

@@ -43,46 +43,67 @@ export class SignUpPage extends BasePage {
}
async goto(): Promise<void> {
// Navigate to the sign up page
await super.goto("/sign-up");
}
async gotoInvite(shareUrl: string): Promise<void> {
// Navigate to the share url
await super.goto(shareUrl);
}
async verifyPageLoaded(): Promise<void> {
// Verify the sign up page is loaded
await expect(this.page.getByRole("heading", { name: "Sign up" })).toBeVisible();
await expect(this.emailInput).toBeVisible();
await expect(this.submitButton).toBeVisible();
await this.waitForPageLoad();
}
async fillName(name: string): Promise<void> {
// Fill the name input
await this.nameInput.fill(name);
}
async fillCompany(company?: string): Promise<void> {
// Fill the company input
if (company) {
await this.companyInput.fill(company);
}
}
async fillEmail(email: string): Promise<void> {
// Fill the email input
await this.emailInput.fill(email);
}
async fillPassword(password: string): Promise<void> {
// Fill the password input
await this.passwordInput.fill(password);
}
async fillConfirmPassword(confirmPassword: string): Promise<void> {
// Fill the confirm password input
await this.confirmPasswordInput.fill(confirmPassword);
}
async fillInvitationToken(token?: string | null): Promise<void> {
// Fill the invitation token input
if (token) {
await this.invitationTokenInput.fill(token);
}
}
async acceptTermsIfPresent(accept: boolean = true): Promise<void> {
// Only in cloud env; check presence before interacting
// Accept the terms and conditions if present
if (await this.termsCheckbox.isVisible()) {
if (accept) {
await this.termsCheckbox.click();
@@ -91,25 +112,32 @@ export class SignUpPage extends BasePage {
}
async submit(): Promise<void> {
// Submit the sign up form
await this.submitButton.click();
}
async signup(data: SignUpData): Promise<void> {
// Fill the sign up form
await this.fillName(data.name);
await this.fillCompany(data.company);
await this.fillCompany(data.company ?? undefined);
await this.fillEmail(data.email);
await this.fillPassword(data.password);
await this.fillConfirmPassword(data.confirmPassword);
await this.fillInvitationToken(data.invitationToken ?? undefined);
await this.acceptTermsIfPresent(data.acceptTerms ?? true);
await this.submit();
}
async verifyRedirectToLogin(): Promise<void> {
// Verify redirect to login page
await expect(this.page).toHaveURL("/sign-in");
}
async verifyRedirectToEmailVerification(): Promise<void> {
// Verify redirect to email verification page
await expect(this.page).toHaveURL("/email-verification");
}
}

View File

@@ -1,7 +1,7 @@
### E2E Tests: User Sign-Up
**Suite ID:** `SIGNUP-E2E`
**Feature:** New user registration flow.
**Feature:** New user registration.
---
@@ -18,7 +18,7 @@
**Preconditions:**
- Application is running, email domain & password is acceptable for sign-up.
- No existing data in Prowler is required; the test can run on a clean state.
- `E2E_NEW_PASSWORD` environment variable must be set with a valid password for the test.
- `E2E_NEW_USER_PASSWORD` environment variable must be set with a valid password for the test.
### Flow Steps:
1. Navigate to the Sign up page.
@@ -38,6 +38,6 @@
### Notes:
- Test data uses a random base36 suffix to avoid collisions with email.
- The test requires the `E2E_NEW_PASSWORD` environment variable to be set before running.
- The test requires the `E2E_NEW_USER_PASSWORD` environment variable to be set before running.

View File

@@ -8,10 +8,10 @@ test.describe("Sign Up Flow", () => {
"should register a new user successfully",
{ tag: ["@critical", "@e2e", "@signup", "@SIGNUP-E2E-001"] },
async ({ page }) => {
const password = process.env.E2E_NEW_PASSWORD;
const password = process.env.E2E_NEW_USER_PASSWORD;
if (!password) {
throw new Error("E2E_NEW_PASSWORD environment variable is not set");
throw new Error("E2E_NEW_USER_PASSWORD environment variable is not set");
}
const signUpPage = new SignUpPage(page);