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);
}
});