/** * US-8 — redteam fills technical details of a simulation. * Covers AC-8.1 → AC-8.6 (AC-8.6 defers to US-10 for the autocomplete UI detail). */ import { test, expect } from '@playwright/test'; import { adminToken, createEngagement, deleteEngagement, deleteUserByUsername, ensureUser, login, makeClient, } from '../fixtures/api'; import { seedTokenInStorage } from '../fixtures/auth'; const REDTEAM_USER = 'us8-redteam'; const SOC_USER = 'us8-soc'; const PASS = 'us8-pass-strong'; interface Simulation { id: number; engagement_id: number; name: string; status: string; [key: string]: unknown; } async function createSimulation( token: string, engagementId: number, name = 'US-8 sim', ): Promise { const client = makeClient(token); const r = await client.post(`/engagements/${engagementId}/simulations`, { name }); if (r.status !== 201) { throw new Error(`create simulation failed: ${r.status} ${JSON.stringify(r.data)}`); } return r.data as Simulation; } async function deleteSimulation(token: string, simId: number): Promise { const client = makeClient(token); await client.delete(`/simulations/${simId}`); } test.describe('US-8 — redteam fill simulation details', () => { let redteamToken: string; let socToken: string; let engagementId: number; test.beforeAll(async () => { 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; const eng = await createEngagement(redteamToken, { name: 'US-8 Test Engagement', start_date: '2026-01-01', }); engagementId = eng.id; }); test.afterAll(async () => { try { const tok = await adminToken(); await deleteEngagement(tok, engagementId); for (const u of [REDTEAM_USER, SOC_USER]) { await deleteUserByUsername(tok, u); } } catch { /* noop */ } }); test('AC-8.1 — PATCH accepts all redteam fields (partial OK)', async () => { const sim = await createSimulation(redteamToken, engagementId, 'AC-8.1 sim'); const client = makeClient(redteamToken); const patch = { name: 'Updated name', technique_ids: ['T1059'], description: 'Some description', commands: 'cmd /c whoami\ncmd /c ipconfig', prerequisites: 'Admin shell', executed_at: '2026-05-01T12:00:00', execution_result: 'Success', }; const r = await client.patch(`/simulations/${sim.id}`, patch); expect(r.status).toBe(200); expect(r.data.name).toBe('Updated name'); // sprint 3: techniques array replaces scalar scalars expect(Array.isArray(r.data.techniques)).toBe(true); expect(r.data.techniques[0].id).toBe('T1059'); expect(r.data.techniques[0].name).toBe('Command and Scripting Interpreter'); expect(r.data.description).toBe('Some description'); expect(r.data.commands).toBe('cmd /c whoami\ncmd /c ipconfig'); expect(r.data.prerequisites).toBe('Admin shell'); expect(r.data.execution_result).toBe('Success'); // Partial PATCH (only name) should also work const rPartial = await client.patch(`/simulations/${sim.id}`, { name: 'Partial update' }); expect(rPartial.status).toBe(200); expect(rPartial.data.name).toBe('Partial update'); await deleteSimulation(redteamToken, sim.id); }); test('AC-8.2 — auto-transition pending→in_progress on PATCH with non-empty redteam field', async () => { const sim = await createSimulation(redteamToken, engagementId, 'AC-8.2 sim'); expect(sim.status).toBe('pending'); const client = makeClient(redteamToken); // PATCH with a non-empty redteam field triggers auto-transition const r = await client.patch(`/simulations/${sim.id}`, { name: 'trigger transition' }); expect(r.status).toBe(200); expect(r.data.status).toBe('in_progress'); await deleteSimulation(redteamToken, sim.id); }); test('AC-8.2 — auto-transition does NOT trigger for soc PATCH', async () => { // Create sim then transition it to review_required so SOC can patch it const sim = await createSimulation(redteamToken, engagementId, 'AC-8.2 soc no-trigger'); const rtClient = makeClient(redteamToken); // Move to in_progress first await rtClient.patch(`/simulations/${sim.id}`, { name: 'AC-8.2 soc no-trigger' }); // Move to review_required const tr = await rtClient.post(`/simulations/${sim.id}/transition`, { to: 'review_required', }); expect(tr.status).toBe(200); // Now SOC patches soc-only fields — status must NOT change const socClient = makeClient(socToken); const rSoc = await socClient.patch(`/simulations/${sim.id}`, { soc_comment: 'noted' }); expect(rSoc.status).toBe(200); expect(rSoc.data.status).toBe('review_required'); await deleteSimulation(redteamToken, sim.id); }); test('AC-8.3 — commands stored and returned as raw multiline string', async () => { const sim = await createSimulation(redteamToken, engagementId, 'AC-8.3 sim'); const client = makeClient(redteamToken); const cmds = 'cmd1\ncmd2\ncmd3'; const r = await client.patch(`/simulations/${sim.id}`, { commands: cmds }); expect(r.status).toBe(200); expect(r.data.commands).toBe(cmds); await deleteSimulation(redteamToken, sim.id); }); test('AC-8.4 — invalid executed_at → 400; null clears field', async () => { const sim = await createSimulation(redteamToken, engagementId, 'AC-8.4 sim'); const client = makeClient(redteamToken); const rBad = await client.patch(`/simulations/${sim.id}`, { executed_at: 'not-a-date', }); expect(rBad.status).toBe(400); expect(rBad.data.error).toMatch(/invalid executed_at/i); // Valid ISO 8601 const rOk = await client.patch(`/simulations/${sim.id}`, { executed_at: '2026-05-01T09:00:00', }); expect(rOk.status).toBe(200); // Null clears the field const rNull = await client.patch(`/simulations/${sim.id}`, { executed_at: null }); expect(rNull.status).toBe(200); await deleteSimulation(redteamToken, sim.id); }); test('AC-8.5 — edit page shows Red Team and SOC sections; redteam sees both editable, name validation', async ({ page, context, }) => { const sim = await createSimulation(redteamToken, engagementId, 'AC-8.5 sim'); await seedTokenInStorage(context, redteamToken); await page.goto(`/engagements/${engagementId}/simulations/${sim.id}/edit`); // Both sections visible await expect(page.getByRole('heading', { name: /red team/i })).toBeVisible(); await expect(page.getByRole('heading', { name: /soc/i })).toBeVisible(); // Name field is present and enabled for redteam const nameField = page.locator('#sim-name'); await expect(nameField).toBeVisible(); await expect(nameField).toBeEnabled(); // Clear the name, try to save → client validation error // Sprint 4: "Save Red Team" button renamed to "Save" await nameField.fill(''); await page.getByRole('button', { name: /^save$/i }).click(); await expect(page.getByText(/name is required/i)).toBeVisible(); await deleteSimulation(redteamToken, sim.id); }); test('AC-8.6 — MITRE technique picker accessible via inline search on the edit form', async ({ page, context, }) => { const sim = await createSimulation(redteamToken, engagementId, 'AC-8.6 sim'); await seedTokenInStorage(context, redteamToken); await page.goto(`/engagements/${engagementId}/simulations/${sim.id}/edit`); // Sprint 4: picker opens by clicking the inline placeholder text await page.getByText(/search technique/i).click(); // MitreTechniquePicker renders an input with combobox role await expect(page.getByRole('combobox', { name: /mitre technique/i })).toBeVisible(); await deleteSimulation(redteamToken, sim.id); }); });