leasePort({closed:true}) ignored its option: it probed an ephemeral port and
closed it, so a parallel test file could bind it and answer a request the
test expected to fail (preview.test.mjs:209 saw 200 instead of 502).
leaseClosedPort holds a bound, non-listening socket in a perl child until the
test's after hook. Connects still get ECONNREFUSED, bind() from another
process gets EADDRINUSE, and the kernel's ephemeral allocation skips the port.
Lane mode keeps its band and per-port lock, and releases the socket before
the lock. leasePort now refuses a closed option, so a stale call cannot get
the unheld port again.
Unanswerable callers (webui preview, serve, service) move to leaseClosedPort(t).
Callers that listen on the port themselves (stack-release rehearse, control-board
serve, tester-cli) keep leasePort and drop the ignored option.
824 lines
51 KiB
JavaScript
824 lines
51 KiB
JavaScript
import { leaseClosedPort } from '../../../scripts/testing/lane-fixtures.mjs';
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { spawn } from 'node:child_process';
|
|
import { createServer } from 'node:http';
|
|
import { once } from 'node:events';
|
|
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { dirname, join, relative } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
// scripts/webui-service.sh (row 75, #1569). Nothing here starts a unit or
|
|
// reaches the real systemd: install and uninstall run with --no-reload or a
|
|
// stand-in systemctl on PATH, and XDG_CONFIG_HOME points into a temp dir.
|
|
// Row 83 (#1576) adds --send, taken only with verified OIDC through --sso. Row 77
|
|
// (#1571) adds --copy, the running-unit refusals and the uid rule; every run
|
|
// gets a stand-in systemctl, since install now reads the unit's state, and
|
|
// install runs with --allow-worktree, since the suite runs in worktrees.
|
|
const REPO = fileURLToPath(new URL('../../..', import.meta.url)).replace(/\/$/, '');
|
|
const SCRIPT = join(REPO, 'scripts', 'webui-service.sh');
|
|
const NODE_DIR = dirname(process.execPath);
|
|
const UID = '6f1c0e2a9b8d7c4e5f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e';
|
|
const PREVIEW = ['--public-host', 'console.example.com', '--auth-header', 'X-authentik-uid', '--allow-user', UID];
|
|
const PREVIEW_FLAGS = `--public-host console.example.com --auth-header X-authentik-uid --allow-user ${UID}`;
|
|
const SSO_SEND = ['--sso', '--send'];
|
|
// A copy as `copy` leaves it, reduced to the file install looks for.
|
|
const COPY_ID = '0123456789abcdef0123456789abcdef01234567';
|
|
const INSTALL = ['--copy', COPY_ID, '--allow-worktree'];
|
|
const copyRoot = (dir) => join(dir, '.local', 'share', 'mosaic-stack', 'copies', COPY_ID);
|
|
function fakeCopy(dir) {
|
|
mkdirSync(join(copyRoot(dir), 'packages', 'webui', 'src'), { recursive: true });
|
|
writeFileSync(join(copyRoot(dir), 'packages', 'webui', 'src', 'cli.mjs'), '');
|
|
return copyRoot(dir);
|
|
}
|
|
const SEND_REFUSAL = /^webui-service: --send needs --sso; header preview is read-only$/m;
|
|
const TEMPLATE = readFileSync(join(REPO, 'packages', 'webui', 'systemd', 'mosaic-webui.service.in'), 'utf8');
|
|
|
|
// The unit as the base script renders it (row 75): the template with its four
|
|
// placeholders filled in, nothing else. Row 83 adds --send and must leave
|
|
// every unit rendered without it byte for byte as this. Row 190: the
|
|
// default instance deletes the @CONFIG_ENV@ line, so the unit is unchanged.
|
|
function filled(flags, root = REPO) {
|
|
const base = '/usr/local/bin:/usr/bin:/bin';
|
|
const path = `:${base}:`.includes(`:${NODE_DIR}:`) ? base : `${NODE_DIR}:${base}`;
|
|
return TEMPLATE.replace('@CONFIG_ENV@\n', '').replaceAll('@REPO@', root).replaceAll('@PATH@', path).replaceAll('@NODE@', process.execPath).replaceAll('@FLAGS@', flags);
|
|
}
|
|
|
|
// A loopback port with nothing listening, so status's /healthz request
|
|
// reaches no real Console.
|
|
async function closedPort(t) { return leaseClosedPort(t); }
|
|
|
|
function tmp(t) {
|
|
const dir = mkdtempSync(join(tmpdir(), 'webui-service-'));
|
|
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
fakeCopy(dir);
|
|
return dir;
|
|
}
|
|
|
|
// The bus unit as `systemctl show -p LoadState -p WorkingDirectory -p ExecStart` prints it,
|
|
// started from `root`.
|
|
const busShow = (root) => `LoadState=loaded\nWorkingDirectory=${root}\nExecStart={ path=${root}/scripts/mosaic ; argv[]=${root}/scripts/mosaic bus start acme ; ignore_errors=no ; start_time=[n/a] ; stop_time=[n/a] ; pid=0 ; code=(null) ; status=0/0 }\n`;
|
|
const BUS_UNITS = '[email protected] loaded active running Mosaic bus host (acme)\n';
|
|
|
|
// A stand-in systemctl that prints `lines` for the Console's list-units and
|
|
// logs every call. With `listFails`, that list-units exits 1 after printing
|
|
// to stderr. The bus calls install makes are answered apart: `busUnits` for
|
|
// the mosaic-bus@ list-units, and `bus` for show, by default a bus started
|
|
// from the run's own copy ($HOME, as run() sets it). `busFails` makes both
|
|
// bus calls fail. `busOther` answers show for [email protected]
|
|
// apart from `bus`, so a second loaded unit can differ from the first.
|
|
function standIn(dir, lines = '', { listFails = false, bus = null, busUnits = BUS_UNITS, busFails = false, busOther = null } = {}) {
|
|
const bin = join(dir, 'bin');
|
|
mkdirSync(bin);
|
|
writeFileSync(join(dir, 'list-units.txt'), lines);
|
|
writeFileSync(join(dir, 'bus-units.txt'), busUnits);
|
|
if (bus !== null) writeFileSync(join(dir, 'bus-show.txt'), bus);
|
|
if (busOther !== null) writeFileSync(join(dir, 'bus-show-other.txt'), busOther);
|
|
const fail = 'echo "Failed to connect to bus" >&2; exit 1';
|
|
const show = bus !== null ? `cat "${dir}/bus-show.txt"` : `r="$HOME/.local/share/mosaic-stack/copies/${COPY_ID}"; printf '%s' "${busShow('$r')}"`;
|
|
writeFileSync(join(bin, 'systemctl'), `#!/bin/sh
|
|
echo "$*" >> "${dir}/calls.txt"
|
|
case "$*" in
|
|
*list-units*mosaic-bus@*) ${busFails ? fail : `cat "${dir}/bus-units.txt"`} ;;
|
|
*show*[email protected]*) ${busFails ? fail : busOther !== null ? `cat "${dir}/bus-show-other.txt"` : show} ;;
|
|
*show*mosaic-bus@*) ${busFails ? fail : show} ;;
|
|
*list-units*) ${listFails ? fail : `cat "${dir}/list-units.txt"`} ;;
|
|
*is-enabled*) echo enabled ;;
|
|
*is-active*) echo active ;;
|
|
esac
|
|
exit 0
|
|
`);
|
|
chmodSync(join(bin, 'systemctl'), 0o755);
|
|
return bin;
|
|
}
|
|
|
|
// Without a stand-in of its own, a run gets one that lists nothing (the
|
|
// unit not loaded), so no run reaches the real systemd.
|
|
function quiet(dir) {
|
|
const home = join(dir, '.quiet');
|
|
if (!existsSync(home)) { mkdirSync(home); standIn(home); }
|
|
return join(home, 'bin');
|
|
}
|
|
// Every call but the reads: the Console's state and install's bus check.
|
|
const READS = /^--user (list-units --no-legend --plain mosaic-webui\.service|list-units --all --no-legend --plain mosaic-bus@\*\.service|show mosaic-bus@\S+ -p LoadState -p WorkingDirectory -p ExecStart)$/;
|
|
const otherCalls = (file) => readFileSync(file, 'utf8').split('\n').filter(Boolean).filter((c) => !READS.test(c));
|
|
|
|
// Async, so an in-process server can answer the script's /healthz request.
|
|
async function run(dir, args, bin = null) {
|
|
const env = { PATH: `${bin ?? quiet(dir)}:${NODE_DIR}:/usr/bin:/bin`, HOME: dir, XDG_CONFIG_HOME: join(dir, 'config') };
|
|
const child = spawn(SCRIPT, args, { env });
|
|
let stdout = '', stderr = '';
|
|
child.stdout.on('data', (d) => { stdout += d; });
|
|
child.stderr.on('data', (d) => { stderr += d; });
|
|
const [status] = await once(child, 'close');
|
|
return { status, stdout, stderr };
|
|
}
|
|
|
|
const execStart = (unit) => unit.split('\n').find((l) => l.startsWith('ExecStart='));
|
|
|
|
// Row 176: dropping flag propagation, port guards, or status reporting must
|
|
// fail these checks. All installs remain inside the existing fake-copy fixture.
|
|
test('tester service render carries the tester port alongside existing flags', async (t) => {
|
|
const dir = tmp(t);
|
|
for (const port of ['1024', '7333', '65535']) {
|
|
const r = await run(dir, ['render', '--tester-port', port, ...SSO_SEND]);
|
|
assert.equal(r.status, 0, r.stderr);
|
|
assert.match(execStart(r.stdout), new RegExp(` --tester-port ${port}( |$)`));
|
|
assert.match(execStart(r.stdout), / --port 7332 /);
|
|
assert.ok(execStart(r.stdout).endsWith(` --sso --send --tester-port ${port}`));
|
|
}
|
|
});
|
|
|
|
test('tester service refuses forwarded 7332 even with another main port', async (t) => {
|
|
const dir = tmp(t);
|
|
for (const main of ['7332', '7444']) {
|
|
const r = await run(dir, ['render', '--port', main, '--tester-port', '7332']);
|
|
assert.equal(r.status, 4, r.stderr);
|
|
assert.equal(r.stdout, '');
|
|
assert.match(r.stderr, /tester.port.*7332/i);
|
|
}
|
|
});
|
|
|
|
test('tester service invalid ports refuse render and install with exit 4 and leave the unit alone', async (t) => {
|
|
const dir = tmp(t);
|
|
const unitDir = join(dir, 'units');
|
|
const target = join(unitDir, 'mosaic-webui.service');
|
|
assert.equal((await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload'])).status, 0);
|
|
const before = readFileSync(target, 'utf8');
|
|
for (const args of [
|
|
['--tester-port', ''], ['--tester-port', '1023'], ['--tester-port', '65536'],
|
|
['--tester-port', '-1'], ['--tester-port', '3.5'], ['--tester-port', '1e4'],
|
|
['--tester-port', 'abc'], ['--tester-port', '7333 '], ['--tester-port', '7333\n'],
|
|
['--port', '7444', '--tester-port', '7444'],
|
|
['--tester-port', '7444', '--port', '7444'],
|
|
]) {
|
|
for (const verb of [['render'], ['install', ...INSTALL, '--dir', unitDir, '--no-reload']]) {
|
|
const r = await run(dir, [...verb, ...args]);
|
|
assert.equal(r.status, 4, `${verb[0]} ${JSON.stringify(args)}: ${r.stderr}`);
|
|
assert.equal(r.stdout, '');
|
|
assert.match(r.stderr, /tester.port/i);
|
|
assert.equal(readFileSync(target, 'utf8'), before);
|
|
}
|
|
}
|
|
assert.equal((await run(dir, ['render', '--tester-port'])).status, 4);
|
|
assert.deepEqual(readdirSync(unitDir), ['mosaic-webui.service']);
|
|
});
|
|
|
|
// Row 180 (Dewey's row 176 N2): the length counts as written, before the zeros go, as serve
|
|
// counts it. 007333 is six digits and refuses; 07333 is five and renders.
|
|
test('tester service counts the digits as written before it strips leading zeros', async (t) => {
|
|
const dir = tmp(t);
|
|
const six = await run(dir, ['render', '--tester-port', '007333']);
|
|
assert.equal(six.status, 4, six.stderr);
|
|
assert.equal(six.stdout, '');
|
|
assert.equal(six.stderr, 'webui-service: --tester-port must be 1024..65535, different from --port and never 7332: 007333\n');
|
|
const five = await run(dir, ['render', '--tester-port', '07333']);
|
|
assert.equal(five.status, 0, five.stderr);
|
|
assert.ok(execStart(five.stdout).endsWith(' --tester-port 07333'), execStart(five.stdout));
|
|
});
|
|
|
|
test('tester service install and status show the tester port without changing send reporting', async (t) => {
|
|
const dir = tmp(t);
|
|
const unitDir = join(dir, 'units');
|
|
const bin = standIn(dir);
|
|
const port = await closedPort(t);
|
|
const installed = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), '--tester-port', '1024', ...SSO_SEND], bin);
|
|
assert.equal(installed.status, 0, installed.stderr);
|
|
assert.match(installed.stdout, /^serve: .* --tester-port 1024( |$)/m);
|
|
assert.match(execStart(readFileSync(join(unitDir, 'mosaic-webui.service'), 'utf8')), / --tester-port 1024( |$)/);
|
|
const status = await run(dir, ['status', '--dir', unitDir], bin);
|
|
assert.equal(status.status, 0, status.stderr);
|
|
assert.match(status.stdout, /^tester: http:\/\/127\.0\.0\.1:1024(?: .*)?$/m);
|
|
assert.match(status.stdout, /^send: on /m);
|
|
assert.equal((await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port)], bin)).status, 0);
|
|
const plain = await run(dir, ['status', '--dir', unitDir], bin);
|
|
assert.match(plain.stdout, /^tester: off$/m);
|
|
assert.match(plain.stdout, /^send: off /m);
|
|
for (const verb of ['status', 'uninstall']) {
|
|
const refused = await run(dir, [verb, '--dir', unitDir, '--tester-port', '7333'], bin);
|
|
assert.equal(refused.status, 4);
|
|
}
|
|
assert.ok(existsSync(join(unitDir, 'mosaic-webui.service')));
|
|
});
|
|
|
|
test('render prints the unit without the preview flags', async (t) => {
|
|
const dir = tmp(t);
|
|
const r = await run(dir, ['render']);
|
|
assert.equal(r.status, 0, r.stderr);
|
|
assert.match(r.stdout, new RegExp(`^WorkingDirectory=${REPO.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'm'));
|
|
assert.match(r.stdout, /^Environment=PATH=\S*\/usr\/local\/bin:\/usr\/bin:\/bin$/m);
|
|
assert.equal(execStart(r.stdout), `ExecStart=${process.execPath} ${REPO}/packages/webui/src/cli.mjs serve --port 7332`);
|
|
for (const line of ['Restart=on-failure', 'RestartPreventExitStatus=2', 'NoNewPrivileges=yes', 'WantedBy=default.target']) {
|
|
assert.ok(r.stdout.split('\n').includes(line), line);
|
|
}
|
|
assert.doesNotMatch(r.stdout, /@[A-Z]+@/, 'every placeholder is filled');
|
|
});
|
|
|
|
test('render carries every serve flag, the preview set included', async (t) => {
|
|
const dir = tmp(t);
|
|
const r = await run(dir, ['render', '--port', '7340', '--business', 'mosaic.dev_1', '--board', 'http://127.0.0.1:7331', ...PREVIEW]);
|
|
assert.equal(r.status, 0, r.stderr);
|
|
assert.equal(execStart(r.stdout), `ExecStart=${process.execPath} ${REPO}/packages/webui/src/cli.mjs serve --port 7340 --business mosaic.dev_1 --board http://127.0.0.1:7331 --public-host console.example.com --auth-header X-authentik-uid --allow-user ${UID}`);
|
|
});
|
|
|
|
test('a partial preview set is refused', async (t) => {
|
|
const dir = tmp(t);
|
|
for (const args of [PREVIEW.slice(0, 2), PREVIEW.slice(2, 6), [...PREVIEW.slice(0, 2), ...PREVIEW.slice(4, 6)]]) {
|
|
const r = await run(dir, ['render', ...args]);
|
|
assert.equal(r.status, 1, args.join(' '));
|
|
assert.equal(r.stdout, '');
|
|
assert.match(r.stderr, /all three or none/);
|
|
}
|
|
});
|
|
|
|
test('a value the unit cannot carry is refused, for every flag', async (t) => {
|
|
const dir = tmp(t);
|
|
const set = (flag, value) => {
|
|
const preview = PREVIEW.slice();
|
|
const i = preview.indexOf(flag);
|
|
if (i >= 0) { preview[i + 1] = value; return preview; }
|
|
return [flag, value];
|
|
};
|
|
for (const flag of ['--business', '--board', '--public-host', '--auth-header', '--allow-user']) {
|
|
for (const bad of [' x', '%n', '\n', '$HOME', '"', '\\', '|', '&']) {
|
|
const value = flag === '--board' ? `http://127.0.0.1:7331${bad}` : `a${bad}b`;
|
|
const r = await run(dir, ['render', ...set(flag, value)]);
|
|
assert.equal(r.status, 1, `${flag} ${JSON.stringify(value)}`);
|
|
assert.equal(r.stdout, '', `${flag} ${JSON.stringify(value)}`);
|
|
}
|
|
}
|
|
for (const port of ['0', '65536', '7a', '', '-1', '999999']) {
|
|
const r = await run(dir, ['render', '--port', port]);
|
|
assert.equal(r.status, 1, `port ${JSON.stringify(port)}`);
|
|
}
|
|
for (const args of [['--business', 'Mosaic'], ['--business', '-x'], ['--business', 'a'.repeat(65)], ['--board', 'https://127.0.0.1:7331'], ['--board', '127.0.0.1:7331'], set('--public-host', 'console.example.com:443'), set('--public-host', 'https://console.example.com'), set('--auth-header', 'X_uid')]) {
|
|
const r = await run(dir, ['render', ...args]);
|
|
assert.equal(r.status, 1, args.join(' '));
|
|
}
|
|
});
|
|
|
|
test('the rendered unit names none of the human proof agent markers and sets no agent variable', async (t) => {
|
|
const dir = tmp(t);
|
|
const source = readFileSync(join(REPO, 'packages', 'bus', 'src', 'human.mjs'), 'utf8');
|
|
const markers = [...source.match(/const MARKERS = \[([^\]]*)\]/)[1].matchAll(/'([A-Z_]+)'/g)].map((m) => m[1]);
|
|
assert.ok(markers.length >= 7 && markers.includes('MOSAIC_BUS_CAP') && markers.includes('CLAUDECODE'), markers.join(' '));
|
|
for (const args of [[], [...PREVIEW, '--business', 'mosaic'], SSO_SEND]) {
|
|
const r = await run(dir, ['render', ...args]);
|
|
assert.equal(r.status, 0, r.stderr);
|
|
for (const marker of markers) assert.ok(!r.stdout.includes(marker), marker);
|
|
const envLines = r.stdout.split('\n').filter((l) => /^\s*(Environment|EnvironmentFile|PassEnvironment|UnsetEnvironment)\s*=/.test(l));
|
|
assert.deepEqual(envLines.map((l) => l.split('=')[1]), ['PATH']);
|
|
assert.doesNotMatch(r.stdout, /^[^#]*\b(MOSAIC_|CLAUDE|CODEX|PI_)/m);
|
|
}
|
|
});
|
|
|
|
test('install into --dir with --no-reload writes the file whole, then reports unchanged', async (t) => {
|
|
const dir = tmp(t);
|
|
const unitDir = join(dir, 'units');
|
|
const bin = standIn(dir);
|
|
const rendered = await run(dir, ['render', '--copy', COPY_ID, '--business', 'mosaic']);
|
|
assert.equal(rendered.stdout, filled(`--checkout ${REPO} --port 7332 --business mosaic`, copyRoot(dir)));
|
|
const r = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--business', 'mosaic'], bin);
|
|
assert.equal(r.status, 0, r.stderr);
|
|
const target = join(unitDir, 'mosaic-webui.service');
|
|
assert.match(r.stdout, new RegExp(`^written: ${target}$`, 'm'));
|
|
assert.match(r.stdout, /systemctl --user enable --now mosaic-webui/);
|
|
assert.equal(readFileSync(target, 'utf8'), rendered.stdout);
|
|
assert.equal(statSync(target).mode & 0o777, 0o644);
|
|
assert.deepEqual(readdirSync(unitDir), ['mosaic-webui.service'], 'no temp file left behind');
|
|
const again = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--business', 'mosaic'], bin);
|
|
assert.equal(again.status, 0, again.stderr);
|
|
assert.match(again.stdout, new RegExp(`^unchanged: ${target}$`, 'm'));
|
|
assert.deepEqual(otherCalls(join(dir, 'calls.txt')), [], 'with --no-reload, only the state read');
|
|
// A refused value writes nothing and leaves the installed unit alone.
|
|
const bad = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--business', 'a b'], bin);
|
|
assert.equal(bad.status, 1);
|
|
assert.equal(readFileSync(target, 'utf8'), rendered.stdout);
|
|
assert.deepEqual(readdirSync(unitDir), ['mosaic-webui.service']);
|
|
const fresh = join(dir, 'never');
|
|
assert.equal((await run(dir, ['install', ...INSTALL, '--dir', fresh, '--no-reload', '--port', '0'], bin)).status, 1);
|
|
assert.ok(!existsSync(fresh), 'a refusal creates no directory');
|
|
});
|
|
|
|
test('install without --no-reload runs daemon-reload and nothing that starts the unit', async (t) => {
|
|
const dir = tmp(t);
|
|
const bin = standIn(dir);
|
|
const r = await run(dir, ['install', ...INSTALL, '--dir', join(dir, 'units')], bin);
|
|
assert.equal(r.status, 0, r.stderr);
|
|
assert.match(r.stdout, /^daemon-reload done$/m);
|
|
assert.equal(readFileSync(join(dir, 'calls.txt'), 'utf8'), '--user list-units --all --no-legend --plain mosaic-bus@*.service\n--user show [email protected] -p LoadState -p WorkingDirectory -p ExecStart\n--user list-units --no-legend --plain mosaic-webui.service\n--user daemon-reload\n');
|
|
});
|
|
|
|
test('uninstall refuses while the unit is running, with or without --no-reload, and removes it once it is not', async (t) => {
|
|
const dir = tmp(t);
|
|
const unitDir = join(dir, 'units');
|
|
const target = join(unitDir, 'mosaic-webui.service');
|
|
assert.equal((await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload'])).status, 0);
|
|
for (const state of ['active', 'activating', 'deactivating', 'reloading']) {
|
|
const bin = standIn(mkdtempSync(join(dir, 'si-')), `mosaic-webui.service loaded ${state} running Mosaic Console\n`);
|
|
// N3: --no-reload skips only daemon-reload, never the state read.
|
|
for (const args of [['uninstall', '--dir', unitDir], ['uninstall', '--dir', unitDir, '--no-reload']]) {
|
|
const r = await run(dir, args, bin);
|
|
assert.equal(r.status, 1, `${state} ${args.join(' ')}`);
|
|
assert.equal(r.stderr, `webui-service: refusing: mosaic-webui.service is still running (${state}); Jason or Mos stops it first (systemctl --user stop mosaic-webui)\n`);
|
|
assert.equal(r.stdout, '');
|
|
assert.ok(existsSync(target), `still installed while ${state}`);
|
|
}
|
|
}
|
|
// A failed unit is not running; uninstall goes ahead.
|
|
const failed = standIn(mkdtempSync(join(dir, 'si-')), 'mosaic-webui.service loaded failed failed Mosaic Console\n');
|
|
assert.equal((await run(dir, ['uninstall', '--dir', unitDir, '--no-reload'], failed)).status, 0);
|
|
assert.ok(!existsSync(target));
|
|
assert.equal((await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload'])).status, 0);
|
|
const bin = standIn(mkdtempSync(join(dir, 'si-')), 'mosaic-webui.service loaded inactive dead Mosaic Console\n');
|
|
const r = await run(dir, ['uninstall', '--dir', unitDir], bin);
|
|
assert.equal(r.status, 0, r.stderr);
|
|
assert.match(r.stdout, new RegExp(`^removed: ${target}$`, 'm'));
|
|
assert.match(r.stdout, /^daemon-reload done$/m);
|
|
assert.ok(!existsSync(target));
|
|
const absent = await run(dir, ['uninstall', '--dir', unitDir, '--no-reload'], bin);
|
|
assert.equal(absent.status, 0);
|
|
assert.match(absent.stdout, /^absent: /m);
|
|
});
|
|
|
|
test('status prints the unit state and GET /healthz on the installed port', async (t) => {
|
|
const dir = tmp(t);
|
|
const unitDir = join(dir, 'units');
|
|
const bin = standIn(dir);
|
|
const server = createServer((req, res) => {
|
|
if (req.url === '/healthz') { res.writeHead(200, { 'content-type': 'application/json' }); res.end('{"ok":true}\n'); return; }
|
|
res.writeHead(404); res.end();
|
|
});
|
|
server.listen(0, '127.0.0.1');
|
|
await once(server, 'listening');
|
|
t.after(() => server.close());
|
|
const { port } = server.address();
|
|
assert.equal((await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port)])).status, 0);
|
|
const r = await run(dir, ['status', '--dir', unitDir], bin);
|
|
assert.equal(r.status, 0, r.stderr);
|
|
assert.match(r.stdout, new RegExp(`^copy: ${COPY_ID}$`, 'm'));
|
|
assert.match(r.stdout, /^ {2}enabled: enabled$/m);
|
|
assert.match(r.stdout, /^ {2}active: {2}active$/m);
|
|
assert.match(r.stdout, new RegExp(`^healthz: http://127\\.0\\.0\\.1:${port}/healthz 200 \\{"ok":true\\}$`, 'm'));
|
|
server.close();
|
|
await once(server, 'close');
|
|
const down = await run(dir, ['status', '--dir', unitDir], bin);
|
|
assert.equal(down.status, 0, down.stderr);
|
|
assert.match(down.stdout, /^healthz: \S+ unreachable \(ECONNREFUSED\)$/m);
|
|
const none = await run(dir, ['status', '--dir', join(dir, 'empty')], bin);
|
|
assert.match(none.stdout, /^healthz: not checked; no unit at /m);
|
|
});
|
|
|
|
test('usage: an unknown verb or flag, and start or enable, exit 4', async (t) => {
|
|
const dir = tmp(t);
|
|
for (const args of [[], ['start'], ['stop'], ['enable'], ['restart'], ['render', '--bogus'], ['render', '--port'], ['render', '--dir', dir], ['render', '--no-reload'], ['uninstall', '--port', '7332'], ['status', '--business', 'mosaic'], ['status', '--no-reload'], ['render', '--send', 'yes', ...PREVIEW], ['render', '--allow-worktree'], ['render', '--copies', dir], ['uninstall', '--copy', COPY_ID], ['uninstall', '--allow-worktree'], ['status', '--copy', COPY_ID], ['status', '--allow-worktree'], ['install', '--copy']]) {
|
|
const r = await run(dir, args);
|
|
assert.equal(r.status, 4, args.join(' '));
|
|
assert.match(r.stderr, /^usage: scripts\/webui-service\.sh/);
|
|
}
|
|
});
|
|
|
|
test('without --send, render is the template with its placeholders filled, byte for byte', async (t) => {
|
|
const dir = tmp(t);
|
|
for (const [args, flags] of [
|
|
[[], '--port 7332'],
|
|
[['--port', '7340'], '--port 7340'],
|
|
[['--business', 'mosaic'], '--port 7332 --business mosaic'],
|
|
[PREVIEW, `--port 7332 ${PREVIEW_FLAGS}`],
|
|
[['--port', '7340', '--business', 'mosaic.dev_1', '--board', 'http://127.0.0.1:7331', ...PREVIEW], `--port 7340 --business mosaic.dev_1 --board http://127.0.0.1:7331 ${PREVIEW_FLAGS}`],
|
|
]) {
|
|
const r = await run(dir, ['render', ...args]);
|
|
assert.equal(r.status, 0, r.stderr);
|
|
assert.equal(r.stdout, filled(flags), args.join(' '));
|
|
}
|
|
});
|
|
|
|
test('render --sso --send ends ExecStart with --send and changes nothing else', async (t) => {
|
|
const dir = tmp(t);
|
|
const extra = ['--port', '7340', '--business', 'mosaic'];
|
|
for (const args of [['--sso', '--send'], ['--send', '--sso'], ['--send', ...extra, '--sso']]) {
|
|
const plain = args.filter((a) => a !== '--send');
|
|
const flags = `${plain.includes('--port') ? '--port 7340 --business mosaic' : '--port 7332'} --sso`;
|
|
const r = await run(dir, ['render', ...args]);
|
|
assert.equal(r.status, 0, r.stderr);
|
|
assert.equal(r.stdout, filled(`${flags} --send`), args.join(' '));
|
|
assert.ok(execStart(r.stdout).endsWith(' --sso --send'), execStart(r.stdout));
|
|
const without = await run(dir, ['render', ...plain]);
|
|
assert.equal(without.status, 0, without.stderr);
|
|
const a = r.stdout.split('\n'), b = without.stdout.split('\n');
|
|
assert.equal(a.length, b.length);
|
|
assert.deepEqual(a.map((l, i) => (l === b[i] ? null : [l, b[i]])).filter(Boolean), [[`${execStart(without.stdout)} --send`, execStart(without.stdout)]]);
|
|
}
|
|
});
|
|
|
|
test('render refuses duplicate --send under --sso', async (t) => {
|
|
const dir = tmp(t);
|
|
const r = await run(dir, ['render', '--sso', '--send', '--send']);
|
|
assert.equal(r.status, 1, r.stderr);
|
|
assert.equal(r.stdout, '');
|
|
assert.match(r.stderr, /^webui-service: --send given twice$/m);
|
|
});
|
|
|
|
test('--send without --sso, including every header-preview shape, is refused and writes nothing', async (t) => {
|
|
const dir = tmp(t);
|
|
const pairs = [PREVIEW.slice(0, 2), PREVIEW.slice(2, 4), PREVIEW.slice(4, 6)];
|
|
const partial = [[], ['--port', '7340'], ['--business', 'mosaic'], ...pairs, [...pairs[0], ...pairs[1]], [...pairs[0], ...pairs[2]], [...pairs[1], ...pairs[2]], PREVIEW];
|
|
for (const args of partial) {
|
|
const r = await run(dir, ['render', '--send', ...args]);
|
|
assert.equal(r.status, 1, args.join(' '));
|
|
assert.equal(r.stdout, '', args.join(' '));
|
|
assert.match(r.stderr, SEND_REFUSAL, args.join(' '));
|
|
}
|
|
const bin = standIn(dir);
|
|
const fresh = join(dir, 'never');
|
|
for (const args of partial) {
|
|
const r = await run(dir, ['install', ...INSTALL, '--dir', fresh, '--no-reload', ...args, '--send'], bin);
|
|
assert.equal(r.status, 1, args.join(' '));
|
|
assert.match(r.stderr, SEND_REFUSAL, args.join(' '));
|
|
}
|
|
assert.ok(!existsSync(fresh), 'a refusal creates no directory');
|
|
const unitDir = join(dir, 'units');
|
|
const target = join(unitDir, 'mosaic-webui.service');
|
|
assert.equal((await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', ...PREVIEW], bin)).status, 0);
|
|
const before = readFileSync(target, 'utf8');
|
|
const r = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--send', ...PREVIEW.slice(0, 4)], bin);
|
|
assert.equal(r.status, 1);
|
|
assert.equal(readFileSync(target, 'utf8'), before, 'the installed unit is left alone');
|
|
assert.deepEqual(readdirSync(unitDir), ['mosaic-webui.service']);
|
|
assert.deepEqual(otherCalls(join(dir, 'calls.txt')), [], 'no systemctl call but the one state read');
|
|
});
|
|
|
|
test('install --send into --dir with --no-reload writes the sending unit, and status says whether it sends', async (t) => {
|
|
const dir = tmp(t);
|
|
const unitDir = join(dir, 'units');
|
|
const target = join(unitDir, 'mosaic-webui.service');
|
|
const port = await closedPort(t);
|
|
const flags = `--port ${port} --sso`;
|
|
const r = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), ...SSO_SEND]);
|
|
assert.equal(r.status, 0, r.stderr);
|
|
assert.match(r.stdout, new RegExp(`^written: ${target}$`, 'm'));
|
|
// Lead decision 102: Jason or Mos starts it, no other agent.
|
|
assert.match(r.stdout, /^this script starts nothing: .* Jason\nstarts and restarts it, or Mos under lead decision 102 .*\ntrail as Jason\); no other agent does:$/m);
|
|
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
assert.match(r.stdout, new RegExp(`^serve: \\S+ ${esc(copyRoot(dir))}/packages/webui/src/cli\\.mjs serve --checkout ${esc(REPO)} ${flags} --send$`, 'm'));
|
|
assert.equal(readFileSync(target, 'utf8'), filled(`--checkout ${REPO} ${flags} --send`, copyRoot(dir)));
|
|
assert.equal(statSync(target).mode & 0o777, 0o644);
|
|
assert.deepEqual(readdirSync(unitDir), ['mosaic-webui.service']);
|
|
const bin = standIn(dir);
|
|
const sending = await run(dir, ['status', '--dir', unitDir], bin);
|
|
assert.equal(sending.status, 0, sending.stderr);
|
|
assert.match(sending.stdout, /^send: on \(--send; human messages to roles\)$/m);
|
|
assert.match(sending.stdout, /^healthz: \S+ unreachable \(ECONNREFUSED\)$/m);
|
|
const again = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), '--send', '--sso'], bin);
|
|
assert.match(again.stdout, new RegExp(`^unchanged: ${target}$`, 'm'));
|
|
const readOnly = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), '--sso'], bin);
|
|
assert.match(readOnly.stdout, new RegExp(`^written: ${target}$`, 'm'));
|
|
assert.equal(readFileSync(target, 'utf8'), filled(`--checkout ${REPO} ${flags}`, copyRoot(dir)));
|
|
const off = await run(dir, ['status', '--dir', unitDir], bin);
|
|
assert.match(off.stdout, /^send: off \(read-only\)$/m);
|
|
assert.doesNotMatch(off.stdout, /^send: on/m);
|
|
// The only systemctl calls are status's reads and install's state read.
|
|
assert.deepEqual(otherCalls(join(dir, 'calls.txt')).filter((c) => !/^--user (is-enabled|is-active|show) mosaic-webui\.service/.test(c)), []);
|
|
const none = await run(dir, ['status', '--dir', join(dir, 'empty')], bin);
|
|
assert.doesNotMatch(none.stdout, /^send:/m, 'no unit, no send line');
|
|
// This malformed identity makes --send the --allow-user value and is not a
|
|
// duplicate-flag guard; the dedicated --sso --send --send case above is.
|
|
const before = readFileSync(target, 'utf8');
|
|
for (const args of [['--public-host', 'console.example.com', '--auth-header', 'X-authentik-username', '--allow-user', '--send']]) {
|
|
const r = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), ...args]);
|
|
assert.equal(r.status, 1, args.join(' '));
|
|
assert.match(r.stderr, /^webui-service: allowed user must start with a letter or digit: --send$/m);
|
|
}
|
|
assert.equal(readFileSync(target, 'utf8'), before);
|
|
});
|
|
|
|
test('uninstall and status take no --send', async (t) => {
|
|
const dir = tmp(t);
|
|
const bin = standIn(dir);
|
|
const unitDir = join(dir, 'units');
|
|
assert.equal((await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', ...SSO_SEND])).status, 0);
|
|
for (const args of [['uninstall', '--dir', unitDir, '--send'], ['uninstall', '--dir', unitDir, '--no-reload', '--send'], ['status', '--dir', unitDir, '--send']]) {
|
|
const r = await run(dir, args, bin);
|
|
assert.equal(r.status, 4, args.join(' '));
|
|
assert.match(r.stderr, /^usage: scripts\/webui-service\.sh/);
|
|
assert.equal(r.stdout, '', args.join(' '));
|
|
}
|
|
assert.ok(existsSync(join(unitDir, 'mosaic-webui.service')), 'still installed');
|
|
assert.ok(!existsSync(join(dir, 'calls.txt')), 'no systemctl call');
|
|
});
|
|
|
|
// Row 77 (#1571) from here on.
|
|
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
|
|
test('install refuses while the unit is running, in any state but inactive or failed, and changes nothing', async (t) => {
|
|
const dir = tmp(t);
|
|
const unitDir = join(dir, 'units');
|
|
const target = join(unitDir, 'mosaic-webui.service');
|
|
assert.equal((await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload'])).status, 0);
|
|
const before = readFileSync(target, 'utf8');
|
|
for (const state of ['active', 'activating', 'deactivating', 'reloading']) {
|
|
const home = mkdtempSync(join(dir, 'si-'));
|
|
const bin = standIn(home, `mosaic-webui.service loaded ${state} running Mosaic Console\n`);
|
|
for (const reload of [[], ['--no-reload']]) {
|
|
const r = await run(dir, ['install', ...INSTALL, '--dir', unitDir, ...reload, '--port', '7444'], bin);
|
|
assert.equal(r.status, 1, `${state} ${reload}`);
|
|
assert.equal(r.stderr, `webui-service: refusing: mosaic-webui.service is running (${state}); Jason or Mos stops it first (systemctl --user stop mosaic-webui), then install, then start it\n`);
|
|
assert.equal(r.stdout, '');
|
|
}
|
|
assert.deepEqual(otherCalls(join(home, 'calls.txt')), [], 'no daemon-reload');
|
|
assert.equal(readFileSync(target, 'utf8'), before, `unit left alone while ${state}`);
|
|
assert.deepEqual(readdirSync(unitDir), ['mosaic-webui.service'], 'no temp file left');
|
|
}
|
|
// inactive, failed, another unit's line, or nothing listed: install goes ahead.
|
|
for (const lines of ['mosaic-webui.service loaded inactive dead Mosaic Console\n', 'mosaic-webui.service loaded failed failed Mosaic Console\n', '[email protected] loaded active running Bus\n', '']) {
|
|
const bin = standIn(mkdtempSync(join(dir, 'si-')), lines);
|
|
const r = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', '7444'], bin);
|
|
assert.equal(r.status, 0, `${JSON.stringify(lines)}: ${r.stderr}`);
|
|
}
|
|
});
|
|
|
|
test('when the state read fails, install and uninstall refuse with one line and change nothing', async (t) => {
|
|
const dir = tmp(t);
|
|
const unitDir = join(dir, 'units');
|
|
const target = join(unitDir, 'mosaic-webui.service');
|
|
assert.equal((await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload'])).status, 0);
|
|
const before = readFileSync(target, 'utf8');
|
|
const home = mkdtempSync(join(dir, 'si-'));
|
|
const bin = standIn(home, '', { listFails: true });
|
|
const refusal = 'webui-service: refusing: systemctl --user list-units --no-legend --plain mosaic-webui.service failed, so the unit\'s state is unknown; nothing was changed\n';
|
|
for (const args of [['install', ...INSTALL, '--dir', unitDir, '--port', '7444'], ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', '7444'], ['uninstall', '--dir', unitDir], ['uninstall', '--dir', unitDir, '--no-reload']]) {
|
|
const r = await run(dir, args, bin);
|
|
assert.equal(r.status, 1, args.join(' '));
|
|
assert.equal(r.stderr, refusal, args.join(' '));
|
|
assert.equal(r.stdout, '');
|
|
}
|
|
assert.deepEqual(otherCalls(join(home, 'calls.txt')), [], 'no daemon-reload');
|
|
assert.equal(readFileSync(target, 'utf8'), before);
|
|
assert.deepEqual(readdirSync(unitDir), ['mosaic-webui.service']);
|
|
});
|
|
|
|
test('install needs --copy naming a copy that exists, and refuses outside the canonical checkout without --allow-worktree', async (t) => {
|
|
const dir = tmp(t);
|
|
const unitDir = join(dir, 'units');
|
|
const cases = [
|
|
[[], /^webui-service: install needs --copy ID, a copy made with: scripts\/webui-service\.sh copy COMMIT$/m],
|
|
[['--copy', COPY_ID.slice(0, 12)], /^webui-service: --copy must be a full 40-character commit id: 0123456789ab$/m],
|
|
[['--copy', COPY_ID.toUpperCase()], /^webui-service: --copy must be a full 40-character commit id: /m],
|
|
[['--copy', `${COPY_ID}0`], /^webui-service: --copy must be a full 40-character commit id: /m],
|
|
[['--copy', 'f'.repeat(40)], new RegExp(`^webui-service: no copy ${'f'.repeat(40)} in ${esc(join(dir, '.local', 'share', 'mosaic-stack', 'copies'))}; make it with: scripts/webui-service\\.sh copy ${'f'.repeat(40)}$`, 'm')],
|
|
[['--copy', COPY_ID, '--copies', 'relative'], /^webui-service: copies directory must be an absolute path: relative$/m],
|
|
[['--copy', COPY_ID, '--copies', join(dir, 'no copies')], /^webui-service: copies directory contains a character the unit cannot carry: /m],
|
|
];
|
|
for (const [copy, refusal] of cases) {
|
|
const r = await run(dir, ['install', '--allow-worktree', ...copy, '--dir', unitDir, '--no-reload']);
|
|
assert.equal(r.status, 1, copy.join(' '));
|
|
assert.match(r.stderr, refusal, copy.join(' '));
|
|
assert.ok(!existsSync(unitDir), copy.join(' '));
|
|
}
|
|
// This suite runs in a git worktree, whose .git is a file: without
|
|
// --allow-worktree, install refuses before it reads anything else.
|
|
if (!statSync(join(REPO, '.git')).isDirectory()) {
|
|
const r = await run(dir, ['install', '--copy', COPY_ID, '--dir', unitDir, '--no-reload']);
|
|
assert.equal(r.status, 1);
|
|
assert.equal(r.stderr, `webui-service: refusing: ${REPO} is not the canonical checkout (its .git is not a directory); run copy and install from the canonical checkout\n`);
|
|
assert.ok(!existsSync(unitDir));
|
|
}
|
|
// --copies moves where the copy is looked for.
|
|
const other = join(dir, 'other');
|
|
mkdirSync(join(other, COPY_ID, 'packages', 'webui', 'src'), { recursive: true });
|
|
writeFileSync(join(other, COPY_ID, 'packages', 'webui', 'src', 'cli.mjs'), '');
|
|
const otherBus = join(dir, 'other-bus');
|
|
mkdirSync(otherBus);
|
|
const r = await run(dir, ['install', ...INSTALL, '--copies', other, '--dir', unitDir, '--no-reload'], standIn(otherBus, '', { bus: busShow(join(other, COPY_ID)) }));
|
|
assert.equal(r.status, 0, r.stderr);
|
|
assert.equal(readFileSync(join(unitDir, 'mosaic-webui.service'), 'utf8'), filled(`--checkout ${REPO} --port 7332`, join(other, COPY_ID)));
|
|
});
|
|
|
|
// The bus accepts only its own human-cli.mjs (packages/bus/src/human.mjs),
|
|
// so a Console on a copy needs its bus on that same copy. install reads the
|
|
// bus unit's effective ExecStart (a drop-in counts) and refuses otherwise.
|
|
test('install refuses unless the bus unit runs from the same copy, naming both paths, and changes nothing', async (t) => {
|
|
const dir = tmp(t);
|
|
const unitDir = join(dir, 'units');
|
|
const copy = copyRoot(dir);
|
|
const install = (bin, extra = []) => run(dir, ['install', ...INSTALL, ...extra, '--dir', unitDir, '--no-reload'], bin);
|
|
let n = 0;
|
|
const sys = (opts, lines = '') => { const d = join(dir, `s${n++}`); mkdirSync(d); return { d, bin: standIn(d, lines, opts) }; };
|
|
const refused = (r, line) => {
|
|
assert.equal(r.status, 1);
|
|
assert.equal(r.stderr, `webui-service: refusing: ${line}\n`);
|
|
assert.ok(!existsSync(unitDir));
|
|
};
|
|
|
|
// The bus runs from the checkout, as mosaic-bus@ does today: refused,
|
|
// with the bus's path and the copy's.
|
|
let s = sys({ bus: busShow(REPO) });
|
|
refused(await install(s.bin), `[email protected] runs from ${REPO}/scripts/mosaic, not the copy ${copy}; the bus accepts only its own human-cli.mjs, so every Console bus call would fail human-required until the bus runs from the same copy`);
|
|
// Refused before the Console state is read.
|
|
assert.ok(!readFileSync(join(s.d, 'calls.txt'), 'utf8').includes('mosaic-webui'));
|
|
// The same with --business, which names the one unit to read.
|
|
s = sys({ bus: busShow(REPO) });
|
|
refused(await install(s.bin, ['--business', 'acme']), `[email protected] runs from ${REPO}/scripts/mosaic, not the copy ${copy}; the bus accepts only its own human-cli.mjs, so every Console bus call would fail human-required until the bus runs from the same copy`);
|
|
assert.equal(readFileSync(join(s.d, 'calls.txt'), 'utf8'), '--user show [email protected] -p LoadState -p WorkingDirectory -p ExecStart\n');
|
|
// A path that only starts with the copy's name is not inside it.
|
|
s = sys({ bus: busShow(`${copy}-old`) });
|
|
refused(await install(s.bin), `[email protected] runs from ${copy}-old/scripts/mosaic, not the copy ${copy}; the bus accepts only its own human-cli.mjs, so every Console bus call would fail human-required until the bus runs from the same copy`);
|
|
s = sys({bus:busShow(copy).replace(`WorkingDirectory=${copy}`, 'WorkingDirectory=/different')});
|
|
refused(await install(s.bin), `[email protected] WorkingDirectory is /different, not the copy ${copy}; nothing was changed`);
|
|
// Two loaded bus units: each must run from the copy.
|
|
s = sys({ busUnits: `${BUS_UNITS}[email protected] loaded active running Mosaic bus host (other)\n` });
|
|
assert.equal((await install(s.bin)).status, 0);
|
|
assert.match(readFileSync(join(s.d, 'calls.txt'), 'utf8'), /show mosaic-bus@other\.service -p LoadState -p WorkingDirectory -p ExecStart/);
|
|
rmSync(unitDir, { recursive: true });
|
|
// The second loaded unit is off the copy: refused, naming the second unit and its path.
|
|
const two = `${BUS_UNITS}[email protected] loaded active running Mosaic bus host (other)\n`;
|
|
s = sys({ busUnits: two, bus: busShow(copy), busOther: busShow(REPO) });
|
|
refused(await install(s.bin), `[email protected] runs from ${REPO}/scripts/mosaic, not the copy ${copy}; the bus accepts only its own human-cli.mjs, so every Console bus call would fail human-required until the bus runs from the same copy`);
|
|
assert.match(readFileSync(join(s.d, 'calls.txt'), 'utf8'), /show mosaic-bus@acme\.service -p LoadState -p WorkingDirectory -p ExecStart\n.*show mosaic-bus@other\.service -p LoadState -p WorkingDirectory -p ExecStart/s);
|
|
|
|
// No bus unit installed for the business: refused.
|
|
s = sys({ bus: 'LoadState=not-found\nExecStart=\n' });
|
|
refused(await install(s.bin, ['--business', 'acme']), `no bus unit [email protected] is installed (LoadState not-found); the Console on the copy ${copy} needs its bus on that copy`);
|
|
// Only loaded counts: a unit systemd shows at any other LoadState is not the bus, even with an
|
|
// ExecStart on the copy.
|
|
for (const state of ['bad-setting', 'error', 'masked']) {
|
|
s = sys({ bus: busShow(copy).replace('LoadState=loaded', `LoadState=${state}`) });
|
|
refused(await install(s.bin, ['--business', 'acme']), `no bus unit [email protected] is installed (LoadState ${state}); the Console on the copy ${copy} needs its bus on that copy`);
|
|
}
|
|
// No --business and no mosaic-bus@ unit loaded: refused.
|
|
s = sys({ busUnits: '' });
|
|
refused(await install(s.bin), `no mosaic-bus@ unit is loaded, and the Console on the copy ${copy} needs its bus on that copy; name the business with --business ID`);
|
|
// A loaded unit with no ExecStart path: refused.
|
|
s = sys({ bus: 'LoadState=loaded\nExecStart=\n' });
|
|
refused(await install(s.bin), '[email protected] has no ExecStart path, so where its bus runs from is unknown; nothing was changed');
|
|
// A failed systemctl call: one line, nothing changed.
|
|
s = sys({ busFails: true });
|
|
refused(await install(s.bin), 'systemctl --user list-units --all --no-legend --plain mosaic-bus@*.service failed, so the bus unit is unknown; nothing was changed');
|
|
s = sys({ busFails: true });
|
|
refused(await install(s.bin, ['--business', 'acme']), 'systemctl --user show [email protected] failed, so the bus unit is unknown; nothing was changed');
|
|
|
|
// The bus runs from the same copy: accepted, with or without --business.
|
|
for (const extra of [[], ['--business', 'acme']]) {
|
|
s = sys({ bus: busShow(copy) });
|
|
const r = await install(s.bin, extra);
|
|
assert.equal(r.status, 0, r.stderr);
|
|
assert.ok(existsSync(join(unitDir, 'mosaic-webui.service')));
|
|
rmSync(unitDir, { recursive: true });
|
|
}
|
|
});
|
|
|
|
test('render --copy runs the copy and reads the queue in the checkout; without --copy the unit is unchanged', async (t) => {
|
|
const dir = tmp(t);
|
|
const r = await run(dir, ['render', '--copy', COPY_ID, ...SSO_SEND]);
|
|
assert.equal(r.status, 0, r.stderr);
|
|
assert.equal(r.stdout, filled(`--checkout ${REPO} --port 7332 --sso --send`, copyRoot(dir)));
|
|
assert.match(r.stdout, new RegExp(`^WorkingDirectory=${esc(copyRoot(dir))}$`, 'm'));
|
|
assert.match(r.stdout, new RegExp(`^ExecStart=\\S+ ${esc(copyRoot(dir))}/packages/webui/src/cli\\.mjs serve --checkout ${esc(REPO)} `, 'm'));
|
|
const plain = await run(dir, ['render', ...PREVIEW]);
|
|
assert.equal(plain.stdout, filled(`--port 7332 ${PREVIEW_FLAGS}`));
|
|
assert.ok(!execStart(plain.stdout).includes('--checkout'));
|
|
// render only prints: it takes no --allow-worktree and needs no canonical checkout.
|
|
assert.equal((await run(dir, ['render', '--copy', 'f'.repeat(40)])).status, 1);
|
|
});
|
|
|
|
test('the uid header takes only an Authentik uid; the username header still takes a username', async (t) => {
|
|
const dir = tmp(t);
|
|
const unitDir = join(dir, 'units');
|
|
const head = ['--public-host', 'console.example.com', '--auth-header'];
|
|
for (const header of ['X-authentik-uid', 'x-authentik-uid', 'X-AUTHENTIK-UID', 'x-AUTHENTIK-uid']) {
|
|
const ok = await run(dir, ['render', ...head, header, '--allow-user', UID]);
|
|
assert.equal(ok.status, 0, `${header}: ${ok.stderr}`);
|
|
for (const user of [UID.slice(1), `${UID}0`, UID.toUpperCase(), `${UID.slice(0, 63)}g`, 'jason', '[email protected]']) {
|
|
for (const verb of [['render'], ['install', ...INSTALL, '--dir', unitDir, '--no-reload']]) {
|
|
const r = await run(dir, [...verb, ...head, header, '--allow-user', user]);
|
|
assert.equal(r.status, 1, `${verb[0]} ${header} ${user}`);
|
|
assert.equal(r.stderr, `webui-service: with --auth-header ${header} the allowed user must be an Authentik uid, 64 lower-case hex characters (^[0-9a-f]{64}$, lead decision 101): ${user}\n`);
|
|
}
|
|
}
|
|
}
|
|
assert.ok(!existsSync(unitDir), 'no refused install wrote a unit');
|
|
for (const user of ['jason', '[email protected]', UID]) {
|
|
const r = await run(dir, ['render', ...head, 'X-authentik-username', '--allow-user', user]);
|
|
assert.equal(r.status, 0, `${user}: ${r.stderr}`);
|
|
assert.ok(execStart(r.stdout).endsWith(` --allow-user ${user}`));
|
|
}
|
|
});
|
|
|
|
test('a preview value cannot start with - or .', async (t) => {
|
|
const dir = tmp(t);
|
|
const cases = [
|
|
[['--public-host', '-x.example.com', '--auth-header', 'X-authentik-uid', '--allow-user', UID], 'public host must be a hostname, no port or scheme: -x.example.com'],
|
|
[['--public-host', '.example.com', '--auth-header', 'X-authentik-uid', '--allow-user', UID], 'public host must be a hostname, no port or scheme: .example.com'],
|
|
[['--public-host', 'console.example.com', '--auth-header', '-X-authentik-uid', '--allow-user', UID], 'auth header must be a header name: -X-authentik-uid'],
|
|
[['--public-host', 'console.example.com', '--auth-header', 'X-authentik-username', '--allow-user', '-jason'], 'allowed user must start with a letter or digit: -jason'],
|
|
[['--public-host', 'console.example.com', '--auth-header', 'X-authentik-username', '--allow-user', '.jason'], 'allowed user must start with a letter or digit: .jason'],
|
|
[['--public-host', 'console.example.com', '--auth-header', 'X-authentik-username', '--allow-user', '--port'], 'allowed user must start with a letter or digit: --port'],
|
|
];
|
|
for (const [args, refusal] of cases) {
|
|
const r = await run(dir, ['render', ...args]);
|
|
assert.equal(r.status, 1, args.join(' '));
|
|
assert.equal(r.stderr, `webui-service: ${refusal}\n`);
|
|
}
|
|
});
|
|
|
|
test('status names the copy and whether refactor has moved past it, and says when the unit runs from no copy', async (t) => {
|
|
const dir = tmp(t);
|
|
const unitDir = join(dir, 'units');
|
|
const target = join(unitDir, 'mosaic-webui.service');
|
|
const bin = standIn(mkdtempSync(join(dir, 'si-')));
|
|
const port = await closedPort(t);
|
|
const head = await new Promise((resolve, reject) => {
|
|
const git = spawn('git', ['-C', REPO, 'rev-parse', '--verify', 'refs/heads/refactor']);
|
|
let out = '';
|
|
git.stdout.on('data', (d) => { out += d; });
|
|
git.on('close', (code) => (code === 0 ? resolve(out.trim()) : reject(new Error('no refs/heads/refactor'))));
|
|
});
|
|
// status only reads the unit file, so a hand-written one stands in.
|
|
mkdirSync(unitDir);
|
|
const unit = (wd) => writeFileSync(target, `[Service]\nWorkingDirectory=${wd}\nExecStart=/usr/bin/node ${wd}/packages/webui/src/cli.mjs serve --port ${port}\n`);
|
|
unit(join(dir, 'copies', head));
|
|
let r = await run(dir, ['status', '--dir', unitDir], bin);
|
|
assert.equal(r.status, 0, r.stderr);
|
|
assert.match(r.stdout, new RegExp(`^copy: ${head}\nrefactor: ${head}, the copy's commit$`, 'm'));
|
|
unit(join(dir, 'copies', COPY_ID));
|
|
r = await run(dir, ['status', '--dir', unitDir], bin);
|
|
assert.match(r.stdout, new RegExp(`^copy: ${COPY_ID}\nrefactor: ${head}, another commit; the copy is ${COPY_ID}$`, 'm'));
|
|
unit(REPO);
|
|
r = await run(dir, ['status', '--dir', unitDir], bin);
|
|
assert.match(r.stdout, new RegExp(`^copy: none; the unit runs from ${esc(REPO)}$`, 'm'));
|
|
assert.ok(!/^refactor:/m.test(r.stdout));
|
|
// Only a whole 40-hex last part is a copy: 40 hex plus a character, or 41 hex, is not.
|
|
for (const last of [`${COPY_ID}x`, `x${COPY_ID}`, `${COPY_ID}0`]) {
|
|
unit(join(dir, 'copies', last));
|
|
r = await run(dir, ['status', '--dir', unitDir], bin);
|
|
assert.equal(r.status, 0, r.stderr);
|
|
assert.match(r.stdout, new RegExp(`^copy: none; the unit runs from ${esc(join(dir, 'copies', last))}$`, 'm'), last);
|
|
assert.ok(!/^refactor:/m.test(r.stdout), last);
|
|
}
|
|
// status reads; it calls nothing that changes the unit.
|
|
assert.deepEqual(readFileSync(join(dir, readdirSync(dir).find((d) => d.startsWith('si-')), 'calls.txt'), 'utf8').split('\n').filter(Boolean).filter((c) => !/^--user (is-enabled|is-active|show) mosaic-webui\.service( |$)/.test(c)), []);
|
|
});
|
|
|
|
// Row 178: only fixture paths and stand-in systemctl; render reads metadata, not content.
|
|
test('proxy proof render carries both flags but never opens the fixture file', async t => {
|
|
const dir = tmp(t), file = join(dir, 'proof.fixture');
|
|
// Invalid proof bytes deliberately prove render does not validate/read contents.
|
|
writeFileSync(file, 'not-a-valid-proof', { mode: 0o600 });
|
|
const r = await run(dir, ['render', ...PREVIEW, '--proof-header', 'X-Console-Proof', '--proof-file', file]);
|
|
assert.equal(r.status, 0, r.stderr);
|
|
assert.ok(execStart(r.stdout).includes(` --proof-header X-Console-Proof --proof-file ${file}`));
|
|
assert.ok(!r.stdout.includes('not-a-valid-proof')); assert.equal(r.stderr, '');
|
|
});
|
|
test('proxy proof render refuses incomplete non-preview missing unsafe-mode and repo/copy paths', async t => {
|
|
const dir = tmp(t), file = join(dir, 'proof.fixture'); writeFileSync(file, 'fixture', { mode: 0o600 });
|
|
const insideDir = mkdtempSync(join(REPO, 'packages/webui/tests/proof-render-'));
|
|
t.after(() => rmSync(insideDir, { recursive: true, force: true }));
|
|
const inside = join(insideDir, 'proof.fixture'); writeFileSync(inside, 'fixture', { mode: 0o600 });
|
|
const copy = join(copyRoot(dir), 'proof.fixture'); writeFileSync(copy, 'fixture', { mode: 0o600 });
|
|
const flags = ['--proof-header', 'x-console-proof', '--proof-file', file];
|
|
const spaced = join(dir, 'proof fixture'); writeFileSync(spaced, 'fixture', { mode: 0o600 });
|
|
const proxy = ['--public-host', 'console.example.com', '--auth-header', 'x-user', '--allow-user', 'jason'];
|
|
const sets = [[...PREVIEW, '--proof-header', 'x-console-proof'], [...PREVIEW, '--proof-file', file], flags,
|
|
...[join(dir, 'missing'), inside, copy].map(f => [...PREVIEW, '--proof-header', 'x-console-proof', '--proof-file', f]),
|
|
// C1 (#1676 comment 29374): each case isolates a render guard with otherwise valid metadata.
|
|
[...PREVIEW, '--proof-header', 'bad_header', '--proof-file', file],
|
|
[...proxy, '--proof-header', 'X-User', '--proof-file', file],
|
|
[...PREVIEW, '--proof-header', 'x-console-proof', '--proof-file', relative(process.cwd(), file)],
|
|
[...PREVIEW, '--proof-header', 'x-console-proof', '--proof-file', spaced]];
|
|
for (const args of sets) {
|
|
const r = await run(dir, ['render', ...args]); assert.equal(r.status, 1); assert.equal(r.stdout, ''); assert.match(r.stderr, /proof/);
|
|
}
|
|
// Fake the render caller's uid, leaving the fixture's actual owner unchanged.
|
|
const bin = quiet(dir), id = join(bin, 'id');
|
|
writeFileSync(id, `#!/bin/sh\nif [ "$#" -eq 1 ] && [ "$1" = -u ]; then printf '%s\\n' '${process.getuid() + 1}'; else exec /usr/bin/id "$@"; fi\n`, { mode: 0o755 });
|
|
try {
|
|
const owner = await run(dir, ['render', ...PREVIEW, ...flags], bin);
|
|
assert.equal(owner.status, 1); assert.equal(owner.stdout, ''); assert.match(owner.stderr, /proof/);
|
|
} finally { rmSync(id); }
|
|
chmodSync(file, 0o644);
|
|
const bad = await run(dir, ['render', ...PREVIEW, ...flags]); assert.equal(bad.status, 1); assert.match(bad.stderr, /proof/);
|
|
});
|
|
|
|
test('proxy proof status retains read-only and tester reporting with both proof flags', async t => {
|
|
const dir = tmp(t), unitDir = join(dir, 'units'), file = join(dir, 'proof.fixture');
|
|
writeFileSync(file, 'fixture', { mode: 0o600 });
|
|
const port = await closedPort(t), bin = standIn(dir);
|
|
const r = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), ...PREVIEW, '--tester-port', '1024', '--proof-header', 'x-console-proof', '--proof-file', file], bin);
|
|
assert.equal(r.status, 0, r.stderr);
|
|
const status = await run(dir, ['status', '--dir', unitDir], bin);
|
|
assert.equal(status.status, 0, status.stderr);
|
|
assert.match(status.stdout, /send: off \(read-only\)/);
|
|
assert.match(status.stdout, /tester: http:\/\/127\.0\.0\.1:1024/);
|
|
});
|
|
|
|
test('row196 socket flag renders only for supporting targets and refuses mixed transports',async t=>{
|
|
const dir=tmp(t);
|
|
const good=await run(dir,['render','--tester-socket']);
|
|
assert.equal(good.status,0,good.stderr);assert.match(execStart(good.stdout),/ --tester-socket$/);
|
|
const old=await run(dir,['render','--copy',COPY_ID,'--tester-socket']);
|
|
assert.equal(old.status,1);assert.match(old.stderr,/tester.socket.*support/i);
|
|
const both=await run(dir,['render','--tester-socket','--tester-port','7333']);
|
|
assert.equal(both.status,4);
|
|
});
|