feat: sprint 6 — engagement export (md/csv/pdf) #9

Merged
knacky merged 20 commits from sprint/6-export into main 2026-06-09 16:19:02 +00:00
2 changed files with 37 additions and 15 deletions
Showing only changes of commit aeb4bdb025 - Show all commits

View File

@@ -19,6 +19,16 @@ import {
makeClient,
type Engagement,
} from '../fixtures/api';
const CSV_HEADER_COLS = [
'Scénario',
'Test',
'Source de log',
'Commentaires SOC',
'Exécution',
'Logs remontés au SIEM',
'Cyber incident',
];
import { seedTokenInStorage } from '../fixtures/auth';
const ADMIN_USER = 'us29-admin';
@@ -91,8 +101,6 @@ test.describe('US-29 — Export formats (admin + redteam)', () => {
let adminTok: string;
let redteamTok: string;
let engagement: Engagement;
let sim1: Simulation;
let sim2: Simulation;
test.beforeAll(async () => {
await ensureUser(ADMIN_USER, PASS, 'admin');
@@ -106,8 +114,8 @@ test.describe('US-29 — Export formats (admin + redteam)', () => {
start_date: '2026-01-15',
status: 'active',
});
sim1 = await createSimulation(adminTok, engagement.id, 'US29 Sim Alpha');
sim2 = await createSimulation(adminTok, engagement.id, 'US29 Sim Beta');
await createSimulation(adminTok, engagement.id, 'US29 Sim Alpha');
await createSimulation(adminTok, engagement.id, 'US29 Sim Beta');
});
test.afterAll(async () => {
@@ -162,13 +170,14 @@ test.describe('US-29 — Export formats (admin + redteam)', () => {
const content = await fs.readFile(filePath!, 'utf-8');
// Must contain engagement name
// Must contain engagement name and start date in the header section
expect(content).toContain('US29 Export Engagement');
// Must contain simulation names
expect(content).toContain('2026-01-15');
// Must use the 7-column GFM table layout
expect(content).toContain('| Scénario |');
// Simulation names appear in the Scénario column
expect(content).toContain('US29 Sim Alpha');
expect(content).toContain('US29 Sim Beta');
// Must contain start date
expect(content).toContain('2026-01-15');
// Suggested filename from Content-Disposition must end in .md
const suggestedName = download.suggestedFilename();
@@ -203,10 +212,11 @@ test.describe('US-29 — Export formats (admin + redteam)', () => {
// 1 header + 2 simulation rows
expect(rows.count).toBe(3);
// Header must mention 'name' column
expect(rows.headerLine).toContain('name');
// Header must be exactly the 7 FR columns
const headerCells = rows.headerLine.split(',').map((c) => c.trim().replace(/^"|"$/g, ''));
expect(headerCells).toEqual(CSV_HEADER_COLS);
// Simulation data rows must contain simulation names
// Scénario column (index 0) contains simulation names
expect(rows.dataText).toContain('US29 Sim Alpha');
expect(rows.dataText).toContain('US29 Sim Beta');

View File

@@ -16,9 +16,18 @@ import {
deleteUserByUsername,
ensureUser,
login,
makeClient,
} from '../fixtures/api';
const CSV_HEADER_COLS = [
'Scénario',
'Test',
'Source de log',
'Commentaires SOC',
'Exécution',
'Logs remontés au SIEM',
'Cyber incident',
];
const ADMIN_USER = 'us31-admin';
const PASS = 'us31-pass-strong!';
@@ -142,8 +151,10 @@ test.describe('US-31 — Export robustness', () => {
);
expect(response.status()).toBe(200);
const text = await response.text();
// Must contain engagement name in the header section
// Engagement header section present
expect(text).toContain('US31 empty engagement');
// With 0 simulations the GFM table is absent (no rows to render)
expect(text).not.toContain('| Scénario |');
} finally {
await deleteEngagement(adminTok, engagement.id);
}
@@ -167,8 +178,9 @@ test.describe('US-31 — Export robustness', () => {
// Count rows via RFC-4180-aware counter (handles embedded newlines in quoted cells)
const rowCount = countCsvRows(text);
expect(rowCount).toBe(1);
// The single row is the header; must contain 'name' column
expect(text.trim()).toContain('name');
// The single row is the header with exactly the 7 FR columns
const headerCells = text.trim().split(',').map((c) => c.trim().replace(/^"|"$/g, ''));
expect(headerCells).toEqual(CSV_HEADER_COLS);
} finally {
await deleteEngagement(adminTok, engagement.id);
}