/** * 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, makeClient, } from '../fixtures/api'; 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(); // Must contain engagement name in the header section expect(text).toContain('US31 empty engagement'); } 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; must contain 'name' column expect(text.trim()).toContain('name'); } finally { await deleteEngagement(adminTok, engagement.id); } }); });