diff --git a/dashboard/tests/e2e/profile/profile.spec.ts b/dashboard/tests/e2e/profile/profile.spec.ts new file mode 100644 index 0000000..9016a2f --- /dev/null +++ b/dashboard/tests/e2e/profile/profile.spec.ts @@ -0,0 +1,150 @@ +import { expect, type Page } from '@playwright/test'; +import { test } from '../fixtures/ids'; +import { CleanupRegistry } from '../fixtures/cleanup'; +import { adminApi } from '../fixtures/api'; + +// Phase B7 — Profile + API Keys (/admin/profile). Covers the +// mint/reveal/revoke flow, the app-binding mutual-exclusion guard, +// and adversarial inputs. + +const cleanup = new CleanupRegistry(); +test.afterEach(async () => { + await cleanup.run(); +}); + +async function createApp(slug: string): Promise { + const api = await adminApi(); + try { + const res = await api.post('/api/v1/admin/apps', { data: { slug, name: slug } }); + expect(res.ok()).toBe(true); + return ((await res.json()) as { id: string }).id; + } finally { + await api.dispose(); + } +} + +async function openMintForm(page: Page): Promise { + await page.goto('/admin/profile'); + await page.getByRole('button', { name: /\+ Mint API key/ }).click(); +} + +async function registerKeyCleanupByName(name: string): Promise { + const api = await adminApi(); + try { + const res = await api.get('/api/v1/admin/api-keys'); + const all = (await res.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(); + } +} + +test.describe('B7 profile + API keys', () => { + test('mint instance-wide key: reveal → ack → key appears in list', async ({ page }) => { + const name = `e2e-mint-${Date.now()}`; + await openMintForm(page); + await page.getByLabel('Name').fill(name); + // Pick a non-instance scope so we don't need to worry about + // mutual exclusion here. The scope-chip is a