import { expect, test } from '@playwright/test'; import { contextForUser } from './helpers'; const BASE_URL = process.env.E2E_BASE_URL ?? 'http://localhost:5173'; /** * Site-Admin system panel (issue #86): the maintenance-job table with a * working, audit-logged manual trigger, the audit viewer finding entries by * actor, the backup card, and Site-Admin-only access. */ test('lists maintenance jobs and triggers one manually', async ({ browser }) => { const admin = await contextForUser(browser, BASE_URL, 'fixture-admin'); const page = await admin.newPage(); await page.goto('/admin/system'); const jobsTable = page.locator('.system-jobs__table'); await expect(jobsTable).toBeVisible(); // All registered jobs appear (language-neutral: row count + button). // Keep in sync with the scheduler registrations: trash-purge, // version-thinning, page-compaction, data-export-purge, // notification-digest, orphan-file-sweep (#194), audit-retention (#196). await expect(jobsTable.locator('tbody tr')).toHaveCount(7); const firstRow = jobsTable.locator('tbody tr').first(); await firstRow.getByRole('button').click(); await expect(page.locator('.system-jobs__notice')).toBeVisible(); // A completed run shows truthful last-run data: a date and a duration. await expect(firstRow.locator('td').nth(2)).not.toHaveText('—'); await expect(firstRow.locator('td').nth(3)).not.toHaveText('—'); // The trigger itself lands in the audit log, attributed to the admin. const audit = page.locator('.system-audit__table'); await page.locator('.system-audit__filters input[type="text"]').fill('fixture-admin'); await page.locator('.system-audit__filters select').selectOption('job.triggered'); await page.locator('.system-audit__filters button[type="submit"]').click(); await expect(audit.locator('tbody tr').first()).toContainText('job:'); await admin.close(); }); test('the audit viewer finds a grant change by actor', async ({ browser }) => { // The fixture grant change happens as the shared pond's owner via the API // (the UI flow itself is covered by #55's pack). const owner = await contextForUser(browser, BASE_URL, 'fixture-user'); const ponds = await owner.request.get('/api/v1/ponds'); expect(ponds.ok()).toBe(true); const pond = ((await ponds.json()) as { id: string; type: string }[]).find( (p) => p.type === 'shared', ); expect(pond).toBeTruthy(); const me = await owner.request.get('/api/v1/auth/me'); const ownerId = ((await me.json()) as { id: string }).id; const created = await owner.request.post(`/api/v1/ponds/${pond!.id}/grants`, { data: { subjectType: 'user', subjectId: ownerId, role: 'reader', scopeType: 'pond', scopeId: null, effect: 'allow', }, }); expect(created.ok()).toBe(true); const grantId = ((await created.json()) as { id: string }).id; await owner.close(); const admin = await contextForUser(browser, BASE_URL, 'fixture-admin'); const page = await admin.newPage(); await page.goto('/admin/system'); await page.locator('.system-audit__filters input[type="text"]').fill('fixture-user'); await page.locator('.system-audit__filters select').selectOption('grant.created'); await page.locator('.system-audit__filters button[type="submit"]').click(); const firstRow = page.locator('.system-audit__table tbody tr').first(); await expect(firstRow).toContainText(`pond:${pond!.id}`); await expect(firstRow).toContainText('reader'); await admin.close(); // Cleanup so the pack is repeatable without stacking grants. const cleanup = await contextForUser(browser, BASE_URL, 'fixture-user'); await cleanup.request.delete(`/api/v1/ponds/${pond!.id}/grants/${grantId}`); await cleanup.close(); }); test('shows the backup card state', async ({ browser }) => { const admin = await contextForUser(browser, BASE_URL, 'fixture-admin'); const page = await admin.newPage(); await page.goto('/admin/system'); // CI stacks run without the sidecar → the unavailable notice; a stage with // backups shows the freshness badge instead. Either way the card renders. const card = page.locator('.system-backup'); await expect(card).toBeVisible(); await expect(card.locator('.system-backup__unavailable, .system-badge').first()).toBeVisible(); await admin.close(); }); test('regular users cannot open the system panel', async ({ browser }) => { const user = await contextForUser(browser, BASE_URL, 'fixture-user'); const page = await user.newPage(); const response = await user.request.get('/api/v1/admin/system/jobs'); expect(response.status()).toBe(403); await page.goto('/admin/system'); // The RequireSiteAdmin route guard keeps non-admins out of the panel. await expect(page.locator('.system-jobs__table')).toHaveCount(0); await user.close(); });