diff --git a/.github/workflows/ui-e2e-tests.yml b/.github/workflows/ui-e2e-tests.yml index 39b430073a..5a27b3b12c 100644 --- a/.github/workflows/ui-e2e-tests.yml +++ b/.github/workflows/ui-e2e-tests.yml @@ -33,6 +33,8 @@ jobs: E2E_M365_SECRET_ID: ${{ secrets.E2E_M365_SECRET_ID }} E2E_M365_TENANT_ID: ${{ secrets.E2E_M365_TENANT_ID }} E2E_M365_CERTIFICATE_CONTENT: ${{ secrets.E2E_M365_CERTIFICATE_CONTENT }} + E2E_KUBERNETES_CONTEXT: ${{ secrets.E2E_KUBERNETES_CONTEXT }} + E2E_KUBERNETES_KUBECONFIG_PATH: ${{ secrets.E2E_KUBERNETES_KUBECONFIG_PATH }} steps: - name: Checkout repository diff --git a/ui/tests/providers/providers-page.ts b/ui/tests/providers/providers-page.ts index 8be6d3e0e3..0d4b7da31e 100644 --- a/ui/tests/providers/providers-page.ts +++ b/ui/tests/providers/providers-page.ts @@ -19,6 +19,12 @@ export interface M365ProviderData { alias?: string; } +// Kubernetes provider data +export interface KubernetesProviderData { + context: string; + alias?: string; +} + // AWS credential options export const AWS_CREDENTIAL_OPTIONS = { AWS_ROLE_ARN: "role", @@ -71,6 +77,19 @@ export interface M365ProviderCredential { certificateContent?:string; } +// Kubernetes credential options +export const KUBERNETES_CREDENTIAL_OPTIONS = { + KUBECONFIG_CONTENT: "kubeconfig" +} as const; + +// Kubernetes credential type +type KubernetesCredentialType = (typeof KUBERNETES_CREDENTIAL_OPTIONS)[keyof typeof KUBERNETES_CREDENTIAL_OPTIONS]; + +// Kubernetes provider credential +export interface KubernetesProviderCredential { + type: KubernetesCredentialType; + kubeconfigContent:string; +} // Providers page export class ProvidersPage extends BasePage { @@ -124,6 +143,10 @@ export class ProvidersPage extends BasePage { readonly m365TenantIdInput: Locator; readonly m365CertificateContentInput: Locator; + // Kubernetes provider form elements + readonly kubernetesContextInput: Locator; + readonly kubernetesKubeconfigContentInput: Locator; + // Delete button readonly deleteProviderConfirmationButton: Locator; @@ -131,9 +154,7 @@ export class ProvidersPage extends BasePage { super(page); // Button to add a new cloud provider - this.addProviderButton = page.getByText("Add Cloud Provider", { - exact: true, - }); + this.addProviderButton = page.getByRole("button", { name: "Add Cloud Provider", exact: true }); // Table displaying existing providers this.providersTable = page.getByRole("table"); @@ -157,23 +178,27 @@ export class ProvidersPage extends BasePage { this.githubProviderRadio = page.getByRole("radio", { name: /GitHub/i }); // AWS provider form inputs - this.accountIdInput = page.getByLabel("Account ID"); + this.accountIdInput = page.getByRole("textbox", { name: "Account ID" }); // AZURE provider form inputs - this.azureSubscriptionIdInput = page.getByLabel("Subscription ID"); - this.azureClientIdInput = page.getByLabel("Client ID"); - this.azureClientSecretInput = page.getByLabel("Client Secret"); - this.azureTenantIdInput = page.getByLabel("Tenant 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.azureTenantIdInput = page.getByRole("textbox", { name: "Tenant ID" }); // M365 provider form inputs - this.m365domainIdInput = page.getByLabel("Domain ID"); - this.m365ClientIdInput = page.getByLabel("Client ID"); - this.m365ClientSecretInput = page.getByLabel("Client Secret"); - this.m365TenantIdInput = page.getByLabel("Tenant ID"); - this.m365CertificateContentInput = page.getByLabel("Certificate Content"); + 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" }); + + // Kubernetes provider form inputs + this.kubernetesContextInput = page.getByRole("textbox", { name: "Context" }); + this.kubernetesKubeconfigContentInput = page.getByRole("textbox", { name: "Kubeconfig Content" }); // Alias input - this.aliasInput = page.getByLabel("Provider alias (optional)"); + this.aliasInput = page.getByRole("textbox", { name: "Provider alias (optional)" }); // Navigation buttons in the form (next and back) this.nextButton = page @@ -208,12 +233,12 @@ export class ProvidersPage extends BasePage { }); // Inputs for IAM Role credentials - this.roleArnInput = page.getByLabel("Role ARN"); - this.externalIdInput = page.getByLabel("External ID"); + this.roleArnInput = page.getByRole("textbox", { name: "Role ARN" }); + this.externalIdInput = page.getByRole("textbox", { name: "External ID" }); // Inputs for static credentials - this.accessKeyIdInput = page.getByLabel("Access Key ID"); - this.secretAccessKeyInput = page.getByLabel("Secret Access Key"); + this.accessKeyIdInput = page.getByRole("textbox", { name: "Access Key ID" }); + this.secretAccessKeyInput = page.getByRole("textbox", { name: "Secret Access Key" }); // Delete button this.deleteProviderConfirmationButton = page.locator( @@ -255,6 +280,14 @@ export class ProvidersPage extends BasePage { await this.waitForPageLoad(); } + async selectKubernetesProvider(): Promise { + // Select the Kubernetes provider + + await this.kubernetesProviderRadio.click({ force: true }); + await this.waitForPageLoad(); + } + + async fillAWSProviderDetails(data: AWSProviderData): Promise { // Fill the AWS provider details @@ -285,6 +318,15 @@ export class ProvidersPage extends BasePage { } } + async fillKubernetesProviderDetails(data: KubernetesProviderData): Promise { + // Fill the Kubernetes provider details + + await this.kubernetesContextInput.fill(data.context); + 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. @@ -458,6 +500,14 @@ export class ProvidersPage extends BasePage { } } + async fillKubernetesCredentials(credentials: KubernetesProviderCredential): Promise { + // Fill the Kubernetes credentials form + + if (credentials.kubeconfigContent) { + await this.kubernetesKubeconfigContentInput.fill(credentials.kubeconfigContent); + } + } + async verifyPageLoaded(): Promise { // Verify the providers page is loaded @@ -489,6 +539,12 @@ export class ProvidersPage extends BasePage { await expect(this.m365CertificateCredentialsRadio).toBeVisible(); } + async verifyKubernetesCredentialsPageLoaded(): Promise { + // Verify the Kubernetes credentials page is loaded + + await expect(this.page).toHaveTitle(/Prowler/); + await expect(this.kubernetesContextInput).toBeVisible(); + } async verifyLaunchScanPageLoaded(): Promise { diff --git a/ui/tests/providers/providers.md b/ui/tests/providers/providers.md index 7ed8a74755..0b7229d659 100644 --- a/ui/tests/providers/providers.md +++ b/ui/tests/providers/providers.md @@ -265,3 +265,61 @@ - Provider cleanup performed before each test to ensure clean state - Requires valid Microsoft 365 tenant with certificate-based authentication - Certificate must be properly configured and have sufficient permissions for security scanning + +--- + +## Test Case: `PROVIDER-E2E-006` - Add Kubernetes Provider with Kubeconfig Content + +**Priority:** `critical` + +**Tags:** + +- type → @e2e, @serial +- feature → @providers +- provider → @kubernetes + +**Description/Objective:** Validates the complete flow of adding a new Kubernetes provider using kubeconfig content authentication + +**Preconditions:** + +- Admin user authentication required (admin.auth.setup setup) +- Environment variables configured: E2E_KUBERNETES_CONTEXT, E2E_KUBERNETES_KUBECONFIG_PATH +- Kubeconfig file must exist at the specified path +- Remove any existing provider with the same Context before starting the test +- This test must be run serially and never in parallel with other tests, as it requires the Context not to be already registered beforehand. + +### Flow Steps: + +1. Navigate to providers page +2. Click "Add Provider" button +3. Select Kubernetes provider type +4. Fill provider details (context and alias) +5. Verify credentials page is loaded +6. Fill Kubernetes credentials (kubeconfig content) +7. Launch initial scan +8. Verify redirect to provider management page + +### Expected Result: + +- Kubernetes provider successfully added with kubeconfig content +- Initial scan launched successfully +- User redirected to provider details page + +### Key verification points: + +- Provider page loads correctly +- Connect account page displays Kubernetes option +- Provider details form accepts context and alias +- Credentials page loads with kubeconfig content field +- Kubeconfig content is properly filled in the correct field +- Launch scan page appears +- Successful redirect to provider page after scan launch + +### Notes: + +- Test uses environment variables for Kubernetes context and kubeconfig file path +- Kubeconfig content is read from file and used for authentication +- Provider cleanup performed before each test to ensure clean state +- Requires valid Kubernetes cluster with accessible kubeconfig +- Kubeconfig must have sufficient permissions for security scanning +- Test validates that kubeconfig content goes to the correct field (not the context field) diff --git a/ui/tests/providers/providers.spec.ts b/ui/tests/providers/providers.spec.ts index f906a7c8d5..f0f3b0475d 100644 --- a/ui/tests/providers/providers.spec.ts +++ b/ui/tests/providers/providers.spec.ts @@ -11,7 +11,11 @@ import { M365ProviderData, M365ProviderCredential, M365_CREDENTIAL_OPTIONS, + KubernetesProviderData, + KubernetesProviderCredential, + KUBERNETES_CREDENTIAL_OPTIONS, } from "./providers-page"; +import fs from "fs"; test.describe.serial("AddProvider", () => { test.describe("Add AWS Provider", () => { @@ -423,4 +427,95 @@ test.describe.serial("AddProvider", () => { }, ); }); + + test.describe("Add Kubernetes Provider", () => { + // Providers page object + let providersPage: ProvidersPage; + + // Test data from environment variables + const context = process.env.E2E_KUBERNETES_CONTEXT; + const kubeconfigPath = process.env.E2E_KUBERNETES_KUBECONFIG_PATH; + + // Validate required environment variables + if (!context || !kubeconfigPath) { + throw new Error( + "E2E_KUBERNETES_CONTEXT and E2E_KUBERNETES_KUBECONFIG_PATH environment variables are not set", + ); + } + + // Read kubeconfig content from file + const kubeconfigContent = fs.readFileSync(kubeconfigPath, 'utf8'); + + // Verify kubeconfig file exists + if (!fs.existsSync(kubeconfigPath)) { + throw new Error(`Kubeconfig file not found at ${kubeconfigPath}`); + } + + // Setup before each test + test.beforeEach(async ({ page }) => { + providersPage = new ProvidersPage(page); + // Clean up existing provider to ensure clean test state + await helpers.deleteProviderIfExists(page, context); + }); + + // Use admin authentication for provider management + test.use({ storageState: "playwright/.auth/admin_user.json" }); + + test( + "should add a new Kubernetes provider with kubeconfig context", + { + tag: [ + "@critical", + "@e2e", + "@providers", + "@kubernetes", + "@serial", + "@PROVIDER-E2E-006", + ], + }, + async ({ page }) => { + // Prepare test data for Kubernetes provider + const kubernetesProviderData: KubernetesProviderData = { + context: context, + alias: "Test E2E Kubernetes Account - Kubeconfig Context", + }; + + // Prepare static credentials + const kubernetesCredentials: KubernetesProviderCredential = { + type: KUBERNETES_CREDENTIAL_OPTIONS.KUBECONFIG_CONTENT, + kubeconfigContent: kubeconfigContent, + }; + + // Navigate to providers page + await providersPage.goto(); + await providersPage.verifyPageLoaded(); + + // Start adding new provider + await providersPage.clickAddProvider(); + await providersPage.verifyConnectAccountPageLoaded(); + + // Select Kubernetes provider + await providersPage.selectKubernetesProvider(); + + // Fill provider details + await providersPage.fillKubernetesProviderDetails(kubernetesProviderData); + await providersPage.clickNext(); + + // Verify credentials page is loaded + await providersPage.verifyKubernetesCredentialsPageLoaded(); + + // Fill static credentials details + await providersPage.fillKubernetesCredentials(kubernetesCredentials); + await providersPage.clickNext(); + + // Launch scan + await providersPage.verifyLaunchScanPageLoaded(); + await providersPage.clickNext(); + + // Wait for redirect to provider page + await providersPage.verifyLoadProviderPageAfterNewProvider(); + }, + ); + + }); });