diff --git a/.github/workflows/deploy-builder-api.yml b/.github/workflows/deploy-builder-api.yml index f5fa3d5..1c18727 100644 --- a/.github/workflows/deploy-builder-api.yml +++ b/.github/workflows/deploy-builder-api.yml @@ -50,7 +50,7 @@ jobs: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('builder-api/pom.xml') }} restore-keys: | - ${{ runner.os }}-maven- + ${{ runner.os }}-maven-${{ hashFiles('builder-api/pom.xml') }} # Configure Workload Identity Federation and generate an access token - id: 'auth' diff --git a/.github/workflows/run-e2e-tests.yml b/.github/workflows/run-e2e-tests.yml new file mode 100644 index 0000000..d57dfc8 --- /dev/null +++ b/.github/workflows/run-e2e-tests.yml @@ -0,0 +1,123 @@ +name: Run E2e Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + +env: + PROJECT_ID: 'benefit-decision-toolkit-play' + WORKLOAD_IDENTITY_PROVIDER: 'projects/1034049717668/locations/global/workloadIdentityPools/github-actions-google-cloud/providers/github' + +jobs: + run-e2e-tests: + runs-on: 'ubuntu-latest' + + # Add these permissions for Workload Identity Federation + permissions: + contents: read + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: lts/* + + # Devbox Setup # + - name: 'Create .env file' # Devbox needs a .env file to exist, even if it's empty + run: touch .env + + - name: Rename env files + run: | + mv builder-frontend/.env.example builder-frontend/.env + mv builder-api/.env.example builder-api/.env + + - name: 'Install devbox' # Setup devbox which includes Node.js, Firebase CLI, and Google Cloud SDK + uses: 'jetify-com/devbox-install-action@v0.12.0' + with: + enable-cache: true + + # Cache Maven dependencies to speed up builds + - name: 'Cache Maven dependencies' + uses: 'actions/cache@v4' + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('builder-api/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + - id: 'auth' # Configure Workload Identity Federation and generate an access token + name: 'Authenticate to Google Cloud' + uses: 'google-github-actions/auth@v2' + with: + workload_identity_provider: '${{ env.WORKLOAD_IDENTITY_PROVIDER }}' + service_account: cicd-build-deploy-api@benefit-decision-toolkit-play.iam.gserviceaccount.com + project_id: ${{ env.PROJECT_ID }} + + - name: Cache node modules + uses: actions/cache@v4 + with: + path: builder-frontend/node_modules + key: ${{ runner.os }}-node-${{ hashFiles('builder-frontend/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Install frontend dependencies + working-directory: builder-frontend + run: devbox run install-builder-frontend-ci + + - name: Load E2E emulator data + run: | + rm -rf emulator-data + cp -r e2e/e2e-emulator-data emulator-data + + - name: Run all Devbox services + run: devbox services up -b + continue-on-error: true + + # E2E Testing # + - name: Install Playwright dependencies + run: npm ci + working-directory: e2e + + - name: Install Playwright Browsers + run: npx playwright install --with-deps + working-directory: e2e + + # - name: Wait for Firebase emulators to be available + # uses: nev7n/wait_for_response@v1 + # with: + # url: 'http://localhost:4000/' + # responseCode: 200 + # timeout: 90000 + # interval: 1000 + # continue-on-error: true + + - name: Wait for App to be available + uses: nev7n/wait_for_response@v1 + with: + url: 'http://localhost:5173/' + responseCode: 200 + timeout: 90000 + interval: 1000 + continue-on-error: true + + - name: Run Playwright tests + run: npx playwright test + working-directory: e2e + continue-on-error: true + + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: e2e/playwright-report/ + retention-days: 30 + + # Devbox Cleanup # + - name: Stop all Devbox services + run: devbox services stop + continue-on-error: true diff --git a/devbox.json b/devbox.json index 276b7bf..3719688 100644 --- a/devbox.json +++ b/devbox.json @@ -7,7 +7,8 @@ "firebase-tools@latest", "google-cloud-sdk@latest", "nodejs@22", - "bruno-cli@latest" + "bruno-cli@latest", + "process-compose@latest" ], "env_from": ".env", "shell": { diff --git a/devbox.lock b/devbox.lock index f731282..07cb54a 100644 --- a/devbox.lock +++ b/devbox.lock @@ -314,6 +314,54 @@ } } }, + "process-compose@latest": { + "last_modified": "2025-11-23T21:50:36Z", + "resolved": "github:NixOS/nixpkgs/ee09932cedcef15aaf476f9343d1dea2cb77e261#process-compose", + "source": "devbox-search", + "version": "1.78.0", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/d00iad83k88x0fq045h5pzhfj9ibdd3a-process-compose-1.78.0", + "default": true + } + ], + "store_path": "/nix/store/d00iad83k88x0fq045h5pzhfj9ibdd3a-process-compose-1.78.0" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/zx59c11mchqfjpl0yvv2fad87pf3p8mn-process-compose-1.78.0", + "default": true + } + ], + "store_path": "/nix/store/zx59c11mchqfjpl0yvv2fad87pf3p8mn-process-compose-1.78.0" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/hwipxpsgzpv8d51xvqdjyx3vbzaaf3rr-process-compose-1.78.0", + "default": true + } + ], + "store_path": "/nix/store/hwipxpsgzpv8d51xvqdjyx3vbzaaf3rr-process-compose-1.78.0" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/vw9d8v9r0kp44lmizysj7idmqyf9747l-process-compose-1.78.0", + "default": true + } + ], + "store_path": "/nix/store/vw9d8v9r0kp44lmizysj7idmqyf9747l-process-compose-1.78.0" + } + } + }, "quarkus@latest": { "last_modified": "2025-08-11T16:06:55Z", "resolved": "github:NixOS/nixpkgs/4e942f9ef5b35526597c354d1ded817d1c285ef1#quarkus", diff --git a/e2e/.auth/user.json b/e2e/.auth/user.json new file mode 100644 index 0000000..f4ec355 --- /dev/null +++ b/e2e/.auth/user.json @@ -0,0 +1,4 @@ +{ + "cookies": [], + "origins": [] +} \ No newline at end of file diff --git a/e2e/.gitignore b/e2e/.gitignore new file mode 100644 index 0000000..335bd46 --- /dev/null +++ b/e2e/.gitignore @@ -0,0 +1,8 @@ + +# Playwright +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/.auth/ diff --git a/e2e/e2e-emulator-data/auth_export/accounts.json b/e2e/e2e-emulator-data/auth_export/accounts.json new file mode 100644 index 0000000..4bf0586 --- /dev/null +++ b/e2e/e2e-emulator-data/auth_export/accounts.json @@ -0,0 +1,25 @@ +{ + "kind": "identitytoolkit#DownloadAccountResponse", + "users": [ + { + "localId": "E2uB0h1FpSUKq5rGObr9jvmbn15E", + "lastLoginAt": "1766942325287", + "emailVerified": false, + "email": "test@example.com", + "salt": "fakeSaltirMZBjlsDBLmp2MfKjTv", + "passwordHash": "fakeHash:salt=fakeSaltirMZBjlsDBLmp2MfKjTv:password=testpassword123", + "passwordUpdatedAt": 1766942325288, + "validSince": "1766942325", + "createdAt": "1766942325287", + "providerUserInfo": [ + { + "providerId": "password", + "email": "test@example.com", + "federatedId": "test@example.com", + "rawId": "test@example.com" + } + ], + "lastRefreshAt": "2025-12-28T17:18:45.288Z" + } + ] +} \ No newline at end of file diff --git a/e2e/e2e-emulator-data/auth_export/config.json b/e2e/e2e-emulator-data/auth_export/config.json new file mode 100644 index 0000000..6f240f7 --- /dev/null +++ b/e2e/e2e-emulator-data/auth_export/config.json @@ -0,0 +1 @@ +{"signIn":{"allowDuplicateEmails":false},"emailPrivacyConfig":{"enableImprovedEmailPrivacy":false}} \ No newline at end of file diff --git a/e2e/e2e-emulator-data/firebase-export-metadata.json b/e2e/e2e-emulator-data/firebase-export-metadata.json new file mode 100644 index 0000000..a6f1f92 --- /dev/null +++ b/e2e/e2e-emulator-data/firebase-export-metadata.json @@ -0,0 +1,16 @@ +{ + "version": "14.11.2", + "firestore": { + "version": "1.19.8", + "path": "firestore_export", + "metadata_file": "firestore_export/firestore_export.overall_export_metadata" + }, + "auth": { + "version": "14.11.2", + "path": "auth_export" + }, + "storage": { + "version": "14.11.2", + "path": "storage_export" + } +} \ No newline at end of file diff --git a/e2e/e2e-emulator-data/firestore_export/all_namespaces/all_kinds/all_namespaces_all_kinds.export_metadata b/e2e/e2e-emulator-data/firestore_export/all_namespaces/all_kinds/all_namespaces_all_kinds.export_metadata new file mode 100644 index 0000000..dc98eba Binary files /dev/null and b/e2e/e2e-emulator-data/firestore_export/all_namespaces/all_kinds/all_namespaces_all_kinds.export_metadata differ diff --git a/e2e/e2e-emulator-data/firestore_export/all_namespaces/all_kinds/output-0 b/e2e/e2e-emulator-data/firestore_export/all_namespaces/all_kinds/output-0 new file mode 100644 index 0000000..cf1cd1b Binary files /dev/null and b/e2e/e2e-emulator-data/firestore_export/all_namespaces/all_kinds/output-0 differ diff --git a/e2e/e2e-emulator-data/firestore_export/firestore_export.overall_export_metadata b/e2e/e2e-emulator-data/firestore_export/firestore_export.overall_export_metadata new file mode 100644 index 0000000..b211c52 Binary files /dev/null and b/e2e/e2e-emulator-data/firestore_export/firestore_export.overall_export_metadata differ diff --git a/e2e/e2e-emulator-data/storage_export/buckets.json b/e2e/e2e-emulator-data/storage_export/buckets.json new file mode 100644 index 0000000..0c9f327 --- /dev/null +++ b/e2e/e2e-emulator-data/storage_export/buckets.json @@ -0,0 +1,7 @@ +{ + "buckets": [ + { + "id": "demo-bdt-dev.appspot.com" + } + ] +} \ No newline at end of file diff --git a/e2e/package-lock.json b/e2e/package-lock.json new file mode 100644 index 0000000..2d07d32 --- /dev/null +++ b/e2e/package-lock.json @@ -0,0 +1,97 @@ +{ + "name": "e2e", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "e2e", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.57.0", + "@types/node": "^24.10.1" + } + }, + "node_modules/@playwright/test": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", + "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/e2e/package.json b/e2e/package.json new file mode 100644 index 0000000..9922da5 --- /dev/null +++ b/e2e/package.json @@ -0,0 +1,14 @@ +{ + "name": "e2e", + "version": "1.0.0", + "main": "index.js", + "scripts": {}, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "devDependencies": { + "@playwright/test": "^1.57.0", + "@types/node": "^24.10.1" + } +} diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts new file mode 100644 index 0000000..28aa991 --- /dev/null +++ b/e2e/playwright.config.ts @@ -0,0 +1,66 @@ +import { defineConfig } from '@playwright/test'; +import path from 'path'; + +const authFile = path.join(__dirname, '.auth/user.json'); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('')`. */ + baseURL: 'http://localhost:5173', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + // Setup project - runs authentication before other tests + { + name: 'setup', + testMatch: /auth\.setup\.ts/, + }, + + // Main test project - depends on setup for authentication + { + name: 'chromium', + use: { + browserName: 'chromium', + // Use saved authentication state + storageState: authFile, + }, + dependencies: ['setup'], + }, + + // { + // name: 'firefox', + // use: { browserName: 'firefox' }, + // }, + + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://localhost:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/e2e/tests/smokeTest.spec.ts b/e2e/tests/smokeTest.spec.ts new file mode 100644 index 0000000..00c3e1d --- /dev/null +++ b/e2e/tests/smokeTest.spec.ts @@ -0,0 +1,33 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Smoke Tests', () => { + test.beforeEach(async ({ page }) => { + // Navigate to login page + await page.goto('/login'); + + // Fill in credentials (using Firebase emulator test user) + await page.locator('#email').fill('test@example.com'); + await page.locator('#password').fill('testpassword123'); + + // Click sign in button + await page.getByRole('button', { name: 'Sign In' }).click(); + + // Wait for navigation to home page after successful login + await expect(page).toHaveURL('/'); + }); + + test('user can view landing page after login', async ({ page }) => { + // Already on home page after beforeEach login + + // Verify the page title + await expect(page).toHaveTitle(/Benefit Decision Toolkit/); + + // Verify key elements of the landing page are visible + // Header with logout button indicates user is authenticated + await expect(page.getByText('Logout')).toBeVisible(); + + // Navigation tabs should be visible + await expect(page.getByRole('button', { name: 'Screeners' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Eligibility checks' })).toBeVisible(); + }); +}); diff --git a/firebase-export-1764791128917hV0xYz/auth_export/accounts.json b/firebase-export-1764791128917hV0xYz/auth_export/accounts.json new file mode 100644 index 0000000..f3a008d --- /dev/null +++ b/firebase-export-1764791128917hV0xYz/auth_export/accounts.json @@ -0,0 +1 @@ +{"kind":"identitytoolkit#DownloadAccountResponse","users":[{"localId":"hLDb1FqwfMGKZJLCjwNpNjmal3JC","createdAt":"1761583652308","lastLoginAt":"1764189369529","displayName":"Chicken Orange","providerUserInfo":[{"providerId":"google.com","rawId":"6608011541666817252291145694249152281893","federatedId":"6608011541666817252291145694249152281893","displayName":"Chicken Orange","email":"chicken.orange.260@example.com","screenName":"orange_chicken"}],"validSince":"1764791128","email":"chicken.orange.260@example.com","emailVerified":true,"disabled":false}]} \ No newline at end of file diff --git a/firebase-export-1764791128917hV0xYz/auth_export/config.json b/firebase-export-1764791128917hV0xYz/auth_export/config.json new file mode 100644 index 0000000..6f240f7 --- /dev/null +++ b/firebase-export-1764791128917hV0xYz/auth_export/config.json @@ -0,0 +1 @@ +{"signIn":{"allowDuplicateEmails":false},"emailPrivacyConfig":{"enableImprovedEmailPrivacy":false}} \ No newline at end of file diff --git a/firebase-export-1764791128917hV0xYz/firestore_export/all_namespaces/all_kinds/all_namespaces_all_kinds.export_metadata b/firebase-export-1764791128917hV0xYz/firestore_export/all_namespaces/all_kinds/all_namespaces_all_kinds.export_metadata new file mode 100644 index 0000000..77fdec5 Binary files /dev/null and b/firebase-export-1764791128917hV0xYz/firestore_export/all_namespaces/all_kinds/all_namespaces_all_kinds.export_metadata differ diff --git a/firebase-export-1764791128917hV0xYz/firestore_export/all_namespaces/all_kinds/output-0 b/firebase-export-1764791128917hV0xYz/firestore_export/all_namespaces/all_kinds/output-0 new file mode 100644 index 0000000..1394c67 Binary files /dev/null and b/firebase-export-1764791128917hV0xYz/firestore_export/all_namespaces/all_kinds/output-0 differ diff --git a/firebase-export-1764791128917hV0xYz/firestore_export/firestore_export.overall_export_metadata b/firebase-export-1764791128917hV0xYz/firestore_export/firestore_export.overall_export_metadata new file mode 100644 index 0000000..a11bbd1 Binary files /dev/null and b/firebase-export-1764791128917hV0xYz/firestore_export/firestore_export.overall_export_metadata differ