From 670d9a8f87f3eb80b64714d4f4f7bb502772d8b5 Mon Sep 17 00:00:00 2001 From: StylusFrost Date: Thu, 23 Oct 2025 15:10:13 +0200 Subject: [PATCH] test(ui): refactor ProvidersPage and enhance M365 provider tests - Refactored the ProvidersPage class to improve the structure and clarity of provider data and credential handling for AWS, AZURE, and M365 providers. - Introduced new methods for filling provider details and credentials, ensuring better organization and maintainability of the test code. - Updated M365 provider tests to utilize the new structure, including verification of credential pages and improved error handling during provider management. - Enhanced the test setup to ensure a clean state by deleting existing providers before each test. --- ui/tests/providers/providers-page.ts | 1085 ++++++++++++++++---------- ui/tests/providers/providers.spec.ts | 24 +- 2 files changed, 682 insertions(+), 427 deletions(-) diff --git a/ui/tests/providers/providers-page.ts b/ui/tests/providers/providers-page.ts index ed22ad5cd3..4857ddb879 100644 --- a/ui/tests/providers/providers-page.ts +++ b/ui/tests/providers/providers-page.ts @@ -1,439 +1,688 @@ -import { test } from "@playwright/test"; -import * as helpers from "../helpers"; -import { - ProvidersPage, - AWSProviderData, - AWSProviderCredential, - AWS_CREDENTIAL_OPTIONS, - AZUREProviderData, - AZUREProviderCredential, - AZURE_CREDENTIAL_OPTIONS, - M365ProviderData, - M365ProviderCredential, - M365_CREDENTIAL_OPTIONS, -} from "./providers-page"; -import { ScansPage } from "../scans/scans-page"; -import fs from "fs"; +import { Page, Locator, expect } from "@playwright/test"; +import { BasePage } from "../base-page"; -test.describe("Add Provider", () => { - test.describe.serial("Add AWS Provider", () => { - // Providers page object - let providersPage: ProvidersPage; - let scansPage: ScansPage; - // Test data from environment variables - const accountId = process.env.E2E_AWS_PROVIDER_ACCOUNT_ID; - const accessKey = process.env.E2E_AWS_PROVIDER_ACCESS_KEY; - const secretKey = process.env.E2E_AWS_PROVIDER_SECRET_KEY; - const roleArn = process.env.E2E_AWS_PROVIDER_ROLE_ARN; +// AWS provider data +export interface AWSProviderData { + accountId: string; + alias?: string; +} - // Validate required environment variables - if (!accountId) { - throw new Error( - "E2E_AWS_PROVIDER_ACCOUNT_ID environment variable is not set", - ); - } +// AZURE provider data +export interface AZUREProviderData { + subscriptionId: string; + alias?: string; +} - // Setup before each test - test.beforeEach(async ({ page }) => { - providersPage = new ProvidersPage(page); - // Clean up existing provider to ensure clean test state - await providersPage.deleteProviderIfExists(accountId); +// M365 provider data +export interface M365ProviderData { + domainId: string; + alias?: string; +} + +// AWS credential options +export const AWS_CREDENTIAL_OPTIONS = { + AWS_ROLE_ARN: "role", + AWS_CREDENTIALS: "credentials" +} as const; + +// AWS credential type +type AWSCredentialType = (typeof AWS_CREDENTIAL_OPTIONS)[keyof typeof AWS_CREDENTIAL_OPTIONS]; + +// AWS provider credential +export interface AWSProviderCredential { + type: AWSCredentialType; + roleArn?: string; + externalId?: string; + accessKeyId?: string; + secretAccessKey?: string; +} + +// AZURE credential options +export const AZURE_CREDENTIAL_OPTIONS = { + AZURE_CREDENTIALS: "credentials" +} as const; + +// AZURE credential type +type AZURECredentialType = (typeof AZURE_CREDENTIAL_OPTIONS)[keyof typeof AZURE_CREDENTIAL_OPTIONS]; + +// AZURE provider credential +export interface AZUREProviderCredential { + type: AZURECredentialType; + clientId:string; + clientSecret:string; + tenantId:string; +} + +// M365 credential options +export const M365_CREDENTIAL_OPTIONS = { + M365_CREDENTIALS: "credentials", + M365_CERTIFICATE_CREDENTIALS: "certificate" +} as const; + +// M365 credential type +type M365CredentialType = (typeof M365_CREDENTIAL_OPTIONS)[keyof typeof M365_CREDENTIAL_OPTIONS]; + +// M365 provider credential +export interface M365ProviderCredential { + type: M365CredentialType; + clientId:string; + clientSecret?:string; + tenantId:string; + certificateContent?:string; +} + +// Providers page +export class ProvidersPage extends BasePage { + + // Button to add a new cloud provider + 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; + + // M365 credentials type selection + readonly m365StaticCredentialsRadio: Locator; + readonly m365CertificateCredentialsRadio: Locator; + + // AWS role credentials form + readonly roleArnInput: Locator; + readonly externalIdInput: Locator; + + // AWS static credentials form + readonly accessKeyIdInput: Locator; + readonly secretAccessKeyInput: Locator; + + // AZURE provider form elements + readonly azureSubscriptionIdInput: Locator; + readonly azureClientIdInput: Locator; + readonly azureClientSecretInput: Locator; + readonly azureTenantIdInput: Locator; + + // M365 provider form elements + readonly m365domainIdInput: Locator; + readonly m365ClientIdInput: Locator; + readonly m365ClientSecretInput: Locator; + readonly m365TenantIdInput: Locator; + readonly m365CertificateContentInput: Locator; + + // Delete button + readonly deleteProviderConfirmationButton: Locator; + + constructor(page: Page) { + super(page); + + // Button to add a new cloud provider + this.addProviderButton = page.getByRole("button", { name: "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.getByRole("textbox", { name: "Account ID" }); + + // AZURE provider form inputs + this.azureSubscriptionIdInput = page.getByRole("textbox", { name: "Subscription ID" }); + this.azureClientIdInput = page.getByRole("textbox", { name: "Client ID" }); + this.azureClientSecretInput = page.getByRole("textbox", { name: "Client Secret" }); + this.azureTenantIdInput = page.getByRole("textbox", { name: "Tenant ID" }); + + // M365 provider form inputs + this.m365domainIdInput = page.getByRole("textbox", { name: "Domain ID" }); + this.m365ClientIdInput = page.getByRole("textbox", { name: "Client ID" }); + this.m365ClientSecretInput = page.getByRole("textbox", { name: "Client Secret" }); + this.m365TenantIdInput = page.getByRole("textbox", { name: "Tenant ID" }); + this.m365CertificateContentInput = page.getByRole("textbox", { name: "Certificate Content" }); + + // Alias input + this.aliasInput = page.getByRole("textbox", { name: "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 }); + + // Button to launch a scan + this.launchScanButton = page.getByRole("button", { + name: "Launch scan", + exact: true, }); - // 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: AWSProviderCredential = { - type: AWS_CREDENTIAL_OPTIONS.AWS_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( - AWS_CREDENTIAL_OPTIONS.AWS_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 - scansPage = new ScansPage(page); - await scansPage.verifyPageLoaded(); - }, - ); - - 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: AWSProviderCredential = { - type: AWS_CREDENTIAL_OPTIONS.AWS_ROLE_ARN, - 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( - AWS_CREDENTIAL_OPTIONS.AWS_ROLE_ARN, - ); - 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 - scansPage = new ScansPage(page); - await scansPage.verifyPageLoaded(); - }, - ); - }); - - test.describe.serial("Add AZURE Provider", () => { - // Providers page object - let providersPage: ProvidersPage; - let scansPage: ScansPage; - - // Test data from environment variables - const subscriptionId = process.env.E2E_AZURE_SUBSCRIPTION_ID; - const clientId = process.env.E2E_AZURE_CLIENT_ID; - const clientSecret = process.env.E2E_AZURE_SECRET_ID; - const tenantId = process.env.E2E_AZURE_TENANT_ID; - - // Validate required environment variables - if (!subscriptionId || !clientId || !clientSecret || !tenantId) { - throw new Error( - "E2E_AZURE_SUBSCRIPTION_ID, E2E_AZURE_CLIENT_ID, E2E_AZURE_SECRET_ID, and E2E_AZURE_TENANT_ID environment variables are not set", - ); - } - - // Setup before each test - test.beforeEach(async ({ page }) => { - providersPage = new ProvidersPage(page); - // Clean up existing provider to ensure clean test state - await providersPage.deleteProviderIfExists(subscriptionId); + // 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, }); - // Use admin authentication for provider management - test.use({ storageState: "playwright/.auth/admin_user.json" }); - - test( - "should add a new Azure provider with static credentials", - { - tag: [ - "@critical", - "@e2e", - "@providers", - "@azure", - "@serial", - "@PROVIDER-E2E-003", - ], - }, - async ({ page }) => { - // Prepare test data for AZURE provider - const azureProviderData: AZUREProviderData = { - subscriptionId: subscriptionId, - alias: "Test E2E AZURE Account - Credentials", - }; - - // Prepare static credentials - const azureCredentials: AZUREProviderCredential = { - type: AZURE_CREDENTIAL_OPTIONS.AZURE_CREDENTIALS, - clientId: clientId, - clientSecret: clientSecret, - tenantId: tenantId, - }; - - // Navigate to providers page - await providersPage.goto(); - await providersPage.verifyPageLoaded(); - - // Start adding new provider - await providersPage.clickAddProvider(); - await providersPage.verifyConnectAccountPageLoaded(); - - // Select AZURE provider - await providersPage.selectAZUREProvider(); - - // Fill provider details - await providersPage.fillAZUREProviderDetails(azureProviderData); - await providersPage.clickNext(); - - // Fill static credentials details - await providersPage.fillAZURECredentials(azureCredentials); - await providersPage.clickNext(); - - // Launch scan - await providersPage.verifyLaunchScanPageLoaded(); - await providersPage.clickNext(); - - // Wait for redirect to scan page - scansPage = new ScansPage(page); - await scansPage.verifyPageLoaded(); - }, - ); - }); - - test.describe.serial("Add M365 Provider", () => { - // Providers page object - let providersPage: ProvidersPage; - let scansPage: ScansPage; - - // Test data from environment variables - const domainId = process.env.E2E_M365_DOMAIN_ID; - const clientId = process.env.E2E_M365_CLIENT_ID; - const tenantId = process.env.E2E_M365_TENANT_ID; - - // Validate required environment variables - if (!domainId || !clientId || !tenantId) { - throw new Error( - "E2E_M365_DOMAIN_ID, E2E_M365_CLIENT_ID, and E2E_M365_TENANT_ID environment variables are not set", - ); - } - - // Setup before each test - test.beforeEach(async ({ page }) => { - providersPage = new ProvidersPage(page); - // Clean up existing provider to ensure clean test state - await providersPage.deleteProviderIfExists(domainId); + // Radios for selecting M365 credentials method + this.m365StaticCredentialsRadio = page.getByRole("radio", { + name: /App Client Secret Credentials/i, + }); + this.m365CertificateCredentialsRadio = page.getByRole("radio", { + name: /App Certificate Credentials/i, }); - // Use admin authentication for provider management - test.use({ storageState: "playwright/.auth/admin_user.json" }); + // Inputs for IAM Role credentials + this.roleArnInput = page.getByRole("textbox", { name: "Role ARN" }); + this.externalIdInput = page.getByRole("textbox", { name: "External ID" }); - test( - "should add a new M365 provider with static credentials", - { - tag: [ - "@critical", - "@e2e", - "@providers", - "@m365", - "@serial", - "@PROVIDER-E2E-004", - ], - }, - async ({ page }) => { - // Validate required environment variables - const clientSecret = process.env.E2E_M365_SECRET_ID; + // Inputs for static credentials + this.accessKeyIdInput = page.getByRole("textbox", { name: "Access Key ID" }); + this.secretAccessKeyInput = page.getByRole("textbox", { name: "Secret Access Key" }); - if (!clientSecret) { - throw new Error("E2E_M365_SECRET_ID environment variable is not set"); + // Delete button in confirmation modal + this.deleteProviderConfirmationButton = page.getByRole("button", { + name: "Delete", + exact: true, + }); + } + + async goto(): Promise { + // Go to the providers page + + await super.goto("/providers"); + } + + async clickAddProvider(): Promise { + // Click the add provider button + + 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 selectAZUREProvider(): Promise { + + // Prefer label-based click for radios, force if overlay intercepts + await this.azureProviderRadio.click({ force: true }); + await this.waitForPageLoad(); + } + + async selectM365Provider(): Promise { + // Select the M365 provider + + await this.m365ProviderRadio.click({ force: true }); + await this.waitForPageLoad(); + } + + + async fillAWSProviderDetails(data: AWSProviderData): Promise { + // Fill the AWS provider details + + await this.accountIdInput.fill(data.accountId); + + if (data.alias) { + await this.aliasInput.fill(data.alias); + } + } + + async fillAZUREProviderDetails(data: AZUREProviderData): Promise { + // Fill the AWS provider details + + await this.azureSubscriptionIdInput.fill(data.subscriptionId); + + if (data.alias) { + await this.aliasInput.fill(data.alias); + } + } + + async fillM365ProviderDetails(data: M365ProviderData): Promise { + // Fill the M365 provider details + + await this.m365domainIdInput.fill(data.domainId); + + 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. + + // Get the current page URL + const url = this.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(); + + // Wait for either success (redirect to scans) or error message to appear + // The error container has multiple p.text-danger elements, we want the first one with the technical error + const errorMessage = this.page.locator("p.text-danger").first(); + + 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"}`, + ); + } } - // Prepare test data for M365 provider - const m365ProviderData: M365ProviderData = { - domainId: domainId, - alias: "Test E2E M365 Account - Credentials", - }; - - // Prepare static credentials - const m365Credentials: M365ProviderCredential = { - type: M365_CREDENTIAL_OPTIONS.M365_CREDENTIALS, - clientId: clientId, - clientSecret: clientSecret, - tenantId: tenantId, - }; - - // 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.selectM365Provider(); - - // Fill provider details - await providersPage.fillM365ProviderDetails(m365ProviderData); - await providersPage.clickNext(); - - // Select static credentials type - await providersPage.selectM365CredentialsType( - M365_CREDENTIAL_OPTIONS.M365_CREDENTIALS, - ); - - // Verify M365 credentials page is loaded - await providersPage.verifyM365CredentialsPageLoaded(); - - // Fill static credentials details - await providersPage.fillM365Credentials(m365Credentials); - await providersPage.clickNext(); - - // Launch scan - await providersPage.verifyLaunchScanPageLoaded(); - await providersPage.clickNext(); - - // Wait for redirect to scan page - scansPage = new ScansPage(page); - await scansPage.verifyPageLoaded(); - }, - ); - - test( - "should add a new M365 provider with certificate", - { - tag: [ - "@critical", - "@e2e", - "@providers", - "@m365", - "@serial", - "@PROVIDER-E2E-005", - ], - }, - async ({ page }) => { - // Validate required environment variables - const certificateContent = process.env.E2E_M365_CERTIFICATE_CONTENT; - - if (!certificateContent) { + } 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( - "E2E_M365_CERTIFICATE_CONTENT environment variable is not set", + `Test connection failed with error: ${errorText?.trim() || "Unknown error"}`, ); } + // Re-throw original error if no error message found + throw error; + } - // Prepare test data for M365 provider - const m365ProviderData: M365ProviderData = { - domainId: domainId, - alias: "Test E2E M365 Account - Certificate", - }; + return; + } - // Prepare static credentials - const m365Credentials: M365ProviderCredential = { - type: M365_CREDENTIAL_OPTIONS.M365_CERTIFICATE_CREDENTIALS, - clientId: clientId, - tenantId: tenantId, - certificateContent: certificateContent, - }; + // 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; - // Navigate to providers page - await providersPage.goto(); - await providersPage.verifyPageLoaded(); + // 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, + }); - // Start adding new provider - await providersPage.clickAddProvider(); - await providersPage.verifyConnectAccountPageLoaded(); + if (await btn.count()) { + await btn.click(); + await this.waitForPageLoad(); + return; + } + } - // Select M365 provider - await providersPage.selectM365Provider(); - - // Fill provider details - await providersPage.fillM365ProviderDetails(m365ProviderData); - await providersPage.clickNext(); - - // Select static credentials type - await providersPage.selectM365CredentialsType( - M365_CREDENTIAL_OPTIONS.M365_CERTIFICATE_CREDENTIALS, - ); - - // Verify M365 certificate credentials page is loaded - await providersPage.verifyM365CertificateCredentialsPageLoaded(); - - // Fill static credentials details - await providersPage.fillM365CertificateCredentials(m365Credentials); - await providersPage.clickNext(); - - // Launch scan - await providersPage.verifyLaunchScanPageLoaded(); - await providersPage.clickNext(); - - // Wait for redirect to scan page - scansPage = new ScansPage(page); - await scansPage.verifyPageLoaded(); - }, + // 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", ); - }); -}); \ No newline at end of file + } + + async selectCredentialsType(type: AWSCredentialType): Promise { + // 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) { + await this.staticCredentialsRadio.click({ force: true }); + } else { + throw new Error(`Invalid AWS credential type: ${type}`); + } + // Wait for the page to load + await this.waitForPageLoad(); + } + + async selectM365CredentialsType(type: M365CredentialType): Promise { + // 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) { + await this.m365CertificateCredentialsRadio.click({ force: true }); + } else { + throw new Error(`Invalid M365 credential type: ${type}`); + } + // Wait for the page to load + await this.waitForPageLoad(); + } + + async fillRoleCredentials(credentials: AWSProviderCredential): 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: AWSProviderCredential): 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 fillAZURECredentials(credentials: AZUREProviderCredential): Promise { + // Fill the azure credentials form + + if (credentials.clientId) { + await this.azureClientIdInput.fill(credentials.clientId); + } + if (credentials.clientSecret) { + await this.azureClientSecretInput.fill(credentials.clientSecret); + } + if (credentials.tenantId) { + await this.azureTenantIdInput.fill(credentials.tenantId); + } + } + + async fillM365Credentials(credentials: M365ProviderCredential): Promise { + // Fill the m365 credentials form + + if (credentials.clientId) { + await this.m365ClientIdInput.fill(credentials.clientId); + } + if (credentials.clientSecret) { + await this.m365ClientSecretInput.fill(credentials.clientSecret); + } + if (credentials.tenantId) { + await this.m365TenantIdInput.fill(credentials.tenantId); + } + } + + async fillM365CertificateCredentials(credentials: M365ProviderCredential): Promise { + // Fill the m365 certificate credentials form + + if (credentials.clientId) { + await this.m365ClientIdInput.fill(credentials.clientId); + } + if (credentials.certificateContent) { + await this.m365CertificateContentInput.fill(credentials.certificateContent); + } + if (credentials.tenantId) { + await this.m365TenantIdInput.fill(credentials.tenantId); + } + } + + async verifyPageLoaded(): Promise { + // Verify the providers page is loaded + + await expect(this.page).toHaveTitle(/Prowler/); + await expect(this.addProviderButton).toBeVisible(); + await this.page.waitForLoadState('networkidle'); + } + + 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 verifyM365CredentialsPageLoaded(): Promise { + // Verify the M365 credentials page is loaded + + await expect(this.page).toHaveTitle(/Prowler/); + await expect(this.m365ClientIdInput).toBeVisible(); + await expect(this.m365ClientSecretInput).toBeVisible(); + await expect(this.m365TenantIdInput).toBeVisible(); + } + + async verifyM365CertificateCredentialsPageLoaded(): Promise { + // Verify the M365 certificate credentials page is loaded + + await expect(this.page).toHaveTitle(/Prowler/); + await expect(this.m365ClientIdInput).toBeVisible(); + await expect(this.m365TenantIdInput).toBeVisible(); + await expect(this.m365CertificateContentInput).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 verifyLoadProviderPageAfterNewProvider(): Promise { + // Verify the provider page is loaded + + await this.waitForPageLoad(); + 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(); + + // Get the matching rows + const matchingRows = this.providersTable.locator("tbody tr", { + hasText: providerUID, + }); + + // Verify the number of matching rows is 1 + const count = await matchingRows.count(); + if (count !== 1) return false; + return true; + } + + async deleteProviderIfExists(providerUID: string): Promise { + // 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 => { + 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 => { + 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 }); + } +} diff --git a/ui/tests/providers/providers.spec.ts b/ui/tests/providers/providers.spec.ts index de12cad506..6471a17863 100644 --- a/ui/tests/providers/providers.spec.ts +++ b/ui/tests/providers/providers.spec.ts @@ -1,5 +1,4 @@ import { test } from "@playwright/test"; -import * as helpers from "../helpers"; import { ProvidersPage, AWSProviderData, @@ -13,7 +12,6 @@ import { M365_CREDENTIAL_OPTIONS, } from "./providers-page"; import { ScansPage } from "../scans/scans-page"; -import fs from "fs"; test.describe("Add Provider", () => { test.describe.serial("Add AWS Provider", () => { @@ -266,9 +264,10 @@ test.describe("Add Provider", () => { ); }); - test.describe("Add M365 Provider", () => { + test.describe.serial("Add M365 Provider", () => { // Providers page object let providersPage: ProvidersPage; + let scansPage: ScansPage; // Test data from environment variables const domainId = process.env.E2E_M365_DOMAIN_ID; @@ -286,7 +285,7 @@ test.describe("Add Provider", () => { test.beforeEach(async ({ page }) => { providersPage = new ProvidersPage(page); // Clean up existing provider to ensure clean test state - await helpers.deleteProviderIfExists(page, domainId); + await providersPage.deleteProviderIfExists(domainId); }); // Use admin authentication for provider management @@ -344,6 +343,8 @@ test.describe("Add Provider", () => { await providersPage.selectM365CredentialsType( M365_CREDENTIAL_OPTIONS.M365_CREDENTIALS, ); + + // Verify M365 credentials page is loaded await providersPage.verifyM365CredentialsPageLoaded(); // Fill static credentials details @@ -354,8 +355,9 @@ test.describe("Add Provider", () => { await providersPage.verifyLaunchScanPageLoaded(); await providersPage.clickNext(); - // Wait for redirect to provider page - await providersPage.verifyLoadProviderPageAfterNewProvider(); + // Wait for redirect to scan page + scansPage = new ScansPage(page); + await scansPage.verifyPageLoaded(); }, ); @@ -414,7 +416,9 @@ test.describe("Add Provider", () => { await providersPage.selectM365CredentialsType( M365_CREDENTIAL_OPTIONS.M365_CERTIFICATE_CREDENTIALS, ); - await providersPage.verifyM365CredentialsPageLoaded(); + + // Verify M365 certificate credentials page is loaded + await providersPage.verifyM365CertificateCredentialsPageLoaded(); // Fill static credentials details await providersPage.fillM365CertificateCredentials(m365Credentials); @@ -424,9 +428,11 @@ test.describe("Add Provider", () => { await providersPage.verifyLaunchScanPageLoaded(); await providersPage.clickNext(); - // Wait for redirect to provider page - await providersPage.verifyLoadProviderPageAfterNewProvider(); + // Wait for redirect to scan page + scansPage = new ScansPage(page); + await scansPage.verifyPageLoaded(); }, ); }); + });