/** * US-28 — Admin/redteam access templates from the nav. * Covers AC-28.1 (Templates link in topbar), AC-28.2 (ProtectedRoute SOC redirect), * AC-28.3 (page is always edit-capable, no read-only mode). */ import { test, expect } from '@playwright/test'; import { adminToken, deleteUserByUsername, ensureUser, login, } from '../fixtures/api'; import { seedTokenInStorage } from '../fixtures/auth'; const REDTEAM_USER = 'us28-redteam'; const SOC_USER = 'us28-soc'; const PASS = 'us28-pass-strong'; test.describe('US-28 — templates nav', () => { let redteamToken: string; let socToken: string; let adminTok: string; test.beforeAll(async () => { adminTok = await adminToken(); await ensureUser(REDTEAM_USER, PASS, 'redteam'); await ensureUser(SOC_USER, PASS, 'soc'); redteamToken = (await login(REDTEAM_USER, PASS)).token; socToken = (await login(SOC_USER, PASS)).token; }); test.afterAll(async () => { try { const tok = await adminToken(); for (const u of [REDTEAM_USER, SOC_USER]) await deleteUserByUsername(tok, u); } catch { /* noop */ } }); // AC-28.1 — Templates nav link visible to admin + redteam test('AC-28.1 — redteam sees "Templates" link in topbar nav', async ({ page, context, }) => { await seedTokenInStorage(context, redteamToken); await page.goto('/engagements'); const link = page.getByRole('link', { name: /^templates$/i }); await expect(link).toBeVisible(); await expect(link).toHaveAttribute('href', '/admin/templates'); }); test('AC-28.1 — admin sees "Templates" link in topbar nav', async ({ page, context, }) => { await seedTokenInStorage(context, adminTok); await page.goto('/engagements'); await expect(page.getByRole('link', { name: /^templates$/i })).toBeVisible(); }); test('AC-28.1 — SOC does NOT see "Templates" link in topbar nav', async ({ page, context, }) => { await seedTokenInStorage(context, socToken); await page.goto('/engagements'); // Wait for page to fully load before asserting absence await page.waitForLoadState('networkidle'); await expect(page.getByRole('link', { name: /^templates$/i })).not.toBeVisible(); }); test('AC-28.1 — clicking Templates link navigates to /admin/templates', async ({ page, context, }) => { await seedTokenInStorage(context, redteamToken); await page.goto('/engagements'); await page.getByRole('link', { name: /^templates$/i }).click(); await expect(page).toHaveURL(/\/admin\/templates/); }); // AC-28.2 — SOC typing /admin/templates URL directly → redirected test('AC-28.2 — SOC direct URL /admin/templates → redirected to /engagements', async ({ page, context, }) => { await seedTokenInStorage(context, socToken); await page.goto('/admin/templates'); await expect(page).toHaveURL(/\/engagements/, { timeout: 5_000 }); }); test('AC-28.2 — SOC direct URL /admin/templates/new → redirected to /engagements', async ({ page, context, }) => { await seedTokenInStorage(context, socToken); await page.goto('/admin/templates/new'); await expect(page).toHaveURL(/\/engagements/, { timeout: 5_000 }); }); // AC-28.3 — Page assumes canEdit=true (form inputs are never disabled for admin/redteam) test('AC-28.3 — TemplateFormPage for redteam: name input is editable (no read-only mode)', async ({ page, context, }) => { await seedTokenInStorage(context, redteamToken); await page.goto('/admin/templates/new'); const nameInput = page.getByLabel(/name/i).first(); await expect(nameInput).toBeVisible(); await expect(nameInput).toBeEnabled(); // Should be able to type await nameInput.fill('AC-28.3 editability test'); await expect(nameInput).toHaveValue('AC-28.3 editability test'); }); test('AC-28.3 — admin has same edit access on /admin/templates', async ({ page, context, }) => { await seedTokenInStorage(context, adminTok); await page.goto('/admin/templates'); // Page loads (not redirected) await expect(page).toHaveURL(/\/admin\/templates/); await expect(page.getByRole('heading', { name: 'Templates', exact: true })).toBeVisible({ timeout: 5_000 }); // "New" link in header when templates exist, "New template" in empty state — either is fine await expect(page.getByRole('link', { name: /new( template)?/i }).first()).toBeVisible(); }); });