const { test } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const vm = require('node:vm'); const { pathToFileURL } = require('node:url'); const { JSDOM, VirtualConsole } = require('jsdom'); const { Console } = require('node:console'); // Page console goes to stderr: node:test parses the child's stdout. const pageConsole = () => new VirtualConsole().forwardTo(new Console({ stdout: process.stderr, stderr: process.stderr })); const express = require('express'); const jwt = require('jsonwebtoken'); const sessions = require('../src/utils/sessions'); const platform = require('../src/utils/platform'); const root = path.join(__dirname, '..'); const secret = 'isolated-regression-secret'; // Execute real modules with explicit boundary fakes; no database or network access. function load(file, mocks) { const module = { exports: {} }; vm.runInNewContext(fs.readFileSync(path.join(root, file), 'utf8'), { module, exports: module.exports, console, process: { env: { JWT_SECRET: secret } }, require(name) { assert.ok(Object.hasOwn(mocks, name), 'Unexpected import: ' + name); return mocks[name]; } }, { filename: file }); return module.exports; } function response() { return { code: 200, body: null, cookies: [], cleared: [], status(code) { this.code = code; return this; }, json(body) { this.body = body; return this; }, cookie(...args) { this.cookies.push(args); }, clearCookie(...args) { this.cleared.push(args); } }; } function endpoint(router, method, route) { return router.stack.find(layer => layer.route && layer.route.path === route && layer.route.methods[method]) .route.stack.find(layer => layer.method === method).handle; } const authStub = { authMiddleware() {}, moderatorMiddleware() {} }; test('auth rejects missing last session and session-store errors, preserving valid web/mobile activity', async () => { const token = jwt.sign({ userId: 7 }, secret, { expiresIn: '1h' }); for (const scenario of [ { mode: 'missing', code: 401 }, { mode: 'missing', mobile: true, code: 401 }, { mode: 'error', code: 503 }, { mode: 'valid', code: 200 }, { mode: 'valid', method: 'POST', minutes: 11, refresh: true, code: 200 }, { mode: 'valid', minutes: 11, code: 200 }, { mode: 'valid', minutes: 1500, code: 401 }, { mode: 'valid', minutes: 1500, mobile: true, method: 'POST', code: 200 } ]) { const writes = []; const db = { async get(sql, params) { if (sql.includes('FROM users')) return { id: 7, role: 'user', disabled: false }; if (sql.includes('COUNT')) return { count: 0 }; // Last session was revoked. assert.equal(params[0], sessions.hashToken(token)); if (scenario.mode === 'error') throw new Error('private database diagnostic'); return scenario.mode === 'missing' ? null : { id: 'session-7', last_activity: new Date(Date.now() - (scenario.minutes || 0) * 60000) }; }, async run(sql) { writes.push(sql); } }; const { authMiddleware } = load('src/middleware/auth.js', { jsonwebtoken: jwt, '../db/database': db, '../utils/sessions': sessions, '../utils/platform': platform }); const req = { headers: scenario.mobile ? { 'x-client': 'mobile', authorization: 'Bearer ' + token } : {}, cookies: scenario.mobile ? {} : { ped_auth: token }, method: scenario.method || 'GET' }; const res = response(); let next = 0; await authMiddleware(req, res, () => next++); assert.equal(res.code, scenario.code); assert.equal(next, scenario.code === 200 ? 1 : 0); assert.equal(res.cookies.length, scenario.refresh ? 1 : 0); if (scenario.code === 200) assert.equal(req.sessionId, 'session-7'); assert.equal(writes.some(sql => sql.startsWith('UPDATE')), scenario.method === 'POST' && scenario.code === 200 && scenario.minutes > 10); if (scenario.mobile) assert.ok(!writes.some(sql => sql.startsWith('DELETE'))); if (scenario.mode === 'error') assert.equal(res.body.error, 'Authentication temporarily unavailable'); } }); function encounterServer() { const rows = []; const db = { async getSetting() { return '7'; }, async get(sql, p) { if (sql.includes('idempotency_key')) return rows.find(row => row.key === p[1]); if (sql.includes('LOWER(label)')) return rows.find(row => row.label.toLowerCase() === p[1].toLowerCase()); if (sql.includes('SELECT id, version')) return rows.find(row => String(row.id) === String(p[0])); throw new Error('Unexpected query: ' + sql); }, async run(sql, p) { if (sql.startsWith('INSERT')) { const row = { id: rows.length + 1, label: p[1], transcript: p[3], note: p[4], key: p[7], version: 1 }; rows.push(row); return { lastInsertRowid: row.id }; } assert.ok(sql.startsWith('UPDATE')); const versioned = sql.includes('version=$6'); const row = rows.find(row => String(row.id) === String(p[versioned ? 6 : 5])); assert.ok(row); Object.assign(row, { label: p[0], transcript: p[1], note: p[2] }); if (versioned) row.version = p[5]; return { changes: 1 }; } }; const router = load('src/routes/encounters.js', { express, '../db/database': db, '../middleware/auth': authStub, '../utils/logger': { audit() {}, error() {} }, '../utils/crypto': { encryptString: value => value } }); const save = endpoint(router, 'post', '/encounters/saved'); return { rows, async post(body) { const res = response(); await save({ user: { id: 7 }, body }, res); return res; } }; } function edBrowser(server, storage = {}) { const dom = new JSDOM('
' + '
' + '', { url: 'https://isolated.invalid', runScripts: 'outside-only', virtualConsole: pageConsole() }); const w = dom.window; for (const [key, value] of Object.entries(storage.session || {})) w.sessionStorage.setItem(key, value); for (const [key, value] of Object.entries(storage.local || {})) w.localStorage.setItem(key, value); w.showToast = () => {}; w.showBusy = () => { w.busy = true; }; w.hideBusy = () => { w.busy = false; }; w.HTMLElement.prototype.scrollIntoView = () => {}; w.getAuthHeaders = () => ({}); const pending = []; w.fetch = (url, opts = {}) => { assert.ok(['/api/encounters/saved', '/api/ed-encounters/finalize'].includes(url)); if (opts.method !== 'POST') return Promise.resolve({ json: async () => ({ encounters: server.rows }) }); return new Promise((resolve, reject) => pending.push({ url, body: JSON.parse(opts.body), resolve, reject })); }; w.eval(fs.readFileSync(path.join(root, 'public/js/accountBoundary.js'), 'utf8')); w.AccountBoundary.enter({ id: 7 }, true); w.eval(fs.readFileSync(path.join(root, 'public/js/encounters.js'), 'utf8')); // ED is a module in production: isolate its lexical state, keeping real event handlers. w.eval('(function() {\n' + fs.readFileSync(path.join(root, 'public/js/ed-encounters.js'), 'utf8') + '\n})();'); w.document.dispatchEvent(new w.CustomEvent('tabChanged', { detail: { tab: 'ed' } })); return { w, pending, close: () => w.close(), save(label) { w.document.getElementById('ed-label').value = label; w.document.getElementById('btn-ed-save').click(); }, newPatient() { w.document.getElementById('btn-ed-new').click(); }, snapshot() { return { session: { ...w.sessionStorage }, local: { ...w.localStorage } }; }, async complete(index = 0, loseResponse = false) { const request = pending.splice(index, 1)[0]; assert.ok(request, 'Expected a save request'); const res = request.url === '/api/ed-encounters/finalize' ? { code: 200, body: { success: true, finalNote: 'Consolidated note', mdm: null } } : await server.post(request.body); if (loseResponse) request.reject(new Error('Simulated lost response')); else request.resolve({ status: res.code, json: async () => res.body }); await new Promise(resolve => setImmediate(resolve)); return res; } }; } const draftStorage = { local: { 'ped_ed_draft_v1:owner:7': JSON.stringify({ label: 'Patient A', state: { stage: 1, stages: [{ transcript: 'First transcript', note: 'First stage note' }], finalized: false } }) } }; test('two new ED drafts get distinct UUIDs, preserving the first; same draft retry/reload keeps identity', async () => { const server = encounterServer(); let browser = edBrowser(server, draftStorage); try { browser.save('Patient A'); const keyA = browser.pending[0].body.idempotency_key; assert.match(keyA, /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/); browser.save('Patient A'); assert.equal(browser.pending.length, 1, 'Double-click is suppressed'); await browser.complete(0, true); assert.equal(browser.w._savedEncId_ed, undefined); browser.save('Patient A'); assert.equal(browser.pending[0].body.idempotency_key, keyA); await browser.complete(0, true); const storage = browser.snapshot(); browser.close(); browser = edBrowser(server, storage); browser.save('Patient A'); assert.equal(browser.pending[0].body.idempotency_key, keyA); assert.equal((await browser.complete()).code, 200, 'Lost-response retry bypasses same-row label conflict'); assert.equal(server.rows.length, 1); assert.equal(browser.w._savedEncId_ed, 1); const savedStorage = browser.snapshot(); browser.close(); browser = edBrowser(server, savedStorage); browser.save('Patient A'); assert.equal(String(browser.pending[0].body.id), '1'); await browser.complete(); const first = { ...server.rows[0] }; assert.equal(first.transcript, 'First transcript'); assert.equal(first.note, 'First stage note'); browser.newPatient(); browser.save('Patient B'); assert.equal(browser.pending[0].body.id, null); assert.notEqual(browser.pending[0].body.idempotency_key, keyA); await browser.complete(); assert.equal(server.rows.length, 2); assert.deepEqual(server.rows[0], first); assert.equal(browser.w._savedEncId_ed, 2); } finally { browser.close(); } }); test('delayed old ED save cannot assign its ID or unlock a new patient save', async () => { const server = encounterServer(); const browser = edBrowser(server); try { browser.save('Old patient'); browser.newPatient(); browser.save('New patient'); assert.equal(browser.pending.length, 2, 'New identity can save while old request is pending'); await browser.complete(0); assert.equal(browser.w._savedEncId_ed, null); assert.equal(browser.w.sessionStorage.getItem('_savedEncId_ed:owner:7'), null); browser.save('New patient'); assert.equal(browser.pending.length, 1, 'Old callback must not release new save lock'); await browser.complete(); assert.equal(browser.w._savedEncId_ed, 2); assert.equal(browser.w.sessionStorage.getItem('_savedEncId_ed:owner:7'), '2'); } finally { browser.close(); } }); test('backend rejects ambiguous legacy ED key reuse without overwriting its first row', async () => { const server = encounterServer(); const body = { label: 'Legacy A', enc_type: 'ed', idempotency_key: 'ed-draft-new', generated_note: 'Original' }; assert.equal((await server.post(body)).code, 200); const first = { ...server.rows[0] }; assert.equal((await server.post({ ...body, label: 'Legacy B', generated_note: 'Other patient' })).code, 409); assert.equal((await server.post(body)).code, 409, 'Ambiguous legacy retries require loading by ID'); assert.deepEqual(server.rows, [first]); assert.equal((await server.post({ ...body, id: 1, generated_note: 'Explicit update' })).code, 200); assert.equal(server.rows[0].note, 'Explicit update'); }); test('ED final save reuses draft UUID after lost response; delayed finalize cannot save a reset patient', async () => { const server = encounterServer(); const browser = edBrowser(server, draftStorage); try { browser.save('Patient A'); const key = browser.pending[0].body.idempotency_key; await browser.complete(0, true); browser.w.document.getElementById('btn-ed-finalize').click(); await browser.complete(); assert.equal(browser.pending[0].body.idempotency_key, key); assert.equal(browser.pending[0].body.status, 'final'); await browser.complete(); assert.equal(server.rows.length, 1); assert.equal(server.rows[0].note, 'Consolidated note'); } finally { browser.close(); } const delayed = edBrowser(server, draftStorage); try { delayed.w.document.getElementById('btn-ed-finalize').click(); assert.equal(delayed.pending.length, 1); delayed.newPatient(); assert.equal(delayed.w.busy, false, 'Reset clears the old finalization busy indicator'); await delayed.complete(); assert.equal(delayed.pending.length, 0, 'Old finalize must not initiate a save with new identity'); assert.equal(delayed.w._savedEncId_ed, null); assert.equal(delayed.w.document.getElementById('ed-final-note-card'), null); } finally { delayed.close(); } });