// Security self-check for the API. Boots the real server against a throwaway // DATA_DIR and exercises the boundaries that matter: who may write, what may be // written, who may read a photo back, and who may moderate the strip. // // npm test (from docker/backend/) // // Node only — no test framework, no network beyond loopback. import { spawn } from 'node:child_process'; import { existsSync, mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); const PORT = Number(process.env.TEST_PORT || 3411); const BASE = `http://127.0.0.1:${PORT}/api`; const ADMIN_EMAIL = 'admin@test.local'; const DATA_DIR = mkdtempSync(join(tmpdir(), 'recipescam-sec-')); // A 1x1 PNG, and the first bytes of a JPEG (all the sniffer looks at). const PNG = Buffer.from( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64', ); const JPEG_HEAD = Buffer.concat([Buffer.from([0xff, 0xd8, 0xff, 0xe0]), Buffer.alloc(64)]); let pass = 0; let fail = 0; const check = (name, ok, detail = '') => { if (ok) { pass++; console.log(`PASS ${name}`); } else { fail++; console.log(`FAIL ${name}${detail ? ` :: ${detail}` : ''}`); } }; // One cookie jar per actor, so "signed in as A" cannot leak into B. function actor() { let cookie = ''; return { get cookie() { return cookie; }, async req(path, init = {}) { const headers = { ...(init.headers ?? {}) }; if (cookie) headers.cookie = cookie; const res = await fetch(BASE + path, { ...init, headers }); const set = res.headers.getSetCookie?.() ?? (res.headers.get('set-cookie') ? [res.headers.get('set-cookie')] : []); for (const line of set) { const value = line.split(';')[0]; if (value.startsWith('rc_session=')) cookie = value.endsWith('=') ? '' : value; } const text = await res.text(); let body = null; try { body = text ? JSON.parse(text) : null; } catch { body = text; } return { status: res.status, headers: res.headers, setCookie: set.join(' | '), body }; }, signup(email, password = 'supersecret1') { return this.req('/auth/signup', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ email, password }), }); }, upload(bytes, type) { return this.req('/photos', { method: 'POST', headers: { 'content-type': type }, body: bytes }); }, avatar(bytes, type) { return this.req('/auth/avatar', { method: 'POST', headers: { 'content-type': type }, body: bytes }); }, }; } // Run the sources, not a possibly stale build: the point of this suite is to // test the code as written. const tsx = join(ROOT, 'node_modules/.bin/tsx'); const entry = existsSync(tsx) ? [tsx, 'src/server.ts'] : ['dist/server.js']; const server = spawn(process.execPath, entry, { cwd: ROOT, env: { ...process.env, PORT: String(PORT), DATA_DIR, ADMIN_EMAILS: ADMIN_EMAIL, NODE_ENV: 'test' }, stdio: ['ignore', 'pipe', 'pipe'], }); let serverLog = ''; server.stdout.on('data', (d) => (serverLog += d)); server.stderr.on('data', (d) => (serverLog += d)); async function waitForServer() { for (let i = 0; i < 100; i++) { try { const res = await fetch(`${BASE}/health`); if (res.ok) return true; } catch { /* not up yet */ } await new Promise((r) => setTimeout(r, 100)); } return false; } try { if (!(await waitForServer())) throw new Error(`server never came up:\n${serverLog}`); const stamp = Date.now(); const admin = actor(); const user = actor(); const other = actor(); // ---- accounts ----------------------------------------------------------- check('health responds', (await fetch(`${BASE}/health`)).ok); const badEmail = await user.signup('not-an-email'); check('signup rejects a malformed email', badEmail.status === 400, `got ${badEmail.status}`); const badPw = await user.signup(`short${stamp}@test.local`, 'short'); check('signup rejects a short password', badPw.status === 400, `got ${badPw.status}`); const adminSignup = await admin.signup(ADMIN_EMAIL); check('admin account signs up', adminSignup.status === 201, `got ${adminSignup.status}`); const userSignup = await user.signup(`contributor${stamp}@test.local`); check('contributor account signs up', userSignup.status === 201, `got ${userSignup.status}`); await other.signup(`other${stamp}@test.local`); const cookie = userSignup.setCookie; check('session cookie is HttpOnly', /HttpOnly/i.test(cookie), cookie); check('session cookie is SameSite', /SameSite=Lax/i.test(cookie), cookie); check('session cookie is not Secure over plain http', !/;\s*Secure/i.test(cookie), cookie); check('session token is 256 bits of hex', /rc_session=[0-9a-f]{64}/.test(cookie), cookie); // The proxy hands nginx the public scheme; a TLS visitor must get Secure. const tls = actor(); const tlsSignup = await tls.req('/auth/signup', { method: 'POST', headers: { 'content-type': 'application/json', 'x-forwarded-proto': 'https' }, body: JSON.stringify({ email: `tls${stamp}@test.local`, password: 'supersecret1' }), }); check('session cookie is Secure behind https', /;\s*Secure/i.test(tlsSignup.setCookie), tlsSignup.setCookie); // Documented, not fixed: the 409 is a deliberate UX choice and doubles as an // account-existence oracle. const dup = await actor().signup(ADMIN_EMAIL); check('duplicate signup is a 409 (known user-enumeration oracle)', dup.status === 409, `got ${dup.status}`); const noUser = await actor().req('/auth/login', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ email: `ghost${stamp}@test.local`, password: 'supersecret1' }), }); check('unknown email and wrong password look identical', noUser.status === 401, `got ${noUser.status}`); const anonMe = await actor().req('/auth/me'); check('signed out is a 200 with no user', anonMe.status === 200 && anonMe.body?.user === null, JSON.stringify(anonMe.body)); const ownMe = await user.req('/auth/me'); check('/auth/me reports the signed-in account', ownMe.body?.user?.email === `contributor${stamp}@test.local`, JSON.stringify(ownMe.body)); // ---- rate limiting ------------------------------------------------------ const brute = actor(); const bruteEmail = `brute${stamp}@test.local`; let limited = 0; for (let i = 0; i < 21; i++) { const res = await brute.req('/auth/login', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ email: bruteEmail, password: 'wrong-password' }), }); if (res.status === 429) limited++; } check('login attempts are rate limited', limited === 1, `429s: ${limited}`); // ---- upload boundary ---------------------------------------------------- check('guest cannot upload', (await actor().upload(PNG, 'image/png')).status === 401); check( 'non-image content-type is refused', (await user.upload(Buffer.from(''), 'image/svg+xml')).status === 415, ); check( 'svg bytes under an image content-type is refused', (await user.upload(Buffer.from(''), 'image/jpeg')).status === 415, ); check( 'a declared type that disagrees with the bytes is refused', (await user.upload(JPEG_HEAD, 'image/png')).status === 415, ); check('empty body is refused', (await user.upload(Buffer.alloc(0), 'image/png')).status === 400); const tooBig = Buffer.concat([PNG, Buffer.alloc(13 * 1024 * 1024)]); check('an over-limit body is refused', (await user.upload(tooBig, 'image/png')).status === 413); const created = await user.upload(PNG, 'image/png'); check('a signed-in member can upload a real PNG', created.status === 201, JSON.stringify(created.body)); // ---- what the public may read ------------------------------------------ const list = await actor().req('/photos'); const listed = list.body?.photos ?? []; check('the strip is readable anonymously', list.status === 200); check( 'the public strip leaks no owner', listed.length > 0 && !('userId' in listed[0]) && !('email' in listed[0]) && !('file' in listed[0]), JSON.stringify(listed[0]), ); const id = created.body?.photo?.id; const served = await fetch(`${BASE}/photos/${id}/file`); check('a contributed photo is served', served.status === 200); check('the served photo keeps its sniffed type', served.headers.get('content-type') === 'image/png'); check('the served photo is nosniff', served.headers.get('x-content-type-options') === 'nosniff'); check('the served photo is sandboxed', (served.headers.get('content-security-policy') ?? '').includes('sandbox')); check('the bytes round-trip intact', Buffer.from(await served.arrayBuffer()).equals(PNG)); check('an unknown id is a 404', (await fetch(`${BASE}/photos/999999/file`)).status === 404); check('a non-numeric id is a 404', (await fetch(`${BASE}/photos/abc/file`)).status === 404); check( 'an id cannot escape the uploads directory', (await fetch(`${BASE}/photos/..%2f..%2fetc%2fpasswd/file`)).status === 404, ); // ---- the member's own folder -------------------------------------------- check('a guest has no folder', (await actor().req('/photos/mine')).status === 401); const folder = await user.req('/photos/mine'); check( 'a member lists their own photos', folder.status === 200 && (folder.body?.photos ?? []).some((p) => p.id === id), JSON.stringify(folder.body).slice(0, 120), ); const stranger = actor(); await stranger.signup(`stranger${stamp}@test.local`); check("a fresh account's folder is empty", ((await stranger.req('/photos/mine')).body?.photos ?? []).length === 0); // The strip's own labels ride the query string: the body is the image. const labels = { tag: '#KODAK_PORTRA_400', title: 'Golden Hour Portrait', meta: 'ISO 400 · GRAIN 35 · WARMTH +18' }; const labelled = await user.req( `/photos?tag=${encodeURIComponent(labels.tag)}&title=${encodeURIComponent(labels.title)}&meta=${encodeURIComponent(labels.meta)}`, { method: 'POST', headers: { 'content-type': 'image/png' }, body: PNG }, ); check( 'an upload carries its strip labels', labelled.status === 201 && labelled.body?.photo?.tag === labels.tag && labelled.body?.photo?.title === labels.title && labelled.body?.photo?.meta === labels.meta, JSON.stringify(labelled.body), ); const labelledId = labelled.body?.photo?.id; const publicRow = ((await actor().req('/photos')).body?.photos ?? []).find((p) => p.id === labelledId); check('the labels reach the public strip', publicRow?.tag === labels.tag && publicRow?.meta === labels.meta); const capped = await user.req( `/photos?tag=${'x'.repeat(200)}&meta=${encodeURIComponent('bad\u0007line')}`, { method: 'POST', headers: { 'content-type': 'image/png' }, body: PNG }, ); check('a label is length-capped', capped.body?.photo?.tag?.length === 64, `len ${capped.body?.photo?.tag?.length}`); check('a label is control-stripped', capped.body?.photo?.meta === 'bad line', JSON.stringify(capped.body?.photo?.meta)); check( 'a member cannot delete a photo they do not own', (await stranger.req(`/photos/${labelledId}`, { method: 'DELETE' })).status === 404, ); check('the stranger’s delete leaves the file alone', (await fetch(`${BASE}/photos/${labelledId}/file`)).status === 200); const ownDelete = await user.req(`/photos/${labelledId}`, { method: 'DELETE' }); check('a member deletes their own photo', ownDelete.status === 204, `got ${ownDelete.status}`); check('the deleted file is gone from disk', (await fetch(`${BASE}/photos/${labelledId}/file`)).status === 404); check( 'the row leaves the folder', !((await user.req('/photos/mine')).body?.photos ?? []).some((p) => p.id === labelledId), ); // The curator removes anyone's through the same route — the moderation screen // keeps its own two admin endpoints, this one just shares the job. const spare = (await user.upload(PNG, 'image/png')).body?.photo; check('an admin deletes through the member route too', (await admin.req(`/photos/${spare?.id}`, { method: 'DELETE' })).status === 204); check('a deleted photo leaves the strip', !((await actor().req('/photos')).body?.photos ?? []).some((p) => p.id === spare?.id)); // ---- moderation --------------------------------------------------------- check('a guest cannot moderate', (await actor().req('/admin/photos')).status === 401); const forbidden = await user.req('/admin/photos'); check('a plain member is 403, not 200', forbidden.status === 403, `got ${forbidden.status}`); const adminList = await admin.req('/admin/photos'); const rows = adminList.body?.photos ?? []; check('an admin lists contributions', adminList.status === 200 && rows.length > 0); check('the admin listing carries the owner', rows.some((r) => /@test\.local$/.test(r.email ?? ''))); check('a fresh upload lands in the strip slot', rows.find((r) => r.id === id)?.slot === 'strip'); check('a plain member cannot delete', (await user.req(`/admin/photos/${id}`, { method: 'DELETE' })).status === 403); // ---- placement ---------------------------------------------------------- const patch = (path, body) => admin.req(path, { method: 'PATCH', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }); check( 'a plain member cannot place a photo', (await user.req(`/admin/photos/${id}`, { method: 'PATCH', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ slot: 'qr' }), })).status === 403, ); const placed = await patch(`/admin/photos/${id}`, { slot: 'qr' }); check('an admin moves a photo to a live slot', placed.status === 200 && placed.body?.slot === 'qr', JSON.stringify(placed.body)); check( 'the slot is public, the owner is not', ((await actor().req('/photos')).body?.photos ?? []).find((r) => r.id === id)?.slot === 'qr', ); check('an unknown slot is refused', (await patch(`/admin/photos/${id}`, { slot: 'nope' })).status === 400); check('placing an unknown photo is a 404', (await patch('/admin/photos/999999', { slot: 'qr' })).status === 404); const deleted = await admin.req(`/admin/photos/${id}`, { method: 'DELETE' }); check('an admin deletes a contribution', deleted.status === 204, `got ${deleted.status}`); check('the deleted file is gone from disk', (await fetch(`${BASE}/photos/${id}/file`)).status === 404); check('the deleted row is gone from the listing', !((await admin.req('/admin/photos')).body?.photos ?? []).some((r) => r.id === id)); // ---- quota -------------------------------------------------------------- const quota = actor(); await quota.signup(`quota${stamp}@test.local`); let last = 0; for (let i = 0; i < 13; i++) last = (await quota.upload(PNG, 'image/png')).status; check('uploads are capped per account', last === 429, `13th upload: ${last}`); // The cap is a member fair-use rule; the curator stocks the landing page from // one account, so it must not apply to the allowlist. let adminLast = 0; for (let i = 0; i < 13; i++) adminLast = (await admin.upload(PNG, 'image/png')).status; check('the admin is exempt from the member quota', adminLast === 201, `13th admin upload: ${adminLast}`); const cleared = await admin.req('/admin/photos', { method: 'DELETE' }); check('an admin clears the strip in one call', cleared.status === 200 && cleared.body?.removed > 0, JSON.stringify(cleared.body)); check('the strip is empty afterwards', ((await actor().req('/photos')).body?.photos ?? []).length === 0); // ---- profile: an account edits itself ---------------------------------- const JSON_HDR = { 'content-type': 'application/json' }; const edit = (a, body) => a.req('/auth/me', { method: 'PATCH', headers: JSON_HDR, body: JSON.stringify(body) }); const member = actor(); await member.signup(`profile${stamp}@test.local`); const anonEdit = await edit(actor(), { password: 'another-secret-1', currentPassword: 'supersecret1' }); check('a profile edit needs a session', anonEdit.status === 401, `got ${anonEdit.status}`); const badCurrent = await edit(member, { password: 'another-secret-1', currentPassword: 'not-the-password' }); check('a profile edit needs the current password', badCurrent.status === 403, `got ${badCurrent.status}`); const takenEmail = await edit(member, { email: ADMIN_EMAIL, currentPassword: 'supersecret1' }); check('a profile edit refuses a taken email', takenEmail.status === 409, `got ${takenEmail.status}`); const shortNew = await edit(member, { password: 'short', currentPassword: 'supersecret1' }); check('a profile edit refuses a short password', shortNew.status === 400, `got ${shortNew.status}`); const newEmail = `renamed${stamp}@test.local`; const renamed = await edit(member, { email: newEmail, currentPassword: 'supersecret1' }); check('an admin-visible profile edit changes the email', renamed.status === 200 && renamed.body?.user?.email === newEmail, JSON.stringify(renamed.body)); const login = (email, password) => actor().req('/auth/login', { method: 'POST', headers: JSON_HDR, body: JSON.stringify({ email, password }) }); check('the account logs in under the new email', (await login(newEmail, 'supersecret1')).status === 200); check('the old email no longer logs in', (await login(`profile${stamp}@test.local`, 'supersecret1')).status === 401); // The session that made the edit is the same row, so it also changes the password. const newPassword = 'second-secret-1'; const rekeyed = await edit(member, { password: newPassword, currentPassword: 'supersecret1' }); check('an account changes its own password', rekeyed.status === 200, `got ${rekeyed.status}`); check('the old password stops working', (await login(newEmail, 'supersecret1')).status === 401); check('the new password works', (await login(newEmail, newPassword)).status === 200); // ---- admin: the account list ------------------------------------------- const anonUsers = await actor().req('/admin/users'); check('the user list is not public', anonUsers.status === 401, `got ${anonUsers.status}`); const memberUsers = await member.req('/admin/users'); check('a member cannot read the user list', memberUsers.status === 403, `got ${memberUsers.status}`); const adminUsers = await admin.req('/admin/users'); const adminRow = (adminUsers.body?.users ?? []).find((u) => u.email === ADMIN_EMAIL); check('an admin reads the user list', adminUsers.status === 200 && Array.isArray(adminUsers.body?.users), `got ${adminUsers.status}`); check('the list flags the allowlisted account', adminRow?.admin === true, JSON.stringify(adminRow)); check('the list counts each account’s photos', typeof adminRow?.photos === 'number', JSON.stringify(adminRow)); // ---- avatar: the picture beside the name ------------------------------- const memberId = renamed.body?.user?.id; const noSession = await actor().avatar(PNG, 'image/png'); check('an avatar upload needs a session', noSession.status === 401, `got ${noSession.status}`); const badAvatar = await member.avatar(Buffer.from(''), 'image/png'); check('an avatar upload sniffs the bytes', badAvatar.status === 415, `got ${badAvatar.status}`); const gaveAvatar = await member.avatar(PNG, 'image/png'); const avatarUrl = gaveAvatar.body?.user?.avatar; check('a member uploads an avatar', gaveAvatar.status === 200 && typeof avatarUrl === 'string', JSON.stringify(gaveAvatar.body)); check('the avatar URL points at the account', new RegExp(`^/api/users/${memberId}/avatar\\?v=[0-9a-f]{32}$`).test(String(avatarUrl)), String(avatarUrl)); const servedAvatar = await fetch(`http://127.0.0.1:${PORT}${avatarUrl}`); check('an avatar is served without a session', servedAvatar.status === 200, `got ${servedAvatar.status}`); check('an avatar carries its image type', servedAvatar.headers.get('content-type') === 'image/png', String(servedAvatar.headers.get('content-type'))); check('an avatar is cacheable for a long time', (servedAvatar.headers.get('cache-control') ?? '').includes('immutable'), String(servedAvatar.headers.get('cache-control'))); check('the avatar bytes round-trip intact', Buffer.from(await servedAvatar.arrayBuffer()).equals(PNG)); const replaced = await member.avatar(JPEG_HEAD, 'image/jpeg'); const replacedUrl = replaced.body?.user?.avatar; check('a second avatar replaces the first', replaced.status === 200 && replacedUrl !== avatarUrl, JSON.stringify(replaced.body)); check('the replaced avatar file is gone', !existsSync(join(DATA_DIR, 'avatars', `${String(avatarUrl).split('?v=')[1]}.png`))); check('the new avatar resolves', (await fetch(`http://127.0.0.1:${PORT}${replacedUrl}`)).status === 200); const ghost = await actor().req('/users/999999/avatar'); check('an unknown account has no avatar', ghost.status === 404, `got ${ghost.status}`); // The moderation list has to show the same face beside the email. const listedUsers = (await admin.req('/admin/users')).body?.users ?? []; const memberRow = listedUsers.find((u) => u.id === memberId); check('the user list carries each account’s picture', memberRow?.avatar === replacedUrl, JSON.stringify(memberRow)); // ---- moderation: block, remove, delete an account ----------------------- const target = actor(); await target.signup(`moderated${stamp}@test.local`); const targetId = (await target.req('/auth/me')).body?.user?.id; const targetPhoto = (await target.upload(PNG, 'image/png')).body?.photo; const targetPhotoUrl = `http://127.0.0.1:${PORT}/api/photos/${targetPhoto?.id}/file`; const moderate = (a, id, body) => a.req(`/admin/users/${id}`, { method: 'PATCH', headers: JSON_HDR, body: JSON.stringify(body) }); check('a fresh account is moderatable', Number.isInteger(targetId) && Number.isInteger(targetPhoto?.id), `${targetId}/${targetPhoto?.id}`); const anonModerate = await moderate(actor(), targetId, { blocked: true }); check('moderating needs a session', anonModerate.status === 401, `got ${anonModerate.status}`); const memberModerate = await moderate(member, targetId, { blocked: true }); check('a member cannot moderate', memberModerate.status === 403, `got ${memberModerate.status}`); const selfModerate = await moderate(admin, adminRow?.id, { blocked: true }); check('an allowlisted account cannot be moderated', selfModerate.status === 403, `got ${selfModerate.status}`); const ghostModerate = await moderate(admin, 999999, { blocked: true }); check('an unknown account is a 404', ghostModerate.status === 404, `got ${ghostModerate.status}`); const badModerate = await moderate(admin, targetId, { blocked: 'yes' }); check('a moderation body is type-checked', badModerate.status === 400, `got ${badModerate.status}`); const blocked = await moderate(admin, targetId, { blocked: true }); check('an admin blocks an account', blocked.status === 200 && blocked.body?.user?.blocked === true, JSON.stringify(blocked.body)); const blockedLogin = await login(`moderated${stamp}@test.local`, 'supersecret1'); check('a blocked account cannot sign in', blockedLogin.status === 403 && blockedLogin.body?.error === 'account blocked', JSON.stringify(blockedLogin.body)); check('a blocked session stops being a user', (await target.req('/auth/me')).body?.user === null, JSON.stringify((await target.req('/auth/me')).body)); const blockedRow = ((await admin.req('/admin/users')).body?.users ?? []).find((u) => u.id === targetId); check('the list flags a blocked account', blockedRow?.blocked === true, JSON.stringify(blockedRow)); const unblocked = await moderate(admin, targetId, { blocked: false }); check('an admin unblocks an account', unblocked.status === 200 && unblocked.body?.user?.blocked === false, JSON.stringify(unblocked.body)); check('an unblocked account signs in again', (await login(`moderated${stamp}@test.local`, 'supersecret1')).status === 200); check('its photo is back on the strip', ((await actor().req('/photos')).body?.photos ?? []).some((p) => p.id === targetPhoto?.id)); const removed = await moderate(admin, targetId, { removed: true }); check('an admin removes an account', removed.status === 200 && removed.body?.user?.removed === true, JSON.stringify(removed.body)); const removedLogin = await login(`moderated${stamp}@test.local`, 'supersecret1'); check('a removed account cannot sign in', removedLogin.status === 403 && removedLogin.body?.error === 'account removed', JSON.stringify(removedLogin.body)); check('a removed account’s photos leave the strip', !((await actor().req('/photos')).body?.photos ?? []).some((p) => p.id === targetPhoto?.id)); const removedRow = ((await admin.req('/admin/users')).body?.users ?? []).find((u) => u.id === targetId); check('the list flags a removed account', removedRow?.removed === true, JSON.stringify(removedRow)); const restored = await moderate(admin, targetId, { removed: false }); check('an admin restores an account', restored.status === 200 && restored.body?.user?.removed === false, JSON.stringify(restored.body)); check('a restored account signs in again', (await login(`moderated${stamp}@test.local`, 'supersecret1')).status === 200); const hardDelete = await admin.req(`/admin/users/${targetId}`, { method: 'DELETE' }); check('an admin deletes an account outright', hardDelete.status === 204, `got ${hardDelete.status}`); check('a deleted account leaves the list', !((await admin.req('/admin/users')).body?.users ?? []).some((u) => u.id === targetId)); check('a deleted account cannot sign in', (await login(`moderated${stamp}@test.local`, 'supersecret1')).status === 401); check('a deleted account’s photo file is unlinked', (await fetch(targetPhotoUrl)).status === 404); // ---- pre-existing guarantees still hold --------------------------------- const foreignRecipe = await user.req('/recipes/1', { method: 'DELETE' }); check("another account's recipe is not deletable", foreignRecipe.status === 404, `got ${foreignRecipe.status}`); const missing = await actor().req('/nope'); check('unknown routes keep the single error shape', missing.status === 404 && missing.body?.error === 'not_found'); } catch (err) { fail++; console.log(`FAIL harness :: ${err && err.stack ? err.stack : err}`); console.log(serverLog.slice(-2000)); } finally { server.kill('SIGTERM'); rmSync(DATA_DIR, { recursive: true, force: true }); } console.log(`\n${pass} passed, ${fail} failed`); process.exit(fail === 0 ? 0 : 1);