From 5ecfd6ea205497d1827ae737b236fa180d1a0e78 Mon Sep 17 00:00:00 2001 From: "Pablo Fernandez Guerra (PFE)" <148432447+pfe-nazaries@users.noreply.github.com> Date: Wed, 17 Jun 2026 16:17:35 +0200 Subject: [PATCH] test(ui): add Okta provider form E2E test (#11600) Co-authored-by: Pablo F.G Co-authored-by: Claude Opus 4.8 Co-authored-by: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com> --- .github/workflows/ui-e2e-tests-v2.yml | 3 + ui/tests/providers/providers-page.ts | 76 +++++++++++++++++++++ ui/tests/providers/providers.md | 60 +++++++++++++++++ ui/tests/providers/providers.spec.ts | 95 +++++++++++++++++++++++++++ ui/types/env.d.ts | 5 ++ 5 files changed, 239 insertions(+) diff --git a/.github/workflows/ui-e2e-tests-v2.yml b/.github/workflows/ui-e2e-tests-v2.yml index cc3953274a..ed407e890d 100644 --- a/.github/workflows/ui-e2e-tests-v2.yml +++ b/.github/workflows/ui-e2e-tests-v2.yml @@ -77,6 +77,9 @@ jobs: E2E_ALIBABACLOUD_ACCESS_KEY_ID: ${{ secrets.E2E_ALIBABACLOUD_ACCESS_KEY_ID }} E2E_ALIBABACLOUD_ACCESS_KEY_SECRET: ${{ secrets.E2E_ALIBABACLOUD_ACCESS_KEY_SECRET }} E2E_ALIBABACLOUD_ROLE_ARN: ${{ secrets.E2E_ALIBABACLOUD_ROLE_ARN }} + E2E_OKTA_DOMAIN: ${{ secrets.E2E_OKTA_DOMAIN }} + E2E_OKTA_CLIENT_ID: ${{ secrets.E2E_OKTA_CLIENT_ID }} + E2E_OKTA_BASE64_PRIVATE_KEY: ${{ secrets.E2E_OKTA_BASE64_PRIVATE_KEY }} E2E_GOOGLEWORKSPACE_CUSTOMER_ID: ${{ secrets.E2E_GOOGLEWORKSPACE_CUSTOMER_ID }} E2E_GOOGLEWORKSPACE_SERVICE_ACCOUNT_JSON: ${{ secrets.E2E_GOOGLEWORKSPACE_SERVICE_ACCOUNT_JSON }} E2E_GOOGLEWORKSPACE_DELEGATED_USER: ${{ secrets.E2E_GOOGLEWORKSPACE_DELEGATED_USER }} diff --git a/ui/tests/providers/providers-page.ts b/ui/tests/providers/providers-page.ts index 2196cd4fae..5aeb3723a6 100644 --- a/ui/tests/providers/providers-page.ts +++ b/ui/tests/providers/providers-page.ts @@ -81,6 +81,28 @@ export interface GoogleWorkspaceProviderCredential { delegatedUser: string; } +// Okta provider data +export interface OktaProviderData { + orgDomain: string; + alias?: string; +} + +// Okta credential options +export const OKTA_CREDENTIAL_OPTIONS = { + OKTA_PRIVATE_KEY_JWT: "private_key_jwt", +} as const; + +// Okta credential type +type OktaCredentialType = + (typeof OKTA_CREDENTIAL_OPTIONS)[keyof typeof OKTA_CREDENTIAL_OPTIONS]; + +// Okta provider credential (OAuth 2.0 Private Key JWT) +export interface OktaProviderCredential { + type: OktaCredentialType; + clientId: string; + privateKey: string; +} + // AWS credential options export const AWS_CREDENTIAL_OPTIONS = { AWS_ROLE_ARN: "role", @@ -267,12 +289,18 @@ export class ProvidersPage extends BasePage { readonly ociProviderRadio: Locator; readonly alibabacloudProviderRadio: Locator; readonly googleworkspaceProviderRadio: Locator; + readonly oktaProviderRadio: Locator; // Google Workspace provider form elements readonly googleworkspaceCustomerIdInput: Locator; readonly googleworkspaceServiceAccountJsonInput: Locator; readonly googleworkspaceDelegatedUserInput: Locator; + // Okta provider form elements + readonly oktaOrgDomainInput: Locator; + readonly oktaClientIdInput: Locator; + readonly oktaPrivateKeyInput: Locator; + // Vercel provider form elements readonly vercelProviderRadio: Locator; readonly vercelTeamIdInput: Locator; @@ -522,6 +550,18 @@ export class ProvidersPage extends BasePage { name: /Delegated User Email/i, }); + // Okta + this.oktaProviderRadio = page.getByRole("option", { + name: /Okta/i, + }); + this.oktaOrgDomainInput = page.getByRole("textbox", { + name: "Org Domain", + }); + this.oktaClientIdInput = page.getByRole("textbox", { name: "Client ID" }); + this.oktaPrivateKeyInput = page.getByRole("textbox", { + name: "Private Key", + }); + // Vercel this.vercelProviderRadio = page.getByRole("option", { name: /Vercel/i, @@ -1322,6 +1362,41 @@ export class ProvidersPage extends BasePage { await expect(this.googleworkspaceDelegatedUserInput).toBeVisible(); } + async selectOktaProvider(): Promise { + await this.selectProviderRadio(this.oktaProviderRadio); + } + + async fillOktaProviderDetails(data: OktaProviderData): Promise { + // Fill the Okta provider details (org domain is lowercased by the form) + + await this.oktaOrgDomainInput.fill(data.orgDomain); + + if (data.alias) { + await this.aliasInput.fill(data.alias); + } + } + + async fillOktaCredentials( + credentials: OktaProviderCredential, + ): Promise { + // Fill the Okta OAuth 2.0 Private Key JWT credentials form + + if (credentials.clientId) { + await this.oktaClientIdInput.fill(credentials.clientId); + } + if (credentials.privateKey) { + await this.oktaPrivateKeyInput.fill(credentials.privateKey); + } + } + + async verifyOktaCredentialsPageLoaded(): Promise { + // Verify the Okta credentials page is loaded + + await this.verifyPageHasProwlerTitle(); + await expect(this.oktaClientIdInput).toBeVisible(); + await expect(this.oktaPrivateKeyInput).toBeVisible(); + } + async selectVercelProvider(): Promise { await this.selectProviderRadio(this.vercelProviderRadio); } @@ -1374,6 +1449,7 @@ export class ProvidersPage extends BasePage { await expect(this.githubProviderRadio).toBeVisible(); await expect(this.alibabacloudProviderRadio).toBeVisible(); await expect(this.googleworkspaceProviderRadio).toBeVisible(); + await expect(this.oktaProviderRadio).toBeVisible(); await expect(this.vercelProviderRadio).toBeVisible(); } diff --git a/ui/tests/providers/providers.md b/ui/tests/providers/providers.md index dcdbccb75f..944638a75a 100644 --- a/ui/tests/providers/providers.md +++ b/ui/tests/providers/providers.md @@ -1071,3 +1071,63 @@ - API Token is a masked (password) field; the only credential required for Vercel - Provider cleanup performed before each test to ensure clean state - Requires a valid Vercel API Token with read permissions to the resources to assess + +--- + +## Test Case: `PROVIDER-E2E-019` - Add Okta Provider with OAuth 2.0 Private Key JWT Credentials + +**Priority:** `critical` + +**Tags:** + +- type → @e2e, @serial +- feature → @providers +- provider → @okta + +**Description/Objective:** Validates the complete flow of adding a new Okta provider using OAuth 2.0 Private Key JWT authentication (Client ID and PEM-encoded private key) tied to an Okta Org Domain. + +**Preconditions:** + +- Admin user authentication required (admin.auth.setup setup) +- Environment variables configured: E2E_OKTA_DOMAIN, E2E_OKTA_CLIENT_ID, E2E_OKTA_BASE64_PRIVATE_KEY +- Remove any existing provider with the same Org Domain before starting the test +- This test must be run serially and never in parallel with other tests, as it requires the Org Domain not to be already registered beforehand. + +### Flow Steps + +1. Navigate to providers page +2. Click "Add Provider" button +3. Select Okta provider type +4. Fill provider details (org domain and alias) +5. Verify Okta credentials page is loaded +6. Fill Okta credentials (client ID and PEM-encoded private key) +7. Launch initial scan +8. Verify redirect to Scans page +9. Verify scheduled scan status in Scans table (provider exists and scan name is "scheduled scan") + +### Expected Result + +- Okta provider successfully added with OAuth 2.0 Private Key JWT credentials +- Initial scan launched successfully +- User redirected to Scans page +- Scheduled scan appears in Scans table with correct provider and scan name + +### Key verification points + +- Provider page loads correctly +- Connect account page displays Okta option +- Provider details form accepts org domain (Okta-managed domain, e.g. acme.okta.com) and alias +- Credentials page loads with Client ID input and Private Key textarea +- Credentials are properly filled in the correct fields +- Launch scan page appears +- Successful redirect to Scans page after scan launch +- Provider exists in Scans table (verified by org domain) +- Scan name field contains "scheduled scan" + +### Notes + +- Test uses environment variables for Okta credentials +- Org Domain is lowercased by the form; cleanup and scan verification use the normalized value +- Private Key is provided as base64-encoded PEM content and decoded before use (multi-line content) +- Provider cleanup performed before each test to ensure clean state +- Requires a valid Okta API Services app with a registered public key (JWK) matching the provided private key diff --git a/ui/tests/providers/providers.spec.ts b/ui/tests/providers/providers.spec.ts index 819b85bfe2..b5c9e53df9 100644 --- a/ui/tests/providers/providers.spec.ts +++ b/ui/tests/providers/providers.spec.ts @@ -30,6 +30,9 @@ import { GoogleWorkspaceProviderData, GoogleWorkspaceProviderCredential, GOOGLEWORKSPACE_CREDENTIAL_OPTIONS, + OktaProviderData, + OktaProviderCredential, + OKTA_CREDENTIAL_OPTIONS, VercelProviderData, VercelProviderCredential, VERCEL_CREDENTIAL_OPTIONS, @@ -1517,6 +1520,98 @@ test.describe("Add Provider", () => { }, ); }); + + test.describe.serial("Add Okta Provider", () => { + let providersPage: ProvidersPage; + let scansPage: ScansPage; + + // Test data from environment variables + // Org Domain is lowercased by the form, so normalize here to match the + // stored provider UID used for cleanup and scan verification. + const orgDomain = (process.env.E2E_OKTA_DOMAIN ?? "").toLowerCase(); + const clientId = process.env.E2E_OKTA_CLIENT_ID ?? ""; + const privateKeyB64 = process.env.E2E_OKTA_BASE64_PRIVATE_KEY ?? ""; + + // Setup before each test + test.beforeEach(async ({ page }) => { + test.skip( + !orgDomain || !clientId || !privateKeyB64, + "Okta E2E env vars are not set", + ); + providersPage = new ProvidersPage(page); + await deleteProviderIfExists(providersPage, orgDomain!); + }); + + // Use admin authentication for provider management + test.use({ storageState: "playwright/.auth/admin_user.json" }); + + test( + "should add a new Okta provider with OAuth 2.0 Private Key JWT credentials", + { + tag: [ + "@critical", + "@e2e", + "@providers", + "@okta", + "@serial", + "@PROVIDER-E2E-019", + ], + }, + async ({ page }) => { + // The Okta app private key is PEM-encoded (multi-line), so it is passed + // base64-encoded via the environment variable and decoded here. + const privateKey = Buffer.from(privateKeyB64, "base64").toString( + "utf8", + ); + + // Prepare test data for Okta provider + const oktaProviderData: OktaProviderData = { + orgDomain: orgDomain, + alias: "Test E2E Okta Account - Private Key JWT", + }; + + // Prepare OAuth 2.0 Private Key JWT credentials + const oktaCredentials: OktaProviderCredential = { + type: OKTA_CREDENTIAL_OPTIONS.OKTA_PRIVATE_KEY_JWT, + clientId: clientId, + privateKey: privateKey, + }; + + // Navigate to providers page + await providersPage.goto(); + await providersPage.verifyPageLoaded(); + + // Start adding new provider + await providersPage.clickAddProvider(); + await providersPage.verifyConnectAccountPageLoaded(); + + // Select Okta provider + await providersPage.selectOktaProvider(); + + // Fill provider details (org domain and alias) + await providersPage.fillOktaProviderDetails(oktaProviderData); + await providersPage.clickNext(); + + // Verify Okta credentials page is loaded + await providersPage.verifyOktaCredentialsPageLoaded(); + + // Fill OAuth 2.0 Private Key JWT credentials + await providersPage.fillOktaCredentials(oktaCredentials); + await providersPage.clickNext(); + + // Launch scan + await providersPage.verifyLaunchScanPageLoaded(); + await providersPage.clickNext(); + + // Wait for redirect to scan page + scansPage = new ScansPage(page); + await scansPage.verifyPageLoaded(); + + // Verify scan status is "Scheduled scan" + await scansPage.verifyScheduledScanStatus(orgDomain); + }, + ); + }); }); test.describe("Update Provider Credentials", () => { diff --git a/ui/types/env.d.ts b/ui/types/env.d.ts index 6f74d366dd..3daedd9706 100644 --- a/ui/types/env.d.ts +++ b/ui/types/env.d.ts @@ -119,6 +119,11 @@ declare global { E2E_GOOGLEWORKSPACE_SERVICE_ACCOUNT_JSON?: string; E2E_GOOGLEWORKSPACE_DELEGATED_USER?: string; + // E2E Okta + E2E_OKTA_DOMAIN?: string; + E2E_OKTA_CLIENT_ID?: string; + E2E_OKTA_BASE64_PRIVATE_KEY?: string; + // E2E Vercel E2E_VERCEL_TEAM_ID?: string; E2E_VERCEL_API_TOKEN?: string;