import test from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, mkdirSync, writeFileSync, rmSync, symlinkSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { buildApp } from '../server/app.mjs'; import { openStore } from '../server/db.mjs'; const VISITOR = '12345678-1234-4123-8123-123456789abc'; const DOG = 'a'.repeat(64); const ORIGIN = 'https://arrival.example'; async function fixture(t, options = {}) { const root = mkdtempSync(join(tmpdir(), 'arrival-api-')); const distRoot = join(root, 'dist'); mkdirSync(join(distRoot, 'assets'), { recursive: true }); mkdirSync(join(distRoot, 'fonts')); mkdirSync(join(distRoot, 'models')); writeFileSync(join(distRoot, 'index.html'), 'arrival fixture'); writeFileSync(join(distRoot, 'assets', 'index-AbCd1234.js'), 'export const loaded = true'); writeFileSync(join(distRoot, 'assets', 'worker-ZyXw9876.js'), 'self.onmessage = () => {}'); writeFileSync(join(distRoot, 'assets', 'ort-wasm.wasm'), Buffer.from([0, 97, 115, 109])); writeFileSync(join(distRoot, 'fonts', 'font.woff'), 'font'); writeFileSync(join(distRoot, 'models', 'model.onnx'), 'model'); writeFileSync(join(distRoot, 'config.json'), 'private-fixture-secret'); writeFileSync(join(distRoot, 'counts.sqlite'), 'private-fixture-secret'); writeFileSync(join(distRoot, 'counts.sqlite-wal'), 'private-fixture-secret'); writeFileSync(join(distRoot, 'counts.sqlite-shm'), 'private-fixture-secret'); writeFileSync(join(root, 'outside.txt'), 'private-fixture-secret'); symlinkSync(join(root, 'outside.txt'), join(distRoot, 'assets', 'escaped.txt')); const store = options.store ?? openStore({ dbPath: join(root, 'private', 'counts.sqlite') }); const app = await buildApp({ store, distRoot, publicOrigin: ORIGIN, rateLimit: { max: 10000, timeWindow: 60000 }, ...options }); t.after(async () => { await app.close(); rmSync(root, { recursive: true, force: true }); }); return { app, store, root }; } function post(app, url, payload, headers = {}) { return app.inject({ method: 'POST', url, payload, headers }); } function assertError(response, status) { assert.equal(response.statusCode, status, response.body); assert.match(response.headers['content-type'], /application\/json/); assert.equal(response.headers['cache-control'], 'no-store'); assert.doesNotMatch(response.body, /12345678|aaaaaaaaaa|sqlite|private-fixture-secret|visitor_id|dog_id|body\//i); } test('visit and arrival contracts use real SQLite and concurrent duplicates stay at one', async t => { const { app, store } = await fixture(t); assert.deepEqual((await app.inject('/api/arrivals/count')).json(), { count: 0 }); assert.equal((await post(app, '/api/visits', { visitor_id: VISITOR })).statusCode, 204); const replies = await Promise.all(Array.from({ length: 20 }, () => post(app, '/api/arrivals', { dog_id: DOG }))); for (const reply of replies) { assert.equal(reply.statusCode, 204); assert.equal(reply.body, ''); } assert.equal(store.arrivalCount(), 1); const count = await app.inject('/api/arrivals/count'); assert.deepEqual(count.json(), { count: 1 }); assert.equal(count.headers['cache-control'], 'no-store'); }); test('exact schemas reject extra keys, coercion, invalid IDs, array/null and malformed JSON', async t => { const { app } = await fixture(t); for (const payload of [{}, { visitor_id: 1 }, { visitor_id: null }, { visitor_id: [VISITOR] }, { visitor_id: VISITOR, photo: 'private-fixture-secret' }, { visitor_id: VISITOR, name: 'private-fixture-secret' }, { visitor_id: VISITOR, visit_date: '2026-10-08' }, [], null]) { assertError(await post(app, '/api/visits', JSON.stringify(payload), { 'content-type': 'application/json' }), 400); } for (const id of ['x', VISITOR.replace('-4', '-1'), VISITOR.replace('-8123', '-7123'), `${VISITOR}\n`, VISITOR.toUpperCase()]) assertError(await post(app, '/api/visits', { visitor_id: id }), 400); for (const payload of [{}, { dog_id: 1 }, { dog_id: DOG.toUpperCase() }, { dog_id: DOG.slice(1) }, { dog_id: `${DOG}\n` }, { dog_id: DOG, photo: 'private-fixture-secret' }, { dog_id: DOG, name: 'private-fixture-secret' }, { dog_id: DOG, visitor_id: VISITOR }]) assertError(await post(app, '/api/arrivals', payload), 400); assertError(await post(app, '/api/arrivals', '{"dog_id":', { 'content-type': 'application/json' }), 400); assertError(await post(app, '/api/arrivals', JSON.stringify({ dog_id: DOG }), { 'content-type': 'text/plain' }), 415); assertError(await post(app, '/api/arrivals', JSON.stringify({ dog_id: DOG }), { 'content-type': 'application/x-www-form-urlencoded' }), 415); assertError(await post(app, '/api/arrivals', JSON.stringify({ dog_id: DOG, extra: 'x'.repeat(1024) }), { 'content-type': 'application/json' }), 413); }); test('Origin validation uses configured public origin and allows absent Origin', async t => { const { app } = await fixture(t); for (const origin of [undefined, ORIGIN]) { const headers = origin ? { origin } : {}; assert.equal((await post(app, '/api/visits', { visitor_id: VISITOR }, headers)).statusCode, 204); } for (const origin of ['https://foreign.example', 'null', 'not-an-origin', `${ORIGIN}/path`, `${ORIGIN}, https://foreign.example`]) { assertError(await post(app, '/api/visits', { visitor_id: VISITOR }, { origin, host: 'foreign.example', 'x-forwarded-host': 'foreign.example' }), 403); } assert.equal((await post(app, '/api/visits', { visitor_id: VISITOR }, { origin: ORIGIN, host: 'forged.example' })).statusCode, 204); }); test('API rate limit is 60 per connection IP; forged forwarded headers do not evade it', async t => { const { app } = await fixture(t, { rateLimit: { max: 60, timeWindow: 60000 } }); for (let i = 0; i < 60; i++) assert.equal((await app.inject({ url: '/api/arrivals/count', headers: { 'x-forwarded-for': `203.0.113.${i}` } })).statusCode, 200); assertError(await app.inject({ url: '/api/arrivals/count', headers: { 'x-forwarded-for': '198.51.100.99' } }), 429); assert.equal((await app.inject({ url: '/api/arrivals/count', remoteAddress: '192.0.2.12' })).statusCode, 200); assert.equal((await app.inject('/')).statusCode, 200); }); test('closed/unavailable store returns generic 503 without identifiers or paths', async t => { const { app, store } = await fixture(t); store.close(); assertError(await app.inject('/api/arrivals/count'), 503); assertError(await post(app, '/api/visits', { visitor_id: VISITOR }), 503); assertError(await post(app, '/api/arrivals', { dog_id: DOG }), 503); }); test('app close owns its store exactly once and import never listens', async t => { let closed = 0; const { app } = await fixture(t, { store: { close() { closed++; }, arrivalCount() { return 0; } } }); assert.equal(app.server.listening, false); await app.close(); await app.close(); assert.equal(closed, 1); }); test('unknown API and POST routes return JSON 404, never SPA HTML', async t => { const { app } = await fixture(t); for (const method of ['GET', 'POST']) for (const url of ['/api', '/api/', '/api/nope', '/api/monthly-visitors', '/api/visits/nope']) assertError(await app.inject({ method, url, headers: { accept: 'text/html' } }), 404); assertError(await app.inject({ method: 'POST', url: '/somewhere', headers: { accept: 'text/html' } }), 404); }); test('static MIME/cache and SPA navigation preserve worker/WASM/font/model asset routes', async t => { const { app } = await fixture(t); for (const url of ['/', '/index.html', '/journey/window']) { const response = await app.inject({ url, headers: { accept: 'text/html' } }); assert.equal(response.statusCode, 200); assert.match(response.body, /arrival fixture/); assert.equal(response.headers['cache-control'], 'no-store'); } assert.equal((await app.inject({ method: 'HEAD', url: '/journey/window', headers: { accept: 'text/html' } })).statusCode, 200); for (const url of ['/assets/index-AbCd1234.js', '/assets/worker-ZyXw9876.js']) { const response = await app.inject(url); assert.equal(response.statusCode, 200); assert.match(response.headers['content-type'], /javascript/); assert.equal(response.headers['cache-control'], 'public,max-age=31536000,immutable'); } const wasm = await app.inject('/assets/ort-wasm.wasm'); assert.equal(wasm.statusCode, 200); assert.equal(wasm.headers['content-type'], 'application/wasm'); assert.equal(wasm.headers['cache-control'], 'no-cache'); for (const url of ['/fonts/font.woff', '/models/model.onnx']) assert.equal((await app.inject(url)).statusCode, 200); for (const url of ['/assets/nope.js', '/models/missing.onnx', '/route.json', '/missing']) assertError(await app.inject({ url, headers: { accept: url === '/missing' ? '*/*' : 'text/html' } }), 404); }); test('traversal, symlink escapes and guessed data/config files are never static responses', async t => { const { app } = await fixture(t); for (const url of ['/../outside.txt', '/%2e%2e/outside.txt', '/%2e%2e%2foutside.txt', '/assets/%2e%2e/%2e%2e/outside.txt', '/assets/escaped.txt', '/counts.sqlite', '/counts.sqlite-wal', '/counts.sqlite-shm', '/config.json', '/.env', '/.git/config', '/server/config.mjs', '/api/%2e%2e%2findex.html']) { const response = await app.inject({ url, headers: { accept: 'text/html' } }); assert.ok([400, 404].includes(response.statusCode), `${url}: ${response.statusCode}`); assert.doesNotMatch(response.body, /private-fixture-secret|arrival fixture/); } }); test('unhashed existing GLB assets never receive an immutable build cache policy', async t => { const { app, root } = await fixture(t); const directory = join(root, 'dist', 'assets', 'window'); mkdirSync(directory); writeFileSync(join(directory, 'grandma-house-surround.glb'), 'geometry fixture'); const response = await app.inject('/assets/window/grandma-house-surround.glb?v=2'); assert.equal(response.statusCode, 200); assert.equal(response.headers['cache-control'], 'no-cache'); }); test('percent-encoded API route aliases share the same IP limit, including writes', async t => { const { app, store } = await fixture(t, { rateLimit: { max: 1, timeWindow: 60000 } }); assert.equal((await post(app, '/api/arrivals', { dog_id: DOG })).statusCode, 204); for (const url of ['/%61pi/arrivals', '/a%70i/arrivals', '/ap%69/arrivals']) assertError(await post(app, url, { dog_id: 'f'.repeat(64) }), 429); assert.equal(store.arrivalCount(), 1); assertError(await app.inject('/%61pi/arrivals/count'), 429); }); test('framework malformed-URL errors never echo request identifiers and are no-store', async t => { const { app } = await fixture(t); const response = await app.inject('/api/%ZZ/private-fixture-secret'); assertError(response, 400); assert.doesNotMatch(response.body, /%ZZ|private-fixture-secret|FST_ERR_BAD_URL/); }); test('same-root symlink aliases cannot expose blocked database or configuration files', async t => { const { app, root } = await fixture(t); for (const [name, target] of [['alias.txt', 'counts.sqlite'], ['config-alias.txt', 'config.json'], ['wal-alias.txt', 'counts.sqlite-wal']]) { symlinkSync(join(root, 'dist', target), join(root, 'dist', name)); const response = await app.inject(`/${name}`); assertError(response, 404); } }); test('unknown API paths return JSON 404 before unsupported or malformed body parsing', async t => { const { app } = await fixture(t); for (const url of ['/api', '/api/nope', '/api/visits/nope']) { assertError(await post(app, url, 'private-fixture-secret', { 'content-type': 'text/plain' }), 404); assertError(await post(app, url, '{"private-fixture-secret":', { 'content-type': 'application/json' }), 404); } }); test('absolute-form HTTP targets cannot bypass same-origin or API rate guards', async t => { const { app, store } = await fixture(t, { rateLimit: { max: 1, timeWindow: 60000 } }); const { default: inject } = await import('light-my-request'); for (const origin of ['https://foreign.example', ORIGIN]) { const response = await inject((request, reply) => { request.url = 'http://arrival.example/api/arrivals'; app.routing(request, reply); }, { url: '/api/arrivals', method: 'POST', payload: { dog_id: DOG }, headers: { origin } }); assertError(response, 400); } assert.equal(store.arrivalCount(), 0); }); test('raw unnormalized traversal is rejected before the routing/static boundary', async t => { const { app } = await fixture(t); const { default: inject } = await import('light-my-request'); for (const target of ['/api/%2e%2e/index.html', '/../index.html', '/assets/../index.html']) { const response = await inject((request, reply) => { request.url = target; app.routing(request, reply); }, { url: '/', headers: { accept: 'text/html' } }); assertError(response, 404); } });