Harden publish/build/install scripts and fix audit findings
Some checks failed
ci / test (pull_request) Has been cancelled
Some checks failed
ci / test (pull_request) Has been cancelled
- publish-deb: keep the token out of the process list (curl -K config file via mktemp, no JSON round-trip through node argv), mktemp the response file with trap cleanup, add --max-time to the upload - build-deb: umask 022 + chmod -R a+rX so the payload is world-readable even when built with umask 077 - install-apt: only fall back to [trusted=yes] on an actual signature verification failure; other apt-get update failures stay fatal - auth logout: warn that a manually supplied token stays active on the server and point at the web UI revocation page - repo import-batch: resolve the source token inside the per-item try so one bad item no longer aborts the whole batch - auth status: print me.login (the /user response has no username field) and exit 1 when not authenticated
This commit is contained in:
parent
ee0cb85c7b
commit
33079afb33
7 changed files with 279 additions and 38 deletions
|
|
@ -11,6 +11,9 @@
|
|||
# Requirements: bash, node/npm, dpkg-deb, gzip. Runs lintian when available.
|
||||
|
||||
set -euo pipefail
|
||||
# The payload must be world-readable regardless of the builder's umask
|
||||
# (with umask 077, `stoke` would be unreadable for non-root after install).
|
||||
umask 022
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
VERSION="$(node -p "require('$ROOT/package.json').version")"
|
||||
|
|
@ -68,8 +71,9 @@ EOF
|
|||
gzip -9n -c "$STAGE/changelog" > "$DOC/changelog.gz"
|
||||
|
||||
# Normalize permissions regardless of the builder's umask: no group/other
|
||||
# write anywhere, executable entry point.
|
||||
# write anywhere, everything readable, executable entry point.
|
||||
chmod -R go-w "$PKG/usr"
|
||||
chmod -R a+rX "$PKG/usr"
|
||||
chmod 0755 "$LIB/src/cli.js"
|
||||
|
||||
# --- control -----------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -106,7 +106,19 @@ fi
|
|||
# in the upstream signing library). Try the properly signed source first so
|
||||
# this heals automatically once the forge is fixed; otherwise fall back to
|
||||
# [trusted=yes] — package integrity then relies on HTTPS to our own forge.
|
||||
if ! update_only_source "$LIST"; then
|
||||
# The fallback is only justified for an actual signature-verification
|
||||
# failure: a transient network error must not permanently disable
|
||||
# verification, so any other `apt-get update` failure is fatal.
|
||||
update_failed=""
|
||||
update_output="$(update_only_source "$LIST" 2>&1)" || update_failed=1
|
||||
[ -z "$update_output" ] || echo "$update_output"
|
||||
if [ -n "$update_failed" ]; then
|
||||
if ! echo "$update_output" | grep -Eiq 'NO_PUBKEY|KEYEXPIRED|not signed|no longer signed|signature'; then
|
||||
echo "error: apt-get update failed for the new source (see above), but not" >&2
|
||||
echo "with a signature-verification error. Refusing to fall back to" >&2
|
||||
echo "[trusted=yes]; fix the underlying problem and re-run." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo
|
||||
echo "WARNING: signature verification failed (known Forgejo registry issue" >&2
|
||||
echo "with sqv-based apt). Falling back to [trusted=yes]; transport" >&2
|
||||
|
|
|
|||
|
|
@ -26,17 +26,37 @@ COMPONENT="${4:-main}"
|
|||
|
||||
[ -f "$DEB" ] || { echo "error: no such file: $DEB" >&2; exit 1; }
|
||||
|
||||
CONFIG_JSON="$(node -e "const c = require('$ROOT/src/config').loadConfig(); if (c) process.stdout.write(JSON.stringify(c));" 2>/dev/null || true)"
|
||||
TOKEN="${STOKE_TOKEN:-$(node -pe "(JSON.parse(process.argv[1] || '{}').token) || ''" "$CONFIG_JSON")}"
|
||||
FORGE_URL="${FORGE_URL:-$(node -pe "(JSON.parse(process.argv[1] || '{}').url) || 'https://forgejo.heavyduty.builders'" "$CONFIG_JSON")}"
|
||||
# Read the config field inside node so the values (token included) never
|
||||
# pass through this script's argv or environment, where they would show up
|
||||
# in the process list. A corrupt config makes node fail with the real parse
|
||||
# error — surfaced by `set -e` — instead of a misleading "no token" message.
|
||||
read_config_field() {
|
||||
node -e "
|
||||
const c = require('$ROOT/src/config').loadConfig();
|
||||
process.stdout.write(String((c && c.$1) || ''));
|
||||
"
|
||||
}
|
||||
|
||||
TOKEN="${STOKE_TOKEN:-$(read_config_field token)}"
|
||||
FORGE_URL="${FORGE_URL:-$(read_config_field url)}"
|
||||
FORGE_URL="${FORGE_URL:-https://forgejo.heavyduty.builders}"
|
||||
|
||||
[ -n "$TOKEN" ] || { echo "error: no token. Set STOKE_TOKEN or run: stoke auth login" >&2; exit 1; }
|
||||
|
||||
# The token goes to curl through a config file (passed with -K) instead of a
|
||||
# -H argument so it never appears in the process list; response body and
|
||||
# config file are mktemp'd and cleaned up on exit.
|
||||
CURL_CONFIG="$(mktemp)"
|
||||
RESPONSE="$(mktemp)"
|
||||
trap 'rm -f "$CURL_CONFIG" "$RESPONSE"' EXIT
|
||||
chmod 0600 "$CURL_CONFIG"
|
||||
printf 'header = "Authorization: token %s"\n' "$TOKEN" > "$CURL_CONFIG"
|
||||
|
||||
URL="$FORGE_URL/api/packages/$OWNER/debian/pool/$DISTRIBUTION/$COMPONENT/upload"
|
||||
echo "Uploading $(basename "$DEB") to $URL"
|
||||
|
||||
STATUS="$(curl -sS -o /tmp/stoke-publish-response.$$ -w '%{http_code}' \
|
||||
-X PUT -H "Authorization: token $TOKEN" \
|
||||
STATUS="$(curl -sS -o "$RESPONSE" -w '%{http_code}' --max-time 300 \
|
||||
-X PUT -K "$CURL_CONFIG" \
|
||||
--upload-file "$DEB" "$URL")"
|
||||
|
||||
case "$STATUS" in
|
||||
|
|
@ -44,9 +64,7 @@ case "$STATUS" in
|
|||
409) echo "Already published (409): this exact version already exists in the registry." ;;
|
||||
*)
|
||||
echo "error: upload failed with HTTP $STATUS" >&2
|
||||
cat /tmp/stoke-publish-response.$$ >&2 || true
|
||||
rm -f /tmp/stoke-publish-response.$$
|
||||
cat "$RESPONSE" >&2 || true
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
rm -f /tmp/stoke-publish-response.$$
|
||||
|
|
|
|||
59
src/cli.js
59
src/cli.js
|
|
@ -194,7 +194,7 @@ auth
|
|||
config = {
|
||||
url,
|
||||
login: me.login,
|
||||
username: me.username || me.login,
|
||||
username: me.login,
|
||||
email: me.email,
|
||||
token,
|
||||
tokenId: null,
|
||||
|
|
@ -213,7 +213,7 @@ auth
|
|||
config = {
|
||||
url,
|
||||
login,
|
||||
username: me.username || login,
|
||||
username: login,
|
||||
email: me.email,
|
||||
token: tokenRes.sha1,
|
||||
tokenId: tokenRes.id,
|
||||
|
|
@ -263,6 +263,11 @@ auth
|
|||
} else {
|
||||
console.log(`Skipping remote revocation (no password provided). Token ${config.tokenId} stays active on ${config.url}; revoke it from the web UI under Settings > Applications.`);
|
||||
}
|
||||
} else if (!config.tokenId && !options.localOnly) {
|
||||
// A manually supplied token (-t/--token-file) has no id stoke knows,
|
||||
// so it cannot be revoked remotely. Say so instead of leaving the
|
||||
// user thinking logout disabled it.
|
||||
console.log(`The stored token was provided manually and cannot be revoked remotely; it stays active on ${config.url}. Revoke it from the web UI under Settings > Applications.`);
|
||||
}
|
||||
|
||||
clearConfig();
|
||||
|
|
@ -282,7 +287,7 @@ auth
|
|||
const config = loadConfig();
|
||||
if (!config || !config.token) {
|
||||
console.log('Not authenticated.');
|
||||
return;
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = ForgejoClient.fromConfig(config);
|
||||
|
|
@ -293,7 +298,7 @@ auth
|
|||
}
|
||||
console.log('Instance: ', config.url);
|
||||
console.log('Login: ', me.login);
|
||||
console.log('Username: ', me.username);
|
||||
console.log('Username: ', me.login);
|
||||
console.log('Email: ', me.email);
|
||||
console.log('Token path: ', getConfigPath());
|
||||
} catch (err) {
|
||||
|
|
@ -574,29 +579,31 @@ repo
|
|||
}
|
||||
|
||||
const service = item.service || 'github';
|
||||
const isPrivate = item.public ? false : Boolean(item.private);
|
||||
const payload = {
|
||||
clone_addr: from,
|
||||
repo_name: name,
|
||||
repo_owner: item.owner || item.repo_owner || config.login,
|
||||
service,
|
||||
description: item.description || undefined,
|
||||
private: isPrivate,
|
||||
issues: normalizeBool(item.issues, true),
|
||||
labels: normalizeBool(item.labels, true),
|
||||
milestones: normalizeBool(item.milestones, true),
|
||||
pull_requests: normalizeBool(item.pull_requests, true),
|
||||
releases: normalizeBool(item.releases, true),
|
||||
wiki: normalizeBool(item.wiki, true),
|
||||
lfs: normalizeBool(item.lfs, false),
|
||||
auth_token: resolveSourceToken(item.github_token, service),
|
||||
};
|
||||
|
||||
Object.keys(payload).forEach((key) => {
|
||||
if (payload[key] === undefined) delete payload[key];
|
||||
});
|
||||
|
||||
try {
|
||||
// Token resolution happens per item, inside the try: a missing
|
||||
// source token must fail this item, not abort the whole batch.
|
||||
const isPrivate = item.public ? false : Boolean(item.private);
|
||||
const payload = {
|
||||
clone_addr: from,
|
||||
repo_name: name,
|
||||
repo_owner: item.owner || item.repo_owner || config.login,
|
||||
service,
|
||||
description: item.description || undefined,
|
||||
private: isPrivate,
|
||||
issues: normalizeBool(item.issues, true),
|
||||
labels: normalizeBool(item.labels, true),
|
||||
milestones: normalizeBool(item.milestones, true),
|
||||
pull_requests: normalizeBool(item.pull_requests, true),
|
||||
releases: normalizeBool(item.releases, true),
|
||||
wiki: normalizeBool(item.wiki, true),
|
||||
lfs: normalizeBool(item.lfs, false),
|
||||
auth_token: resolveSourceToken(item.github_token, service),
|
||||
};
|
||||
|
||||
Object.keys(payload).forEach((key) => {
|
||||
if (payload[key] === undefined) delete payload[key];
|
||||
});
|
||||
|
||||
const result = await client.migrateRepo(payload);
|
||||
console.log(`Imported: ${result.full_name} -> ${result.html_url}`);
|
||||
results.push({ name, status: 'ok', url: result.html_url });
|
||||
|
|
|
|||
46
test/build-deb.test.js
Normal file
46
test/build-deb.test.js
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { execFileSync, spawnSync } = require('node:child_process');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const SCRIPT = path.join(ROOT, 'scripts', 'build-deb.sh');
|
||||
const pkg = require('../package.json');
|
||||
|
||||
// Builds the .deb with a restrictive umask (npm stubbed out — dependency
|
||||
// installation is irrelevant to permissions and would need the network) and
|
||||
// asserts the payload is world-readable: with umask 077 and no explicit
|
||||
// normalization, `stoke` would be unusable for non-root after install.
|
||||
test('payload files are world-readable even when built with umask 077', () => {
|
||||
const bin = fs.mkdtempSync(path.join(os.tmpdir(), 'stoke-build-test-'));
|
||||
fs.writeFileSync(path.join(bin, 'npm'), '#!/usr/bin/env bash\nexit 0\n', { mode: 0o755 });
|
||||
const distDir = path.join(ROOT, 'dist');
|
||||
const deb = path.join(distDir, `stoke_${pkg.version}_all.deb`);
|
||||
const distExisted = fs.existsSync(distDir);
|
||||
try {
|
||||
const res = spawnSync('bash', ['-c', 'umask 077 && exec bash "$1"', 'bash', SCRIPT], {
|
||||
encoding: 'utf8',
|
||||
env: { ...process.env, PATH: `${bin}:${process.env.PATH}` },
|
||||
});
|
||||
assert.equal(res.status, 0, res.stderr);
|
||||
|
||||
const listing = execFileSync('bash', ['-c', 'dpkg-deb --fsys-tarfile "$1" | tar -tv', 'bash', deb], {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
const entries = listing.trim().split('\n').filter((l) => l.includes('/usr/'));
|
||||
assert.ok(entries.length > 0, 'payload listing must not be empty');
|
||||
for (const line of entries) {
|
||||
const perms = line.split(/\s+/)[0];
|
||||
if (perms.startsWith('l')) continue; // symlink target perms are irrelevant
|
||||
assert.equal(perms[7], 'r', `not world-readable: ${line}`);
|
||||
assert.equal(perms[5], '-', `group-writable: ${line}`);
|
||||
assert.equal(perms[8], '-', `other-writable: ${line}`);
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(bin, { recursive: true, force: true });
|
||||
fs.rmSync(deb, { force: true });
|
||||
if (!distExisted) fs.rmSync(distDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
117
test/cli.test.js
117
test/cli.test.js
|
|
@ -40,7 +40,14 @@ test('global --config flag overrides the config location', () => {
|
|||
// "Not authenticated" instead of silently using the default config.
|
||||
const missing = path.join(os.tmpdir(), `stoke-missing-${process.pid}.json`);
|
||||
const res = run(['--config', missing, 'auth', 'status']);
|
||||
assert.equal(res.status, 0);
|
||||
assert.equal(res.status, 1);
|
||||
assert.match(res.stdout, /Not authenticated/);
|
||||
});
|
||||
|
||||
test('auth status exits 1 when not authenticated', () => {
|
||||
const missing = path.join(os.tmpdir(), `stoke-none-${process.pid}.json`);
|
||||
const res = run(['auth', 'status'], { STOKE_CONFIG_FILE: missing });
|
||||
assert.equal(res.status, 1);
|
||||
assert.match(res.stdout, /Not authenticated/);
|
||||
});
|
||||
|
||||
|
|
@ -720,3 +727,111 @@ test('pr review --commit sends commit_id only when given', async () => {
|
|||
fs.unlinkSync(cfg);
|
||||
}
|
||||
});
|
||||
|
||||
test('auth status prints the login field instead of undefined', async () => {
|
||||
const cfg = path.join(os.tmpdir(), `stoke-cfg-${process.pid}-status.json`);
|
||||
const TIMEOUT_MS = 5000;
|
||||
let timer;
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
const fail = (err) => {
|
||||
clearTimeout(timer);
|
||||
try { server.close(); } catch { /* already closed */ }
|
||||
reject(err instanceof Error ? err : new Error(String(err)));
|
||||
};
|
||||
const server = http.createServer((req, res) => {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ login: 'bot', email: 'bot@forge.test' }));
|
||||
});
|
||||
timer = setTimeout(() => fail(new Error('timeout')), TIMEOUT_MS);
|
||||
server.listen(0, '127.0.0.1', async () => {
|
||||
const { port } = server.address();
|
||||
fs.writeFileSync(cfg, JSON.stringify({ url: `http://127.0.0.1:${port}`, token: 'tok' }));
|
||||
try {
|
||||
const res = await spawnAsync(['auth', 'status'], { STOKE_CONFIG_FILE: cfg });
|
||||
clearTimeout(timer);
|
||||
server.close(() => resolve(res));
|
||||
} catch (err) {
|
||||
fail(err);
|
||||
}
|
||||
});
|
||||
}).finally(() => clearTimeout(timer));
|
||||
|
||||
try {
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.match(result.stdout, /Login:\s+bot/);
|
||||
assert.match(result.stdout, /Username:\s+bot/);
|
||||
assert.doesNotMatch(result.stdout, /undefined/);
|
||||
} finally {
|
||||
fs.unlinkSync(cfg);
|
||||
}
|
||||
});
|
||||
|
||||
test('auth logout warns that a manually supplied token stays active on the server', () => {
|
||||
// tokenId: null is what `auth login -t/--token-file` stores.
|
||||
const cfg = path.join(os.tmpdir(), `stoke-cfg-${process.pid}-logout.json`);
|
||||
fs.writeFileSync(cfg, JSON.stringify({
|
||||
url: 'https://forge.test', login: 'alice', username: 'alice', token: 'tok', tokenId: null,
|
||||
}));
|
||||
const res = run(['auth', 'logout'], { STOKE_CONFIG_FILE: cfg });
|
||||
assert.equal(res.status, 0, res.stderr);
|
||||
assert.match(res.stdout, /Settings > Applications/);
|
||||
assert.match(res.stdout, /Local credentials removed/);
|
||||
assert.equal(fs.existsSync(cfg), false, 'config must be removed');
|
||||
});
|
||||
|
||||
test('repo import-batch fails the item with a missing source token but continues the batch', async () => {
|
||||
const cfg = path.join(os.tmpdir(), `stoke-cfg-${process.pid}-batch.json`);
|
||||
const manifest = path.join(os.tmpdir(), `stoke-manifest-${process.pid}.json`);
|
||||
// A PATH without `gh` and no GITHUB_TOKEN makes token resolution fail for
|
||||
// the first (tokenless) GitHub item; the second carries its own token.
|
||||
const emptyBin = fs.mkdtempSync(path.join(os.tmpdir(), 'stoke-empty-bin-'));
|
||||
fs.writeFileSync(manifest, JSON.stringify([
|
||||
{ name: 'bad', from: 'https://github.com/o/bad.git' },
|
||||
{ name: 'good', from: 'https://github.com/o/good.git', github_token: 'gh-tok' },
|
||||
]));
|
||||
const TIMEOUT_MS = 5000;
|
||||
let timer;
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
const fail = (err) => {
|
||||
clearTimeout(timer);
|
||||
try { server.close(); } catch { /* already closed */ }
|
||||
reject(err instanceof Error ? err : new Error(String(err)));
|
||||
};
|
||||
const migrated = [];
|
||||
const server = http.createServer((req, res) => {
|
||||
let data = '';
|
||||
req.on('data', (c) => { data += c; });
|
||||
req.on('end', () => {
|
||||
migrated.push(JSON.parse(data).repo_name);
|
||||
res.writeHead(201, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ full_name: `o/${JSON.parse(data).repo_name}`, html_url: 'https://forge.test/o/r' }));
|
||||
});
|
||||
});
|
||||
timer = setTimeout(() => fail(new Error('timeout')), TIMEOUT_MS);
|
||||
server.listen(0, '127.0.0.1', async () => {
|
||||
const { port } = server.address();
|
||||
fs.writeFileSync(cfg, JSON.stringify({ url: `http://127.0.0.1:${port}`, token: 'tok', login: 'o' }));
|
||||
// Empty PATH hides `gh`; empty GITHUB_TOKEN is falsy for the CLI.
|
||||
const env = { STOKE_CONFIG_FILE: cfg, PATH: emptyBin, GITHUB_TOKEN: '' };
|
||||
try {
|
||||
const res = await spawnAsync(['repo', 'import-batch', '-f', manifest], env);
|
||||
clearTimeout(timer);
|
||||
server.close(() => resolve({ res, migrated }));
|
||||
} catch (err) {
|
||||
fail(err);
|
||||
}
|
||||
});
|
||||
}).finally(() => clearTimeout(timer));
|
||||
|
||||
try {
|
||||
assert.equal(result.res.status, 1, 'partial batch must exit 1');
|
||||
assert.match(result.res.stderr, /Failed to import bad: No GitHub token found/);
|
||||
assert.match(result.res.stdout, /Imported: o\/good/);
|
||||
assert.match(result.res.stdout, /Batch complete: 1\/2 imported/);
|
||||
assert.deepEqual(result.migrated, ['good'], 'only the valid item may reach the server');
|
||||
} finally {
|
||||
fs.unlinkSync(cfg);
|
||||
fs.unlinkSync(manifest);
|
||||
fs.rmSync(emptyBin, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,12 +13,14 @@ const SCRIPT = path.join(__dirname, '..', 'scripts', 'install-apt.sh');
|
|||
// candAfterUpdate Candidate after any `apt-get update`
|
||||
// candAfterNodesource Candidate after an update once nodesource.list exists
|
||||
// releaseStatus HTTP status curl reports for the registry Release file
|
||||
// updateFailOutput when set, `apt-get update` against the forgejo source
|
||||
// fails with this output until the list is [trusted=yes]
|
||||
// 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.
|
||||
const cleanups = [];
|
||||
process.on('exit', () => { for (const dir of cleanups) fs.rmSync(dir, { recursive: true, force: true }); });
|
||||
|
||||
function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexistingNodesourceList, releaseStatus }) {
|
||||
function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexistingNodesourceList, releaseStatus, updateFailOutput }) {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'stoke-apt-test-'));
|
||||
cleanups.push(root);
|
||||
const bin = path.join(root, 'bin');
|
||||
|
|
@ -58,8 +60,10 @@ function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexi
|
|||
].join('\n'));
|
||||
stub('apt-get', [
|
||||
'echo "apt-get $*" >> "$STATE_DIR/apt-get.log"',
|
||||
'is_update=0',
|
||||
'for a in "$@"; do',
|
||||
' if [ "$a" = update ]; then',
|
||||
' is_update=1',
|
||||
' 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',
|
||||
|
|
@ -67,6 +71,17 @@ function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexi
|
|||
' fi',
|
||||
' fi',
|
||||
'done',
|
||||
// Updates of the forgejo source fail with UPDATE_FAIL_OUTPUT until the
|
||||
// list is rewritten with [trusted=yes], like sqv rejecting the signature.
|
||||
'if [ "$is_update" = 1 ] && [ -n "${UPDATE_FAIL_OUTPUT:-}" ]; then',
|
||||
' case " $* " in',
|
||||
' *forgejo*)',
|
||||
' if ! grep -q "trusted=yes" "$STOKE_APT_ETC/sources.list.d/forgejo-heavy-duty.list" 2>/dev/null; then',
|
||||
' echo "$UPDATE_FAIL_OUTPUT"',
|
||||
' exit 1',
|
||||
' fi;;',
|
||||
' esac',
|
||||
'fi',
|
||||
'exit 0',
|
||||
].join('\n'));
|
||||
|
||||
|
|
@ -82,6 +97,7 @@ function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexi
|
|||
CAND_AFTER_UPDATE: candAfterUpdate || '',
|
||||
CAND_AFTER_NODESOURCE: candAfterNodesource || '',
|
||||
RELEASE_STATUS: releaseStatus || '',
|
||||
UPDATE_FAIL_OUTPUT: updateFailOutput || '',
|
||||
LC_ALL: 'es_ES.UTF-8', // localized environment; the script must force C
|
||||
},
|
||||
});
|
||||
|
|
@ -96,6 +112,7 @@ function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexi
|
|||
nodesourceKey: read(path.join(aptEtc, 'keyrings', 'nodesource.asc')),
|
||||
nodesourceKeyMode: mode(path.join(aptEtc, 'keyrings', 'nodesource.asc')),
|
||||
forgeKeyMode: mode(path.join(aptEtc, 'keyrings', 'forgejo-heavy-duty.asc')),
|
||||
forgeList: read(path.join(aptEtc, 'sources.list.d', 'forgejo-heavy-duty.list')),
|
||||
aptGetLog: read(path.join(state, 'apt-get.log')) || '',
|
||||
};
|
||||
// Drop the throwaway tree after we have read everything we need.
|
||||
|
|
@ -171,3 +188,25 @@ test('registry Release file present: proceeds with the install', () => {
|
|||
assert.equal(s.res.status, 0, s.res.stderr);
|
||||
assert.match(s.aptGetLog, /install -y stoke/);
|
||||
});
|
||||
|
||||
test('apt update network failure: refuses to fall back to [trusted=yes]', () => {
|
||||
const s = runScenario({
|
||||
candInitial: '22.23.1-1nodesource1',
|
||||
updateFailOutput: 'Err:1 https://forge.test heavy-duty InRelease\n Could not resolve host: forge.test',
|
||||
});
|
||||
assert.notEqual(s.res.status, 0);
|
||||
assert.match(s.res.stderr, /Refusing to fall back/);
|
||||
assert.doesNotMatch(s.forgeList, /trusted=yes/, 'source must stay signature-verified');
|
||||
assert.doesNotMatch(s.aptGetLog, /install -y stoke/);
|
||||
});
|
||||
|
||||
test('apt update signature failure: falls back to [trusted=yes] and installs', () => {
|
||||
const s = runScenario({
|
||||
candInitial: '22.23.1-1nodesource1',
|
||||
updateFailOutput: 'E: The repository \'https://forge.test heavy-duty InRelease\' is not signed.',
|
||||
});
|
||||
assert.equal(s.res.status, 0, s.res.stderr);
|
||||
assert.match(s.res.stderr, /WARNING: signature verification failed/);
|
||||
assert.match(s.forgeList, /deb \[trusted=yes\] /);
|
||||
assert.match(s.aptGetLog, /install -y stoke/);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue