test(ui): add AWS provider management E2E tests

- Introduced new E2E tests for adding AWS providers with both static and role-based credentials.
- Updated Playwright configuration to include a new test suite for providers.
- Enhanced the UI workflow by adding necessary environment variables for E2E testing.
- Created helper functions for provider management actions and validations.
This commit is contained in:
StylusFrost
2025-10-17 13:41:40 +02:00
parent cef7fcc24b
commit d8ca60a4ab
7 changed files with 673 additions and 4 deletions
+6
View File
@@ -18,6 +18,12 @@ jobs:
AUTH_TRUST_HOST: true
NEXTAUTH_URL: 'http://localhost:3000'
NEXT_PUBLIC_API_BASE_URL: 'http://localhost:8080/api/v1'
E2E_ADMIN_USER: ${{ secrets.E2E_ADMIN_USER }}
E2E_ADMIN_PASSWORD: ${{ secrets.E2E_ADMIN_PASSWORD }}
E2E_AWS_PROVIDER_ACCOUNT_ID: ${{ secrets.E2E_AWS_PROVIDER_ACCOUNT_ID }}
E2E_AWS_PROVIDER_ACCESS_KEY: ${{ secrets.E2E_AWS_PROVIDER_ACCESS_KEY }}
E2E_AWS_PROVIDER_SECRET_KEY: ${{ secrets.E2E_AWS_PROVIDER_SECRET_KEY }}
E2E_AWS_PROVIDER_ROLE_ARN: ${{ secrets.E2E_AWS_PROVIDER_ROLE_ARN }}
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
+4 -4
View File
@@ -15,10 +15,10 @@
"format:check": "./node_modules/.bin/prettier --check ./app",
"format:write": "./node_modules/.bin/prettier --config .prettierrc.json --write ./app",
"prepare": "husky",
"test:e2e": "playwright test --project=chromium --project=sign-up",
"test:e2e:ui": "playwright test --project=chromium --project=sign-up --ui",
"test:e2e:debug": "playwright test --project=chromium --project=sign-up --debug",
"test:e2e:headed": "playwright test --project=chromium --project=sign-up --headed",
"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:report": "playwright show-report",
"test:e2e:install": "playwright install"
},
+6
View File
@@ -88,6 +88,12 @@ export default defineConfig({
name: "sign-up",
testMatch: "sign-up.spec.ts",
},
// This project runs the providers test suite
{
name: "providers",
testMatch: "providers.spec.ts",
dependencies: ["admin.auth.setup"],
},
],
webServer: {
+8
View File
@@ -1,5 +1,6 @@
import { Page, expect } from "@playwright/test";
import { SignInPage, SignInCredentials } from "./sign-in/sign-in-page";
import { ProvidersPage } from "./providers/providers-page";
export const ERROR_MESSAGES = {
INVALID_CREDENTIALS: "Invalid email or password",
@@ -187,3 +188,10 @@ export async function verifySessionValid(page: Page) {
expect(session.refreshToken).toBeTruthy();
return session;
}
export async function deleteProviderIfExists(page: Page, accountId: string) {
const providersPage = new ProvidersPage(page);
if (await providersPage.verifyProviderExists(accountId)) {
await providersPage.actionDeleteProvider(accountId);
}
}
+384
View File
@@ -0,0 +1,384 @@
import { Page, Locator, expect } from "@playwright/test";
import { BasePage } from "../base-page";
export interface AWSProviderData {
accountId: string;
alias?: string;
roleArn?: string;
externalId?: string;
accessKeyId?: string;
secretAccessKey?: string;
}
export interface ProviderCredentials {
type: "role" | "credentials";
roleArn?: string;
externalId?: string;
accessKeyId?: string;
secretAccessKey?: string;
}
export class ProvidersPage extends BasePage {
readonly addProviderButton: Locator;
readonly providersTable: Locator;
// Provider selection elements
readonly awsProviderRadio: Locator;
readonly gcpProviderRadio: Locator;
readonly azureProviderRadio: Locator;
readonly m365ProviderRadio: Locator;
readonly kubernetesProviderRadio: Locator;
readonly githubProviderRadio: Locator;
// AWS provider form elements
readonly accountIdInput: Locator;
readonly aliasInput: Locator;
readonly nextButton: Locator;
readonly backButton: Locator;
readonly saveButton: Locator;
readonly launchScanButton: Locator;
// AWS credentials type selection
readonly roleCredentialsRadio: Locator;
readonly staticCredentialsRadio: Locator;
// AWS role credentials form
readonly roleArnInput: Locator;
readonly externalIdInput: Locator;
// AWS static credentials form
readonly accessKeyIdInput: Locator;
readonly secretAccessKeyInput: Locator;
// Delete button
readonly deleteProviderConfirmationButton: Locator;
constructor(page: Page) {
super(page);
// Button to add a new cloud provider
this.addProviderButton = page.getByText("Add Cloud Provider", {
exact: true,
});
// Table displaying existing providers
this.providersTable = page.getByRole("table");
// Radio buttons to select the type of cloud provider
this.awsProviderRadio = page.getByRole("radio", {
name: /Amazon Web Services/i,
});
this.gcpProviderRadio = page.getByRole("radio", {
name: /Google Cloud Platform/i,
});
this.azureProviderRadio = page.getByRole("radio", {
name: /Microsoft Azure/i,
});
this.m365ProviderRadio = page.getByRole("radio", {
name: /Microsoft 365/i,
});
this.kubernetesProviderRadio = page.getByRole("radio", {
name: /Kubernetes/i,
});
this.githubProviderRadio = page.getByRole("radio", { name: /GitHub/i });
// AWS provider form inputs
this.accountIdInput = page.getByLabel("Account ID");
this.aliasInput = page.getByLabel("Provider alias (optional)");
// Navigation buttons in the form (next and back)
this.nextButton = page
.locator("form")
.getByRole("button", { name: "Next", exact: true });
this.backButton = page.getByRole("button", { name: "Back" });
// Button to save the form
this.saveButton = page.getByRole("button", { name: "Save", exact: true });
this.launchScanButton = page.getByRole("button", {
name: "Launch scan",
exact: true,
});
// Radios for selecting AWS credentials method
this.roleCredentialsRadio = page.getByRole("radio", {
name: /Connect assuming IAM Role/i,
});
this.staticCredentialsRadio = page.getByRole("radio", {
name: /Connect via Credentials/i,
});
// Inputs for IAM Role credentials
this.roleArnInput = page.getByLabel("Role ARN");
this.externalIdInput = page.getByLabel("External ID");
// Inputs for static credentials
this.accessKeyIdInput = page.getByLabel("Access Key ID");
this.secretAccessKeyInput = page.getByLabel("Secret Access Key");
// Delete button
this.deleteProviderConfirmationButton = page.locator(
'button[aria-label="Delete"]',
);
}
async goto(): Promise<void> {
await super.goto("/providers");
}
async clickAddProvider(): Promise<void> {
await this.addProviderButton.click();
await this.waitForPageLoad();
}
async selectAWSProvider(): Promise<void> {
// Prefer label-based click for radios, force if overlay intercepts
await this.awsProviderRadio.click({ force: true });
await this.waitForPageLoad();
}
async fillAWSProviderDetails(data: AWSProviderData): Promise<void> {
await this.accountIdInput.fill(data.accountId);
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.
const url = this.page.url(); // Get the current page URL
// 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();
return;
}
// If "Save" is not present, try clicking the "Next" button
if (await this.nextButton.count()) {
await this.nextButton.click();
await this.waitForPageLoad();
return;
}
}
// If on the "test-connection" step, click the "Launch scan" button
if (/\/providers\/test-connection/.test(url)) {
const buttonByText = this.page
.locator("button")
.filter({ hasText: "Launch scan" });
await buttonByText.click();
await this.waitForPageLoad();
return;
}
// Fallback logic: try finding any common primary action buttons in expected order
const candidates = [
{ 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;
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,
});
if (await btn.count()) {
await btn.click();
await this.waitForPageLoad();
return;
}
}
// If none of the expected action buttons are present, throw an error
throw new Error(
"Could not find an actionable Next/Save/Launch scan button on this step",
);
}
async selectCredentialsType(type: "role" | "credentials"): Promise<void> {
// Ensure we are on the add-credentials page where the selector exists
await expect(this.page).toHaveURL(/\/providers\/add-credentials/);
if (type === "role") {
await this.roleCredentialsRadio.click({ force: true });
} else {
await this.staticCredentialsRadio.click({ force: true });
}
await this.waitForPageLoad();
}
async fillRoleCredentials(credentials: ProviderCredentials): Promise<void> {
// Fill the role credentials form
if (credentials.accessKeyId) {
await this.accessKeyIdInput.fill(credentials.accessKeyId);
}
if (credentials.secretAccessKey) {
await this.secretAccessKeyInput.fill(credentials.secretAccessKey);
}
if (credentials.roleArn) {
await this.roleArnInput.fill(credentials.roleArn);
}
if (credentials.externalId) {
// External ID may be prefilled and disabled; only fill if enabled
if (await this.externalIdInput.isEnabled()) {
await this.externalIdInput.fill(credentials.externalId);
}
}
}
async fillStaticCredentials(credentials: ProviderCredentials): Promise<void> {
// Fill the static credentials form
if (credentials.accessKeyId) {
await this.accessKeyIdInput.fill(credentials.accessKeyId);
}
if (credentials.secretAccessKey) {
await this.secretAccessKeyInput.fill(credentials.secretAccessKey);
}
}
async verifyPageLoaded(): Promise<void> {
// Verify the providers page is loaded
await expect(this.page).toHaveTitle(/Prowler/);
await expect(this.addProviderButton).toBeVisible();
}
async verifyConnectAccountPageLoaded(): Promise<void> {
// Verify the connect account page is loaded
await expect(this.page).toHaveTitle(/Prowler/);
await expect(this.awsProviderRadio).toBeVisible();
}
async verifyCredentialsPageLoaded(): Promise<void> {
// Verify the credentials page is loaded
await expect(this.page).toHaveTitle(/Prowler/);
await expect(this.roleCredentialsRadio).toBeVisible();
}
async verifyLaunchScanPageLoaded(): Promise<void> {
// Verify the launch scan page is loaded
await expect(this.page).toHaveTitle(/Prowler/);
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 verifyProviderExists(providerUID: string): Promise<boolean> {
// Filter search by providerUID
await this.filterProvidersByProviderUID(providerUID);
// Verify if table has 1 row
if (!(await this.verifySingleRowForProviderUID(providerUID))) {
return false;
}
return true;
}
async verifyLoadProviderPageAfterNewProvider(): Promise<void> {
// Verify the provider page is loaded
await expect(this.page).toHaveTitle(/Prowler/);
await expect(this.providersTable).toBeVisible();
}
async verifySingleRowForProviderUID(providerUID: string): Promise<boolean> {
// Verify if table has 1 row and that row contains providerUID
await expect(this.providersTable).toBeVisible();
const matchingRows = this.providersTable.locator("tbody tr", {
hasText: providerUID,
});
const count = await matchingRows.count();
if (count !== 1) return false;
return true;
}
async filterProvidersByProviderUID(providerUID: string): Promise<void> {
// Filter providers by providerUID
await this.goto();
await expect(this.providersTable).toBeVisible();
const searchInput = this.page.getByPlaceholder(/search|filter/i);
if (!searchInput) {
throw new Error("No search input available");
}
// Clear existing content and type the providerUID
await searchInput.fill("");
await searchInput.type(providerUID, { delay: 20 });
// Try to submit the filter if the input acts on Enter
try {
await searchInput.press("Enter");
} catch (error) {
// Enter press might not be needed for all UIs
}
// Wait for table to update
await this.waitForPageLoad();
}
async actionDeleteProvider(providerUID: string): Promise<void> {
// Delete the provider
// Filter search by providerUID
await this.filterProvidersByProviderUID(providerUID);
await expect(this.providersTable).toBeVisible();
// Find the first row in the filtered results
const firstRow = this.providersTable.locator("tbody tr").first();
await expect(firstRow).toBeVisible();
const lastCell = firstRow.locator("td").last();
const actionButton = lastCell.locator("button").first();
await actionButton.click();
// Wait for the dropdown menu to appear
await this.page.waitForSelector('[role="menu"], [role="menuitem"]', {
timeout: 5000,
});
const deleteMenuItem = this.page.getByRole("menuitem", {
name: /delete.*provider/i,
});
await expect(deleteMenuItem).toBeVisible();
// Click the delete menu item with force to handle unstable elements
await deleteMenuItem.click({ force: true });
// Wait for the delete confirmation modal to appear
await this.page.waitForSelector(
'[role="dialog"], .modal, [data-testid*="modal"]',
{ timeout: 10000 },
);
// Find the delete confirmation button with multiple approaches
let deleteButton = this.deleteProviderConfirmationButton;
await expect(deleteButton).toBeVisible();
// Click the delete button with force to handle unstable elements
await deleteButton.click({ force: true });
// Wait for the modal to disappear and the page to update
const modalToCheck = this.page
.locator('[role="dialog"], .modal, [data-testid*="modal"]')
.first();
await expect(modalToCheck).not.toBeVisible();
await this.waitForPageLoad();
}
}
+109
View File
@@ -0,0 +1,109 @@
### E2E Tests: AWS Provider Management
**Suite ID:** `PROVIDER-E2E`
**Feature:** AWS Provider Management - Add and configure AWS cloud providers with different authentication methods
---
## Test Case: `PROVIDER-E2E-001` - Add AWS Provider with Static Credentials
**Priority:** `critical`
**Tags:**
- type → @e2e, @serial
- feature → @providers
- provider → @aws
**Description/Objective:** Validates the complete flow of adding a new AWS provider using static access key credentials
**Preconditions:**
- Admin user authentication required (admin.auth.setup setup)
- Environment variables configured: E2E_AWS_PROVIDER_ACCOUNT_ID, E2E_AWS_PROVIDER_ACCESS_KEY and E2E_AWS_PROVIDER_SECRET_KEY
- 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 "credentials" authentication type
6. Fill static credentials (access key and secret key)
7. Launch initial scan
8. Verify redirect to provider management page
### Expected Result:
- AWS provider successfully added with static credentials
- Initial scan launched successfully
- User redirected to provider details page
### Key verification points:
- Provider page loads correctly
- Connect account page displays AWS option
- Credentials form accepts static credentials
- Launch scan page appears
- Successful redirect to provider page after scan launch
### Notes:
- Test uses environment variables for AWS credentials
- Provider cleanup performed before each test to ensure clean state
- Requires valid AWS account with appropriate permissions
---
## Test Case: `PROVIDER-E2E-002` - Add AWS Provider with Assume Role Credentials Access Key and Secret Key
**Priority:** `critical`
**Tags:**
- type → @e2e, @serial
- feature → @providers
- provider → @aws
**Description/Objective:** Validates the complete flow of adding a new AWS provider using role-based authentication with Access Key and Secret Key
**Preconditions:**
- Admin user authentication required (admin.auth.setup setup)
- Environment variables configured: E2E_AWS_PROVIDER_ACCOUNT_ID, E2E_AWS_PROVIDER_ACCESS_KEY, E2E_AWS_PROVIDER_SECRET_KEY, 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. Fill role credentials (access key, secret key, and role ARN)
7. Launch initial scan
8. Verify redirect to provider management page
### Expected Result:
- AWS provider successfully added with role credentials
- Initial scan launched successfully
- User redirected to provider details page
### Key verification points:
- Provider page loads correctly
- 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
### Notes:
- Test uses environment variables for AWS credentials and role ARN
- Provider cleanup performed before each test to ensure clean state
- Requires valid AWS account with role assumption permissions
- Role ARN must be properly configured
+156
View File
@@ -0,0 +1,156 @@
import { test } from "@playwright/test";
import * as helpers from "../helpers";
import {
ProvidersPage,
AWSProviderData,
ProviderCredentials,
} from "./providers-page";
// Configure serial execution for provider tests
test.describe.serial("Add AWS Provider", () => {
let providersPage: ProvidersPage;
// Test data from environment variables
const accountId = process.env.E2E_AWS_PROVIDER_ACCOUNT_ID;
const accessKey = process.env.E2E_AWS_PROVIDER_ACCESS_KEY;
const secretKey = process.env.E2E_AWS_PROVIDER_SECRET_KEY;
const roleArn = process.env.E2E_AWS_PROVIDER_ROLE_ARN;
// Validate required environment variables
if (!accountId) {
throw new Error(
"E2E_AWS_PROVIDER_ACCOUNT_ID environment variable is not set",
);
}
// Setup before each test
test.beforeEach(async ({ page }) => {
providersPage = new ProvidersPage(page);
// Clean up existing provider to ensure clean test state
await helpers.deleteProviderIfExists(page, accountId);
});
// Use admin authentication for provider management
test.use({ storageState: "playwright/.auth/admin_user.json" });
test(
"should add a new AWS provider with static credentials",
{
tag: ["@critical", "@e2e", "@providers", "@aws", "@serial", "@PROVIDER-E2E-001"],
},
async ({ page }) => {
// Validate required environment variables
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",
);
}
// Prepare test data for AWS provider
const awsProviderData: AWSProviderData = {
accountId: accountId,
alias: "Test E2E AWS Account - Credentials",
};
// Prepare static credentials
const staticCredentials: ProviderCredentials = {
type: "credentials",
accessKeyId: accessKey,
secretAccessKey: secretKey,
};
// 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 static credentials type
await providersPage.selectCredentialsType("credentials");
await providersPage.verifyCredentialsPageLoaded();
// Fill static credentials
await providersPage.fillStaticCredentials(staticCredentials);
await providersPage.clickNext();
// Launch scan
await providersPage.verifyLaunchScanPageLoaded();
await providersPage.clickNext();
// Wait for redirect to provider page
await providersPage.verifyLoadProviderPageAfterNewProvider();
}
)
test(
"should add a new AWS provider with assume role credentials with Access Key and Secret Key",
{
tag: ["@critical", "@e2e", "@providers", "@aws","@serial", "@PROVIDER-E2E-002"],
},
async ({ page }) => {
// Validate required environment variables
if (!accountId || !accessKey || !secretKey || !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",
);
}
// Prepare test data for AWS provider
const awsProviderData: AWSProviderData = {
accountId: accountId,
alias: "Test E2E AWS Account - Credentials",
};
// Prepare role-based credentials
const roleCredentials: ProviderCredentials = {
type: "role",
accessKeyId: accessKey,
secretAccessKey: secretKey,
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("role");
await providersPage.verifyCredentialsPageLoaded();
// Fill role credentials
await providersPage.fillRoleCredentials(roleCredentials);
await providersPage.clickNext();
// Launch scan
await providersPage.verifyLaunchScanPageLoaded();
await providersPage.clickNext();
// Wait for redirect to provider page
await providersPage.verifyLoadProviderPageAfterNewProvider();
}
);
});