/** * US-7 — redteam creates a simulation inside an engagement. * Covers AC-7.1 → AC-7.6. */ 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 = 'us7-redteam'; const SOC_USER = 'us7-soc'; const PASS = 'us7-pass-strong'; interface Simulation { id: number; engagement_id: number; name: string; status: string; created_at: string; [key: string]: unknown; } async function createSimulation( token: string, engagementId: number, payload: { name: string }, ): Promise { const client = makeClient(token); const r = await client.post(`/engagements/${engagementId}/simulations`, payload); 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-7 — simulation create', () => { 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-7 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-7.1 — POST creates simulation with status pending, name required', async () => { const sim = await createSimulation(redteamToken, engagementId, { name: 'Test sim 7.1' }); expect(sim.id).toBeTruthy(); expect(sim.engagement_id).toBe(engagementId); expect(sim.name).toBe('Test sim 7.1'); expect(sim.status).toBe('pending'); expect(sim.created_at).toBeTruthy(); // name required: blank name → 400 const client = makeClient(redteamToken); const r = await client.post(`/engagements/${engagementId}/simulations`, { name: '' }); expect(r.status).toBe(400); await deleteSimulation(redteamToken, sim.id); }); test('AC-7.2 — soc role → 403 on POST', async () => { const client = makeClient(socToken); const r = await client.post(`/engagements/${engagementId}/simulations`, { name: 'soc-blocked', }); expect(r.status).toBe(403); }); test('AC-7.3 — unknown engagement → 404; existing engagement with no sims → empty list', async () => { const client = makeClient(redteamToken); const r404 = await client.post('/engagements/999999/simulations', { name: 'ghost' }); expect(r404.status).toBe(404); // Create a fresh engagement with no sims and verify list is empty const freshEng = await createEngagement(redteamToken, { name: 'US-7 empty engagement', start_date: '2026-01-01', }); const listR = await client.get(`/engagements/${freshEng.id}/simulations`); expect(listR.status).toBe(200); expect(listR.data).toEqual([]); await deleteEngagement(redteamToken, freshEng.id); }); test('AC-7.4 — GET list returns sims ordered by created_at desc', async () => { const client = makeClient(redteamToken); const s1 = await createSimulation(redteamToken, engagementId, { name: 'First sim' }); // Small delay so created_at timestamps differ await new Promise((r) => setTimeout(r, 50)); const s2 = await createSimulation(redteamToken, engagementId, { name: 'Second sim' }); const listR = await client.get(`/engagements/${engagementId}/simulations`); expect(listR.status).toBe(200); const list: Simulation[] = listR.data; expect(list.length).toBeGreaterThanOrEqual(2); // Most recent first const ids = list.map((s) => s.id); expect(ids.indexOf(s2.id)).toBeLessThan(ids.indexOf(s1.id)); await deleteSimulation(redteamToken, s1.id); await deleteSimulation(redteamToken, s2.id); }); test('AC-7.5 — /engagements/:eid shows Simulations section with list + "Nouvelle simulation" button for redteam', async ({ page, context, }) => { const sim = await createSimulation(redteamToken, engagementId, { name: 'Visible sim' }); await seedTokenInStorage(context, redteamToken); await page.goto(`/engagements/${engagementId}`); // Simulations section visible await expect(page.getByRole('heading', { name: /simulations/i })).toBeVisible(); // Required columns for (const col of ['Name', 'MITRE', 'Status', 'Executed at']) { await expect(page.getByRole('columnheader', { name: new RegExp(col, 'i') })).toBeVisible(); } // The created simulation row is visible await expect(page.getByRole('row', { name: /Visible sim/i })).toBeVisible(); // Sprint 5: "New" is now a split-button dropdown (data-testid="new-simulation-btn") await expect(page.getByTestId('new-simulation-btn')).toBeVisible(); // SOC should NOT see "New simulation" dropdown await seedTokenInStorage(context, socToken); await page.goto(`/engagements/${engagementId}`); await expect(page.getByTestId('new-simulation-btn')).not.toBeVisible(); await deleteSimulation(redteamToken, sim.id); }); test('AC-7.6 — clicking a simulation row navigates to /engagements/:eid/simulations/:sid/edit', async ({ page, context, }) => { const sim = await createSimulation(redteamToken, engagementId, { name: 'Click me sim' }); await seedTokenInStorage(context, redteamToken); await page.goto(`/engagements/${engagementId}`); // Click the sim name link await page.getByRole('link', { name: 'Click me sim' }).click(); await page.waitForURL( new RegExp(`/engagements/${engagementId}/simulations/${sim.id}/edit`), ); await expect(page.url()).toContain( `/engagements/${engagementId}/simulations/${sim.id}/edit`, ); await deleteSimulation(redteamToken, sim.id); }); });