diff --git a/.github/workflows/ui-e2e-tests.yml b/.github/workflows/ui-e2e-tests.yml index dea6f1f3e2..a28c250452 100644 --- a/.github/workflows/ui-e2e-tests.yml +++ b/.github/workflows/ui-e2e-tests.yml @@ -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 diff --git a/ui/package.json b/ui/package.json index 40a51c04f1..b8732ad1fd 100644 --- a/ui/package.json +++ b/ui/package.json @@ -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" }, diff --git a/ui/playwright.config.ts b/ui/playwright.config.ts index 59f49f935f..91f3f30b5e 100644 --- a/ui/playwright.config.ts +++ b/ui/playwright.config.ts @@ -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: { diff --git a/ui/tests/helpers.ts b/ui/tests/helpers.ts index 1e93bd396d..fbe151d17d 100644 --- a/ui/tests/helpers.ts +++ b/ui/tests/helpers.ts @@ -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); + } +} diff --git a/ui/tests/providers/providers-page.ts b/ui/tests/providers/providers-page.ts new file mode 100644 index 0000000000..f288cf0b52 --- /dev/null +++ b/ui/tests/providers/providers-page.ts @@ -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 { + await super.goto("/providers"); + } + + async clickAddProvider(): Promise { + await this.addProviderButton.click(); + await this.waitForPageLoad(); + } + + async selectAWSProvider(): Promise { + // Prefer label-based click for radios, force if overlay intercepts + await this.awsProviderRadio.click({ force: true }); + await this.waitForPageLoad(); + } + + async fillAWSProviderDetails(data: AWSProviderData): Promise { + await this.accountIdInput.fill(data.accountId); + if (data.alias) { + await this.aliasInput.fill(data.alias); + } + } + + async clickNext(): Promise { + // 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 { + // 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 { + // 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 { + // 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 { + // Verify the providers page is loaded + await expect(this.page).toHaveTitle(/Prowler/); + await expect(this.addProviderButton).toBeVisible(); + } + + async verifyConnectAccountPageLoaded(): Promise { + // Verify the connect account page is loaded + await expect(this.page).toHaveTitle(/Prowler/); + await expect(this.awsProviderRadio).toBeVisible(); + } + + async verifyCredentialsPageLoaded(): Promise { + // Verify the credentials page is loaded + await expect(this.page).toHaveTitle(/Prowler/); + await expect(this.roleCredentialsRadio).toBeVisible(); + } + + async verifyLaunchScanPageLoaded(): Promise { + // 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 { + // 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 { + // Verify the provider page is loaded + await expect(this.page).toHaveTitle(/Prowler/); + await expect(this.providersTable).toBeVisible(); + } + + async verifySingleRowForProviderUID(providerUID: string): Promise { + // 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 { + // 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 { + // 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(); + } +} diff --git a/ui/tests/providers/providers.md b/ui/tests/providers/providers.md new file mode 100644 index 0000000000..3c160e946f --- /dev/null +++ b/ui/tests/providers/providers.md @@ -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 diff --git a/ui/tests/providers/providers.spec.ts b/ui/tests/providers/providers.spec.ts new file mode 100644 index 0000000000..dd4ba74769 --- /dev/null +++ b/ui/tests/providers/providers.spec.ts @@ -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(); + } + ); +});