diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index b051aced9d..647cdc4244 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to the **Prowler UI** are documented in this file. +## [1.16.2] (Prowler v5.16.2) (UNRELEASED) + +### 🐞 Fixed + +- OCI update credentials form failing silently due to missing provider UID [(#9746)](https://github.com/prowler-cloud/prowler/pull/9746) + +--- + ## [1.16.1] (Prowler v5.16.1) ### 🔄 Changed diff --git a/ui/app/(prowler)/providers/(set-up-provider)/update-credentials/page.tsx b/ui/app/(prowler)/providers/(set-up-provider)/update-credentials/page.tsx index 91afadbc6e..cd6af05043 100644 --- a/ui/app/(prowler)/providers/(set-up-provider)/update-credentials/page.tsx +++ b/ui/app/(prowler)/providers/(set-up-provider)/update-credentials/page.tsx @@ -1,5 +1,7 @@ +import { redirect } from "next/navigation"; import React from "react"; +import { getProvider } from "@/actions/providers/providers"; import { CredentialsUpdateInfo } from "@/components/providers"; import { UpdateViaCredentialsForm, @@ -20,9 +22,24 @@ interface Props { export default async function UpdateCredentialsPage({ searchParams }: Props) { const resolvedSearchParams = await searchParams; - const { type: providerType, via } = resolvedSearchParams; + const { type: providerType, via, id: providerId } = resolvedSearchParams; + + if (!providerId) { + redirect("/providers"); + } + const formType = getProviderFormType(providerType, via); + const formData = new FormData(); + formData.append("id", providerId); + const providerResponse = await getProvider(formData); + + if (providerResponse?.errors) { + redirect("/providers"); + } + + const providerUid = providerResponse?.data?.attributes?.uid; + switch (formType) { case "selector": return ( @@ -30,14 +47,27 @@ export default async function UpdateCredentialsPage({ searchParams }: Props) { ); case "credentials": - return ; + return ( + + ); case "role": - return ; + return ( + + ); case "service-account": return ( - + ); default: diff --git a/ui/components/providers/workflow/forms/update-via-credentials-form.tsx b/ui/components/providers/workflow/forms/update-via-credentials-form.tsx index 613882b381..f838b71478 100644 --- a/ui/components/providers/workflow/forms/update-via-credentials-form.tsx +++ b/ui/components/providers/workflow/forms/update-via-credentials-form.tsx @@ -7,8 +7,10 @@ import { BaseCredentialsForm } from "./base-credentials-form"; export const UpdateViaCredentialsForm = ({ searchParams, + providerUid, }: { searchParams: { type: string; id: string; secretId?: string }; + providerUid?: string; }) => { const providerType = searchParams.type as ProviderType; const providerId = searchParams.id; @@ -24,6 +26,7 @@ export const UpdateViaCredentialsForm = ({ { const providerType = searchParams.type as ProviderType; const providerId = searchParams.id; @@ -24,6 +26,7 @@ export const UpdateViaRoleForm = ({ { const providerType = searchParams.type as ProviderType; const providerId = searchParams.id; @@ -24,6 +26,7 @@ export const UpdateViaServiceAccountForm = ({ = [ - { name: "Next" }, // Try the "Next" button - { name: "Save" }, // Try the "Save" button + const candidates: Array<{ name: string | RegExp; exact?: boolean }> = [ + { name: "Next", exact: true }, // Try the "Next" button (exact match to avoid Next.js dev tools) + { name: "Save", exact: true }, // Try the "Save" button { name: "Launch scan" }, // Try the "Launch scan" button { name: /Continue|Proceed/i }, // Try "Continue" or "Proceed" (case-insensitive) ]; // Try each candidate name and click it if found for (const candidate of candidates) { - const btn = this.page.getByRole("button", { - name: candidate.name, - }); + // Exclude Next.js dev tools button by filtering out buttons with aria-haspopup attribute + const btn = this.page + .getByRole("button", { + name: candidate.name, + exact: candidate.exact, + }) + .and(this.page.locator(":not([aria-haspopup])")); if (await btn.count()) { await btn.click(); @@ -847,7 +851,7 @@ export class ProvidersPage extends BasePage { } async verifyOCICredentialsPageLoaded(): Promise { - // Verify the OCI credentials page is loaded + // Verify the OCI credentials page is loaded (add flow - all fields visible) await this.verifyPageHasProwlerTitle(); await expect(this.ociTenancyIdInput).toBeVisible(); @@ -857,6 +861,17 @@ export class ProvidersPage extends BasePage { await expect(this.ociRegionInput).toBeVisible(); } + async verifyOCIUpdateCredentialsPageLoaded(): Promise { + // Verify the OCI update credentials page is loaded + // Note: Tenancy OCID is hidden in update flow (auto-populated from provider UID) + + await this.verifyPageHasProwlerTitle(); + await expect(this.ociUserIdInput).toBeVisible(); + await expect(this.ociFingerprintInput).toBeVisible(); + await expect(this.ociKeyContentInput).toBeVisible(); + await expect(this.ociRegionInput).toBeVisible(); + } + async verifyPageLoaded(): Promise { // Verify the providers page is loaded @@ -995,4 +1010,42 @@ export class ProvidersPage extends BasePage { throw new Error(`Invalid authentication method: ${method}`); } } + + async clickProviderRowActions(providerUid: string): Promise { + // Click the actions dropdown for a specific provider row + const row = this.providersTable.locator("tbody tr", { + hasText: providerUid, + }); + await expect(row).toBeVisible(); + + // Click the dropdown trigger - it's the last button in the row (after the copy button) + const actionsButton = row.locator("button").last(); + await actionsButton.click(); + } + + async clickUpdateCredentials(providerUid: string): Promise { + // Click update credentials for a specific provider + await this.clickProviderRowActions(providerUid); + + // Wait for dropdown menu to stabilize and click Update Credentials + const updateCredentialsOption = this.page.getByRole("menuitem", { + name: /Update Credentials/i, + }); + await expect(updateCredentialsOption).toBeVisible(); + // Wait a bit for the menu to stabilize before clicking + await this.page.waitForTimeout(100); + await updateCredentialsOption.click({ force: true }); + } + + async verifyUpdateCredentialsPageLoaded(): Promise { + // Verify the update credentials page is loaded + await this.verifyPageHasProwlerTitle(); + await expect(this.page).toHaveURL(/\/providers\/update-credentials/); + } + + async verifyTestConnectionPageLoaded(): Promise { + // Verify the test connection page is loaded + await this.verifyPageHasProwlerTitle(); + await expect(this.page).toHaveURL(/\/providers\/test-connection/); + } } diff --git a/ui/tests/providers/providers.md b/ui/tests/providers/providers.md index 67977b61e4..f58d9edc19 100644 --- a/ui/tests/providers/providers.md +++ b/ui/tests/providers/providers.md @@ -708,3 +708,61 @@ - Provider cleanup performed before each test to ensure clean state - Requires valid OCI account with API Key set up - API Key credential type is automatically used for OCI providers + +--- + +## Test Case: `PROVIDER-E2E-013` - Update OCI Provider Credentials + +**Priority:** `normal` + +**Tags:** + +- type → @e2e, @serial +- feature → @providers +- provider → @oci + +**Description/Objective:** Validates the complete flow of updating credentials for an existing OCI provider. This test verifies that the provider UID is correctly passed to the update credentials form, which is required for OCI credential validation. + +**Preconditions:** + +- Admin user authentication required (admin.auth.setup setup) +- Environment variables configured: E2E_OCI_TENANCY_ID, E2E_OCI_USER_ID, E2E_OCI_FINGERPRINT, E2E_OCI_KEY_CONTENT, E2E_OCI_REGION +- An OCI provider with the specified Tenancy ID must already exist (run PROVIDER-E2E-012 first) +- This test must be run serially and never in parallel with other tests + +### Flow Steps: + +1. Navigate to providers page +2. Verify OCI provider exists in the table +3. Click row actions menu for the OCI provider +4. Click "Update Credentials" option +5. Verify update credentials page is loaded +6. Verify OCI credentials form fields are visible (confirms providerUid is loaded) +7. Fill OCI credentials (user ID, fingerprint, key content, region) +8. Click Next to submit +9. Verify successful navigation to test connection page + +### Expected Result: + +- Update credentials page loads successfully +- OCI credentials form is displayed with all required fields +- Provider UID is correctly passed to the form (hidden field populated) +- Credentials can be updated and submitted +- User is redirected to test connection page after successful update + +### Key verification points: + +- Provider page loads correctly +- OCI provider row is visible in providers table +- Row actions dropdown opens and displays "Update Credentials" option +- Update credentials page URL contains correct parameters +- OCI credentials form displays all fields (tenancy ID, user ID, fingerprint, key content, region) +- Form submission succeeds (no silent failures due to missing provider UID) +- Successful redirect to test connection page + +### Notes: + +- Test uses same environment variables as PROVIDER-E2E-012 (add OCI provider) +- Requires PROVIDER-E2E-012 to be run first to create the OCI provider +- This test validates the fix for OCI update credentials form failing silently due to missing provider UID +- The provider UID is required for OCI credential validation (tenancy field auto-populated from UID) diff --git a/ui/tests/providers/providers.spec.ts b/ui/tests/providers/providers.spec.ts index 865ee0cc4d..07da089891 100644 --- a/ui/tests/providers/providers.spec.ts +++ b/ui/tests/providers/providers.spec.ts @@ -1139,3 +1139,87 @@ test.describe("Add Provider", () => { ); }); }); + +test.describe("Update Provider Credentials", () => { + test.describe.serial("Update OCI Provider Credentials", () => { + let providersPage: ProvidersPage; + + // Test data from environment variables (same as add OCI provider test) + const tenancyId = process.env.E2E_OCI_TENANCY_ID; + const userId = process.env.E2E_OCI_USER_ID; + const fingerprint = process.env.E2E_OCI_FINGERPRINT; + const keyContent = process.env.E2E_OCI_KEY_CONTENT; + const region = process.env.E2E_OCI_REGION; + + // Validate required environment variables + if (!tenancyId || !userId || !fingerprint || !keyContent || !region) { + throw new Error( + "E2E_OCI_TENANCY_ID, E2E_OCI_USER_ID, E2E_OCI_FINGERPRINT, E2E_OCI_KEY_CONTENT, and E2E_OCI_REGION environment variables are not set", + ); + } + + // Setup before each test + test.beforeEach(async ({ page }) => { + providersPage = new ProvidersPage(page); + }); + + // Use admin authentication for provider management + test.use({ storageState: "playwright/.auth/admin_user.json" }); + + test( + "should update OCI provider credentials successfully", + { + tag: [ + "@e2e", + "@providers", + "@oci", + "@serial", + "@PROVIDER-E2E-013", + ], + }, + async () => { + // Prepare updated credentials + const ociCredentials: OCIProviderCredential = { + type: OCI_CREDENTIAL_OPTIONS.OCI_API_KEY, + tenancyId: tenancyId, + userId: userId, + fingerprint: fingerprint, + keyContent: keyContent, + region: region, + }; + + // Navigate to providers page + await providersPage.goto(); + await providersPage.verifyPageLoaded(); + + // Verify OCI provider exists in the table + const providerExists = + await providersPage.verifySingleRowForProviderUID(tenancyId); + if (!providerExists) { + throw new Error( + `OCI provider with tenancy ID ${tenancyId} not found. Run the add OCI provider test first.`, + ); + } + + // Click update credentials for the OCI provider + await providersPage.clickUpdateCredentials(tenancyId); + + // Verify update credentials page is loaded + await providersPage.verifyUpdateCredentialsPageLoaded(); + + // Verify OCI credentials form fields are visible (confirms providerUid is loaded) + // Note: Tenancy OCID is hidden in update flow (auto-populated from provider UID) + await providersPage.verifyOCIUpdateCredentialsPageLoaded(); + + // Fill updated credentials + await providersPage.fillOCICredentials(ociCredentials); + + // Click Next to submit + await providersPage.clickNext(); + + // Verify successful navigation to test connection page + await providersPage.verifyTestConnectionPageLoaded(); + }, + ); + }); +});