diff --git a/.github/test-impact.yml b/.github/test-impact.yml index 7c290eeaa9..874e9eba50 100644 --- a/.github/test-impact.yml +++ b/.github/test-impact.yml @@ -249,6 +249,7 @@ modules: - ui/tests/profile/** - ui/tests/lighthouse/** - ui/tests/home/** + - ui/tests/navigation/** - ui/tests/attack-paths/** - name: api-serializers @@ -275,6 +276,7 @@ modules: - ui/tests/profile/** - ui/tests/lighthouse/** - ui/tests/home/** + - ui/tests/navigation/** - ui/tests/attack-paths/** - name: api-filters @@ -432,6 +434,14 @@ modules: e2e: - ui/tests/lighthouse/** + - name: ui-navigation + match: + - ui/components/layout/** + - ui/tests/navigation/** + tests: [] + e2e: + - ui/tests/navigation/** + - name: ui-overview match: - ui/components/overview/** @@ -464,6 +474,7 @@ modules: - ui/tests/profile/** - ui/tests/lighthouse/** - ui/tests/home/** + - ui/tests/navigation/** - ui/tests/attack-paths/** - name: ui-attack-paths diff --git a/.github/workflows/ui-e2e-tests-v2.yml b/.github/workflows/ui-e2e-tests-v2.yml index 98654699e0..629e1b7661 100644 --- a/.github/workflows/ui-e2e-tests-v2.yml +++ b/.github/workflows/ui-e2e-tests-v2.yml @@ -10,12 +10,12 @@ on: - master - "v5.*" paths: - - '.github/workflows/ui-e2e-tests-v2.yml' - - '.github/test-impact.yml' - - 'ui/**' - - 'api/**' # API changes can affect UI E2E - - '!ui/CHANGELOG.md' - - '!api/CHANGELOG.md' + - ".github/workflows/ui-e2e-tests-v2.yml" + - ".github/test-impact.yml" + - "ui/**" + - "api/**" # API changes can affect UI E2E + - "!ui/CHANGELOG.md" + - "!api/CHANGELOG.md" concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -39,17 +39,19 @@ jobs: (needs.impact-analysis.outputs.has-ui-e2e == 'true' || needs.impact-analysis.outputs.run-all == 'true') runs-on: ubuntu-latest env: - AUTH_SECRET: 'fallback-ci-secret-for-testing' + AUTH_SECRET: "fallback-ci-secret-for-testing" AUTH_TRUST_HOST: true - NEXTAUTH_URL: 'http://localhost:3000' - AUTH_URL: 'http://localhost:3000' - UI_API_BASE_URL: 'http://localhost:8080/api/v1' + NEXTAUTH_URL: "http://localhost:3000" + AUTH_URL: "http://localhost:3000" + UI_API_BASE_URL: "http://localhost:8080/api/v1" E2E_ADMIN_USER: ${{ secrets.E2E_ADMIN_USER }} E2E_ADMIN_PASSWORD: ${{ secrets.E2E_ADMIN_PASSWORD }} E2E_AWS_PROVIDER_ACCOUNT_ID: ${{ secrets.E2E_AWS_PROVIDER_ACCOUNT_ID }} E2E_AWS_PROVIDER_ACCESS_KEY: ${{ secrets.E2E_AWS_PROVIDER_ACCESS_KEY }} E2E_AWS_PROVIDER_SECRET_KEY: ${{ secrets.E2E_AWS_PROVIDER_SECRET_KEY }} E2E_AWS_PROVIDER_ROLE_ARN: ${{ secrets.E2E_AWS_PROVIDER_ROLE_ARN }} + E2E_AWS_ORGANIZATION_ID: ${{ secrets.E2E_AWS_ORGANIZATION_ID }} + E2E_AWS_ORGANIZATION_ROLE_ARN: ${{ secrets.E2E_AWS_ORGANIZATION_ROLE_ARN }} E2E_AZURE_SUBSCRIPTION_ID: ${{ secrets.E2E_AZURE_SUBSCRIPTION_ID }} E2E_AZURE_CLIENT_ID: ${{ secrets.E2E_AZURE_CLIENT_ID }} E2E_AZURE_SECRET_ID: ${{ secrets.E2E_AZURE_SECRET_ID }} @@ -59,7 +61,7 @@ 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: 'kind-kind' + E2E_KUBERNETES_CONTEXT: "kind-kind" E2E_KUBERNETES_KUBECONFIG_PATH: /home/runner/.kube/config E2E_GCP_BASE64_SERVICE_ACCOUNT_KEY: ${{ secrets.E2E_GCP_BASE64_SERVICE_ACCOUNT_KEY }} E2E_GCP_PROJECT_ID: ${{ secrets.E2E_GCP_PROJECT_ID }} @@ -118,6 +120,113 @@ jobs: env: NEEDS_IMPACT_ANALYSIS_OUTPUTS_MODULES: ${{ needs.impact-analysis.outputs.modules }} + - name: Validate E2E prerequisites + shell: bash + env: + IS_FORK_PR: ${{ github.event.pull_request.head.repo.fork }} + run: | + if [[ "${IS_FORK_PR}" == "true" ]]; then + echo "Skipping secret preflight for fork pull request." + exit 0 + fi + + declare -A required=() + + suite_selected() { + [[ "${RUN_ALL_TESTS}" == "true" ]] || + [[ " ${E2E_TEST_PATHS} " == *"ui/tests/$1/"* ]] + } + + require_vars() { + local variable + for variable in "$@"; do + required["${variable}"]=1 + done + } + + if suite_selected auth || suite_selected providers || + suite_selected invitations || suite_selected scans || + suite_selected navigation; then + require_vars E2E_ADMIN_USER E2E_ADMIN_PASSWORD + fi + + if suite_selected sign-up; then + require_vars E2E_NEW_USER_PASSWORD + fi + + if suite_selected invitations; then + require_vars E2E_NEW_USER_PASSWORD E2E_ORGANIZATION_ID + fi + + if suite_selected scans; then + require_vars \ + E2E_AWS_PROVIDER_ACCOUNT_ID \ + E2E_AWS_PROVIDER_ACCESS_KEY \ + E2E_AWS_PROVIDER_SECRET_KEY + fi + + if suite_selected providers; then + require_vars \ + E2E_AWS_PROVIDER_ACCOUNT_ID \ + E2E_AWS_PROVIDER_ACCESS_KEY \ + E2E_AWS_PROVIDER_SECRET_KEY \ + E2E_AWS_PROVIDER_ROLE_ARN \ + E2E_AWS_ORGANIZATION_ID \ + E2E_AWS_ORGANIZATION_ROLE_ARN \ + E2E_AZURE_SUBSCRIPTION_ID \ + E2E_AZURE_CLIENT_ID \ + E2E_AZURE_SECRET_ID \ + E2E_AZURE_TENANT_ID \ + E2E_M365_DOMAIN_ID \ + E2E_M365_CLIENT_ID \ + E2E_M365_SECRET_ID \ + E2E_M365_TENANT_ID \ + E2E_M365_CERTIFICATE_CONTENT \ + E2E_GCP_BASE64_SERVICE_ACCOUNT_KEY \ + E2E_GCP_PROJECT_ID \ + E2E_GITHUB_APP_ID \ + E2E_GITHUB_BASE64_APP_PRIVATE_KEY \ + E2E_GITHUB_USERNAME \ + E2E_GITHUB_PERSONAL_ACCESS_TOKEN \ + E2E_GITHUB_ORGANIZATION \ + E2E_GITHUB_ORGANIZATION_ACCESS_TOKEN \ + E2E_OCI_TENANCY_ID \ + E2E_OCI_USER_ID \ + E2E_OCI_FINGERPRINT \ + E2E_OCI_KEY_CONTENT \ + E2E_ALIBABACLOUD_ACCOUNT_ID \ + E2E_ALIBABACLOUD_ACCESS_KEY_ID \ + E2E_ALIBABACLOUD_ACCESS_KEY_SECRET \ + E2E_ALIBABACLOUD_ROLE_ARN \ + E2E_OKTA_DOMAIN \ + E2E_OKTA_CLIENT_ID \ + E2E_OKTA_BASE64_PRIVATE_KEY \ + E2E_GOOGLEWORKSPACE_CUSTOMER_ID \ + E2E_GOOGLEWORKSPACE_SERVICE_ACCOUNT_JSON \ + E2E_GOOGLEWORKSPACE_DELEGATED_USER \ + E2E_VERCEL_TEAM_ID \ + E2E_VERCEL_API_TOKEN + fi + + missing=() + if (( ${#required[@]} > 0 )); then + while IFS= read -r variable; do + [[ -z "${!variable:-}" ]] && missing+=("${variable}") + done < <(printf '%s\n' "${!required[@]}" | sort) + fi + + if (( ${#missing[@]} > 0 )); then + echo "Missing required E2E variables:" + printf ' - %s\n' "${missing[@]}" + { + echo "## Missing E2E prerequisites" + printf -- "- \`%s\`\n" "${missing[@]}" + } >> "${GITHUB_STEP_SUMMARY}" + exit 1 + fi + + echo "E2E prerequisite preflight passed." + - name: Create k8s Kind Cluster uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1 with: @@ -193,7 +302,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: - node-version-file: 'ui/.nvmrc' + node-version-file: "ui/.nvmrc" - name: Setup pnpm uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 diff --git a/.gitignore b/.gitignore index 6c11a8698c..4545146688 100644 --- a/.gitignore +++ b/.gitignore @@ -173,3 +173,5 @@ GEMINI.md # Docker docker-compose.override.yml docker-compose-dev.override.yml +# Local Pi runtime state +.atl/ diff --git a/ui/.nvmrc b/ui/.nvmrc index 3fe3b1570a..5bf4400f22 100644 --- a/ui/.nvmrc +++ b/ui/.nvmrc @@ -1 +1 @@ -24.13.0 +24.15.0 diff --git a/ui/tests/home/home-page.ts b/ui/tests/home/home-page.ts index e61be49b76..44bf6b5107 100644 --- a/ui/tests/home/home-page.ts +++ b/ui/tests/home/home-page.ts @@ -40,8 +40,8 @@ export class HomePage extends BasePage { this.dashboardCards = page.locator('[data-testid="dashboard-card"]'); this.overviewSection = page.locator('[data-testid="overview-section"]'); - // UI elements - this.logo = page.locator('svg[width="300"]'); + // UI elements - the sidebar logo is the home link's accessible content. + this.logo = page.getByRole("link", { name: "Prowler home" }); } // Navigation methods diff --git a/ui/tests/scans/scans-page.ts b/ui/tests/scans/scans-page.ts index f51b251c2c..667aaaed85 100644 --- a/ui/tests/scans/scans-page.ts +++ b/ui/tests/scans/scans-page.ts @@ -107,7 +107,11 @@ export class ScansPage extends BasePage { await expect(this.launchScanDialog).toBeVisible(); await this.scanProviderSelect.click(); await this.scanProviderSearchInput.fill(uid); - await this.scanProviderOption.first().click(); + + const providerOption = this.scanProviderOption.first(); + await expect(providerOption).toBeVisible(); + await expect(providerOption).toHaveAttribute("aria-disabled", "false"); + await providerOption.click(); } async fillScanNote(note: string): Promise { diff --git a/ui/tests/sign-in-base/sign-in-base-page.ts b/ui/tests/sign-in-base/sign-in-base-page.ts index d94fd57eb3..74a1ebfba1 100644 --- a/ui/tests/sign-in-base/sign-in-base-page.ts +++ b/ui/tests/sign-in-base/sign-in-base-page.ts @@ -67,8 +67,10 @@ export class SignInPage extends BasePage { this.signUpLink = page.getByRole("link", { name: "Sign up" }); this.backButton = page.getByRole("button", { name: "Back" }); - // UI elements - title is a

element, not a heading - this.logo = page.locator('svg[width="300"]'); + // UI elements - ProwlerBrand exposes the deployed brand variant as an image. + this.logo = page.getByRole("img", { + name: /^Prowler (Cloud|Local Server)$/, + }); // Use text matching with exact=true to avoid matching other elements this.pageTitle = page.getByText("Sign in", { exact: true });