Add three new spec files: - us26-templates-crud: API CRUD (AC-26.3–26.7) + UI list/form/delete/redirect (AC-26.8) - us27-instantiate-from-template: template_id copy + name override + 404 + decoupling (AC-27.1–27.3) + no auto-transition/engagement-activate (AC-27.4–27.5) + dropdown UI + picker modal + empty state + SOC gate (AC-27.6–27.7) - us28-templates-nav: Templates link admin+redteam only, SOC redirect, form editable (AC-28.1–28.3) Adapt sprint 2/3 e2e for sprint 5 dropdown: - us4-engagements: getByRole link "New simulation" → getByTestId "new-simulation-btn" - us7-simulation-create: same — split-button dropdown replaced the link Suite: 201 passed (1 pre-existing flaky in us3 re DB state, unrelated to sprint 5). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
137 lines
4.4 KiB
TypeScript
137 lines
4.4 KiB
TypeScript
/**
|
|
* 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();
|
|
});
|
|
});
|