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();
|
||
|
|
});
|
||
|
|
});
|