import { expect, type Page, type Response } from '@playwright/test'; import { test } from '../fixtures/ids'; import { CleanupRegistry } from '../fixtures/cleanup'; import { adminApi } from '../fixtures/api'; // Regression coverage for the slug-vs-UUID admin-endpoint bug fixed by // commit `aa493b9`: every per-app tab (`/admin/apps/[slug]/...`) must // accept the URL slug, not just a UUID. The original report was the // Queues tab; the underlying bug spanned 27 handlers across 6 backend // files (queues_api, files_api, secrets_api, topics_api, // dead_letters_api, triggers_api). This spec hits each tab in // isolation against a freshly-created app — no dependence on the // dev-seeded `default` app. // // What this catches: // - The original parse error ("Cannot parse app_id" / "UUID parsing // failed") surfaced as an error banner in the dashboard. // - Any non-2xx response to /api/v1/admin/apps/... during page load — // a broader net than the literal error text. A regression returning // 500 with a different message would otherwise pass silently. // // What this does NOT catch: // - Authz-role-dependent behavior. Admin-only coverage; members / // viewers have their own specs. // - The queue drilldown's data path (we visit the drilldown but the // queue is empty, so the "no consumer" empty-state branch is what // renders). const PARSE_ERROR = /Cannot parse|UUID parsing failed/i; interface AppCtx { slug: string; } async function createApp(uniqueSlug: (p: string) => string, prefix: string): Promise { const slug = uniqueSlug(prefix); const api = await adminApi(); try { const res = await api.post('/api/v1/admin/apps', { data: { slug, name: slug } }); if (!res.ok()) { throw new Error(`create app ${slug} failed: HTTP ${res.status()} ${await res.text()}`); } } finally { await api.dispose(); } return { slug }; } // Visit `path` and assert nothing broke. Captures every admin-API // response and fails on any 4xx/5xx — a wider net than asserting on a // specific error string. Also keeps the literal-parse-error check so // the test reads as a regression for the original bug. async function expectTabLoadsCleanly(page: Page, path: string): Promise { const failures: string[] = []; const handler = (response: Response) => { const url = response.url(); if (!url.includes('/api/v1/admin/apps/')) return; const status = response.status(); if (status >= 400) { failures.push(`${status} ${url}`); } }; page.on('response', handler); try { await page.goto(path); // Wait for a structural marker instead of `networkidle` — that // hook is officially discouraged for SPAs because background // polling (e.g., the queues page's 5s auto-refresh) keeps the // network "active" indefinitely. The
region is in // +layout.svelte so it's reliable. await expect(page.locator('main')).toBeVisible(); await expect( page.getByText(PARSE_ERROR), `literal parse error on ${path}` ).toHaveCount(0); } finally { page.off('response', handler); } if (failures.length > 0) { throw new Error(`HTTP failures on ${path}:\n ${failures.join('\n ')}`); } } const cleanup = new CleanupRegistry(); test.afterEach(async () => { await cleanup.run(); }); // Each tab covers a different backend file: // - `''` (root) → triggers_api, topics_api, secrets_api, // files trigger metadata, dead-letter count // - `/queues` → queues_api::list_queues // - `/queues/` → queues_api::get_queue (drilldown handler) // - `/files` → files_api::list_files // - `/dead-letters` → dead_letters_api::list + count // - `/users` → users_admin_api::list_users (already // lenient before the bug fix; included for // completeness so the spec doesn't regress // it later) // - `/users/invitations` → app_user_invitation_repo via the same // admin surface const TABS: Array<{ subpath: string; label: string }> = [ { subpath: '', label: 'main' }, { subpath: '/queues', label: 'queues' }, { subpath: '/queues/never-enqueued', label: 'queue drilldown' }, { subpath: '/files', label: 'files' }, { subpath: '/dead-letters', label: 'dead-letters' }, { subpath: '/users', label: 'app users' }, { subpath: '/users/invitations', label: 'app invitations' } ]; test.describe('per-app tab navigation accepts slug URLs', () => { for (const tab of TABS) { test(`${tab.label} loads cleanly`, async ({ page, uniqueSlug }) => { // Register cleanup BEFORE the API call so a flaky create // still gets swept up. CleanupRegistry tolerates 404s, so a // no-op cleanup is harmless if creation failed entirely. const slug = uniqueSlug('nav'); cleanup.app(slug); const api = await adminApi(); try { const res = await api.post('/api/v1/admin/apps', { data: { slug, name: slug } }); expect(res.ok()).toBe(true); } finally { await api.dispose(); } await expectTabLoadsCleanly(page, `/admin/apps/${slug}${tab.subpath}`); }); } test('queues tab specifically — the original bug report', async ({ page, uniqueSlug }) => { // Focused regression test for the original bug report. The slug // is unique (not "default") because dev seeding isn't guaranteed // in CI, but the failure mode being reproduced is identical. const slug = uniqueSlug('queues'); cleanup.app(slug); const api = await adminApi(); try { const res = await api.post('/api/v1/admin/apps', { data: { slug, name: slug } }); expect(res.ok()).toBe(true); } finally { await api.dispose(); } await expectTabLoadsCleanly(page, `/admin/apps/${slug}/queues`); // Positive assertion via stable testid. If a future copy edit // renames the empty-state text, this still anchors to the same // DOM element. await expect(page.getByTestId('queues-empty-state')).toBeVisible(); }); });