import { expect, request, type Page } from '@playwright/test'; import { test } from '../fixtures/ids'; import { CleanupRegistry } from '../fixtures/cleanup'; import { adminApi } from '../fixtures/api'; // Full-stack integration scenarios. Unlike the per-page B1–B8 specs, // these drive a complete user journey across multiple pages and then // verify the data plane / API surface behaves the way the dashboard // promised it would. const API_BASE = process.env.E2E_API_BASE ?? 'http://127.0.0.1:18080'; const cleanup = new CleanupRegistry(); test.afterEach(async () => { await cleanup.run(); }); async function fillCodeMirror(page: Page, locator: string, text: string): Promise { const cm = page.locator(locator).first(); await cm.click(); await page.keyboard.press('ControlOrMeta+A'); await page.keyboard.press('Delete'); await page.keyboard.type(text); } test('end-to-end: app + domain + script + route via dashboard → invoke via public URL', async ({ page, uniqueSlug }) => { const slug = uniqueSlug('public'); const domain = `${slug}.local`; const routePath = `/${slug}/hello`; const scriptName = `${slug}-hello`; const scriptSource = `return #{ statusCode: 200, body: #{ source: "public", slug: "${slug}" } };`; // 1. Create the app from the apps list. await page.goto('/admin/apps'); await page.getByRole('button', { name: 'New app' }).click(); await page.getByLabel('Name').fill(slug); const slugInput = page.getByLabel('Slug'); await slugInput.fill(''); await slugInput.fill(slug); await page.getByRole('button', { name: 'Create app' }).click(); cleanup.app(slug); await expect(page.getByRole('link', { name: new RegExp(slug) })).toBeVisible(); // 2. Open the app and claim the domain on the Domains tab. await page.getByRole('link', { name: new RegExp(slug) }).click(); await expect(page).toHaveURL(new RegExp(`/admin/apps/${slug}$`)); await page.getByRole('button', { name: /^Domains \(\d+\)$/ }).click(); const domainForm = page.locator('form.create-form.inline'); await domainForm.getByPlaceholder(/app\.example\.com/).fill(domain); await domainForm.getByRole('button', { name: /^Add domain$/ }).click(); await expect(page.locator('.domain-row')).toContainText(domain); // 3. Create the script on the Scripts tab. await page.getByRole('button', { name: /^Scripts \(\d+\)$/ }).click(); await page.getByRole('button', { name: /^New script$/ }).click(); await page.getByLabel('Name').fill(scriptName); await fillCodeMirror(page, '.cm-content', scriptSource); await page.getByRole('button', { name: /^Create script$/ }).click(); // 4. Open the script and bind a route on the Routing tab. await page.getByRole('link', { name: new RegExp(scriptName) }).click(); await page.getByRole('button', { name: 'Routing' }).click(); await page.getByRole('button', { name: '+ Add route' }).click(); const routeForm = page.locator('form.route-form'); await routeForm.getByLabel('Path', { exact: true }).fill(routePath); await routeForm.getByLabel('Method').selectOption('GET'); await routeForm.getByLabel(/^Host/).fill(domain); await page.getByRole('button', { name: /^Create route$/ }).click(); await expect(page.locator('.route-list')).toContainText(routePath); // 5. Invoke via the public URL, with the Host header pointing at // the claimed domain. The dev backend listens on 127.0.0.1; the // orchestrator resolves the app from Host, then the route. const publicCtx = await request.newContext({ baseURL: API_BASE }); try { const res = await publicCtx.get(routePath, { headers: { host: domain } }); expect(res.status()).toBe(200); const body = (await res.json()) as { source: string; slug: string }; expect(body.source).toBe('public'); expect(body.slug).toBe(slug); } finally { await publicCtx.dispose(); } }); test('api key minted via dashboard works as a CLI bearer, then revoke disables it', async ({ page }) => { const name = `e2e-cli-${Date.now()}`; // 1. Mint the key from /profile and capture the revealed token. await page.goto('/admin/profile'); await page.getByRole('button', { name: /\+ Mint API key/ }).click(); const mintForm = page.locator('form.mint'); await mintForm.getByPlaceholder('e.g. ci-deploy').fill(name); // script:read is enough to read the scripts list — that's our // "CLI verb" below. await page.locator('label.scope-chip', { hasText: 'script:read' }).click(); await page.getByRole('button', { name: /^Mint key$/ }).click(); const reveal = page.locator('.reveal'); await expect(reveal).toBeVisible(); const rawToken = (await reveal.locator('code.token').textContent())?.trim(); expect(rawToken).toBeTruthy(); await reveal.getByRole('checkbox', { name: /saved this token/i }).check(); await reveal.getByRole('button', { name: /^Done$/ }).click(); // 2. Act like a CLI: call the API directly with Bearer . const cli = await request.newContext({ baseURL: API_BASE, extraHTTPHeaders: { authorization: `Bearer ${rawToken}` } }); try { const ok = await cli.get('/api/v1/admin/scripts'); expect(ok.status()).toBe(200); const body = (await ok.json()) as unknown; expect(Array.isArray(body)).toBe(true); // Sanity: a route the scope doesn't cover must reject. // `script:read` cannot list instance admins (that's // instance:admin territory). const denied = await cli.get('/api/v1/admin/admins'); expect(denied.status()).toBe(403); // 3. Revoke via the dashboard. await page.reload(); const revokeBtn = page.getByRole('button', { name: `Revoke ${name}` }); await expect(revokeBtn).toBeVisible(); await revokeBtn.click(); await page.getByRole('dialog').getByRole('button', { name: /^Revoke$/ }).click(); await expect(revokeBtn).toHaveCount(0); // 4. Same CLI call must now fail auth. const afterRevoke = await cli.get('/api/v1/admin/scripts'); expect(afterRevoke.status()).toBe(401); } finally { await cli.dispose(); } // Belt-and-braces cleanup: if the UI revoke missed, drop via API. const api = await adminApi(); try { const list = await api.get('/api/v1/admin/api-keys'); if (list.ok()) { const all = (await list.json()) as Array<{ id: string; name: string }>; const k = all.find((x) => x.name === name); if (k) cleanup.apiKey(k.id); } } finally { await api.dispose(); } });