← back to Costa Rica
test/server-routes.test.js
98 lines
'use strict';
// Route tests for the INLINE server.js routes — previously untestable because
// server.js called app.listen on import and never exported the app (Cody gate,
// cycle 15/16). Now server.js exports the wired app (listen guarded by
// require.main === module), so we mount it here and drive it over HTTP.
//
// These run against the REAL dev DB (the inline routes use server.js's own pool;
// GET routes are read-only, safe). Closes the cycle-15 debt: the /api/places q>=2
// floor shipped verified-by-EXPLAIN but with no route unit test.
// Pin the site basic-auth gate to known creds BEFORE requiring server.js — its
// dotenv.config() won't override an already-set var, so this wins over .env and the
// requests below authenticate deterministically (the whole site is gated).
process.env.BASIC_AUTH_USER = 'testuser';
process.env.BASIC_AUTH_PASS = 'testpass';
const { test, before, after } = require('node:test');
const assert = require('node:assert');
const http = require('node:http');
const app = require('../server'); // exported app, does NOT listen on import
const AUTH = 'Basic ' + Buffer.from('testuser:testpass').toString('base64');
let server, base;
before(async () => {
await new Promise(r => { server = app.listen(0, r); });
base = `http://127.0.0.1:${server.address().port}`;
});
after(async () => {
server && server.close();
// As of cycle 17 server.js uses lib/db's SINGLE shared pool, so app.locals.pool
// and lib/db.pool are the SAME object — one end() closes every route's connections
// (inline routes + the mounted sub-routers). (Cody gate, cycle 16/17.)
try { await app.locals.pool.end(); } catch { /* already closed */ }
});
function get(path) {
return new Promise((resolve, reject) => {
http.get(base + path, { headers: { authorization: AUTH } }, res => {
let b = '';
res.on('data', c => b += c);
res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, json: JSON.parse(b || '{}') }));
}).on('error', reject);
});
}
test('site basic-auth gate: an UNAUTHENTICATED request is rejected (401) — the gate is active', async () => {
const r = await new Promise((resolve, reject) => {
http.get(base + '/health', res => { let b = ''; res.on('data', c => b += c); res.on('end', () => resolve({ status: res.statusCode })); }).on('error', reject);
});
assert.equal(r.status, 401, 'no credentials -> 401 (whole-site gate)');
});
test('/api/places q>=2 floor: a 1-char q is IGNORED (returns the full unfiltered listing, no scan)', async () => {
const all = await get('/api/places?limit=1');
assert.equal(all.status, 200);
const totalAll = all.json.total;
assert.ok(totalAll > 100, `expected the dev DB to have a large active-places count, got ${totalAll}`);
// 1-char q: the guard skips the LIKE filter -> ~same total as no q. Use a tolerance,
// not exact equality: these are two separately-timed COUNT queries, and a parallel
// real-DB test file (e.g. double-book creates a throwaway ACTIVE place) can insert/
// delete a handful of places between them. The point is "1-char is UNFILTERED", i.e.
// the full-directory magnitude — not a filtered subset — which a tolerance captures.
const oneChar = await get('/api/places?q=a&limit=1');
assert.equal(oneChar.status, 200);
assert.ok(Math.abs(oneChar.json.total - totalAll) < 100,
`a 1-char q must not filter (unfiltered ~${totalAll}, got ${oneChar.json.total})`);
// A real >=2 nonsense term: the filter IS applied -> far fewer than the full directory.
const filtered = await get('/api/places?q=zzqxbyw&limit=1');
assert.equal(filtered.status, 200);
assert.ok(filtered.json.total < totalAll - 100, `a >=2-char filter drastically reduces the count (${filtered.json.total} << ${totalAll})`);
});
test('/api/places pagination: Link header carries rel="next" and the page respects limit', async () => {
const r = await get('/api/places?limit=5&offset=0');
assert.equal(r.status, 200);
assert.ok(Array.isArray(r.json.places) && r.json.places.length <= 5, 'page respects the limit');
assert.equal(r.json.limit, 5);
assert.equal(r.json.offset, 0);
assert.ok(/rel="canonical"/.test(r.headers.link || ''), 'Link header has a canonical rel');
if (r.json.total > 5) assert.ok(/rel="next"/.test(r.headers.link || ''), 'a next page exists -> rel="next" present');
});
test('/api/places limit is clamped to a sane maximum (no unbounded page)', async () => {
const r = await get('/api/places?limit=99999');
assert.equal(r.status, 200);
assert.ok(r.json.limit <= 250, `limit clamped to <= 250, got ${r.json.limit}`);
});
test('/health responds ok (basic liveness of the exported app)', async () => {
const r = await get('/health');
assert.equal(r.status, 200);
assert.equal(r.json.ok, true);
});