diff --git a/server/test/148-eviction-storm.test.js b/server/test/148-eviction-storm.test.js index 1a3036a..8880249 100644 --- a/server/test/148-eviction-storm.test.js +++ b/server/test/148-eviction-storm.test.js @@ -10,13 +10,16 @@ const { spawn } = require('node:child_process'); const ioClient = require('socket.io-client'); const path = require('node:path'); const os = require('node:os'); const fs = require('node:fs'); const crypto = require('node:crypto'); -const PORT = 3955; -const base = `http://127.0.0.1:${PORT}`; +const { freePort } = require('./helpers/free-port'); +let PORT; +let base; const DATA_DIR = path.join(os.tmpdir(), 'st-storm-' + crypto.randomBytes(4).toString('hex')); let proc; const sleep = (ms) => new Promise(r => setTimeout(r, ms)); before(async () => { + PORT = await freePort(); + base = `http://127.0.0.1:${PORT}`; const logFd = fs.openSync(path.join(os.tmpdir(), 'st-storm.log'), 'w'); proc = spawn('node', ['server.js'], { cwd: path.join(__dirname, '..'), diff --git a/server/test/148-half-open.test.js b/server/test/148-half-open.test.js index b960744..208aeae 100644 --- a/server/test/148-half-open.test.js +++ b/server/test/148-half-open.test.js @@ -13,13 +13,15 @@ const WebSocket = require('../node_modules/ws'); const ioClient = require('socket.io-client'); const path = require('node:path'); const os = require('node:os'); const fs = require('node:fs'); const crypto = require('node:crypto'); -const PORT = 3957; -const BASE = `http://127.0.0.1:${PORT}`; +const { freePort } = require('./helpers/free-port'); +let PORT, BASE; const DATA_DIR = path.join(os.tmpdir(), 'st-ho-' + crypto.randomBytes(4).toString('hex')); let proc; const sleep = (ms) => new Promise(r => setTimeout(r, ms)); before(async () => { + PORT = await freePort(); + BASE = `http://127.0.0.1:${PORT}`; const logFd = fs.openSync(path.join(os.tmpdir(), 'st-ho.log'), 'w'); proc = spawn('node', ['server.js'], { cwd: path.join(__dirname, '..'), diff --git a/server/test/156-schedule-read-path.test.js b/server/test/156-schedule-read-path.test.js index 6f6d267..b1f28cd 100644 --- a/server/test/156-schedule-read-path.test.js +++ b/server/test/156-schedule-read-path.test.js @@ -23,8 +23,9 @@ const os = require('node:os'); const fs = require('node:fs'); const crypto = require('node:crypto'); -const PORT = 3900 + (crypto.randomBytes(1)[0] % 90); // avoid clashes with sibling subprocess suites -const BASE = `http://127.0.0.1:${PORT}`; +const { freePort } = require('./helpers/free-port'); +let PORT; // avoid clashes with sibling subprocess suites +let BASE; const DATA_DIR = path.join(os.tmpdir(), 'st-156-test-' + crypto.randomBytes(4).toString('hex')); const LOG = path.join(os.tmpdir(), 'st-156-test-' + crypto.randomBytes(4).toString('hex') + '.log'); @@ -60,6 +61,8 @@ async function loadItem(itemId) { } before(async () => { + PORT = await freePort(); + BASE = `http://127.0.0.1:${PORT}`; const logFd = fs.openSync(LOG, 'w'); proc = spawn('node', ['server.js'], { cwd: path.join(__dirname, '..'), diff --git a/server/test/agency.test.js b/server/test/agency.test.js index 1784fc4..2d5eabb 100644 --- a/server/test/agency.test.js +++ b/server/test/agency.test.js @@ -12,12 +12,14 @@ const os = require('node:os'); const fs = require('node:fs'); const crypto = require('node:crypto'); -const PORT = 3992; -const BASE = `http://127.0.0.1:${PORT}`; +const { freePort } = require('./helpers/free-port'); +let PORT, BASE; const DATA_DIR = path.join(os.tmpdir(), 'st-agency-' + crypto.randomBytes(4).toString('hex')); let proc; before(async () => { + PORT = await freePort(); + BASE = `http://127.0.0.1:${PORT}`; const logFd = fs.openSync(path.join(os.tmpdir(), 'st-agency.log'), 'w'); proc = spawn('node', ['server.js'], { cwd: path.join(__dirname, '..'), diff --git a/server/test/api.test.js b/server/test/api.test.js index bf22bc8..d45427f 100644 --- a/server/test/api.test.js +++ b/server/test/api.test.js @@ -20,8 +20,8 @@ const crypto = require('node:crypto'); const ioClient = require('socket.io-client'); const { PUBLIC_ROUTERS, JWT_ONLY_ROUTERS } = require('../config/api-surface'); -const PORT = 3978; -const BASE = `http://127.0.0.1:${PORT}`; +const { freePort } = require('./helpers/free-port'); +let PORT, BASE; const DATA_DIR = path.join(os.tmpdir(), 'st-api-test-' + crypto.randomBytes(4).toString('hex')); const LOG = path.join(os.tmpdir(), 'st-api-test-' + crypto.randomBytes(4).toString('hex') + '.log'); @@ -38,6 +38,8 @@ const auth = (tok, extra = {}) => ({ headers: { Authorization: 'Bearer ' + tok, const post = (tok, obj, extra) => ({ method: 'POST', ...auth(tok, extra), body: JSON.stringify(obj) }); before(async () => { + PORT = await freePort(); + BASE = `http://127.0.0.1:${PORT}`; const logFd = fs.openSync(LOG, 'w'); proc = spawn('node', ['server.js'], { cwd: path.join(__dirname, '..'), diff --git a/server/test/billing-authz.test.js b/server/test/billing-authz.test.js index 9a7fdf9..0d954f1 100644 --- a/server/test/billing-authz.test.js +++ b/server/test/billing-authz.test.js @@ -14,8 +14,8 @@ const fs = require('node:fs'); const crypto = require('node:crypto'); const Database = require('better-sqlite3'); -const PORT = 4011; -const BASE = `http://127.0.0.1:${PORT}`; +const { freePort } = require('./helpers/free-port'); +let PORT, BASE; const DATA_DIR = path.join(os.tmpdir(), 'st-billauthz-' + crypto.randomBytes(4).toString('hex')); let proc, db; @@ -30,6 +30,8 @@ const setRole = (email, role) => db.prepare('UPDATE users SET role = ? WHERE ema let adminJwt, userJwt, billingToken, billingTokenId, readToken; before(async () => { + PORT = await freePort(); + BASE = `http://127.0.0.1:${PORT}`; const logFd = fs.openSync(path.join(os.tmpdir(), 'st-billauthz.log'), 'w'); proc = spawn('node', ['server.js'], { cwd: path.join(__dirname, '..'), diff --git a/server/test/billing-endpoint.test.js b/server/test/billing-endpoint.test.js index 2267bfb..271b4ff 100644 --- a/server/test/billing-endpoint.test.js +++ b/server/test/billing-endpoint.test.js @@ -13,12 +13,14 @@ const fs = require('node:fs'); const crypto = require('node:crypto'); const Database = require('better-sqlite3'); -const PORT = 3999; -const BASE = `http://127.0.0.1:${PORT}`; +const { freePort } = require('./helpers/free-port'); +let PORT, BASE; const DATA_DIR = path.join(os.tmpdir(), 'st-billing-ep-' + crypto.randomBytes(4).toString('hex')); let proc, db; before(async () => { + PORT = await freePort(); + BASE = `http://127.0.0.1:${PORT}`; const logFd = fs.openSync(path.join(os.tmpdir(), 'st-billing-ep.log'), 'w'); proc = spawn('node', ['server.js'], { cwd: path.join(__dirname, '..'), diff --git a/server/test/billing-token-mint.test.js b/server/test/billing-token-mint.test.js index 0522dea..aa1d835 100644 --- a/server/test/billing-token-mint.test.js +++ b/server/test/billing-token-mint.test.js @@ -14,8 +14,8 @@ const fs = require('node:fs'); const crypto = require('node:crypto'); const Database = require('better-sqlite3'); -const PORT = 4021; -const BASE = `http://127.0.0.1:${PORT}`; +const { freePort } = require('./helpers/free-port'); +let PORT, BASE; const DATA_DIR = path.join(os.tmpdir(), 'st-billmint-' + crypto.randomBytes(4).toString('hex')); process.env.DATA_DIR = DATA_DIR; // so requiring lib/billing-token's deps resolves this db too @@ -25,6 +25,8 @@ const { hashToken } = require('../middleware/apiToken'); let proc, db, minted; before(async () => { + PORT = await freePort(); + BASE = `http://127.0.0.1:${PORT}`; const logFd = fs.openSync(path.join(os.tmpdir(), 'st-billmint.log'), 'w'); proc = spawn('node', ['server.js'], { cwd: path.join(__dirname, '..'), diff --git a/server/test/block-authz.test.js b/server/test/block-authz.test.js index d65812a..b6bb488 100644 --- a/server/test/block-authz.test.js +++ b/server/test/block-authz.test.js @@ -14,12 +14,14 @@ const fs = require('node:fs'); const crypto = require('node:crypto'); const Database = require('better-sqlite3'); -const PORT = 3993; -const BASE = `http://127.0.0.1:${PORT}`; +const { freePort } = require('./helpers/free-port'); +let PORT, BASE; const DATA_DIR = path.join(os.tmpdir(), 'st-blockauthz-' + crypto.randomBytes(4).toString('hex')); let proc, db; before(async () => { + PORT = await freePort(); + BASE = `http://127.0.0.1:${PORT}`; const logFd = fs.openSync(path.join(os.tmpdir(), 'st-blockauthz.log'), 'w'); proc = spawn('node', ['server.js'], { cwd: path.join(__dirname, '..'), diff --git a/server/test/boot-health.test.js b/server/test/boot-health.test.js index 52cc4a3..5f85e59 100644 --- a/server/test/boot-health.test.js +++ b/server/test/boot-health.test.js @@ -14,17 +14,19 @@ const os = require('node:os'); const fs = require('node:fs'); const crypto = require('node:crypto'); const Database = require('better-sqlite3'); +const { freePort } = require('./helpers/free-port'); -const PORT = 3995; -const BASE = `http://127.0.0.1:${PORT}`; const DATA_DIR = path.join(os.tmpdir(), 'st-boot-' + crypto.randomBytes(4).toString('hex')); const DBPATH = path.join(DATA_DIR, 'db', 'remote_display.db'); test('boots + serves /api/status quickly against a pre-bloated table; prune drains in background', async () => { + const PORT = await freePort(); + const BASE = `http://127.0.0.1:${PORT}`; + const SEED_PORT = await freePort(); // 1) Create + migrate the DB in a throwaway boot, then seed a large backlog. { - const p = spawn('node', ['server.js'], { cwd: path.join(__dirname, '..'), env: { ...process.env, DATA_DIR, SELF_HOSTED: 'true', PORT: '3894', NODE_ENV: 'test' }, stdio: 'ignore' }); - for (let i = 0; i < 60; i++) { try { const r = await fetch('http://127.0.0.1:3894/api/status'); if (r.ok) break; } catch { /* */ } await new Promise(r => setTimeout(r, 200)); } + const p = spawn('node', ['server.js'], { cwd: path.join(__dirname, '..'), env: { ...process.env, DATA_DIR, SELF_HOSTED: 'true', PORT: String(SEED_PORT), NODE_ENV: 'test' }, stdio: 'ignore' }); + for (let i = 0; i < 60; i++) { try { const r = await fetch(`http://127.0.0.1:${SEED_PORT}/api/status`); if (r.ok) break; } catch { /* */ } await new Promise(r => setTimeout(r, 200)); } p.kill('SIGKILL'); await new Promise(r => setTimeout(r, 300)); } diff --git a/server/test/content-ack-dedup.test.js b/server/test/content-ack-dedup.test.js index e5904f8..ac3611c 100644 --- a/server/test/content-ack-dedup.test.js +++ b/server/test/content-ack-dedup.test.js @@ -14,8 +14,8 @@ const fs = require('node:fs'); const crypto = require('node:crypto'); const ioClient = require('socket.io-client'); -const PORT = 3984; -const BASE = `http://127.0.0.1:${PORT}`; +const { freePort } = require('./helpers/free-port'); +let PORT, BASE; const DATA_DIR = path.join(os.tmpdir(), 'st-ack-' + crypto.randomBytes(4).toString('hex')); const LOG = path.join(os.tmpdir(), 'st-ack-' + crypto.randomBytes(4).toString('hex') + '.log'); const DEDUP_MS = 600; @@ -24,6 +24,8 @@ let proc; const sleep = (ms) => new Promise(r => setTimeout(r, ms)); before(async () => { + PORT = await freePort(); + BASE = `http://127.0.0.1:${PORT}`; const logFd = fs.openSync(LOG, 'w'); proc = spawn('node', ['server.js'], { cwd: path.join(__dirname, '..'), diff --git a/server/test/content-ack-flood.test.js b/server/test/content-ack-flood.test.js index 934bd4e..f2a3495 100644 --- a/server/test/content-ack-flood.test.js +++ b/server/test/content-ack-flood.test.js @@ -15,14 +15,16 @@ const fs = require('node:fs'); const crypto = require('node:crypto'); const ioClient = require('socket.io-client'); -const PORT = 3985; -const BASE = `http://127.0.0.1:${PORT}`; +const { freePort } = require('./helpers/free-port'); +let PORT, BASE; const DATA_DIR = path.join(os.tmpdir(), 'st-flood-' + crypto.randomBytes(4).toString('hex')); const LOG = path.join(os.tmpdir(), 'st-flood-' + crypto.randomBytes(4).toString('hex') + '.log'); let proc; const sleep = (ms) => new Promise(r => setTimeout(r, ms)); before(async () => { + PORT = await freePort(); + BASE = `http://127.0.0.1:${PORT}`; const logFd = fs.openSync(LOG, 'w'); proc = spawn('node', ['server.js'], { cwd: path.join(__dirname, '..'), diff --git a/server/test/content-ack-valve.test.js b/server/test/content-ack-valve.test.js index 31fb8a2..8970709 100644 --- a/server/test/content-ack-valve.test.js +++ b/server/test/content-ack-valve.test.js @@ -15,14 +15,16 @@ const fs = require('node:fs'); const crypto = require('node:crypto'); const ioClient = require('socket.io-client'); -const PORT = 3986; -const BASE = `http://127.0.0.1:${PORT}`; +const { freePort } = require('./helpers/free-port'); +let PORT, BASE; const DATA_DIR = path.join(os.tmpdir(), 'st-valve-' + crypto.randomBytes(4).toString('hex')); const LOG = path.join(os.tmpdir(), 'st-valve-' + crypto.randomBytes(4).toString('hex') + '.log'); let proc; const sleep = (ms) => new Promise(r => setTimeout(r, ms)); before(async () => { + PORT = await freePort(); + BASE = `http://127.0.0.1:${PORT}`; const logFd = fs.openSync(LOG, 'w'); proc = spawn('node', ['server.js'], { cwd: path.join(__dirname, '..'), diff --git a/server/test/device-block-and-auth.test.js b/server/test/device-block-and-auth.test.js index a8a1f83..15b2027 100644 --- a/server/test/device-block-and-auth.test.js +++ b/server/test/device-block-and-auth.test.js @@ -19,8 +19,8 @@ const crypto = require('node:crypto'); const ioClient = require('socket.io-client'); const Database = require('better-sqlite3'); -const PORT = 3987; -const BASE = `http://127.0.0.1:${PORT}`; +const { freePort } = require('./helpers/free-port'); +let PORT, BASE; const DATA_DIR = path.join(os.tmpdir(), 'st-block-' + crypto.randomBytes(4).toString('hex')); const LOG = path.join(os.tmpdir(), 'st-block-' + crypto.randomBytes(4).toString('hex') + '.log'); const DB_PATH = path.join(DATA_DIR, 'db', 'remote_display.db'); @@ -30,6 +30,8 @@ let tdb; // ONE long-lived operator-style connection (mirrors how the server hol const sleep = (ms) => new Promise(r => setTimeout(r, ms)); before(async () => { + PORT = await freePort(); + BASE = `http://127.0.0.1:${PORT}`; const logFd = fs.openSync(LOG, 'w'); proc = spawn('node', ['server.js'], { cwd: path.join(__dirname, '..'), diff --git a/server/test/device-owner-qr.test.js b/server/test/device-owner-qr.test.js index 1d2e5af..7c5d936 100644 --- a/server/test/device-owner-qr.test.js +++ b/server/test/device-owner-qr.test.js @@ -8,11 +8,14 @@ const assert = require('node:assert/strict'); const { spawn } = require('node:child_process'); const sleep = ms => new Promise(r => setTimeout(r, ms)); -const PORT = 3977; const BASE = `http://127.0.0.1:${PORT}`; +const { freePort } = require('./helpers/free-port'); +let PORT, BASE; const DATA_DIR = path.join(os.tmpdir(), 'st-doqr-' + crypto.randomBytes(4).toString('hex')); let proc, JWT; before(async () => { + PORT = await freePort(); + BASE = `http://127.0.0.1:${PORT}`; const logFd = fs.openSync(path.join(os.tmpdir(), 'st-doqr.log'), 'w'); proc = spawn('node', ['server.js'], { cwd: path.join(__dirname, '..'), env: { ...process.env, DATA_DIR, SELF_HOSTED: 'true', PORT: String(PORT), NODE_ENV: 'test' }, stdio: ['ignore', logFd, logFd] }); let up = false; for (let i = 0; i < 80; i++) { try { if ((await fetch(BASE + '/api/status')).ok) { up = true; break; } } catch {} await sleep(250); } diff --git a/server/test/device-pairing-notify.test.js b/server/test/device-pairing-notify.test.js index ea1ca90..23d5650 100644 --- a/server/test/device-pairing-notify.test.js +++ b/server/test/device-pairing-notify.test.js @@ -16,14 +16,16 @@ const fs = require('node:fs'); const crypto = require('node:crypto'); const ioClient = require('socket.io-client'); -const PORT = 3989; -const BASE = `http://127.0.0.1:${PORT}`; +const { freePort } = require('./helpers/free-port'); +let PORT, BASE; const DATA_DIR = path.join(os.tmpdir(), 'st-pair-' + crypto.randomBytes(4).toString('hex')); const LOG = path.join(os.tmpdir(), 'st-pair-' + crypto.randomBytes(4).toString('hex') + '.log'); let proc, JWT; const sleep = (ms) => new Promise(r => setTimeout(r, ms)); before(async () => { + PORT = await freePort(); + BASE = `http://127.0.0.1:${PORT}`; const logFd = fs.openSync(LOG, 'w'); proc = spawn('node', ['server.js'], { cwd: path.join(__dirname, '..'), env: { ...process.env, DATA_DIR, SELF_HOSTED: 'true', PORT: String(PORT), NODE_ENV: 'test' }, stdio: ['ignore', logFd, logFd] }); let up = false; diff --git a/server/test/device-zone-contract.test.js b/server/test/device-zone-contract.test.js index 6fd6a2c..392d6e0 100644 --- a/server/test/device-zone-contract.test.js +++ b/server/test/device-zone-contract.test.js @@ -25,8 +25,8 @@ const fs = require('node:fs'); const crypto = require('node:crypto'); const Database = require('better-sqlite3'); -const PORT = 3996; -const BASE = `http://127.0.0.1:${PORT}`; +const { freePort } = require('./helpers/free-port'); +let PORT, BASE; const DATA_DIR = path.join(os.tmpdir(), 'st-zone-test-' + crypto.randomBytes(4).toString('hex')); const LOG = path.join(os.tmpdir(), 'st-zone-' + crypto.randomBytes(4).toString('hex') + '.log'); const PW = 'Passw0rd123'; @@ -55,6 +55,8 @@ async function getOrphanCount() { } before(async () => { + PORT = await freePort(); + BASE = `http://127.0.0.1:${PORT}`; const logFd = fs.openSync(LOG, 'w'); proc = spawn('node', ['server.js'], { cwd: path.join(__dirname, '..'), diff --git a/server/test/fingerprint-reclaim.test.js b/server/test/fingerprint-reclaim.test.js index 6b22819..835ea32 100644 --- a/server/test/fingerprint-reclaim.test.js +++ b/server/test/fingerprint-reclaim.test.js @@ -16,8 +16,8 @@ const crypto = require('node:crypto'); const ioClient = require('socket.io-client'); const Database = require('better-sqlite3'); -const PORT = 3988; -const BASE = `http://127.0.0.1:${PORT}`; +const { freePort } = require('./helpers/free-port'); +let PORT, BASE; const DATA_DIR = path.join(os.tmpdir(), 'st-recl-' + crypto.randomBytes(4).toString('hex')); const LOG = path.join(os.tmpdir(), 'st-recl-' + crypto.randomBytes(4).toString('hex') + '.log'); const DB_PATH = path.join(DATA_DIR, 'db', 'remote_display.db'); @@ -25,6 +25,8 @@ let proc, tdb; const sleep = (ms) => new Promise(r => setTimeout(r, ms)); before(async () => { + PORT = await freePort(); + BASE = `http://127.0.0.1:${PORT}`; const logFd = fs.openSync(LOG, 'w'); proc = spawn('node', ['server.js'], { cwd: path.join(__dirname, '..'), diff --git a/server/test/group-sync-clock.test.js b/server/test/group-sync-clock.test.js index 854239f..3b9a628 100644 --- a/server/test/group-sync-clock.test.js +++ b/server/test/group-sync-clock.test.js @@ -10,11 +10,14 @@ const { spawn } = require('node:child_process'); const ioClient = require('../node_modules/socket.io-client'); const sleep = ms => new Promise(r => setTimeout(r, ms)); -const PORT = 3976; const BASE = `http://127.0.0.1:${PORT}`; +const { freePort } = require('./helpers/free-port'); +let PORT, BASE; const DATA_DIR = path.join(os.tmpdir(), 'st-gsync-' + crypto.randomBytes(4).toString('hex')); let proc, JWT; before(async () => { + PORT = await freePort(); + BASE = `http://127.0.0.1:${PORT}`; const logFd = fs.openSync(path.join(os.tmpdir(), 'st-gsync.log'), 'w'); proc = spawn('node', ['server.js'], { cwd: path.join(__dirname, '..'), env: { ...process.env, DATA_DIR, SELF_HOSTED: 'true', PORT: String(PORT), NODE_ENV: 'test' }, stdio: ['ignore', logFd, logFd] }); let up = false; for (let i = 0; i < 80; i++) { try { if ((await fetch(BASE + '/api/status')).ok) { up = true; break; } } catch {} await sleep(250); } diff --git a/server/test/helpers/free-port.js b/server/test/helpers/free-port.js new file mode 100644 index 0000000..427a7ed --- /dev/null +++ b/server/test/helpers/free-port.js @@ -0,0 +1,21 @@ +'use strict'; +const net = require('net'); + +// Allocate a free TCP port from the OS (bind :0 on loopback, read it back, release it). +// Subprocess test suites call this in before() instead of hand-picking a fixed/random port — +// the old 39xx scheme collided under CI load (a random port in the shared range, or a new +// suite reusing one), surfacing as flaky "no such table: devices" / FK errors when two servers +// raced on the same port. An OS-assigned ephemeral port per suite can't collide. +function freePort() { + return new Promise((resolve, reject) => { + const srv = net.createServer(); + srv.unref(); + srv.on('error', reject); + srv.listen(0, '127.0.0.1', () => { + const port = srv.address().port; + srv.close(() => resolve(port)); + }); + }); +} + +module.exports = { freePort }; diff --git a/server/test/loop-lag-integration.test.js b/server/test/loop-lag-integration.test.js index b8f8e87..aebbc97 100644 --- a/server/test/loop-lag-integration.test.js +++ b/server/test/loop-lag-integration.test.js @@ -13,13 +13,15 @@ const fs = require('node:fs'); const crypto = require('node:crypto'); const Database = require('better-sqlite3'); -const PORT = 3982; -const BASE = `http://127.0.0.1:${PORT}`; +const { freePort } = require('./helpers/free-port'); +let PORT, BASE; const DATA_DIR = path.join(os.tmpdir(), 'st-lag-int-' + crypto.randomBytes(4).toString('hex')); const LOG = path.join(os.tmpdir(), 'st-lag-int-' + crypto.randomBytes(4).toString('hex') + '.log'); let proc; before(async () => { + PORT = await freePort(); + BASE = `http://127.0.0.1:${PORT}`; const logFd = fs.openSync(LOG, 'w'); proc = spawn('node', ['server.js'], { cwd: path.join(__dirname, '..'), diff --git a/server/test/mute.test.js b/server/test/mute.test.js index 5def117..e387619 100644 --- a/server/test/mute.test.js +++ b/server/test/mute.test.js @@ -15,8 +15,8 @@ const fs = require('node:fs'); const crypto = require('node:crypto'); const Database = require('better-sqlite3'); -const PORT = 3994; -const BASE = `http://127.0.0.1:${PORT}`; +const { freePort } = require('./helpers/free-port'); +let PORT, BASE; const DATA_DIR = path.join(os.tmpdir(), 'st-mute-test-' + crypto.randomBytes(4).toString('hex')); const LOG = path.join(os.tmpdir(), 'st-mute-' + crypto.randomBytes(4).toString('hex') + '.log'); const PW = 'Passw0rd123'; @@ -33,6 +33,8 @@ const post = (tok, obj) => ({ method: 'POST', ...auth(tok), body: JSON.stringify const put = (tok, obj) => ({ method: 'PUT', ...auth(tok), body: JSON.stringify(obj || {}) }); before(async () => { + PORT = await freePort(); + BASE = `http://127.0.0.1:${PORT}`; const logFd = fs.openSync(LOG, 'w'); proc = spawn('node', ['server.js'], { cwd: path.join(__dirname, '..'), diff --git a/server/test/ota-check.test.js b/server/test/ota-check.test.js index 1c62472..f9ecf75 100644 --- a/server/test/ota-check.test.js +++ b/server/test/ota-check.test.js @@ -12,8 +12,8 @@ const os = require('node:os'); const fs = require('node:fs'); const crypto = require('node:crypto'); -const PORT = 3991; -const BASE = `http://127.0.0.1:${PORT}`; +const { freePort } = require('./helpers/free-port'); +let PORT, BASE; const DATA_DIR = path.join(os.tmpdir(), 'st-ota-' + crypto.randomBytes(4).toString('hex')); const LOG = path.join(os.tmpdir(), 'st-ota-' + crypto.randomBytes(4).toString('hex') + '.log'); let proc, LATEST; @@ -25,6 +25,8 @@ const check = async (version, deviceId) => { }; before(async () => { + PORT = await freePort(); + BASE = `http://127.0.0.1:${PORT}`; // the breaker only reports update_available when an APK actually exists — give the // test server a dummy one (resolveApkPath checks DATA_DIR/ScreenTinker.apk). fs.mkdirSync(DATA_DIR, { recursive: true }); diff --git a/server/test/reconnect-storm-load.test.js b/server/test/reconnect-storm-load.test.js index 45fbb26..8065788 100644 --- a/server/test/reconnect-storm-load.test.js +++ b/server/test/reconnect-storm-load.test.js @@ -28,8 +28,9 @@ const crypto = require('node:crypto'); const ioClient = require('socket.io-client'); const Database = require('better-sqlite3'); -const PORT = 3997; // must be unique across the suite (files run concurrently under `node --test`) -const BASE = `http://127.0.0.1:${PORT}`; +const { freePort } = require('./helpers/free-port'); +let PORT; // must be unique across the suite (files run concurrently under `node --test`) +let BASE; const DATA_DIR = path.join(os.tmpdir(), 'st-storm-' + crypto.randomBytes(4).toString('hex')); const LOG = path.join(os.tmpdir(), 'st-storm-' + crypto.randomBytes(4).toString('hex') + '.log'); let proc, rdb; @@ -37,6 +38,8 @@ let proc, rdb; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); before(async () => { + PORT = await freePort(); + BASE = `http://127.0.0.1:${PORT}`; const logFd = fs.openSync(LOG, 'w'); proc = spawn('node', ['server.js'], { cwd: path.join(__dirname, '..'), diff --git a/server/test/reconnect-throttle-integration.test.js b/server/test/reconnect-throttle-integration.test.js index 59933d8..6bddcc1 100644 --- a/server/test/reconnect-throttle-integration.test.js +++ b/server/test/reconnect-throttle-integration.test.js @@ -16,13 +16,15 @@ const fs = require('node:fs'); const crypto = require('node:crypto'); const ioClient = require('socket.io-client'); -const PORT = 3983; -const BASE = `http://127.0.0.1:${PORT}`; +const { freePort } = require('./helpers/free-port'); +let PORT, BASE; const DATA_DIR = path.join(os.tmpdir(), 'st-thr-int-' + crypto.randomBytes(4).toString('hex')); const LOG = path.join(os.tmpdir(), 'st-thr-int-' + crypto.randomBytes(4).toString('hex') + '.log'); let proc; before(async () => { + PORT = await freePort(); + BASE = `http://127.0.0.1:${PORT}`; const logFd = fs.openSync(LOG, 'w'); proc = spawn('node', ['server.js'], { cwd: path.join(__dirname, '..'), diff --git a/server/test/status-debug-toggle.test.js b/server/test/status-debug-toggle.test.js index e541ccf..eeb4909 100644 --- a/server/test/status-debug-toggle.test.js +++ b/server/test/status-debug-toggle.test.js @@ -13,12 +13,14 @@ const crypto = require('node:crypto'); const Database = require('better-sqlite3'); const ioClient = require('socket.io-client'); -const PORT = 3998; -const BASE = `http://127.0.0.1:${PORT}`; +const { freePort } = require('./helpers/free-port'); +let PORT, BASE; const DATA_DIR = path.join(os.tmpdir(), 'st-statusdbg-' + crypto.randomBytes(4).toString('hex')); let proc, db; before(async () => { + PORT = await freePort(); + BASE = `http://127.0.0.1:${PORT}`; const logFd = fs.openSync(path.join(os.tmpdir(), 'st-statusdbg.log'), 'w'); proc = spawn('node', ['server.js'], { cwd: path.join(__dirname, '..'), diff --git a/server/test/thumbnail-proxy.test.js b/server/test/thumbnail-proxy.test.js index 965042f..db3dc13 100644 --- a/server/test/thumbnail-proxy.test.js +++ b/server/test/thumbnail-proxy.test.js @@ -18,8 +18,8 @@ const fs = require('node:fs'); const crypto = require('node:crypto'); const Database = require('better-sqlite3'); -const PORT = 3990; -const BASE = `http://127.0.0.1:${PORT}`; +const { freePort } = require('./helpers/free-port'); +let PORT, BASE; const DATA_DIR = path.join(os.tmpdir(), 'st-thumb-test-' + crypto.randomBytes(4).toString('hex')); const CONTENT_DIR = path.join(DATA_DIR, 'uploads', 'content'); const LOG = path.join(os.tmpdir(), 'st-thumb-' + crypto.randomBytes(4).toString('hex') + '.log'); @@ -47,6 +47,8 @@ function makeContent(thumbnailPath, { mime = 'image/png' } = {}) { } before(async () => { + PORT = await freePort(); + BASE = `http://127.0.0.1:${PORT}`; // Mock upstream standing in for img.youtube.com. /missing/* -> 404 to exercise the // clean-failure path; everything else -> a 200 image/png. upstream = http.createServer((req, res) => { diff --git a/server/test/totp-keyrotation.test.js b/server/test/totp-keyrotation.test.js index 5d63e8a..be6a976 100644 --- a/server/test/totp-keyrotation.test.js +++ b/server/test/totp-keyrotation.test.js @@ -16,8 +16,8 @@ const fs = require('node:fs'); const crypto = require('node:crypto'); const { authenticator } = require('otplib'); -const PORT = 3980; -const BASE = `http://127.0.0.1:${PORT}`; +const { freePort } = require('./helpers/free-port'); +let PORT, BASE; const DATA_DIR = path.join(os.tmpdir(), 'st-totp-rot-' + crypto.randomBytes(4).toString('hex')); function bootServer(jwtSecret) { @@ -44,6 +44,8 @@ const post = (o) => ({ method: 'POST', headers: { 'Content-Type': 'application/j const postAuth = (tok, o) => ({ method: 'POST', headers: { Authorization: 'Bearer ' + tok, 'Content-Type': 'application/json' }, body: JSON.stringify(o || {}) }); test('#100 key rotation does NOT brick TOTP: recovery survives; TOTP fails cleanly (no 500)', async () => { + PORT = await freePort(); + BASE = `http://127.0.0.1:${PORT}`; let proc = bootServer('keyA-' + crypto.randomBytes(8).toString('hex')); try { await waitUp(); diff --git a/server/test/totp.test.js b/server/test/totp.test.js index 08151bf..c01d3e7 100644 --- a/server/test/totp.test.js +++ b/server/test/totp.test.js @@ -13,8 +13,8 @@ const fs = require('node:fs'); const crypto = require('node:crypto'); const { authenticator } = require('otplib'); -const PORT = 3979; -const BASE = `http://127.0.0.1:${PORT}`; +const { freePort } = require('./helpers/free-port'); +let PORT, BASE; const DATA_DIR = path.join(os.tmpdir(), 'st-totp-test-' + crypto.randomBytes(4).toString('hex')); const LOG = path.join(os.tmpdir(), 'st-totp-' + crypto.randomBytes(4).toString('hex') + '.log'); let proc; @@ -28,6 +28,8 @@ const auth = (tok, extra = {}) => ({ headers: { Authorization: 'Bearer ' + tok, const post = (tok, obj, extra) => ({ method: 'POST', ...auth(tok, extra), body: JSON.stringify(obj || {}) }); before(async () => { + PORT = await freePort(); + BASE = `http://127.0.0.1:${PORT}`; const logFd = fs.openSync(LOG, 'w'); proc = spawn('node', ['server.js'], { cwd: path.join(__dirname, '..'), diff --git a/server/test/v4-core-liveness.test.js b/server/test/v4-core-liveness.test.js index 9b95b8f..01b1e11 100644 --- a/server/test/v4-core-liveness.test.js +++ b/server/test/v4-core-liveness.test.js @@ -78,14 +78,16 @@ test('cross-client conformance FINDING: APK socket.io TRANSPORT backoff diverges }); // ============================ E2E: MIXED FLEET against the real server ============================ -const PORT = 3968; -const BASE = `http://127.0.0.1:${PORT}`; +const { freePort } = require('./helpers/free-port'); +let PORT, BASE; const DATA_DIR = path.join(os.tmpdir(), 'st-v4core-' + crypto.randomBytes(4).toString('hex')); const LOG = path.join(os.tmpdir(), 'st-v4core.log'); let proc, JWT; const sleep = ms => new Promise(r => setTimeout(r, ms)); before(async () => { + PORT = await freePort(); + BASE = `http://127.0.0.1:${PORT}`; const logFd = fs.openSync(LOG, 'w'); proc = spawn('node', ['server.js'], { cwd: path.join(__dirname, '..'), env: { ...process.env, DATA_DIR, SELF_HOSTED: 'true', PORT: String(PORT), NODE_ENV: 'test' }, stdio: ['ignore', logFd, logFd] }); let up = false; diff --git a/server/test/v4-core-refresh-gate.test.js b/server/test/v4-core-refresh-gate.test.js index 81b9b78..8724d99 100644 --- a/server/test/v4-core-refresh-gate.test.js +++ b/server/test/v4-core-refresh-gate.test.js @@ -48,12 +48,15 @@ test('A2 window: reconnects older than 60s drop out (a past flap does not stay D }); // ================= E2E: the shared !isPlaylistRefresh gate + change-detection ================= -const PORT = 3972; const BASE = `http://127.0.0.1:${PORT}`; +const { freePort } = require('./helpers/free-port'); +let PORT, BASE; const DATA_DIR = path.join(os.tmpdir(), 'st-rg-e2e-' + crypto.randomBytes(4).toString('hex')); const LOG = path.join(os.tmpdir(), 'st-rg-e2e.log'); let proc, JWT; before(async () => { + PORT = await freePort(); + BASE = `http://127.0.0.1:${PORT}`; const logFd = fs.openSync(LOG, 'w'); proc = spawn('node', ['server.js'], { cwd: path.join(__dirname, '..'), env: { ...process.env, DATA_DIR, SELF_HOSTED: 'true', PORT: String(PORT), NODE_ENV: 'test' }, stdio: ['ignore', logFd, logFd] }); let up = false; for (let i = 0; i < 80; i++) { try { if ((await fetch(BASE + '/api/status')).ok) { up = true; break; } } catch { /* */ } await sleep(250); } diff --git a/server/test/v4-exit-signal-phase3.test.js b/server/test/v4-exit-signal-phase3.test.js index 953e03a..4dc71ec 100644 --- a/server/test/v4-exit-signal-phase3.test.js +++ b/server/test/v4-exit-signal-phase3.test.js @@ -83,10 +83,13 @@ test('B/wgt CLEAN-CLOSE: pagehide(false) -> clean_exit; BACKGROUNDING pagehide(t }); // ============ PART A — server-side socket / #148 / reconnect-vs-exit safety ============ -const PORT = 3975; const BASE = `http://127.0.0.1:${PORT}`; +const { freePort } = require('./helpers/free-port'); +let PORT, BASE; const DATA_DIR = path.join(os.tmpdir(), 'st-exit3-' + crypto.randomBytes(4).toString('hex')); let proc, JWT; before(async () => { + PORT = await freePort(); + BASE = `http://127.0.0.1:${PORT}`; const logFd = fs.openSync(path.join(os.tmpdir(), 'st-exit3.log'), 'w'); proc = spawn('node', ['server.js'], { cwd: path.join(__dirname, '..'), env: { ...process.env, DATA_DIR, SELF_HOSTED: 'true', PORT: String(PORT), NODE_ENV: 'test' }, stdio: ['ignore', logFd, logFd] }); let up = false; for (let i = 0; i < 80; i++) { try { if ((await fetch(BASE + '/api/status')).ok) { up = true; break; } } catch {} await sleep(250); } diff --git a/server/test/v4-exit-signal.test.js b/server/test/v4-exit-signal.test.js index 7d4558f..e273dab 100644 --- a/server/test/v4-exit-signal.test.js +++ b/server/test/v4-exit-signal.test.js @@ -22,12 +22,15 @@ test('sanitizeExitReason: only crashed/clean_exit accepted; silent + unknown REJ }); // ===== E2E ===== -const PORT = 3974; const BASE = `http://127.0.0.1:${PORT}`; +const { freePort } = require('./helpers/free-port'); +let PORT, BASE; const DATA_DIR = path.join(os.tmpdir(), 'st-exit-' + crypto.randomBytes(4).toString('hex')); const LOG = path.join(os.tmpdir(), 'st-exit.log'); let proc, JWT; before(async () => { + PORT = await freePort(); + BASE = `http://127.0.0.1:${PORT}`; const logFd = fs.openSync(LOG, 'w'); proc = spawn('node', ['server.js'], { cwd: path.join(__dirname, '..'), env: { ...process.env, DATA_DIR, SELF_HOSTED: 'true', PORT: String(PORT), NODE_ENV: 'test' }, stdio: ['ignore', logFd, logFd] }); let up = false; for (let i = 0; i < 80; i++) { try { if ((await fetch(BASE + '/api/status')).ok) { up = true; break; } } catch { /* */ } await sleep(250); }