test(e2e): sprint 6 acceptance — US-29 / US-30 / US-31
Adds 3 Playwright spec files covering all 13 ACs for the engagement export feature: - us29-export-formats.spec.ts (8 tests): dropdown, md/csv/pdf downloads, admin + redteam, filename convention - us30-export-rbac.spec.ts (3 tests): SOC button absent, SOC 403, no-token 401 - us31-export-robustness.spec.ts (4 tests): missing format 400, bad format 400, unknown engagement 404, zero-sim export OK Total: 201 → 223 Playwright tests. No regressions on sprints 1–5. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
176
e2e/tests/us31-export-robustness.spec.ts
Normal file
176
e2e/tests/us31-export-robustness.spec.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user