diff --git a/ui/.gitignore b/ui/.gitignore index 555ceeb8d3..3b905e64d8 100644 --- a/ui/.gitignore +++ b/ui/.gitignore @@ -33,4 +33,5 @@ yarn-error.log* # typescript *.tsbuildinfo -next-env.d.ts \ No newline at end of file +next-env.d.ts +playwright/.auth diff --git a/ui/package.json b/ui/package.json index ea80ad0f32..e0ec108dfc 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", - "test:e2e:ui": "playwright test --ui", - "test:e2e:debug": "playwright test --debug", - "test:e2e:headed": "playwright test --headed", + "test:e2e": "playwright test --project=chromium", + "test:e2e:ui": "playwright test --project=chromium --ui", + "test:e2e:debug": "playwright test --project=chromium --debug", + "test:e2e:headed": "playwright test --project=chromium --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 a680feabdb..5bea00bfe7 100644 --- a/ui/playwright.config.ts +++ b/ui/playwright.config.ts @@ -20,6 +20,72 @@ export default defineConfig({ }, projects: [ + // =========================================== + // Authentication Setup Projects + // =========================================== + // These projects handle user authentication for different permission levels + // Each setup creates authenticated state files that can be reused by test suites + + // Admin user authentication setup + // Creates authenticated state for admin users with full system permissions + { + name: "admin.auth.setup", + testMatch: "admin.auth.setup.ts", + }, + + // Scans management user authentication setup + // Creates authenticated state for users with scan management permissions + { + name: "manage-scans.auth.setup", + testMatch: "manage-scans.auth.setup.ts", + }, + + // Integrations management user authentication setup + // Creates authenticated state for users with integration management permissions + { + name: "manage-integrations.auth.setup", + testMatch: "manage-integrations.auth.setup.ts", + }, + + // Account management user authentication setup + // Creates authenticated state for users with account management permissions + { + name: "manage-account.auth.setup", + testMatch: "manage-account.auth.setup.ts", + }, + + // Cloud providers management user authentication setup + // Creates authenticated state for users with cloud provider management permissions + { + name: "manage-cloud-providers.auth.setup", + testMatch: "manage-cloud-providers.auth.setup.ts", + }, + + // Unlimited visibility user authentication setup + // Creates authenticated state for users with unlimited visibility permissions + { + name: "unlimited-visibility.auth.setup", + testMatch: "unlimited-visibility.auth.setup.ts", + }, + + // Invite and manage users authentication setup + // Creates authenticated state for users with user invitation and management permissions + { + name: "invite-and-manage-users.auth.setup", + testMatch: "invite-and-manage-users.auth.setup.ts", + }, + + // All authentication setups combined + // Runs all authentication setup files to create all user states + { + name: "all.auth.setup", + testMatch: "**/*.auth.setup.ts", + }, + + // =========================================== + // Test Suite Projects + // =========================================== + // These projects run the actual test suites { name: "chromium", use: { ...devices["Desktop Chrome"] }, diff --git a/ui/tests/helpers.ts b/ui/tests/helpers.ts index 9f5efb0fdf..db9c6facbb 100644 --- a/ui/tests/helpers.ts +++ b/ui/tests/helpers.ts @@ -1,4 +1,5 @@ import { Page, expect } from "@playwright/test"; +import { SignInPage, SignInCredentials } from "./page-objects/sign-in-page"; export const ERROR_MESSAGES = { INVALID_CREDENTIALS: "Invalid email or password", @@ -138,6 +139,29 @@ export async function verifyDashboardRoute(page: Page) { await expect(page).toHaveURL("/"); } +export async function authenticateAndSaveState( + page: Page, + email: string, + password: string, + storagePath: string, +) { + if (!email || !password) { + throw new Error('Email and password are required for authentication and save state'); + } + + // Create SignInPage instance + const signInPage = new SignInPage(page); + const credentials: SignInCredentials = { email, password }; + + // Perform authentication steps using Page Object Model + await signInPage.goto(); + await signInPage.login(credentials); + await signInPage.verifySuccessfulLogin(); + + // Save authentication state + await page.context().storageState({ path: storagePath }); +} + export async function getSession(page: Page) { const response = await page.request.get("/api/auth/session"); return response.json(); diff --git a/ui/tests/page-objects/home-page.ts b/ui/tests/page-objects/home-page.ts new file mode 100644 index 0000000000..077591eb4e --- /dev/null +++ b/ui/tests/page-objects/home-page.ts @@ -0,0 +1,125 @@ +import { Page, Locator, expect } from "@playwright/test"; + +export class HomePage { + readonly page: Page; + + // Main content elements + readonly mainContent: Locator; + readonly breadcrumbs: Locator; + readonly overviewHeading: Locator; + + // Navigation elements + readonly navigationMenu: Locator; + readonly userMenu: Locator; + readonly signOutButton: Locator; + + // Dashboard elements + readonly dashboardCards: Locator; + readonly overviewSection: Locator; + + // UI elements + readonly themeToggle: Locator; + readonly logo: Locator; + + constructor(page: Page) { + this.page = page; + + // Main content elements + this.mainContent = page.locator("main"); + this.breadcrumbs = page.getByLabel("Breadcrumbs"); + this.overviewHeading = page.getByRole("heading", { name: "Overview", exact: true }); + + // Navigation elements + this.navigationMenu = page.locator("nav"); + this.userMenu = page.getByRole("button", { name: /user menu/i }); + this.signOutButton = page.getByRole("button", { name: "Sign out" }); + + // Dashboard elements + this.dashboardCards = page.locator('[data-testid="dashboard-card"]'); + this.overviewSection = page.locator('[data-testid="overview-section"]'); + + // UI elements + this.themeToggle = page.getByLabel("Toggle theme"); + this.logo = page.locator('svg[width="300"]'); + } + + // Navigation methods + async goto(): Promise { + await this.page.goto("/"); + await this.waitForPageLoad(); + } + + async waitForPageLoad(): Promise { + await this.page.waitForLoadState("networkidle"); + } + + // Verification methods + async verifyPageLoaded(): Promise { + await expect(this.page).toHaveURL("/"); + await expect(this.mainContent).toBeVisible(); + await expect(this.overviewHeading).toBeVisible(); + await this.waitForPageLoad(); + } + + async verifyBreadcrumbs(): Promise { + await expect(this.breadcrumbs).toBeVisible(); + await expect(this.overviewHeading).toBeVisible(); + } + + async verifyMainContent(): Promise { + await expect(this.mainContent).toBeVisible(); + } + + // Navigation methods + async navigateToOverview(): Promise { + await this.overviewHeading.click(); + } + + async openUserMenu(): Promise { + await this.userMenu.click(); + } + + async signOut(): Promise { + await this.openUserMenu(); + await this.signOutButton.click(); + } + + // Dashboard methods + async verifyDashboardCards(): Promise { + await expect(this.dashboardCards.first()).toBeVisible(); + } + + async verifyOverviewSection(): Promise { + await expect(this.overviewSection).toBeVisible(); + } + + // Utility methods + async refresh(): Promise { + await this.page.reload(); + await this.waitForPageLoad(); + } + + async goBack(): Promise { + await this.page.goBack(); + await this.waitForPageLoad(); + } + + // Accessibility methods + async verifyKeyboardNavigation(): Promise { + // Test tab navigation through main elements + await this.page.keyboard.press("Tab"); + await expect(this.themeToggle).toBeFocused(); + } + + // Wait methods + async waitForRedirect(expectedUrl: string): Promise { + await this.page.waitForURL(expectedUrl); + } + + async waitForContentLoad(): Promise { + await this.page.waitForFunction(() => { + const main = document.querySelector("main"); + return main && main.offsetHeight > 0; + }); + } +} diff --git a/ui/tests/page-objects/sign-in-page.ts b/ui/tests/page-objects/sign-in-page.ts new file mode 100644 index 0000000000..2c426606c7 --- /dev/null +++ b/ui/tests/page-objects/sign-in-page.ts @@ -0,0 +1,316 @@ +import { Page, Locator, expect } from "@playwright/test"; +import { HomePage } from "./home-page"; + +export interface SignInCredentials { + email: string; + password: string; +} + +export interface SocialAuthConfig { + googleEnabled: boolean; + githubEnabled: boolean; +} + +export class SignInPage { + readonly page: Page; + readonly homePage: HomePage; + + // Form elements + readonly emailInput: Locator; + readonly passwordInput: Locator; + readonly loginButton: Locator; + readonly form: Locator; + + // Social authentication buttons + readonly googleButton: Locator; + readonly githubButton: Locator; + readonly samlButton: Locator; + + // Navigation elements + readonly signUpLink: Locator; + readonly backButton: Locator; + + // UI elements + readonly title: Locator; + readonly logo: Locator; + readonly themeToggle: Locator; + + // Error messages + readonly errorMessages: Locator; + readonly loadingIndicator: Locator; + + // SAML specific elements + readonly samlModeTitle: Locator; + readonly samlEmailInput: Locator; + + constructor(page: Page) { + this.page = page; + this.homePage = new HomePage(page); + + // Form elements + this.emailInput = page.getByLabel("Email"); + this.passwordInput = page.getByLabel("Password"); + this.loginButton = page.getByRole("button", { name: "Log in" }); + this.form = page.locator("form"); + + // Social authentication buttons + this.googleButton = page.getByText("Continue with Google"); + this.githubButton = page.getByText("Continue with Github"); + this.samlButton = page.getByText("Continue with SAML SSO"); + + // Navigation elements + this.signUpLink = page.getByRole("link", { name: "Sign up" }); + this.backButton = page.getByText("Back"); + + // UI elements + this.title = page.getByText("Sign in", { exact: true }); + this.logo = page.locator('svg[width="300"]'); + this.themeToggle = page.getByLabel("Toggle theme"); + + // Error messages + this.errorMessages = page.locator('[role="alert"], .error-message, [data-testid="error"]'); + this.loadingIndicator = page.getByText("Loading"); + + // SAML specific elements + this.samlModeTitle = page.getByText("Sign in with SAML SSO"); + this.samlEmailInput = page.getByLabel("Email"); + } + + // Navigation methods + async goto(): Promise { + await this.page.goto("/sign-in"); + await this.waitForPageLoad(); + } + + async waitForPageLoad(): Promise { + await this.page.waitForLoadState("networkidle"); + } + + // Form interaction methods + async fillEmail(email: string): Promise { + await this.emailInput.fill(email); + } + + async fillPassword(password: string): Promise { + await this.passwordInput.fill(password); + } + + async fillCredentials(credentials: SignInCredentials): Promise { + await this.fillEmail(credentials.email); + await this.fillPassword(credentials.password); + } + + async submitForm(): Promise { + await this.loginButton.click(); + } + + async login(credentials: SignInCredentials): Promise { + await this.fillCredentials(credentials); + await this.submitForm(); + } + + // Social authentication methods + async clickGoogleAuth(): Promise { + await this.googleButton.click(); + } + + async clickGithubAuth(): Promise { + await this.githubButton.click(); + } + + async clickSamlAuth(): Promise { + await this.samlButton.click(); + } + + // SAML SSO methods + async toggleSamlMode(): Promise { + await this.clickSamlAuth(); + } + + async goBackFromSaml(): Promise { + await this.backButton.click(); + } + + async fillSamlEmail(email: string): Promise { + await this.samlEmailInput.fill(email); + } + + async submitSamlForm(): Promise { + await this.submitForm(); + } + + // Navigation methods + async goToSignUp(): Promise { + await this.signUpLink.click(); + } + + // Validation and assertion methods + async verifyPageLoaded(): Promise { + await expect(this.page).toHaveTitle(/Prowler/); + await expect(this.logo).toBeVisible(); + await expect(this.title).toBeVisible(); + await this.waitForPageLoad(); + } + + async verifyFormElements(): Promise { + await expect(this.emailInput).toBeVisible(); + await expect(this.passwordInput).toBeVisible(); + await expect(this.loginButton).toBeVisible(); + } + + async verifySocialButtons(config: SocialAuthConfig): Promise { + if (config.googleEnabled) { + await expect(this.googleButton).toBeVisible(); + } + if (config.githubEnabled) { + await expect(this.githubButton).toBeVisible(); + } + await expect(this.samlButton).toBeVisible(); + } + + async verifyNavigationLinks(): Promise { + await expect(this.page.getByText("Need to create an account?")).toBeVisible(); + await expect(this.signUpLink).toBeVisible(); + } + + async verifySuccessfulLogin(): Promise { + await this.homePage.verifyPageLoaded(); + } + + async verifyLoginError(errorMessage: string = "Invalid email or password"): Promise { + await expect(this.page.getByText(errorMessage).first()).toBeVisible(); + await expect(this.page).toHaveURL("/sign-in"); + } + + async verifySamlModeActive(): Promise { + await expect(this.samlModeTitle).toBeVisible(); + await expect(this.passwordInput).not.toBeVisible(); + await expect(this.backButton).toBeVisible(); + } + + async verifyNormalModeActive(): Promise { + await expect(this.title).toBeVisible(); + await expect(this.passwordInput).toBeVisible(); + } + + async verifyLoadingState(): Promise { + await expect(this.loginButton).toHaveAttribute("aria-disabled", "true"); + await expect(this.loadingIndicator).toBeVisible(); + } + + async verifyFormValidation(): Promise { + // Check for common validation messages + const emailError = this.page.getByText("Please enter a valid email address."); + const passwordError = this.page.getByText("Password is required."); + + // At least one validation error should be visible + await expect(emailError.or(passwordError)).toBeVisible(); + } + + // Accessibility methods + async verifyKeyboardNavigation(): Promise { + // Test tab navigation through form elements + await this.page.keyboard.press("Tab"); // Theme toggle + await this.page.keyboard.press("Tab"); // Email field + await expect(this.emailInput).toBeFocused(); + + await this.page.keyboard.press("Tab"); // Password field + await expect(this.passwordInput).toBeFocused(); + + await this.page.keyboard.press("Tab"); // Show password button + await this.page.keyboard.press("Tab"); // Login button + await expect(this.loginButton).toBeFocused(); + } + + async verifyAriaLabels(): Promise { + await expect(this.page.getByRole("textbox", { name: "Email" })).toBeVisible(); + await expect(this.page.getByRole("textbox", { name: "Password" })).toBeVisible(); + await expect(this.page.getByRole("button", { name: "Log in" })).toBeVisible(); + } + + // Utility methods + async clearForm(): Promise { + await this.emailInput.clear(); + await this.passwordInput.clear(); + } + + async isFormValid(): Promise { + const emailValue = await this.emailInput.inputValue(); + const passwordValue = await this.passwordInput.inputValue(); + return emailValue.length > 0 && passwordValue.length > 0; + } + + async getFormErrors(): Promise { + const errorElements = await this.errorMessages.all(); + const errors: string[] = []; + + for (const element of errorElements) { + const text = await element.textContent(); + if (text) { + errors.push(text.trim()); + } + } + + return errors; + } + + // Browser interaction methods + async refresh(): Promise { + await this.page.reload(); + await this.waitForPageLoad(); + } + + async goBack(): Promise { + await this.page.goBack(); + await this.waitForPageLoad(); + } + + // Session management methods + async logout(): Promise { + await this.homePage.signOut(); + } + + async verifyLogoutSuccess(): Promise { + await expect(this.page).toHaveURL("/sign-in"); + await expect(this.title).toBeVisible(); + } + + // Advanced interaction methods + async fillFormWithValidation(credentials: SignInCredentials): Promise { + // Fill email first and check for validation + await this.fillEmail(credentials.email); + await this.page.keyboard.press("Tab"); // Trigger validation + + // Fill password + await this.fillPassword(credentials.password); + } + + async submitFormWithEnterKey(): Promise { + await this.passwordInput.press("Enter"); + } + + async submitFormWithButtonClick(): Promise { + await this.submitForm(); + } + + // Error handling methods + async handleSamlError(): Promise { + const samlError = this.page.getByText("SAML Authentication Error"); + if (await samlError.isVisible()) { + // Handle SAML error if present + console.log("SAML authentication error detected"); + } + } + + // Wait methods + async waitForFormSubmission(): Promise { + await this.page.waitForFunction(() => { + const button = document.querySelector('button[aria-disabled="true"]'); + return button === null; + }); + } + + async waitForRedirect(expectedUrl: string): Promise { + await this.page.waitForURL(expectedUrl); + } +} diff --git a/ui/tests/setups/admin.auth.setup.ts b/ui/tests/setups/admin.auth.setup.ts new file mode 100644 index 0000000000..0e24c242c3 --- /dev/null +++ b/ui/tests/setups/admin.auth.setup.ts @@ -0,0 +1,15 @@ +import { test as authAdminSetup } from '@playwright/test'; +import { authenticateAndSaveState } from '@/tests/helpers'; + +const adminUserFile = 'playwright/.auth/admin_user.json'; + +authAdminSetup('authenticate as admin e2e user', async ({ page }) => { + const adminEmail = process.env.E2E_ADMIN_USER; + const adminPassword = process.env.E2E_ADMIN_PASSWORD; + + if (!adminEmail || !adminPassword) { + throw new Error('E2E_ADMIN_USER and E2E_ADMIN_PASSWORD environment variables are required'); + } + + await authenticateAndSaveState(page, adminEmail, adminPassword, adminUserFile); +}); \ No newline at end of file diff --git a/ui/tests/setups/invite-and-manage-users.auth.setup.ts b/ui/tests/setups/invite-and-manage-users.auth.setup.ts new file mode 100644 index 0000000000..e11eecdaa3 --- /dev/null +++ b/ui/tests/setups/invite-and-manage-users.auth.setup.ts @@ -0,0 +1,15 @@ +import { test as authInviteAndManageUsersSetup } from '@playwright/test'; +import { authenticateAndSaveState } from '@/tests/helpers'; + +const inviteAndManageUsersUserFile = 'playwright/.auth/invite_and_manage_users_user.json'; + +authInviteAndManageUsersSetup('authenticate as invite and manage users e2e user', async ({ page }) => { + const inviteAndManageUsersEmail = process.env.E2E_INVITE_AND_MANAGE_USERS_USER; + const inviteAndManageUsersPassword = process.env.E2E_INVITE_AND_MANAGE_USERS_PASSWORD; + + if (!inviteAndManageUsersEmail || !inviteAndManageUsersPassword) { + throw new Error('E2E_INVITE_AND_MANAGE_USERS_USER and E2E_INVITE_AND_MANAGE_USERS_PASSWORD environment variables are required'); + } + + await authenticateAndSaveState(page, inviteAndManageUsersEmail, inviteAndManageUsersPassword, inviteAndManageUsersUserFile); +}); diff --git a/ui/tests/setups/manage-account.auth.setup.ts b/ui/tests/setups/manage-account.auth.setup.ts new file mode 100644 index 0000000000..4fe5b7b5a2 --- /dev/null +++ b/ui/tests/setups/manage-account.auth.setup.ts @@ -0,0 +1,15 @@ +import { test as authManageAccountSetup } from '@playwright/test'; +import { authenticateAndSaveState } from '@/tests/helpers'; + +const manageAccountUserFile = 'playwright/.auth/manage_account_user.json'; + +authManageAccountSetup('authenticate as manage account e2e user', async ({ page }) => { + const accountEmail = process.env.E2E_MANAGE_ACCOUNT_USER; + const accountPassword = process.env.E2E_MANAGE_ACCOUNT_PASSWORD; + + if (!accountEmail || !accountPassword) { + throw new Error('E2E_MANAGE_ACCOUNT_USER and E2E_MANAGE_ACCOUNT_PASSWORD environment variables are required'); + } + + await authenticateAndSaveState(page, accountEmail, accountPassword, manageAccountUserFile); +}); diff --git a/ui/tests/setups/manage-cloud-providers.auth.setup.ts b/ui/tests/setups/manage-cloud-providers.auth.setup.ts new file mode 100644 index 0000000000..205e2b50e1 --- /dev/null +++ b/ui/tests/setups/manage-cloud-providers.auth.setup.ts @@ -0,0 +1,15 @@ +import { test as authManageCloudProvidersSetup } from '@playwright/test'; +import { authenticateAndSaveState } from '@/tests/helpers'; + +const manageCloudProvidersUserFile = 'playwright/.auth/manage_cloud_providers_user.json'; + +authManageCloudProvidersSetup('authenticate as manage cloud providers e2e user', async ({ page }) => { + const cloudProvidersEmail = process.env.E2E_MANAGE_CLOUD_PROVIDERS_USER; + const cloudProvidersPassword = process.env.E2E_MANAGE_CLOUD_PROVIDERS_PASSWORD; + + if (!cloudProvidersEmail || !cloudProvidersPassword) { + throw new Error('E2E_MANAGE_CLOUD_PROVIDERS_USER and E2E_MANAGE_CLOUD_PROVIDERS_PASSWORD environment variables are required'); + } + + await authenticateAndSaveState(page, cloudProvidersEmail, cloudProvidersPassword, manageCloudProvidersUserFile); +}); diff --git a/ui/tests/setups/manage-integrations.auth.setup.ts b/ui/tests/setups/manage-integrations.auth.setup.ts new file mode 100644 index 0000000000..fb98e4a157 --- /dev/null +++ b/ui/tests/setups/manage-integrations.auth.setup.ts @@ -0,0 +1,15 @@ +import { test as authManageIntegrationsSetup } from '@playwright/test'; +import { authenticateAndSaveState } from '@/tests/helpers'; + +const manageIntegrationsUserFile = 'playwright/.auth/manage_integrations_user.json'; + +authManageIntegrationsSetup('authenticate as integrations e2e user', async ({ page }) => { + const integrationsEmail = process.env.E2E_MANAGE_INTEGRATIONS_USER; + const integrationsPassword = process.env.E2E_MANAGE_INTEGRATIONS_PASSWORD; + + if (!integrationsEmail || !integrationsPassword) { + throw new Error('E2E_MANAGE_INTEGRATIONS_USER and E2E_MANAGE_INTEGRATIONS_PASSWORD environment variables are required'); + } + + await authenticateAndSaveState(page, integrationsEmail, integrationsPassword, manageIntegrationsUserFile); +}); diff --git a/ui/tests/setups/manage-scans.auth.setup.ts b/ui/tests/setups/manage-scans.auth.setup.ts new file mode 100644 index 0000000000..7a8f7e95e2 --- /dev/null +++ b/ui/tests/setups/manage-scans.auth.setup.ts @@ -0,0 +1,15 @@ +import { test as authManageScansSetup } from '@playwright/test'; +import { authenticateAndSaveState } from '@/tests/helpers'; + +const manageScansUserFile = 'playwright/.auth/manage_scans_user.json'; + +authManageScansSetup('authenticate as scans e2e user', async ({ page }) => { + const scansEmail = process.env.E2E_MANAGE_SCANS_USER; + const scansPassword = process.env.E2E_MANAGE_SCANS_PASSWORD; + + if (!scansEmail || !scansPassword) { + throw new Error('E2E_MANAGE_SCANS_USER and E2E_MANAGE_SCANS_PASSWORD environment variables are required'); + } + + await authenticateAndSaveState(page, scansEmail, scansPassword, manageScansUserFile); +}); diff --git a/ui/tests/setups/unlimited-visibility.auth.setup.ts b/ui/tests/setups/unlimited-visibility.auth.setup.ts new file mode 100644 index 0000000000..533158ec70 --- /dev/null +++ b/ui/tests/setups/unlimited-visibility.auth.setup.ts @@ -0,0 +1,15 @@ +import { test as authUnlimitedVisibilitySetup } from '@playwright/test'; +import { authenticateAndSaveState } from '@/tests/helpers'; + +const unlimitedVisibilityUserFile = 'playwright/.auth/unlimited_visibility_user.json'; + +authUnlimitedVisibilitySetup('authenticate as unlimited visibility e2e user', async ({ page }) => { + const unlimitedVisibilityEmail = process.env.E2E_UNLIMITED_VISIBILITY_USER; + const unlimitedVisibilityPassword = process.env.E2E_UNLIMITED_VISIBILITY_PASSWORD; + + if (!unlimitedVisibilityEmail || !unlimitedVisibilityPassword) { + throw new Error('E2E_UNLIMITED_VISIBILITY_USER and E2E_UNLIMITED_VISIBILITY_PASSWORD environment variables are required'); + } + + await authenticateAndSaveState(page, unlimitedVisibilityEmail, unlimitedVisibilityPassword, unlimitedVisibilityUserFile); +});