Update AC-29.2 (Markdown) to assert | Scénario | GFM table header. Update AC-29.3 (CSV) to assert exact 7 FR column names instead of 'name'. Update AC-31.4 (empty engagement) MD to assert table absent, CSV header to assert exact 7 FR columns. Drop unused sim1/sim2 vars and makeClient import (NIT cleanup). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
189 lines
5.7 KiB
TypeScript
189 lines
5.7 KiB
TypeScript
/**
|
|
* US-31 — Export robustness: format validation and edge cases.
|
|
*
|
|
* All 4 ACs use direct API calls (no UI needed) for speed and determinism.
|
|
*
|
|
* AC-31.1: missing ?format → 400 friendly message
|
|
* AC-31.2: ?format=xml → 400 friendly message
|
|
* AC-31.3: engagement 99999 → 404
|
|
* AC-31.4: engagement with 0 simulations → export OK (CSV = 1 header row only)
|
|
*/
|
|
import { test, expect } from '@playwright/test';
|
|
import {
|
|
adminToken,
|
|
createEngagement,
|
|
deleteEngagement,
|
|
deleteUserByUsername,
|
|
ensureUser,
|
|
login,
|
|
} 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!';
|
|
|
|
const BASE_URL = process.env.MIMIC_BASE_URL ?? 'http://localhost:5000';
|
|
|
|
/**
|
|
* RFC-4180-aware row counter — same logic as us29 helper.
|
|
* Embedded newlines inside quoted cells are not counted as row breaks.
|
|
*/
|
|
function countCsvRows(csv: string): number {
|
|
let inQuote = false;
|
|
let rowCount = 0;
|
|
let lineStart = 0;
|
|
|
|
for (let i = 0; i < csv.length; i++) {
|
|
const ch = csv[i];
|
|
if (ch === '"') {
|
|
if (inQuote && csv[i + 1] === '"') {
|
|
i++;
|
|
} else {
|
|
inQuote = !inQuote;
|
|
}
|
|
} else if ((ch === '\n' || ch === '\r') && !inQuote) {
|
|
if (ch === '\r' && csv[i + 1] === '\n') i++;
|
|
const line = csv.slice(lineStart, i).trim();
|
|
if (line.length > 0) rowCount++;
|
|
lineStart = i + 1;
|
|
}
|
|
}
|
|
const tail = csv.slice(lineStart).trim();
|
|
if (tail.length > 0) rowCount++;
|
|
return rowCount;
|
|
}
|
|
|
|
test.describe('US-31 — Export robustness', () => {
|
|
let adminTok: string;
|
|
|
|
test.beforeAll(async () => {
|
|
await ensureUser(ADMIN_USER, PASS, 'admin');
|
|
adminTok = (await login(ADMIN_USER, PASS)).token;
|
|
});
|
|
|
|
test.afterAll(async () => {
|
|
try {
|
|
const rootTok = await adminToken();
|
|
await deleteUserByUsername(rootTok, ADMIN_USER);
|
|
} catch { /* noop */ }
|
|
});
|
|
|
|
// AC-31.1 — missing format → 400
|
|
test('AC-31.1 — GET /export without format → 400 with friendly error', async ({
|
|
request,
|
|
}) => {
|
|
// Need a valid engagement id — use admin to create one transiently
|
|
const engagement = await createEngagement(adminTok, {
|
|
name: 'US31 missing format eng',
|
|
start_date: '2026-01-01',
|
|
});
|
|
|
|
try {
|
|
const response = await request.get(
|
|
`${BASE_URL}/api/engagements/${engagement.id}/export`,
|
|
{ headers: { Authorization: `Bearer ${adminTok}` } },
|
|
);
|
|
expect(response.status()).toBe(400);
|
|
const body = await response.json();
|
|
expect(body).toHaveProperty('error');
|
|
expect(body.error).toMatch(/format/i);
|
|
} finally {
|
|
await deleteEngagement(adminTok, engagement.id);
|
|
}
|
|
});
|
|
|
|
// AC-31.2 — unknown format → 400
|
|
test('AC-31.2 — GET /export?format=xml → 400 with friendly error', async ({
|
|
request,
|
|
}) => {
|
|
const engagement = await createEngagement(adminTok, {
|
|
name: 'US31 bad format eng',
|
|
start_date: '2026-01-01',
|
|
});
|
|
|
|
try {
|
|
const response = await request.get(
|
|
`${BASE_URL}/api/engagements/${engagement.id}/export?format=xml`,
|
|
{ headers: { Authorization: `Bearer ${adminTok}` } },
|
|
);
|
|
expect(response.status()).toBe(400);
|
|
const body = await response.json();
|
|
expect(body).toHaveProperty('error');
|
|
expect(body.error).toMatch(/format/i);
|
|
} finally {
|
|
await deleteEngagement(adminTok, engagement.id);
|
|
}
|
|
});
|
|
|
|
// AC-31.3 — unknown engagement → 404
|
|
test('AC-31.3 — GET /engagements/99999/export?format=md → 404', async ({
|
|
request,
|
|
}) => {
|
|
const response = await request.get(
|
|
`${BASE_URL}/api/engagements/99999/export?format=md`,
|
|
{ headers: { Authorization: `Bearer ${adminTok}` } },
|
|
);
|
|
expect(response.status()).toBe(404);
|
|
});
|
|
|
|
// AC-31.4 — engagement with 0 simulations: export OK
|
|
test('AC-31.4 — engagement with 0 simulations: Markdown export OK (header only)', async ({
|
|
request,
|
|
}) => {
|
|
const engagement = await createEngagement(adminTok, {
|
|
name: 'US31 empty engagement',
|
|
start_date: '2026-01-01',
|
|
});
|
|
|
|
try {
|
|
const response = await request.get(
|
|
`${BASE_URL}/api/engagements/${engagement.id}/export?format=md`,
|
|
{ headers: { Authorization: `Bearer ${adminTok}` } },
|
|
);
|
|
expect(response.status()).toBe(200);
|
|
const text = await response.text();
|
|
// 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);
|
|
}
|
|
});
|
|
|
|
test('AC-31.4 — engagement with 0 simulations: CSV export has only 1 header row', async ({
|
|
request,
|
|
}) => {
|
|
const engagement = await createEngagement(adminTok, {
|
|
name: 'US31 empty CSV engagement',
|
|
start_date: '2026-01-01',
|
|
});
|
|
|
|
try {
|
|
const response = await request.get(
|
|
`${BASE_URL}/api/engagements/${engagement.id}/export?format=csv`,
|
|
{ headers: { Authorization: `Bearer ${adminTok}` } },
|
|
);
|
|
expect(response.status()).toBe(200);
|
|
const text = await response.text();
|
|
// 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 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);
|
|
}
|
|
});
|
|
});
|