- us7: "Nouvelle simulation" → "New simulation" (3 assertions) - us4: "Nouvelle simulation" → "New simulation" (1 assertion) - us9: "Simulation pas encore en revue" → "Simulation not yet ready for review" (1 assertion) - us11: "Marquer en revue" → "Mark for review" (6 assertions), "Clôturer" → /^close$/i (7 assertions) - us12: "Supprimer" → /^delete$/i (4 assertions), "Supprimer la simulation" → "Delete simulation" (1 assertion) No other French strings found in e2e/tests/. Suite: 68/68 pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
246 lines
9.4 KiB
TypeScript
246 lines
9.4 KiB
TypeScript
/**
|
|
* US-11 — workflow transitions.
|
|
* Covers AC-11.1 → AC-11.5.
|
|
*/
|
|
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 = 'us11-redteam';
|
|
const SOC_USER = 'us11-soc';
|
|
const PASS = 'us11-pass-strong';
|
|
|
|
interface Simulation {
|
|
id: number;
|
|
status: string;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
async function createSimulation(
|
|
token: string,
|
|
engagementId: number,
|
|
name = 'US-11 sim',
|
|
): Promise<Simulation> {
|
|
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<void> {
|
|
const client = makeClient(token);
|
|
await client.delete(`/simulations/${simId}`);
|
|
}
|
|
|
|
test.describe('US-11 — workflow transitions', () => {
|
|
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-11 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-11.1 — pending→review_required valid (redteam); invalid target → 409', async () => {
|
|
const rtClient = makeClient(redteamToken);
|
|
|
|
// pending → review_required: valid
|
|
const sim = await createSimulation(redteamToken, engagementId, 'AC-11.1 sim');
|
|
const rOk = await rtClient.post(`/simulations/${sim.id}/transition`, {
|
|
to: 'review_required',
|
|
});
|
|
expect(rOk.status).toBe(200);
|
|
expect(rOk.data.status).toBe('review_required');
|
|
|
|
// Invalid target → 409
|
|
const simBad = await createSimulation(redteamToken, engagementId, 'AC-11.1 bad sim');
|
|
const rBad = await rtClient.post(`/simulations/${simBad.id}/transition`, {
|
|
to: 'done',
|
|
});
|
|
expect(rBad.status).toBe(409);
|
|
expect(rBad.data.error).toMatch(/invalid transition/i);
|
|
|
|
await deleteSimulation(redteamToken, sim.id);
|
|
await deleteSimulation(redteamToken, simBad.id);
|
|
});
|
|
|
|
test('AC-11.1 — in_progress→review_required valid (redteam)', async () => {
|
|
const rtClient = makeClient(redteamToken);
|
|
const sim = await createSimulation(redteamToken, engagementId, 'AC-11.1 in_progress sim');
|
|
|
|
// Trigger in_progress via auto-transition
|
|
await rtClient.patch(`/simulations/${sim.id}`, { name: 'trigger' });
|
|
const r = await rtClient.post(`/simulations/${sim.id}/transition`, {
|
|
to: 'review_required',
|
|
});
|
|
expect(r.status).toBe(200);
|
|
expect(r.data.status).toBe('review_required');
|
|
|
|
await deleteSimulation(redteamToken, sim.id);
|
|
});
|
|
|
|
test('AC-11.2 — review_required→done valid for redteam and soc', async () => {
|
|
const rtClient = makeClient(redteamToken);
|
|
const socClient = makeClient(socToken);
|
|
|
|
// redteam can close
|
|
const simRT = await createSimulation(redteamToken, engagementId, 'AC-11.2 redteam close');
|
|
await rtClient.post(`/simulations/${simRT.id}/transition`, { to: 'review_required' });
|
|
const rRT = await rtClient.post(`/simulations/${simRT.id}/transition`, { to: 'done' });
|
|
expect(rRT.status).toBe(200);
|
|
expect(rRT.data.status).toBe('done');
|
|
|
|
// soc can close
|
|
const simSOC = await createSimulation(redteamToken, engagementId, 'AC-11.2 soc close');
|
|
await rtClient.post(`/simulations/${simSOC.id}/transition`, { to: 'review_required' });
|
|
const rSOC = await socClient.post(`/simulations/${simSOC.id}/transition`, { to: 'done' });
|
|
expect(rSOC.status).toBe(200);
|
|
expect(rSOC.data.status).toBe('done');
|
|
|
|
// done → review_required is invalid (409)
|
|
const rBack = await rtClient.post(`/simulations/${simRT.id}/transition`, {
|
|
to: 'review_required',
|
|
});
|
|
expect(rBack.status).toBe(409);
|
|
|
|
await deleteSimulation(redteamToken, simRT.id);
|
|
await deleteSimulation(redteamToken, simSOC.id);
|
|
});
|
|
|
|
test('AC-11.3 — no backward transitions; no →pending or →in_progress via endpoint', async () => {
|
|
const rtClient = makeClient(redteamToken);
|
|
const sim = await createSimulation(redteamToken, engagementId, 'AC-11.3 sim');
|
|
|
|
// done → pending: invalid
|
|
await rtClient.post(`/simulations/${sim.id}/transition`, { to: 'review_required' });
|
|
await rtClient.post(`/simulations/${sim.id}/transition`, { to: 'done' });
|
|
const rPending = await rtClient.post(`/simulations/${sim.id}/transition`, {
|
|
to: 'pending',
|
|
});
|
|
expect(rPending.status).toBe(409);
|
|
expect(rPending.data.error).toMatch(/invalid transition/i);
|
|
|
|
// →in_progress via endpoint is always invalid
|
|
const simNew = await createSimulation(redteamToken, engagementId, 'AC-11.3 in_progress');
|
|
const rIP = await rtClient.post(`/simulations/${simNew.id}/transition`, {
|
|
to: 'in_progress',
|
|
});
|
|
expect(rIP.status).toBe(409);
|
|
|
|
await deleteSimulation(redteamToken, sim.id);
|
|
await deleteSimulation(redteamToken, simNew.id);
|
|
});
|
|
|
|
test('AC-11.4 — workflow buttons visible per role+status in UI', async ({
|
|
page,
|
|
context,
|
|
}) => {
|
|
const rtClient = makeClient(redteamToken);
|
|
|
|
// pending → "Mark for review" visible for redteam; "Close" hidden
|
|
const simPending = await createSimulation(
|
|
redteamToken,
|
|
engagementId,
|
|
'AC-11.4 pending UI',
|
|
);
|
|
await seedTokenInStorage(context, redteamToken);
|
|
await page.goto(`/engagements/${engagementId}/simulations/${simPending.id}/edit`);
|
|
await expect(page.getByRole('button', { name: /mark for review/i })).toBeVisible();
|
|
await expect(page.getByRole('button', { name: /^close$/i })).toHaveCount(0);
|
|
|
|
// in_progress → "Mark for review" visible
|
|
const simIP = await createSimulation(redteamToken, engagementId, 'AC-11.4 in_progress UI');
|
|
await rtClient.patch(`/simulations/${simIP.id}`, { name: 'trigger' });
|
|
await page.goto(`/engagements/${engagementId}/simulations/${simIP.id}/edit`);
|
|
await expect(page.getByRole('button', { name: /mark for review/i })).toBeVisible();
|
|
await expect(page.getByRole('button', { name: /^close$/i })).toHaveCount(0);
|
|
|
|
// review_required → "Close" visible for redteam; "Mark for review" hidden
|
|
const simRR = await createSimulation(redteamToken, engagementId, 'AC-11.4 review UI');
|
|
await rtClient.post(`/simulations/${simRR.id}/transition`, { to: 'review_required' });
|
|
await page.goto(`/engagements/${engagementId}/simulations/${simRR.id}/edit`);
|
|
await expect(page.getByRole('button', { name: /^close$/i })).toBeVisible();
|
|
await expect(page.getByRole('button', { name: /mark for review/i })).toHaveCount(0);
|
|
|
|
// review_required → "Close" also visible for SOC
|
|
await seedTokenInStorage(context, socToken);
|
|
await page.goto(`/engagements/${engagementId}/simulations/${simRR.id}/edit`);
|
|
await expect(page.getByRole('button', { name: /^close$/i })).toBeVisible();
|
|
|
|
// done → both buttons hidden
|
|
await rtClient.post(`/simulations/${simRR.id}/transition`, { to: 'done' });
|
|
await seedTokenInStorage(context, redteamToken);
|
|
await page.goto(`/engagements/${engagementId}/simulations/${simRR.id}/edit`);
|
|
await expect(page.getByRole('button', { name: /mark for review/i })).toHaveCount(0);
|
|
await expect(page.getByRole('button', { name: /^close$/i })).toHaveCount(0);
|
|
|
|
await deleteSimulation(redteamToken, simPending.id);
|
|
await deleteSimulation(redteamToken, simIP.id);
|
|
await deleteSimulation(redteamToken, simRR.id);
|
|
});
|
|
|
|
test('AC-11.5 — after transition, badge updates in UI (TanStack Query invalidation)', async ({
|
|
page,
|
|
context,
|
|
}) => {
|
|
const rtClient = makeClient(redteamToken);
|
|
const sim = await createSimulation(redteamToken, engagementId, 'AC-11.5 badge sim');
|
|
|
|
await seedTokenInStorage(context, redteamToken);
|
|
await page.goto(`/engagements/${engagementId}/simulations/${sim.id}/edit`);
|
|
|
|
// Initially pending
|
|
const badge = page.getByTestId('simulation-status-badge');
|
|
await expect(badge).toHaveAttribute('data-status', 'pending');
|
|
|
|
// Click "Mark for review"
|
|
await page.getByRole('button', { name: /mark for review/i }).click();
|
|
|
|
// Badge updates to review_required without page reload
|
|
await expect(badge).toHaveAttribute('data-status', 'review_required', { timeout: 5_000 });
|
|
|
|
// "Close" now visible; click it
|
|
await page.getByRole('button', { name: /^close$/i }).click();
|
|
await expect(badge).toHaveAttribute('data-status', 'done', { timeout: 5_000 });
|
|
|
|
// Verify list is also updated: navigate to engagement detail and check badge there
|
|
await page.goto(`/engagements/${engagementId}`);
|
|
const listBadge = page
|
|
.getByRole('row', { name: /AC-11.5 badge sim/i })
|
|
.getByTestId('simulation-status-badge');
|
|
await expect(listBadge).toHaveAttribute('data-status', 'done');
|
|
|
|
await deleteSimulation(redteamToken, sim.id);
|
|
});
|
|
});
|