mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-01-25 02:08:11 +00:00
refactor(ui): redo the whole app with styles (#9234)
This commit is contained in:
@@ -97,7 +97,9 @@ export async function verifyNormalModeActive(page: Page) {
|
||||
}
|
||||
|
||||
export async function logout(page: Page) {
|
||||
await page.getByRole("button", { name: "Sign out" }).click();
|
||||
const navbar = page.locator("header");
|
||||
await navbar.waitFor({ state: "visible" });
|
||||
await navbar.getByRole("button", { name: "Sign out" }).click();
|
||||
}
|
||||
|
||||
export async function verifyLogoutSuccess(page: Page) {
|
||||
@@ -146,7 +148,9 @@ export async function authenticateAndSaveState(
|
||||
storagePath: string,
|
||||
) {
|
||||
if (!email || !password) {
|
||||
throw new Error('Email and password are required for authentication and save state');
|
||||
throw new Error(
|
||||
"Email and password are required for authentication and save state",
|
||||
);
|
||||
}
|
||||
|
||||
// Create SignInPage instance
|
||||
|
||||
@@ -10,7 +10,7 @@ export interface AWSProviderData {
|
||||
accessKeyId?: string;
|
||||
secretAccessKey?: string;
|
||||
}
|
||||
|
||||
|
||||
// AZURE provider data
|
||||
export interface AZUREProviderData {
|
||||
subscriptionId: string;
|
||||
@@ -26,11 +26,12 @@ export interface M365ProviderData {
|
||||
// AWS credential options
|
||||
export const AWS_CREDENTIAL_OPTIONS = {
|
||||
AWS_ROLE_ARN: "role",
|
||||
AWS_CREDENTIALS: "credentials"
|
||||
AWS_CREDENTIALS: "credentials",
|
||||
} as const;
|
||||
|
||||
// AWS credential type
|
||||
type AWSCredentialType = (typeof AWS_CREDENTIAL_OPTIONS)[keyof typeof AWS_CREDENTIAL_OPTIONS];
|
||||
type AWSCredentialType =
|
||||
(typeof AWS_CREDENTIAL_OPTIONS)[keyof typeof AWS_CREDENTIAL_OPTIONS];
|
||||
|
||||
// AWS provider credential
|
||||
export interface AWSProviderCredential {
|
||||
@@ -43,41 +44,42 @@ export interface AWSProviderCredential {
|
||||
|
||||
// AZURE credential options
|
||||
export const AZURE_CREDENTIAL_OPTIONS = {
|
||||
AZURE_CREDENTIALS: "credentials"
|
||||
AZURE_CREDENTIALS: "credentials",
|
||||
} as const;
|
||||
|
||||
// AZURE credential type
|
||||
type AZURECredentialType = (typeof AZURE_CREDENTIAL_OPTIONS)[keyof typeof AZURE_CREDENTIAL_OPTIONS];
|
||||
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;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
tenantId: string;
|
||||
}
|
||||
|
||||
// M365 credential options
|
||||
export const M365_CREDENTIAL_OPTIONS = {
|
||||
M365_CREDENTIALS: "credentials",
|
||||
M365_CERTIFICATE_CREDENTIALS: "certificate"
|
||||
M365_CERTIFICATE_CREDENTIALS: "certificate",
|
||||
} as const;
|
||||
|
||||
// M365 credential type
|
||||
type M365CredentialType = (typeof M365_CREDENTIAL_OPTIONS)[keyof typeof M365_CREDENTIAL_OPTIONS];
|
||||
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;
|
||||
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;
|
||||
@@ -133,8 +135,9 @@ export class ProvidersPage extends BasePage {
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
|
||||
// Button to add a new cloud provider
|
||||
this.addProviderButton = page.getByRole("button", { name: "Add Cloud Provider", exact: true });
|
||||
this.addProviderButton = page.getByRole("link", {
|
||||
name: "Add Cloud Provider",
|
||||
});
|
||||
|
||||
// Table displaying existing providers
|
||||
this.providersTable = page.getByRole("table");
|
||||
@@ -159,22 +162,32 @@ export class ProvidersPage extends BasePage {
|
||||
|
||||
// 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.azureSubscriptionIdInput = page.getByRole("textbox", {
|
||||
name: "Subscription ID",
|
||||
});
|
||||
this.azureClientIdInput = page.getByRole("textbox", { name: "Client ID" });
|
||||
this.azureClientSecretInput = page.getByRole("textbox", { name: "Client Secret" });
|
||||
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.m365ClientSecretInput = page.getByRole("textbox", {
|
||||
name: "Client Secret",
|
||||
});
|
||||
this.m365TenantIdInput = page.getByRole("textbox", { name: "Tenant ID" });
|
||||
this.m365CertificateContentInput = page.getByRole("textbox", { name: "Certificate Content" });
|
||||
|
||||
this.m365CertificateContentInput = page.getByRole("textbox", {
|
||||
name: "Certificate Content",
|
||||
});
|
||||
|
||||
// Alias input
|
||||
this.aliasInput = page.getByRole("textbox", { name: "Provider alias (optional)" });
|
||||
this.aliasInput = page.getByRole("textbox", {
|
||||
name: "Provider alias (optional)",
|
||||
});
|
||||
|
||||
// Navigation buttons in the form (next and back)
|
||||
this.nextButton = page
|
||||
@@ -212,8 +225,12 @@ export class ProvidersPage extends BasePage {
|
||||
this.externalIdInput = page.getByRole("textbox", { name: "External ID" });
|
||||
|
||||
// Inputs for static credentials
|
||||
this.accessKeyIdInput = page.getByRole("textbox", { name: "Access Key ID" });
|
||||
this.secretAccessKeyInput = page.getByRole("textbox", { name: "Secret Access Key" });
|
||||
this.accessKeyIdInput = page.getByRole("textbox", {
|
||||
name: "Access Key ID",
|
||||
});
|
||||
this.secretAccessKeyInput = page.getByRole("textbox", {
|
||||
name: "Secret Access Key",
|
||||
});
|
||||
|
||||
// Delete button in confirmation modal
|
||||
this.deleteProviderConfirmationButton = page.getByRole("button", {
|
||||
@@ -236,19 +253,17 @@ export class ProvidersPage extends BasePage {
|
||||
}
|
||||
|
||||
async selectAWSProvider(): Promise<void> {
|
||||
|
||||
// Prefer label-based click for radios, force if overlay intercepts
|
||||
await this.awsProviderRadio.click({ force: true });
|
||||
await this.waitForPageLoad();
|
||||
}
|
||||
|
||||
async selectAZUREProvider(): Promise<void> {
|
||||
|
||||
// Prefer label-based click for radios, force if overlay intercepts
|
||||
await this.azureProviderRadio.click({ force: true });
|
||||
await this.waitForPageLoad();
|
||||
}
|
||||
|
||||
|
||||
async selectM365Provider(): Promise<void> {
|
||||
// Select the M365 provider
|
||||
|
||||
@@ -256,7 +271,6 @@ export class ProvidersPage extends BasePage {
|
||||
await this.waitForPageLoad();
|
||||
}
|
||||
|
||||
|
||||
async fillAWSProviderDetails(data: AWSProviderData): Promise<void> {
|
||||
// Fill the AWS provider details
|
||||
|
||||
@@ -292,7 +306,7 @@ export class ProvidersPage extends BasePage {
|
||||
// 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();
|
||||
const url = this.page.url();
|
||||
|
||||
// If on the "connect-account" step, click the "Next" button
|
||||
if (/\/providers\/connect-account/.test(url)) {
|
||||
@@ -328,9 +342,9 @@ export class ProvidersPage extends BasePage {
|
||||
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();
|
||||
|
||||
// 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();
|
||||
|
||||
try {
|
||||
// Wait up to 15 seconds for either the error message or redirect
|
||||
await Promise.race([
|
||||
@@ -342,7 +356,9 @@ export class ProvidersPage extends BasePage {
|
||||
|
||||
// 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);
|
||||
const isErrorVisible = await errorMessage
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
if (isErrorVisible) {
|
||||
const errorText = await errorMessage.textContent();
|
||||
throw new Error(
|
||||
@@ -352,7 +368,9 @@ export class ProvidersPage extends BasePage {
|
||||
}
|
||||
} catch (error) {
|
||||
// If timeout or other error, check if error message is present
|
||||
const isErrorVisible = await errorMessage.isVisible().catch(() => false);
|
||||
const isErrorVisible = await errorMessage
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
if (isErrorVisible) {
|
||||
const errorText = await errorMessage.textContent();
|
||||
throw new Error(
|
||||
@@ -416,7 +434,7 @@ export class ProvidersPage extends BasePage {
|
||||
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 });
|
||||
await this.m365CertificateCredentialsRadio.click({ force: true });
|
||||
} else {
|
||||
throw new Error(`Invalid M365 credential type: ${type}`);
|
||||
}
|
||||
@@ -426,7 +444,7 @@ export class ProvidersPage extends BasePage {
|
||||
|
||||
async fillRoleCredentials(credentials: AWSProviderCredential): Promise<void> {
|
||||
// Fill the role credentials form
|
||||
|
||||
|
||||
if (credentials.accessKeyId) {
|
||||
await this.accessKeyIdInput.fill(credentials.accessKeyId);
|
||||
}
|
||||
@@ -444,7 +462,9 @@ export class ProvidersPage extends BasePage {
|
||||
}
|
||||
}
|
||||
|
||||
async fillStaticCredentials(credentials: AWSProviderCredential): Promise<void> {
|
||||
async fillStaticCredentials(
|
||||
credentials: AWSProviderCredential,
|
||||
): Promise<void> {
|
||||
// Fill the static credentials form
|
||||
|
||||
if (credentials.accessKeyId) {
|
||||
@@ -455,7 +475,9 @@ export class ProvidersPage extends BasePage {
|
||||
}
|
||||
}
|
||||
|
||||
async fillAZURECredentials(credentials: AZUREProviderCredential): Promise<void> {
|
||||
async fillAZURECredentials(
|
||||
credentials: AZUREProviderCredential,
|
||||
): Promise<void> {
|
||||
// Fill the azure credentials form
|
||||
|
||||
if (credentials.clientId) {
|
||||
@@ -469,7 +491,9 @@ export class ProvidersPage extends BasePage {
|
||||
}
|
||||
}
|
||||
|
||||
async fillM365Credentials(credentials: M365ProviderCredential): Promise<void> {
|
||||
async fillM365Credentials(
|
||||
credentials: M365ProviderCredential,
|
||||
): Promise<void> {
|
||||
// Fill the m365 credentials form
|
||||
|
||||
if (credentials.clientId) {
|
||||
@@ -483,14 +507,18 @@ export class ProvidersPage extends BasePage {
|
||||
}
|
||||
}
|
||||
|
||||
async fillM365CertificateCredentials(credentials: M365ProviderCredential): Promise<void> {
|
||||
async fillM365CertificateCredentials(
|
||||
credentials: M365ProviderCredential,
|
||||
): Promise<void> {
|
||||
// Fill the m365 certificate credentials form
|
||||
|
||||
if (credentials.clientId) {
|
||||
await this.m365ClientIdInput.fill(credentials.clientId);
|
||||
}
|
||||
if (credentials.certificateContent) {
|
||||
await this.m365CertificateContentInput.fill(credentials.certificateContent);
|
||||
await this.m365CertificateContentInput.fill(
|
||||
credentials.certificateContent,
|
||||
);
|
||||
}
|
||||
if (credentials.tenantId) {
|
||||
await this.m365TenantIdInput.fill(credentials.tenantId);
|
||||
@@ -502,7 +530,7 @@ export class ProvidersPage extends BasePage {
|
||||
|
||||
await expect(this.page).toHaveTitle(/Prowler/);
|
||||
await expect(this.addProviderButton).toBeVisible();
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
await this.page.waitForLoadState("networkidle");
|
||||
}
|
||||
|
||||
async verifyConnectAccountPageLoaded(): Promise<void> {
|
||||
@@ -584,15 +612,15 @@ export class ProvidersPage extends BasePage {
|
||||
// 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
|
||||
@@ -600,7 +628,7 @@ export class ProvidersPage extends BasePage {
|
||||
|
||||
// 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();
|
||||
@@ -610,24 +638,24 @@ export class ProvidersPage extends BasePage {
|
||||
// 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;
|
||||
};
|
||||
|
||||
@@ -635,7 +663,7 @@ export class ProvidersPage extends BasePage {
|
||||
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++) {
|
||||
@@ -643,14 +671,14 @@ export class ProvidersPage extends BasePage {
|
||||
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
|
||||
@@ -660,7 +688,11 @@ export class ProvidersPage extends BasePage {
|
||||
}
|
||||
|
||||
// Find and click the action button (last cell = actions column)
|
||||
const actionButton = targetRow.locator("td").last().locator("button").first();
|
||||
const actionButton = targetRow
|
||||
.locator("td")
|
||||
.last()
|
||||
.locator("button")
|
||||
.first();
|
||||
await expect(actionButton).toBeVisible({ timeout: 5000 });
|
||||
await actionButton.click();
|
||||
|
||||
@@ -672,11 +704,15 @@ export class ProvidersPage extends BasePage {
|
||||
await deleteMenuItem.click();
|
||||
|
||||
// Wait for confirmation modal to appear
|
||||
const modal = this.page.locator('[role="dialog"], .modal, [data-testid*="modal"]').first();
|
||||
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 expect(this.deleteProviderConfirmationButton).toBeVisible({
|
||||
timeout: 5000,
|
||||
});
|
||||
await this.deleteProviderConfirmationButton.click();
|
||||
|
||||
// Wait for modal to close (this indicates deletion was initiated)
|
||||
@@ -693,17 +729,22 @@ export class ProvidersPage extends BasePage {
|
||||
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();
|
||||
// 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();
|
||||
const modal = this.page
|
||||
.locator('[role="dialog"], .modal, [data-testid*="modal"]')
|
||||
.first();
|
||||
await expect(modal).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Select the role credentials
|
||||
this.page.getByRole('option', { name: 'Access & Secret Key' }).click({ force: true });
|
||||
this.page
|
||||
.getByRole("option", { name: "Access & Secret Key" })
|
||||
.click({ force: true });
|
||||
} else {
|
||||
throw new Error(`Invalid authentication method: ${method}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user