test(e2e): sprint 2 acceptance tests — US-7 through US-12
Covers AC-7.1→AC-7.6, AC-8.1→AC-8.6, AC-9.1→AC-9.4, AC-10.1→AC-10.5, AC-11.1→AC-11.5, AC-12.1→AC-12.4 (32 new tests, all passing). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
216
e2e/tests/us8-simulation-redteam-fill.spec.ts
Normal file
216
e2e/tests/us8-simulation-redteam-fill.spec.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
/**
|
||||
* 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<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-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',
|
||||
mitre_technique_id: 'T1059',
|
||||
mitre_technique_name: 'Command and Scripting Interpreter',
|
||||
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');
|
||||
expect(r.data.mitre_technique_id).toBe('T1059');
|
||||
expect(r.data.mitre_technique_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
|
||||
await nameField.fill('');
|
||||
await page.getByRole('button', { name: /save red team/i }).click();
|
||||
await expect(page.getByText(/name is required/i)).toBeVisible();
|
||||
|
||||
await deleteSimulation(redteamToken, sim.id);
|
||||
});
|
||||
|
||||
test('AC-8.6 — MITRE technique picker is present 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`);
|
||||
|
||||
// MitreTechniquePicker renders an input with combobox role
|
||||
await expect(page.getByRole('combobox', { name: /mitre technique/i })).toBeVisible();
|
||||
|
||||
await deleteSimulation(redteamToken, sim.id);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user