#!/usr/bin/env node /** * Acceptance audit: push EVERY file in the pool through the real upload validator * and report exactly what the server refuses, and why. * * Answers one question — "with these settings, which of my photos would be turned * away?" — and answers it empirically rather than by reasoning about limits. Size * caps are only one of the paths that can refuse a file: the magic-byte allowlist, * the decode budget (12000 px axis / 256 MiB alloc, both code constants), the disk * gate and the per-user quota all reject too, and only the running server knows the * interaction between them. * * Deliberately NOT a load test: no personas, no viewers, no think-time. It measures * admission, so it does not wait for the compression backlog to drain. * * SIM_UPLOADERS=8 node e2e/loadtest/acceptance-audit.mjs */ import { readFile, writeFile, mkdir } from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; import { randomUUID } from 'node:crypto'; const __dirname = dirname(fileURLToPath(import.meta.url)); const BASE = process.env.SIM_BASE ?? 'http://localhost:3102'; const API = `${BASE}/api/v1`; const POOL = process.env.SIM_POOL_DIR ?? '/tmp/eventsnap-realpool'; const META = process.env.SIM_POOL_META ?? '/tmp/eventsnap-pool.json'; const ADMIN_PW = process.env.SIM_ADMIN_PW ?? 'admin-test-pw'; const CONC = parseInt(process.env.AUDIT_CONC ?? '4', 10); // Spread across a few accounts, as a real event does — a single uploader would hit // the per-user quota and hourly limit for reasons unrelated to the files themselves. const UPLOADERS = parseInt(process.env.SIM_UPLOADERS ?? '8', 10); const j = async (path, opts = {}) => { const res = await fetch(`${API}${path}`, opts); const text = await res.text(); let body; try { body = text ? JSON.parse(text) : undefined; } catch { body = text; } return { status: res.status, body }; }; async function main() { const meta = JSON.parse(await readFile(META, 'utf8')); console.log(`[pool] ${meta.length} files, ${(meta.reduce((a, f) => a + f.bytes, 0) / 1e9).toFixed(2)} GB`); const admin = ( await j('/admin/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: ADMIN_PW }), }) ).body.jwt; const cfg = (await j('/admin/config', { headers: { Authorization: `Bearer ${admin}` } })).body; const shown = [ 'max_image_size_mb', 'max_video_size_mb', 'upload_rate_per_hour', 'storage_quota_enabled', 'quota_enabled', ]; console.log(`[config] ${shown.map((k) => `${k}=${cfg[k]}`).join(' ')}`); const stats = (await j('/admin/stats', { headers: { Authorization: `Bearer ${admin}` } })).body; console.log( `[disk] ${(stats.disk_free_bytes / 1e9).toFixed(1)} GB free of ${(stats.disk_total_bytes / 1e9).toFixed(1)} GB` ); const guests = []; for (let i = 0; i < UPLOADERS; i++) { const r = await j('/join', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ display_name: `Audit ${i} ${randomUUID().slice(0, 4)}` }), }); guests.push(r.body.jwt); } console.log(`[join] ${guests.length} uploaders\n`); const results = []; let done = 0; const queue = [...meta]; const t0 = Date.now(); const worker = async (slot) => { while (queue.length) { const f = queue.shift(); if (!f) break; const jwt = guests[slot % guests.length]; let buf; try { buf = await readFile(join(POOL, f.name)); } catch (e) { results.push({ ...f, status: -1, msg: `read error: ${e}` }); continue; } const form = new FormData(); form.append('file', new Blob([buf], { type: f.magic }), f.name); form.append('client_upload_id', randomUUID()); let status, body; try { const res = await fetch(`${API}/upload`, { method: 'POST', headers: { Authorization: `Bearer ${jwt}` }, body: form, }); status = res.status; const t = await res.text(); try { body = JSON.parse(t); } catch { body = t; } } catch (e) { status = 0; body = { message: String(e).slice(0, 80) }; } results.push({ name: f.name, bytes: f.bytes, magic: f.magic, w: f.w, h: f.h, status, code: body?.code ?? body?.error, msg: status >= 400 ? String(body?.message ?? '').slice(0, 110) : undefined, }); if (++done % 100 === 0) { const ok = results.filter((r) => r.status === 201).length; console.log( ` ${done}/${meta.length} accepted ${ok} refused ${done - ok} (${((Date.now() - t0) / 1000).toFixed(0)}s)` ); } } }; await Promise.all(Array.from({ length: CONC }, (_, i) => worker(i))); // ── Report ──────────────────────────────────────────────────────────────── const ok = results.filter((r) => r.status === 201); const bad = results.filter((r) => r.status !== 201); const byReason = {}; for (const r of bad) { const key = `${r.status} ${r.msg ?? r.code ?? '?'}`; (byReason[key] ??= []).push(r); } console.log('\n' + '═'.repeat(74)); console.log('ACCEPTANCE AUDIT'); console.log('═'.repeat(74)); console.log( `accepted ${ok.length}/${results.length} (${(ok.reduce((a, r) => a + r.bytes, 0) / 1e9).toFixed(2)} GB)` ); console.log(`refused ${bad.length}\n`); for (const [reason, rows] of Object.entries(byReason).sort((a, b) => b[1].length - a[1].length)) { const sizes = rows.map((r) => r.bytes / 1024 / 1024); console.log(` ${rows.length} x ${reason}`); console.log( ` sizes ${Math.min(...sizes).toFixed(1)}–${Math.max(...sizes).toFixed(1)} MB · types ${[...new Set(rows.map((r) => r.magic))].join(', ')}` ); console.log(` e.g. ${rows.slice(0, 3).map((r) => r.name).join(', ')}`); } if (!bad.length) console.log(' ✓ nothing was refused'); const outDir = join(__dirname, 'results'); await mkdir(outDir, { recursive: true }); const out = join(outDir, `acceptance-${new Date().toISOString().replace(/[:.]/g, '-')}.json`); await writeFile(out, JSON.stringify({ config: cfg, stats, results }, null, 2)); console.log(`\nfull detail → ${out}`); console.log('═'.repeat(74)); } main().catch((e) => { console.error('audit failed:', e); process.exit(1); });