test: stubbed shell tests for the NodeSource bootstrap logic
Covers the scenarios codex-reviewer recommended: suitable candidate already present (incl. epoch stripping), missing metadata healed by a refresh, bootstrap on too-old distro nodejs, bootstrap failure, and the refuse-to-overwrite branch for a user-managed nodesource.list. Every scenario runs under a localized LC_ALL with an apt-cache stub that only emits the English Candidate: label under LC_ALL=C, so locale-safe parsing is regression-tested (mutation-checked: dropping LC_ALL=C fails 3 tests). install-apt.sh gains STOKE_APT_ETC to redirect /etc/apt to a throwaway directory under test, following the script's existing env-override pattern. Real-container flow re-verified on debian:13. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5e99006d04
commit
444470301c
2 changed files with 141 additions and 5 deletions
|
|
@ -18,9 +18,12 @@ FORGE_URL="${FORGE_URL:-https://forgejo.heavyduty.builders}"
|
|||
OWNER="${OWNER:-heavy-duty}"
|
||||
DISTRIBUTION="${DISTRIBUTION:-stable}"
|
||||
COMPONENT="${COMPONENT:-main}"
|
||||
# Where apt configuration lives; overridable so tests can run against a
|
||||
# throwaway directory instead of the real /etc/apt.
|
||||
APT_ETC="${STOKE_APT_ETC:-/etc/apt}"
|
||||
|
||||
KEYRING="/etc/apt/keyrings/forgejo-$OWNER.asc"
|
||||
LIST="/etc/apt/sources.list.d/forgejo-$OWNER.list"
|
||||
KEYRING="$APT_ETC/keyrings/forgejo-$OWNER.asc"
|
||||
LIST="$APT_ETC/sources.list.d/forgejo-$OWNER.list"
|
||||
|
||||
SUDO=""
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
|
|
@ -57,8 +60,8 @@ ensure_nodejs_source() {
|
|||
echo "No apt source seems to provide nodejs >= $NODE_MIN; refreshing apt metadata ..."
|
||||
$SUDO apt-get update || true
|
||||
node_candidate_ok && return 0
|
||||
local ns_keyring="/etc/apt/keyrings/nodesource.asc"
|
||||
local ns_list="/etc/apt/sources.list.d/nodesource.list"
|
||||
local ns_keyring="$APT_ETC/keyrings/nodesource.asc"
|
||||
local ns_list="$APT_ETC/sources.list.d/nodesource.list"
|
||||
if [ -e "$ns_list" ]; then
|
||||
echo "error: nodejs >= $NODE_MIN is unavailable and $ns_list already exists;" >&2
|
||||
echo "refusing to overwrite it. Point it at a Node >= 22 release and re-run." >&2
|
||||
|
|
@ -73,7 +76,7 @@ ensure_nodejs_source() {
|
|||
}
|
||||
|
||||
echo "Adding APT source for $FORGE_URL/$OWNER ..."
|
||||
$SUDO install -d -m 0755 /etc/apt/keyrings
|
||||
$SUDO install -d -m 0755 "$APT_ETC/keyrings"
|
||||
curl -fsSL "$FORGE_URL/api/packages/$OWNER/debian/repository.key" | $SUDO tee "$KEYRING" >/dev/null
|
||||
echo "deb [signed-by=$KEYRING] $FORGE_URL/api/packages/$OWNER/debian $DISTRIBUTION $COMPONENT" \
|
||||
| $SUDO tee "$LIST" >/dev/null
|
||||
|
|
|
|||
133
test/install-apt.test.js
Normal file
133
test/install-apt.test.js
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
|
||||
const SCRIPT = path.join(__dirname, '..', 'scripts', 'install-apt.sh');
|
||||
|
||||
// Runs install-apt.sh against a throwaway apt directory (STOKE_APT_ETC) with
|
||||
// every external command stubbed via PATH. Scenario knobs:
|
||||
// candInitial `apt-cache policy` Candidate before any update
|
||||
// candAfterUpdate Candidate after any `apt-get update`
|
||||
// candAfterNodesource Candidate after an update once nodesource.list exists
|
||||
// The apt-cache stub localizes the "Candidate:" label unless LC_ALL=C is set,
|
||||
// so every scenario doubles as a regression test for locale-safe parsing.
|
||||
function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexistingNodesourceList }) {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'stoke-apt-test-'));
|
||||
const bin = path.join(root, 'bin');
|
||||
const state = path.join(root, 'state');
|
||||
const aptEtc = path.join(root, 'etc', 'apt');
|
||||
fs.mkdirSync(bin, { recursive: true });
|
||||
fs.mkdirSync(state, { recursive: true });
|
||||
fs.mkdirSync(path.join(aptEtc, 'sources.list.d'), { recursive: true });
|
||||
fs.writeFileSync(path.join(state, 'candidate'), candInitial);
|
||||
if (preexistingNodesourceList !== undefined) {
|
||||
fs.writeFileSync(path.join(aptEtc, 'sources.list.d', 'nodesource.list'), preexistingNodesourceList);
|
||||
}
|
||||
|
||||
const stub = (name, body) => {
|
||||
const p = path.join(bin, name);
|
||||
fs.writeFileSync(p, `#!/usr/bin/env bash\n${body}\n`, { mode: 0o755 });
|
||||
};
|
||||
|
||||
// Force the non-root path so every mutation goes through the sudo stub.
|
||||
stub('id', 'echo 1000');
|
||||
stub('sudo', 'exec "$@"');
|
||||
stub('curl', 'echo "FAKE-KEY"');
|
||||
stub('stoke', 'echo 1.2.0');
|
||||
stub('apt-cache', [
|
||||
'cand="$(cat "$STATE_DIR/candidate")"',
|
||||
'[ "$cand" = "absent" ] && exit 0',
|
||||
'label="Candidato"',
|
||||
'[ "${LC_ALL:-}" = "C" ] && label="Candidate"',
|
||||
'printf "nodejs:\\n Installed: (none)\\n %s: %s\\n" "$label" "$cand"',
|
||||
].join('\n'));
|
||||
stub('apt-get', [
|
||||
'echo "apt-get $*" >> "$STATE_DIR/apt-get.log"',
|
||||
'for a in "$@"; do',
|
||||
' if [ "$a" = update ]; then',
|
||||
' if [ -e "$STOKE_APT_ETC/sources.list.d/nodesource.list" ] && [ -n "${CAND_AFTER_NODESOURCE:-}" ]; then',
|
||||
' echo "$CAND_AFTER_NODESOURCE" > "$STATE_DIR/candidate"',
|
||||
' elif [ -n "${CAND_AFTER_UPDATE:-}" ]; then',
|
||||
' echo "$CAND_AFTER_UPDATE" > "$STATE_DIR/candidate"',
|
||||
' fi',
|
||||
' fi',
|
||||
'done',
|
||||
'exit 0',
|
||||
].join('\n'));
|
||||
|
||||
const res = spawnSync('bash', [SCRIPT], {
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: `${bin}:${process.env.PATH}`,
|
||||
STOKE_APT_ETC: aptEtc,
|
||||
STATE_DIR: state,
|
||||
CAND_AFTER_UPDATE: candAfterUpdate || '',
|
||||
CAND_AFTER_NODESOURCE: candAfterNodesource || '',
|
||||
LC_ALL: 'es_ES.UTF-8', // localized environment; the script must force C
|
||||
},
|
||||
});
|
||||
|
||||
const read = (p) => (fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : null);
|
||||
return {
|
||||
res,
|
||||
aptEtc,
|
||||
nodesourceList: read(path.join(aptEtc, 'sources.list.d', 'nodesource.list')),
|
||||
nodesourceKey: read(path.join(aptEtc, 'keyrings', 'nodesource.asc')),
|
||||
aptGetLog: read(path.join(state, 'apt-get.log')) || '',
|
||||
};
|
||||
}
|
||||
|
||||
test('suitable nodejs candidate already available: installs without touching NodeSource', () => {
|
||||
// Epoch-prefixed version also covers the epoch-stripping in the comparison.
|
||||
const s = runScenario({ candInitial: '1:22.23.1-1nodesource1' });
|
||||
assert.equal(s.res.status, 0, s.res.stderr);
|
||||
assert.equal(s.nodesourceList, null);
|
||||
assert.match(s.aptGetLog, /install -y stoke/);
|
||||
});
|
||||
|
||||
test('no cached metadata: refreshes apt lists before deciding, no NodeSource needed', () => {
|
||||
const s = runScenario({ candInitial: 'absent', candAfterUpdate: '22.23.1-1nodesource1' });
|
||||
assert.equal(s.res.status, 0, s.res.stderr);
|
||||
assert.equal(s.nodesourceList, null);
|
||||
assert.match(s.aptGetLog, /install -y stoke/);
|
||||
});
|
||||
|
||||
test('distro nodejs too old: bootstraps NodeSource and installs', () => {
|
||||
const s = runScenario({
|
||||
candInitial: '20.19.2+dfsg-1+deb13u2',
|
||||
candAfterUpdate: '20.19.2+dfsg-1+deb13u2',
|
||||
candAfterNodesource: '22.23.1-1nodesource1',
|
||||
});
|
||||
assert.equal(s.res.status, 0, s.res.stderr);
|
||||
assert.match(s.nodesourceList, /deb \[signed-by=.*nodesource\.asc\] https:\/\/deb\.nodesource\.com\/node_22\.x nodistro main/);
|
||||
assert.equal(s.nodesourceKey, 'FAKE-KEY\n');
|
||||
assert.match(s.aptGetLog, /install -y stoke/);
|
||||
});
|
||||
|
||||
test('bootstrap failure: NodeSource still lacks a suitable nodejs, exits with error', () => {
|
||||
const s = runScenario({
|
||||
candInitial: '20.19.2+dfsg-1+deb13u2',
|
||||
candAfterUpdate: '20.19.2+dfsg-1+deb13u2',
|
||||
candAfterNodesource: '20.19.2+dfsg-1+deb13u2',
|
||||
});
|
||||
assert.notEqual(s.res.status, 0);
|
||||
assert.match(s.res.stderr, /still no nodejs >= 22\.12/);
|
||||
assert.doesNotMatch(s.aptGetLog, /install -y stoke/);
|
||||
});
|
||||
|
||||
test('pre-existing user-managed nodesource.list is never overwritten', () => {
|
||||
const marker = '# user-managed entry\n';
|
||||
const s = runScenario({
|
||||
candInitial: '18.19.1+dfsg-6ubuntu5',
|
||||
candAfterUpdate: '18.19.1+dfsg-6ubuntu5',
|
||||
preexistingNodesourceList: marker,
|
||||
});
|
||||
assert.notEqual(s.res.status, 0);
|
||||
assert.match(s.res.stderr, /refusing to overwrite/);
|
||||
assert.equal(s.nodesourceList, marker);
|
||||
assert.doesNotMatch(s.aptGetLog, /install -y stoke/);
|
||||
});
|
||||
Loading…
Reference in a new issue