// #1873 proxy setup host helpers: restart proof from a fake /proc, the stale init-lock rules, // unit control over a fake systemctl, and receipts that carry no path or key. import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { createHash } from 'node:crypto'; import { PIN } from '../src/common.mjs'; import { installPaths } from '../src/settings.mjs'; import { processTree, pidListensOn, processStartMs, verifyRestart, initLockStatus, clearInitLock, initFiles, initState, fenceState, createUnitControl, releaseState, receiptFile, unitName, } from '../src/admin.mjs'; const code = name => error => error.code === name; const scratch = t => { const dir = fs.mkdtempSync(join(tmpdir(), 'pa-')); t.after(() => fs.rmSync(dir, { recursive: true, force: true })); return dir; }; // A fake /proc: runner pid 100 (start tick 500), its child 101 listens on `port` via socket inode 5555. function fakeProc(t, { port = 8080, listener = 101, start = 500 } = {}) { const proc = join(scratch(t), 'proc'); const stat = (pid, ppid, ticks) => `${pid} (node) S ${ppid} 1 1 0 -1 0 0 0 0 0 0 0 0 0 20 0 1 0 ${ticks} 0 0\n`; fs.mkdirSync(join(proc, 'net'), { recursive: true }); fs.writeFileSync(join(proc, 'stat'), 'cpu 0\nbtime 1000\n'); for (const [pid, ppid, ticks] of [[1, 0, 1], [100, 1, start], [101, 100, start + 1], [200, 1, 1]]) { fs.mkdirSync(join(proc, String(pid), 'fd'), { recursive: true }); fs.writeFileSync(join(proc, String(pid), 'stat'), stat(pid, ppid, ticks)); } fs.symlinkSync('socket:[5555]', join(proc, String(listener), 'fd', '3')); fs.symlinkSync('/dev/null', join(proc, '100', 'fd', '0')); const hex = port.toString(16).toUpperCase().padStart(4, '0'); fs.writeFileSync(join(proc, 'net', 'tcp'), ' sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode\n' + ` 0: 0100007F:${hex} 00000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 5555 1 0 100 0 0 10 0\n` + ` 1: 0100007F:${hex} 0100007F:9999 01 00000000:00000000 00:00000000 00000000 1000 0 7777 1 0 100 0 0 10 0\n`); return proc; } test('processTree follows parent links; pidListensOn finds a listener in a child of the unit main pid', t => { const proc = fakeProc(t); assert.deepEqual(processTree(100, { proc }).sort(), [100, 101]); assert.equal(pidListensOn(100, 8080, { proc }), true); assert.equal(pidListensOn(200, 8080, { proc }), false, 'an unrelated process does not count'); assert.equal(pidListensOn(100, 8081, { proc }), false, 'another port does not count'); assert.equal(processStartMs(100, { proc }), 1000 * 1000 + 5000); assert.equal(processStartMs(999, { proc }), null); }); const evidence = (over = {}) => ({ unit: 'mosaic-proxy.service', confirmed: true, before: { pid: 50, since: 10, active: 'active' }, after: { pid: 100, since: 20, active: 'active', listening: true }, observedAt: new Date(2000).toISOString(), ...over, }); const proof = { fence: { time: new Date(1000).toISOString() }, port: 8080, startMs: () => 1500, listens: (pid, port) => pid === 100 && port === 8080, now: () => 2000 }; test('verifyRestart accepts a confirmed restart to a new live listener started after the fence', () => { const result = verifyRestart(evidence(), proof); assert.deepEqual(result, { unit: 'mosaic-proxy.service', before: { pid: 50 }, after: { pid: 100, startedAt: new Date(1500).toISOString() }, observedAt: new Date(2000).toISOString(), listeningPort: 8080 }); }); test('verifyRestart refuses every unproved restart with proxy-restart-unconfirmed', () => { const cases = [ ['not confirmed', evidence({ confirmed: false }), proof], ['same pid', evidence({ before: { pid: 100, since: 10, active: 'active' } }), proof], ['extra field', { ...evidence(), forged: true }, proof], ['bad unit name', evidence({ unit: '../x.service' }), proof], ['stale observation', evidence({ observedAt: new Date(2000).toISOString() }), { ...proof, now: () => 2000 + 11 * 60 * 1000 }], ['process gone', evidence(), { ...proof, startMs: () => null }], ['started before the fence', evidence(), { ...proof, startMs: () => 900 }], ['fence without time', evidence(), { ...proof, fence: { invalid: true } }], ['not listening', evidence(), { ...proof, listens: () => false }], ['no port', evidence(), { ...proof, port: null }], ['not an object', 'restarted', proof], ]; for (const [name, value, options] of cases) assert.throws(() => verifyRestart(value, options), code('proxy-restart-unconfirmed'), name); }); function dirs(t) { const root = scratch(t); const configDir = join(root, 'config'), dataRoot = join(root, 'data'); fs.mkdirSync(configDir, { recursive: true, mode: 0o700 }); fs.mkdirSync(dataRoot, { recursive: true, mode: 0o700 }); return { root, configDir, dataRoot, files: initFiles(configDir, dataRoot) }; } test('init lock: absent refuses, held names each README blocker, clear needs every condition and writes a receipt', t => { const { root, configDir, dataRoot, files } = dirs(t); const stopped = { stopped: true, running: [] }; assert.deepEqual(initLockStatus({ configDir, dataRoot, buses: stopped }), { present: false, clearable: false, blockers: [] }); assert.throws(() => clearInitLock({ configDir, dataRoot, buses: stopped }), code('proxy-init-lock-absent')); fs.writeFileSync(files.lock, '', { mode: 0o600 }); fs.mkdirSync(files.proxyDir, { mode: 0o700 }); const held = initLockStatus({ configDir, dataRoot, buses: { stopped: false, running: ['organization acme'] } }); assert.equal(held.clearable, false); assert.deepEqual(held.blockers, ['bus running: organization acme', 'the proxy state directory exists']); assert.equal(JSON.stringify(held).includes(root), false, 'blockers name labels, never paths'); assert.throws(() => clearInitLock({ configDir, dataRoot, buses: { stopped: false, running: ['the instance bus'] } }), code('proxy-init-lock-held')); assert.throws(() => clearInitLock({ configDir, dataRoot, buses: stopped }), code('proxy-init-lock-held')); assert.equal(fs.existsSync(files.lock), true); fs.rmdirSync(files.proxyDir); assert.throws(() => clearInitLock({ configDir, dataRoot, buses: undefined }), code('proxy-init-lock-held'), 'no bus proof counts as running'); const receipt = clearInitLock({ configDir, dataRoot, buses: stopped, now: () => new Date(0) }); assert.equal(fs.existsSync(files.lock), false); assert.deepEqual(receipt, { op: 'init-lock.clear', time: new Date(0).toISOString(), conditions: ['bus stopped', 'no proxy.json', 'no management key file', 'no proxy state directory'] }); assert.deepEqual(fs.readFileSync(receiptFile(dataRoot), 'utf8').trim().split('\n').map(JSON.parse), [receipt]); assert.equal((fs.statSync(receiptFile(dataRoot)).mode & 0o777), 0o600); }); test('initState reports partial state from file presence and fenceState reads the fence without writing', t => { const { configDir, dataRoot, files } = dirs(t); assert.deepEqual(initState(configDir, dataRoot), { initialized: false, partial: false, problem: null, settings: null, files: { settings: false, key: false, proxyDir: false, lock: false }, lock: false }); fs.mkdirSync(files.proxyDir, { mode: 0o700 }); assert.equal(initState(configDir, dataRoot).partial, true); assert.equal(fenceState(dataRoot), null); fs.writeFileSync(join(files.proxyDir, 'mosaic-keys.uncertain'), JSON.stringify({ org: 'acme', op: 'apikey.add', time: '2026-11-11T00:00:00.000Z', pid: 7 }), { mode: 0o600 }); assert.deepEqual(fenceState(dataRoot), { org: 'acme', op: 'apikey.add', time: '2026-11-11T00:00:00.000Z', pid: 7 }); fs.chmodSync(join(files.proxyDir, 'mosaic-keys.uncertain'), 0o644); assert.deepEqual(fenceState(dataRoot), { invalid: true }); assert.deepEqual(fs.readdirSync(files.proxyDir), ['mosaic-keys.uncertain']); }); test('releaseState: nothing fetched names the next step without a path', t => { const { root, dataRoot } = dirs(t); const state = releaseState(dataRoot); assert.equal(state.fetched, false); assert.equal(state.verified, false); assert.equal(state.problem, 'proxy-install-missing'); assert.equal(JSON.stringify(state).includes(root), false); }); test('releaseState re-hashes a release file rewritten in place with its mtime set back', async t => { const { dataRoot } = dirs(t); const sha = bytes => createHash('sha256').update(bytes).digest('hex'); const binary = Buffer.from('made-up proxy binary 0001'), panel = Buffer.from('made-up panel'); const pin = { ...PIN, version: '0.0.0-test', binary: { ...PIN.binary, sha256: sha(binary) }, panel: { ...PIN.panel, sha256: sha(panel) } }; const paths = installPaths(dataRoot, pin); fs.mkdirSync(join(paths.dir, 'static'), { recursive: true }); fs.writeFileSync(paths.binary, binary, { mode: 0o755 }); fs.writeFileSync(paths.panel, panel); const when = 1700000000; for (const file of [paths.binary, paths.panel]) fs.utimesSync(file, when, when); assert.equal(releaseState(dataRoot, pin).verified, true); await new Promise(resolve => setTimeout(resolve, 50)); const before = fs.statSync(paths.binary, { bigint: true }); fs.writeFileSync(paths.binary, Buffer.from('made-up proxy binary 0002'), { flag: 'r+' }); fs.utimesSync(paths.binary, when, when); const after = fs.statSync(paths.binary, { bigint: true }); assert.deepEqual([after.ino, after.size, after.mtimeNs, after.mode], [before.ino, before.size, before.mtimeNs, before.mode], 'only ctime tells the files apart'); const state = releaseState(dataRoot, pin); assert.equal(state.verified, false); assert.equal(state.problem, 'proxy-install-invalid'); }); test('unit names follow the instance and refuse a bad instance', () => { assert.equal(unitName(null), 'mosaic-proxy.service'); assert.equal(unitName('dev'), 'mosaic-proxy.service'); assert.equal(unitName('uiorg'), 'mosaic-proxy-uiorg.service'); assert.throws(() => unitName('../x'), code('proxy-unit-usage')); }); // A fake systemctl / proxy-service.sh. `states` is the sequence `systemctl show` reports. function fakeUnit(states, { install = { code: 0, stdout: '', stderr: '' }, verbs = [] } = {}) { let i = 0; const run = async (command, args) => { if (command === 'systemctl' && args[1] === 'show') { const s = states[Math.min(i++, states.length - 1)]; return { code: 0, stdout: `LoadState=${s.load ?? 'loaded'}\nActiveState=${s.active}\nSubState=running\nMainPID=${s.pid ?? 0}\nExecMainStartTimestampMonotonic=${s.since ?? 0}\nNRestarts=0\n`, stderr: '' }; } if (command === 'systemctl') { verbs.push(args[1]); return { code: 0, stdout: '', stderr: '' }; } verbs.push(args.filter(a => !a.endsWith('proxy-service.sh')).join(' ')); return install; }; return createUnitControl({ instance: 'zz', port: () => 18432, run, probe: async () => true, waitMs: 0, pollMs: 0, now: () => new Date(0) }); } test('unit restart is confirmed only when a new main process is active and listening', async () => { const verbs = []; const control = fakeUnit([{ active: 'active', pid: 10, since: 5 }, { active: 'active', pid: 11, since: 6 }], { verbs }); const { evidence: e } = await control.restart(); assert.equal(e.confirmed, true); assert.deepEqual(e.before, { pid: 10, since: 5, active: 'active' }); assert.deepEqual(e.after, { pid: 11, since: 6, active: 'active', listening: true }); assert.equal(e.unit, 'mosaic-proxy-zz.service'); assert.deepEqual(verbs, ['restart']); const same = await fakeUnit([{ active: 'active', pid: 10, since: 5 }, { active: 'active', pid: 10, since: 5 }]).restart(); assert.equal(same.evidence.confirmed, false, 'the same process is not a restart'); const down = await fakeUnit([{ active: 'active', pid: 10, since: 5 }, { active: 'failed', pid: 0, since: 0 }]).restart(); assert.equal(down.evidence.confirmed, false, 'a failed unit is not a restart'); }); test('unit actions map proxy-service refusals by code and refuse a missing unit', async () => { await assert.rejects(fakeUnit([{ load: 'not-found', active: 'inactive' }]).start(), code('proxy-unit-missing')); await assert.rejects(fakeUnit([{ load: 'not-found', active: 'inactive' }]).restart(), code('proxy-unit-missing')); await assert.rejects(fakeUnit([{ active: 'inactive' }], { install: { code: 3, stdout: '', stderr: 'proxy-service: proxy-unit-exists\n' } }).install(), code('proxy-unit-exists')); await assert.rejects(fakeUnit([{ active: 'inactive' }], { install: { code: 3, stdout: '', stderr: 'proxy-service: /home/x secret detail\n' } }).install(), code('proxy-unit-failed')); await assert.rejects(fakeUnit([{ active: 'active', pid: 0 }, { active: 'failed', pid: 0 }]).start(), code('proxy-unit-unhealthy')); const verbs = []; const started = await fakeUnit([{ active: 'inactive' }, { active: 'active', pid: 9, since: 1 }], { verbs }).start(); assert.equal(started.healthy, true); assert.deepEqual(verbs, ['start']); assert.throws(() => createUnitControl({ unitDir: 'relative' }), code('proxy-unit-path')); });