← back to Costa Rica
costa-rica: export the app from server.js so inline routes are testable (Cody gate, cycle 16) — TK-10346
2d3cca4e9483374f56840a89b2546128c7e8135a · 2026-09-23 23:48:04 -0700 · Steve
The ~20 inline server.js routes (/api/places, /api/search, /api/provinces, /api/stats,
/api/leads, /r/:slug, /p/:slug, ...) had ZERO test coverage because server.js called
app.listen on import and never exported the app (surfaced by Cody in cycle 15).
Refactor: `module.exports = app` + guard preflight + app.listen behind
`if (require.main === module)`, and expose the module-scoped pg pool on
`app.locals.pool` so a test can close it. Prod boot is UNCHANGED — server.js is
started only via `node server.js` (pm2 fork mode + npm start), where
require.main === module is true, so preflight + listen run exactly as before.
Nothing in the repo imports ./server (grep-confirmed) except the new test.
Cody gate — prod boot verified BULLETPROOF, empirically (not just reasoned): Cody
read pm2 6.0.14 source + ran a live fork-mode probe confirming ProcessContainerFork
does `_load(script, null, /*isMain*/ true)`, so require.main === module is true the
whole time server.js runs — and the same holds even if someone later adds cluster
`instances` (ProcessContainer does the same _load). preflight ordering below the
export is behaviorally identical (a throw still crashes the pm2 child pre-listen ->
autorestart, unchanged).
Cody also caught a real latent TEST bug + fixed: server.js maintains its OWN pg pool
separate from lib/db.js's shared pool (the mounted sub-routers use lib/db's). Importing
server.js creates BOTH; the test's cleanup only closed app.locals.pool, so the next
test hitting a sub-router (/api/admin, /webhooks) would hang the test process on a
dangling connection. Now closes both. (Root cause — server.js duplicating lib/db's
"single shared" pool — is a follow-up ticket, not this diff.)
Tests (+5, suite 185 -> 190, new test/server-routes.test.js): closes the cycle-15
debt — the /api/places q>=2 floor now has a real route test (a 1-char q returns the
full unfiltered count; a >=2 term filters) — plus the basic-auth gate (401
unauthenticated), pagination Link header, limit clamp, and /health liveness. Run
against the real dev DB via the exported app on an ephemeral port.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic
Files touched
M server.jsA test/server-routes.test.js
Diff
commit 2d3cca4e9483374f56840a89b2546128c7e8135a
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Sep 23 23:48:04 2026 -0700
costa-rica: export the app from server.js so inline routes are testable (Cody gate, cycle 16) — TK-10346
The ~20 inline server.js routes (/api/places, /api/search, /api/provinces, /api/stats,
/api/leads, /r/:slug, /p/:slug, ...) had ZERO test coverage because server.js called
app.listen on import and never exported the app (surfaced by Cody in cycle 15).
Refactor: `module.exports = app` + guard preflight + app.listen behind
`if (require.main === module)`, and expose the module-scoped pg pool on
`app.locals.pool` so a test can close it. Prod boot is UNCHANGED — server.js is
started only via `node server.js` (pm2 fork mode + npm start), where
require.main === module is true, so preflight + listen run exactly as before.
Nothing in the repo imports ./server (grep-confirmed) except the new test.
Cody gate — prod boot verified BULLETPROOF, empirically (not just reasoned): Cody
read pm2 6.0.14 source + ran a live fork-mode probe confirming ProcessContainerFork
does `_load(script, null, /*isMain*/ true)`, so require.main === module is true the
whole time server.js runs — and the same holds even if someone later adds cluster
`instances` (ProcessContainer does the same _load). preflight ordering below the
export is behaviorally identical (a throw still crashes the pm2 child pre-listen ->
autorestart, unchanged).
Cody also caught a real latent TEST bug + fixed: server.js maintains its OWN pg pool
separate from lib/db.js's shared pool (the mounted sub-routers use lib/db's). Importing
server.js creates BOTH; the test's cleanup only closed app.locals.pool, so the next
test hitting a sub-router (/api/admin, /webhooks) would hang the test process on a
dangling connection. Now closes both. (Root cause — server.js duplicating lib/db's
"single shared" pool — is a follow-up ticket, not this diff.)
Tests (+5, suite 185 -> 190, new test/server-routes.test.js): closes the cycle-15
debt — the /api/places q>=2 floor now has a real route test (a 1-char q returns the
full unfiltered count; a >=2 term filters) — plus the basic-auth gate (401
unauthenticated), pagination Link header, limit clamp, and /health liveness. Run
against the real dev DB via the exported app on an ephemeral port.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic
---
server.js | 33 ++++++++++------
test/server-routes.test.js | 96 ++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 118 insertions(+), 11 deletions(-)
diff --git a/server.js b/server.js
index 34036e4..e7da0fa 100644
--- a/server.js
+++ b/server.js
@@ -728,14 +728,25 @@ app.use((err, req, res, next) => {
serverError(res, err, `${req.method} ${req.path}`);
});
-// TK-10346 — fail-closed boot guard: refuse to serve in production if a payment/WhatsApp
-// integration is LIVE without its webhook secret (would silently reject every real webhook
-// -> payments succeed but bookings never confirm). Inert while everything is sandbox.
-require('./lib/preflight').runPreflight({
- getProvider: require('./lib/payments').getProvider,
- whatsapp: require('./lib/whatsapp'),
-});
-
-app.listen(PORT, '0.0.0.0', () => {
- console.log(`[${SITE_NAME}] listening on :${PORT} — gated as ${BA_USER || 'OPEN'} — domain ${SITE_DOMAIN}`);
-});
+// Export the fully-wired app so route tests can import it (supertest-style) WITHOUT
+// binding a port or running the boot guard. Only the real entrypoint (`node server.js`,
+// via pm2 / `npm start`) has require.main === module, so preflight + listen run in
+// prod exactly as before — importing this module (in a test) does neither.
+// Expose the DB pool on app.locals so a test can end() it (this module-scoped pool
+// is otherwise unreachable, and an open pool would keep the test process alive).
+app.locals.pool = pool;
+module.exports = app;
+
+if (require.main === module) {
+ // TK-10346 — fail-closed boot guard: refuse to serve in production if a payment/WhatsApp
+ // integration is LIVE without its webhook secret (would silently reject every real webhook
+ // -> payments succeed but bookings never confirm). Inert while everything is sandbox.
+ require('./lib/preflight').runPreflight({
+ getProvider: require('./lib/payments').getProvider,
+ whatsapp: require('./lib/whatsapp'),
+ });
+
+ app.listen(PORT, '0.0.0.0', () => {
+ console.log(`[${SITE_NAME}] listening on :${PORT} — gated as ${BA_USER || 'OPEN'} — domain ${SITE_DOMAIN}`);
+ });
+}
diff --git a/test/server-routes.test.js b/test/server-routes.test.js
new file mode 100644
index 0000000..29a8900
--- /dev/null
+++ b/test/server-routes.test.js
@@ -0,0 +1,96 @@
+'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();
+ // TWO pools exist: server.js has its own (app.locals.pool, used by the inline
+ // routes) AND lib/db.js has a separate shared pool (used by the mounted
+ // sub-routers /api/app, /api/admin, /api/build, /webhooks). Importing server.js
+ // creates both; close BOTH or a future test that hits a sub-router hangs the test
+ // process on a dangling connection. (Cody gate, cycle 16 — root cause: server.js
+ // duplicating lib/db's pool; follow-up ticket to consolidate.)
+ try { await app.locals.pool.end(); } catch { /* already closed */ }
+ try { await require('../lib/db').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.
+ const oneChar = await get('/api/places?q=a&limit=1');
+ assert.equal(oneChar.status, 200);
+ assert.equal(oneChar.json.total, totalAll, 'a 1-char q must not filter (would be a full-directory scan); total == unfiltered');
+
+ // A real >=2 nonsense term: the filter IS applied -> a strictly smaller total.
+ const filtered = await get('/api/places?q=zzqxbyw&limit=1');
+ assert.equal(filtered.status, 200);
+ assert.ok(filtered.json.total < totalAll, `a >=2-char filter must reduce 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);
+});
← f899e1c cycle 15 docs: YOLO_NOTES ledger + GO-LIVE pg_trgm prerequis
·
back to Costa Rica
·
cycle 16 docs: YOLO_NOTES ledger — server.js app export + in 1d24359 →