-
Notifications
You must be signed in to change notification settings - Fork 0
Admin portal E2Es #84
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
a821481
feat(admin): add E2E test infrastructure with Playwright
victor-cuevas a10dbf7
ci(admin): add E2E job to admin portal workflow
victor-cuevas e796acc
test(admin): add cross-browser E2E testing with Firefox and WebKit
victor-cuevas c6633c3
fix(admin): Fix e2e-admin-setup makefile command
victor-cuevas e3a6967
feat(admin): add automated WCAG 2.1 AA accessibility testing
victor-cuevas 8b532ff
test(admin): Fix auth e2e flakiness
victor-cuevas 0150f79
fix(admin): address PR comments
victor-cuevas File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| // Copyright 2026 CloudBlue LLC | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| // seed-user is a test-only tool that creates a user in the admin portal | ||
| // database without interactive terminal input. Used by E2E tests. | ||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "flag" | ||
| "fmt" | ||
| "os" | ||
| "time" | ||
|
|
||
| "github.com/cloudblue/chaperone/admin/auth" | ||
| "github.com/cloudblue/chaperone/admin/store" | ||
| ) | ||
|
|
||
| func main() { | ||
| if err := run(); err != nil { | ||
| fmt.Fprintln(os.Stderr, err) | ||
| os.Exit(1) | ||
| } | ||
| } | ||
|
|
||
| func run() error { | ||
| dbPath := flag.String("db", "", "Path to SQLite database") | ||
| username := flag.String("username", "", "Username to create") | ||
| password := flag.String("password", "", "Password for the user") | ||
| flag.Parse() | ||
|
|
||
| if *dbPath == "" || *username == "" || *password == "" { | ||
| return fmt.Errorf("usage: seed-user --db <path> --username <name> --password <pass>") | ||
| } | ||
|
|
||
| ctx := context.Background() | ||
|
|
||
| st, err := store.Open(ctx, *dbPath) | ||
| if err != nil { | ||
| return fmt.Errorf("opening database: %w", err) | ||
| } | ||
| defer st.Close() | ||
|
|
||
| svc := auth.NewService(st, 24*time.Hour, 2*time.Hour) | ||
|
|
||
| if err := svc.CreateUser(ctx, *username, *password); err != nil { | ||
| return fmt.Errorf("creating user: %w", err) | ||
| } | ||
|
|
||
| fmt.Printf("User %q created successfully\n", *username) | ||
| return nil | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import { test as setup, expect } from '@playwright/test'; | ||
| import path from 'node:path'; | ||
| import { TEST_USER, TEST_PASSWORD } from './helpers/constants.js'; | ||
|
|
||
| const authFile = path.join(import.meta.dirname, '.auth', 'user.json'); | ||
|
|
||
| setup('authenticate', async ({ page }) => { | ||
| await page.goto('/login'); | ||
| await page.getByTestId('login-username').fill(TEST_USER); | ||
| await page.getByTestId('login-password').fill(TEST_PASSWORD); | ||
| await page.getByTestId('login-submit').click(); | ||
|
|
||
| // Wait until redirected to dashboard | ||
| await expect(page.getByTestId('dashboard-title')).toBeVisible(); | ||
|
|
||
| await page.context().storageState({ path: authFile }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| import { spawn, execSync, execFileSync } from 'node:child_process'; | ||
| import fs from 'node:fs'; | ||
| import os from 'node:os'; | ||
| import path from 'node:path'; | ||
| import { waitForHealth } from './helpers/services.js'; | ||
| import { TEST_USER, PW_CHANGE_USER, TEST_PASSWORD } from './helpers/constants.js'; | ||
|
|
||
| const ROOT = path.resolve(import.meta.dirname, '..', '..', '..'); | ||
|
|
||
| function killPid(envVar) { | ||
| const pid = process.env[envVar]; | ||
| if (pid) { | ||
| try { | ||
| process.kill(Number(pid), 'SIGTERM'); | ||
| } catch { | ||
| // Process may have already exited | ||
| } | ||
| } | ||
| } | ||
|
|
||
| export default async function globalSetup() { | ||
|
arnaugiralt marked this conversation as resolved.
|
||
| const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'chaperone-e2e-')); | ||
| const dbPath = path.join(tmpDir, 'test.db'); | ||
| const binDir = path.join(ROOT, 'bin'); | ||
| const authDir = path.join(import.meta.dirname, '.auth'); | ||
|
|
||
| fs.mkdirSync(authDir, { recursive: true }); | ||
|
|
||
| // Store paths for teardown | ||
| process.env.E2E_TMP_DIR = tmpDir; | ||
| process.env.E2E_DB_PATH = dbPath; | ||
|
|
||
| try { | ||
| // 1. Build admin binary + seed-user | ||
| console.log('[e2e] Building admin binary...'); | ||
| execSync('make build-admin', { cwd: ROOT, stdio: 'pipe' }); | ||
| console.log('[e2e] Building seed-user...'); | ||
| execSync( | ||
| `cd admin && go build -o ../bin/seed-user ./cmd/seed-user`, | ||
| { cwd: ROOT, stdio: 'pipe' }, | ||
| ); | ||
|
|
||
| // 2. Start mock chaperone fleet | ||
| console.log('[e2e] Starting mock chaperone fleet...'); | ||
| const mockProc = spawn( | ||
| 'node', | ||
| [path.join(ROOT, 'test', 'mock-chaperone', 'mock-chaperone.js')], | ||
| { stdio: 'ignore' }, | ||
| ); | ||
| process.env.E2E_MOCK_PID = String(mockProc.pid); | ||
|
|
||
| await waitForHealth('http://127.0.0.1:19091/_ops/health', 15_000); | ||
| console.log('[e2e] Mock fleet ready'); | ||
|
|
||
| // 3. Seed test users | ||
| console.log('[e2e] Seeding test users...'); | ||
| const seedBin = path.join(binDir, 'seed-user'); | ||
| execFileSync(seedBin, ['--db', dbPath, '--username', TEST_USER, '--password', TEST_PASSWORD], { | ||
| cwd: ROOT, | ||
| stdio: 'pipe', | ||
| }); | ||
| execFileSync(seedBin, ['--db', dbPath, '--username', PW_CHANGE_USER, '--password', TEST_PASSWORD], { | ||
| cwd: ROOT, | ||
| stdio: 'pipe', | ||
| }); | ||
|
|
||
| // 4. Start admin server | ||
| console.log('[e2e] Starting admin server...'); | ||
| const adminProc = spawn( | ||
| path.join(binDir, 'chaperone-admin'), | ||
| [], | ||
| { | ||
| stdio: 'ignore', | ||
| env: { | ||
| ...process.env, | ||
| CHAPERONE_ADMIN_SERVER_ADDR: '127.0.0.1:8080', | ||
| CHAPERONE_ADMIN_DATABASE_PATH: dbPath, | ||
| CHAPERONE_ADMIN_SERVER_SECURE_COOKIES: 'false', | ||
| CHAPERONE_ADMIN_SCRAPER_INTERVAL: '3s', | ||
| CHAPERONE_ADMIN_SCRAPER_TIMEOUT: '2s', | ||
| CHAPERONE_ADMIN_LOG_LEVEL: 'warn', | ||
| }, | ||
| }, | ||
| ); | ||
| process.env.E2E_ADMIN_PID = String(adminProc.pid); | ||
|
|
||
| await waitForHealth('http://127.0.0.1:8080/api/health', 15_000); | ||
| console.log('[e2e] Admin server ready'); | ||
| } catch (err) { | ||
| // Kill any processes we spawned before Playwright skips globalTeardown | ||
| killPid('E2E_ADMIN_PID'); | ||
| killPid('E2E_MOCK_PID'); | ||
| throw err; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import fs from 'node:fs'; | ||
|
|
||
| export default async function globalTeardown() { | ||
| // Kill admin server | ||
| const adminPid = process.env.E2E_ADMIN_PID; | ||
| if (adminPid) { | ||
| try { | ||
| process.kill(Number(adminPid), 'SIGTERM'); | ||
| } catch { | ||
| // Process may have already exited | ||
| } | ||
| } | ||
|
|
||
| // Kill mock chaperone fleet | ||
| const mockPid = process.env.E2E_MOCK_PID; | ||
| if (mockPid) { | ||
| try { | ||
| process.kill(Number(mockPid), 'SIGTERM'); | ||
| } catch { | ||
| // Process may have already exited | ||
| } | ||
| } | ||
|
|
||
| // Remove temp directory | ||
| const tmpDir = process.env.E2E_TMP_DIR; | ||
| if (tmpDir) { | ||
| try { | ||
| fs.rmSync(tmpDir, { recursive: true, force: true }); | ||
| } catch { | ||
| // Best-effort cleanup | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| export const TEST_USER = 'admin'; | ||
| export const PW_CHANGE_USER = 'admin-pw-test'; | ||
| export const TEST_PASSWORD = 'testpassword12'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import { test as base, expect } from '@playwright/test'; | ||
| import { TEST_USER, TEST_PASSWORD } from './constants.js'; | ||
|
|
||
| /** | ||
| * Custom fixtures for E2E tests. | ||
| * Provides an authenticated API context with CSRF handling for seeding data. | ||
| */ | ||
| export const test = base.extend({ | ||
| /** | ||
| * An authenticated API request context with CSRF support. | ||
| * Use for seeding instances via the REST API in beforeAll hooks. | ||
| */ | ||
| authedAPI: async ({ playwright }, use) => { | ||
| const ctx = await playwright.request.newContext({ | ||
| baseURL: 'http://127.0.0.1:8080', | ||
| }); | ||
|
|
||
| // Login to get session + CSRF cookies | ||
| const loginRes = await ctx.post('/api/login', { | ||
| data: { username: TEST_USER, password: TEST_PASSWORD }, | ||
| }); | ||
| expect(loginRes.ok()).toBeTruthy(); | ||
|
|
||
| // Extract CSRF token from cookies | ||
| const cookies = await ctx.storageState(); | ||
| const csrfCookie = cookies.cookies.find((c) => c.name === 'csrf_token'); | ||
| if (!csrfCookie) throw new Error('expected csrf_token cookie after login'); | ||
| const csrfToken = csrfCookie.value; | ||
|
|
||
| // Wrap context to auto-include CSRF header on writes | ||
| const originalPost = ctx.post.bind(ctx); | ||
| const originalPut = ctx.put.bind(ctx); | ||
| const originalDelete = ctx.delete.bind(ctx); | ||
|
|
||
| ctx.post = (url, options = {}) => | ||
| originalPost(url, { | ||
| ...options, | ||
| headers: { ...options.headers, 'X-CSRF-Token': csrfToken }, | ||
| }); | ||
| ctx.put = (url, options = {}) => | ||
| originalPut(url, { | ||
| ...options, | ||
| headers: { ...options.headers, 'X-CSRF-Token': csrfToken }, | ||
| }); | ||
| ctx.delete = (url, options = {}) => | ||
| originalDelete(url, { | ||
| ...options, | ||
| headers: { ...options.headers, 'X-CSRF-Token': csrfToken }, | ||
| }); | ||
|
|
||
| await use(ctx); | ||
| await ctx.dispose(); | ||
| }, | ||
| }); | ||
|
|
||
| export { expect }; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.