|
|
|
@@ -11,22 +11,35 @@ 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 the preview set.
|
|
|
|
|
// Row 83 (#1576) adds --send, taken only with the preview set. 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 PREVIEW = ['--public-host', 'console.example.com', '--auth-header', 'X-authentik-uid', '--allow-user', 'jason'];
|
|
|
|
|
const PREVIEW_FLAGS = '--public-host console.example.com --auth-header X-authentik-uid --allow-user jason';
|
|
|
|
|
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}`;
|
|
|
|
|
// 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-console', 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 the preview mode \(--public-host, --auth-header and --allow-user\)$/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.
|
|
|
|
|
function filled(flags) {
|
|
|
|
|
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.replaceAll('@REPO@', REPO).replaceAll('@PATH@', path).replaceAll('@NODE@', process.execPath).replaceAll('@FLAGS@', flags);
|
|
|
|
|
return TEMPLATE.replaceAll('@REPO@', root).replaceAll('@PATH@', path).replaceAll('@NODE@', process.execPath).replaceAll('@FLAGS@', flags);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A loopback port with nothing listening, so status's /healthz request
|
|
|
|
@@ -44,18 +57,35 @@ async function closedPort() {
|
|
|
|
|
function tmp(t) {
|
|
|
|
|
const dir = mkdtempSync(join(tmpdir(), 'webui-service-'));
|
|
|
|
|
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
|
|
|
fakeCopy(dir);
|
|
|
|
|
return dir;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A stand-in systemctl that prints `lines` for list-units and logs every call.
|
|
|
|
|
function standIn(dir, lines = '') {
|
|
|
|
|
// The bus unit as `systemctl show -p LoadState -p ExecStart` prints it,
|
|
|
|
|
// started from `root`.
|
|
|
|
|
const busShow = (root) => `LoadState=loaded\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.
|
|
|
|
|
function standIn(dir, lines = '', { listFails = false, bus = null, busUnits = BUS_UNITS, busFails = false } = {}) {
|
|
|
|
|
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);
|
|
|
|
|
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-console/${COPY_ID}"; printf '%s' "${busShow('$r')}"`;
|
|
|
|
|
writeFileSync(join(bin, 'systemctl'), `#!/bin/sh
|
|
|
|
|
echo "$*" >> "${dir}/calls.txt"
|
|
|
|
|
case "$*" in
|
|
|
|
|
*list-units*) cat "${dir}/list-units.txt" ;;
|
|
|
|
|
*list-units*mosaic-bus@*) ${busFails ? fail : `cat "${dir}/bus-units.txt"`} ;;
|
|
|
|
|
*show*mosaic-bus@*) ${busFails ? fail : show} ;;
|
|
|
|
|
*list-units*) ${listFails ? fail : `cat "${dir}/list-units.txt"`} ;;
|
|
|
|
|
*is-enabled*) echo enabled ;;
|
|
|
|
|
*is-active*) echo active ;;
|
|
|
|
|
esac
|
|
|
|
@@ -65,9 +95,20 @@ exit 0
|
|
|
|
|
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 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 ? `${bin}:` : ''}${NODE_DIR}:/usr/bin:/bin`, HOME: dir, XDG_CONFIG_HOME: join(dir, 'config') };
|
|
|
|
|
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; });
|
|
|
|
@@ -95,7 +136,7 @@ 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 jason`);
|
|
|
|
|
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) => {
|
|
|
|
@@ -153,8 +194,9 @@ test('install into --dir with --no-reload writes the file whole, then reports un
|
|
|
|
|
const dir = tmp(t);
|
|
|
|
|
const unitDir = join(dir, 'units');
|
|
|
|
|
const bin = standIn(dir);
|
|
|
|
|
const rendered = await run(dir, ['render', '--business', 'mosaic']);
|
|
|
|
|
const r = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--business', 'mosaic'], bin);
|
|
|
|
|
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'));
|
|
|
|
@@ -162,41 +204,50 @@ test('install into --dir with --no-reload writes the file whole, then reports un
|
|
|
|
|
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', '--dir', unitDir, '--no-reload', '--business', 'mosaic'], bin);
|
|
|
|
|
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.ok(!existsSync(join(dir, 'calls.txt')), 'no systemctl call with --no-reload');
|
|
|
|
|
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', '--dir', unitDir, '--no-reload', '--business', 'a b'], bin);
|
|
|
|
|
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', '--dir', fresh, '--no-reload', '--port', '0'], bin)).status, 1);
|
|
|
|
|
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', '--dir', join(dir, 'units')], bin);
|
|
|
|
|
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 daemon-reload\n');
|
|
|
|
|
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 ExecStart\n--user list-units --no-legend --plain mosaic-webui.service\n--user daemon-reload\n');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('uninstall refuses while the unit is active, and removes it once it is not', async (t) => {
|
|
|
|
|
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');
|
|
|
|
|
for (const state of ['active', 'activating', 'deactivating']) {
|
|
|
|
|
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`);
|
|
|
|
|
assert.equal((await run(dir, ['install', '--dir', unitDir, '--no-reload'])).status, 0);
|
|
|
|
|
const r = await run(dir, ['uninstall', '--dir', unitDir], bin);
|
|
|
|
|
assert.equal(r.status, 1, state);
|
|
|
|
|
assert.match(r.stderr, /refusing: mosaic-webui\.service is still running/);
|
|
|
|
|
assert.ok(existsSync(target), `still installed while ${state}`);
|
|
|
|
|
// 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);
|
|
|
|
@@ -220,9 +271,10 @@ test('status prints the unit state and GET /healthz on the installed port', asyn
|
|
|
|
|
await once(server, 'listening');
|
|
|
|
|
t.after(() => server.close());
|
|
|
|
|
const { port } = server.address();
|
|
|
|
|
assert.equal((await run(dir, ['install', '--dir', unitDir, '--no-reload', '--port', String(port)])).status, 0);
|
|
|
|
|
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'));
|
|
|
|
@@ -237,7 +289,7 @@ test('status prints the unit state and GET /healthz on the installed port', asyn
|
|
|
|
|
|
|
|
|
|
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]]) {
|
|
|
|
|
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/);
|
|
|
|
@@ -268,7 +320,7 @@ test('render --send with the preview set ends ExecStart with --send and changes
|
|
|
|
|
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(' --allow-user jason --send'), execStart(r.stdout));
|
|
|
|
|
assert.ok(execStart(r.stdout).endsWith(` --allow-user ${UID} --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');
|
|
|
|
@@ -290,20 +342,20 @@ test('--send without the whole preview set is refused and writes nothing', async
|
|
|
|
|
const bin = standIn(dir);
|
|
|
|
|
const fresh = join(dir, 'never');
|
|
|
|
|
for (const args of partial) {
|
|
|
|
|
const r = await run(dir, ['install', '--dir', fresh, '--no-reload', ...args, '--send'], bin);
|
|
|
|
|
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', '--dir', unitDir, '--no-reload', ...PREVIEW], bin)).status, 0);
|
|
|
|
|
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', '--dir', unitDir, '--no-reload', '--send', ...PREVIEW.slice(0, 4)], bin);
|
|
|
|
|
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.ok(!existsSync(join(dir, 'calls.txt')), 'no systemctl call');
|
|
|
|
|
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) => {
|
|
|
|
@@ -312,13 +364,14 @@ test('install --send into --dir with --no-reload writes the sending unit, and st
|
|
|
|
|
const target = join(unitDir, 'mosaic-webui.service');
|
|
|
|
|
const port = await closedPort();
|
|
|
|
|
const flags = `--port ${port} ${PREVIEW_FLAGS}`;
|
|
|
|
|
const r = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--port', String(port), ...PREVIEW, '--send']);
|
|
|
|
|
const r = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), ...PREVIEW, '--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);
|
|
|
|
|
assert.match(r.stdout, new RegExp(`^serve: \\S+ ${REPO.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/packages/webui/src/cli\\.mjs serve ${flags} --send$`, 'm'));
|
|
|
|
|
assert.equal(readFileSync(target, 'utf8'), filled(`${flags} --send`));
|
|
|
|
|
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);
|
|
|
|
@@ -326,34 +379,34 @@ test('install --send into --dir with --no-reload writes the sending unit, and st
|
|
|
|
|
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', '--dir', unitDir, '--no-reload', '--port', String(port), '--send', ...PREVIEW], bin);
|
|
|
|
|
const again = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), '--send', ...PREVIEW], bin);
|
|
|
|
|
assert.match(again.stdout, new RegExp(`^unchanged: ${target}$`, 'm'));
|
|
|
|
|
const readOnly = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--port', String(port), ...PREVIEW], bin);
|
|
|
|
|
const readOnly = await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', '--port', String(port), ...PREVIEW], bin);
|
|
|
|
|
assert.match(readOnly.stdout, new RegExp(`^written: ${target}$`, 'm'));
|
|
|
|
|
assert.equal(readFileSync(target, 'utf8'), filled(flags));
|
|
|
|
|
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; install ran none.
|
|
|
|
|
assert.deepEqual(readFileSync(join(dir, 'calls.txt'), 'utf8').split('\n').filter(Boolean).filter((c) => !/^--user (is-enabled|is-active|show) mosaic-webui\.service/.test(c)), []);
|
|
|
|
|
// 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');
|
|
|
|
|
// serve takes --send as an allowed user, so ExecStart can end with
|
|
|
|
|
// `--allow-user --send` on a unit that doesn't send.
|
|
|
|
|
const oddUser = [...PREVIEW.slice(0, 4), '--allow-user', '--send'];
|
|
|
|
|
for (const [args, ending, state] of [[oddUser, '--allow-user --send', 'off'], [[...oddUser, '--send'], '--allow-user --send --send', 'on']]) {
|
|
|
|
|
const r = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--port', String(port), ...args]);
|
|
|
|
|
assert.equal(r.status, 0, r.stderr);
|
|
|
|
|
assert.ok(execStart(readFileSync(target, 'utf8')).endsWith(` ${ending}`), args.join(' '));
|
|
|
|
|
assert.match((await run(dir, ['status', '--dir', unitDir], bin)).stdout, new RegExp(`^send: ${state} `, 'm'), args.join(' '));
|
|
|
|
|
// serve would take --send as an allowed user; N5 refuses it, so no unit
|
|
|
|
|
// ends with `--allow-user --send` without sending.
|
|
|
|
|
const before = readFileSync(target, 'utf8');
|
|
|
|
|
for (const args of [['--public-host', 'console.example.com', '--auth-header', 'X-authentik-username', '--allow-user', '--send'], ['--public-host', 'console.example.com', '--auth-header', 'X-authentik-username', '--allow-user', '--send', '--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', '--dir', unitDir, '--no-reload', ...PREVIEW, '--send'])).status, 0);
|
|
|
|
|
assert.equal((await run(dir, ['install', ...INSTALL, '--dir', unitDir, '--no-reload', ...PREVIEW, '--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(' '));
|
|
|
|
@@ -363,3 +416,234 @@ test('uninstall and status take no --send', async (t) => {
|
|
|
|
|
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-console'))}; 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 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`);
|
|
|
|
|
// 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 ExecStart/);
|
|
|
|
|
rmSync(unitDir, { recursive: true });
|
|
|
|
|
|
|
|
|
|
// 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`);
|
|
|
|
|
// 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, ...PREVIEW, '--send']);
|
|
|
|
|
assert.equal(r.status, 0, r.stderr);
|
|
|
|
|
assert.equal(r.stdout, filled(`--checkout ${REPO} --port 7332 ${PREVIEW_FLAGS} --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();
|
|
|
|
|
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));
|
|
|
|
|
// 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)), []);
|
|
|
|
|
});
|
|
|
|
|