import { expect, test } from '@playwright/test'; // Phase B8 — Cross-cutting security. Things that aren't tied to a // single page: session handling, secret leakage, error states for // missing resources, and a sanity check that no XSS sink fires // anywhere in the dashboard's main authed routes. const VALID_USERNAME = process.env.E2E_ADMIN_USERNAME ?? 'admin'; const VALID_PASSWORD = process.env.E2E_ADMIN_PASSWORD ?? 'admin'; test.describe('B8 cross-cutting security', () => { test('expired/stale token: any authed call redirects to /login', async ({ page }) => { // Replace the storageState token with an obvious garbage // value; the fetch wrapper treats 401 as "go to /login". await page.goto('/admin/login'); await page.evaluate(() => { localStorage.setItem('picloud.admin.token', 'expired-or-bogus-token'); }); await page.goto('/admin/apps'); await expect(page).toHaveURL(/\/admin\/login$/); }); test('login response cookie is HttpOnly', async ({ request }) => { const res = await request.post('/api/v1/admin/auth/login', { data: { username: VALID_USERNAME, password: VALID_PASSWORD }, headers: { 'content-type': 'application/json' } }); expect(res.ok()).toBe(true); const headers = res.headers(); const setCookie = headers['set-cookie']; // Backend may or may not set a cookie (the dashboard primarily // uses bearer-in-localStorage). If it does, it must be // HttpOnly so XSS can't exfiltrate it. if (setCookie) { expect(setCookie.toLowerCase()).toContain('httponly'); } }); test('bootstrap password is not present in the DOM after login', async ({ page }) => { await page.goto('/admin/apps'); const body = await page.locator('body').innerText(); expect(body).not.toContain(VALID_PASSWORD); }); test('non-existent app slug shows a recoverable error, not a crash', async ({ page }) => { await page.goto('/admin/apps/does-not-exist-e2e-9999'); // Page must render *something* and the layout must remain // intact (header link to Apps still works). await expect(page.getByRole('link', { name: 'Apps' })).toBeVisible(); // And surface the failure to the user — either a "couldn't // load" message or a "back to apps" link. const errorOrBack = page.locator('.error, a[href$="/admin/apps"]'); await expect(errorOrBack.first()).toBeVisible(); }); test('xss probe across major surfaces never fires a dialog', async ({ page }) => { page.on('dialog', async (dialog) => { await dialog.dismiss(); throw new Error( `XSS sink fired — got a ${dialog.type()} dialog: "${dialog.message()}"` ); }); // Cover each main authed route. None should evaluate any // payload that earlier tests may have stored, and none should // inject inline