mirror of
https://github.com/prowler-cloud/prowler.git
synced 2025-12-19 05:17:47 +00:00
test(ui): Add e2e test for OCI Provider (#9347)
Co-authored-by: Alan Buscaglia <gentlemanprogramming@gmail.com>
This commit is contained in:
41
.github/workflows/ui-e2e-tests.yml
vendored
41
.github/workflows/ui-e2e-tests.yml
vendored
@@ -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
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Page, Locator, expect } from "@playwright/test";
|
||||
*/
|
||||
export abstract class BasePage {
|
||||
readonly page: Page;
|
||||
|
||||
|
||||
// Common UI elements that appear on most pages
|
||||
readonly title: Locator;
|
||||
readonly loadingIndicator: Locator;
|
||||
@@ -14,7 +14,7 @@ export abstract class BasePage {
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
|
||||
|
||||
// Common locators that most pages share
|
||||
this.title = page.locator("h1, h2, [role='heading']").first();
|
||||
this.loadingIndicator = page.getByRole("status", { name: "Loading" });
|
||||
@@ -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
|
||||
@@ -119,14 +112,14 @@ export abstract class BasePage {
|
||||
async getFormErrors(): Promise<string[]> {
|
||||
const errorElements = await this.page.locator('[role="alert"], .error-message, [data-testid="error"]').all();
|
||||
const errors: string[] = [];
|
||||
|
||||
|
||||
for (const element of errorElements) {
|
||||
const text = await element.textContent();
|
||||
if (text) {
|
||||
errors.push(text.trim());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
@@ -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` });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
117
ui/tests/invitations/invitations-page.ts
Normal file
117
ui/tests/invitations/invitations-page.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
66
ui/tests/invitations/invitations.md
Normal file
66
ui/tests/invitations/invitations.md
Normal 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
|
||||
105
ui/tests/invitations/invitations.spec.ts
Normal file
105
ui/tests/invitations/invitations.spec.ts
Normal 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();
|
||||
},
|
||||
);
|
||||
});
|
||||
28
ui/tests/profile/profile-page.ts
Normal file
28
ui/tests/profile/profile-page.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
const modal = this.page
|
||||
.locator('[role="dialog"], .modal, [data-testid*="modal"]')
|
||||
.first();
|
||||
|
||||
// Select the role credentials
|
||||
this.page
|
||||
await expect(modal).toBeVisible({ timeout: 10000 });
|
||||
|
||||
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}`);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
55
ui/tests/scans/scans.md
Normal 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.
|
||||
|
||||
|
||||
71
ui/tests/scans/scans.spec.ts
Normal file
71
ui/tests/scans/scans.spec.ts
Normal 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");
|
||||
|
||||
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user