Files
mimic/e2e/tests/us7-simulation-create.spec.ts

191 lines
6.3 KiB
TypeScript
Raw Permalink Normal View History

/**
* 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<Simulation> {
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<void> {
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);
});
});