61 lines
3.4 KiB
JavaScript
61 lines
3.4 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { dirname, join } from 'node:path';
|
|
const secret = 'sk-test-NOT-A-SECRET-1234';
|
|
// JSON.parse quotes a prefix near this fault on Node 26; check partial leaks too.
|
|
const malformed = secret;
|
|
function scratch(t) {
|
|
const root = mkdtempSync(join(tmpdir(), 'r3-'));
|
|
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
return root;
|
|
}
|
|
function write(file, text = malformed) {
|
|
mkdirSync(dirname(file), { recursive: true });
|
|
writeFileSync(file, text, { mode: 0o600 });
|
|
return file;
|
|
}
|
|
import { spawnSync } from 'node:child_process';
|
|
import { readFileSync } from 'node:fs';
|
|
import { fixture } from './helpers.mjs';
|
|
const repo = join(import.meta.dirname, '../../..');
|
|
test('task file JSON refusal preserves exit 2 and file without parser bytes', t => {
|
|
const file = write(join(scratch(t), 'task.json'));
|
|
const r = spawnSync(process.execPath, [join(repo, 'scripts/mosaic-task.mjs'), 'validate', file], { encoding: 'utf8', env: { PATH: process.env.PATH, HOME: process.env.HOME, TMPDIR: process.env.TMPDIR } });
|
|
assert.equal(r.status, 2, r.stderr);
|
|
assert.ok(r.stderr.includes(file), r.stderr);
|
|
assert.match(r.stderr, /task file is not valid JSON/);
|
|
assert.doesNotMatch(r.stdout + r.stderr, /sk-test|Unexpected token/);
|
|
});
|
|
test('task file read refusal retains exit 2, file and filesystem code', t => {
|
|
const file = write(join(scratch(t), 'task.json'), '{}');
|
|
// A directory is refused by lstat before the read. Inject a read failure
|
|
// after that unchanged regular-file gate, including on root-run CI hosts.
|
|
const preload = `import fs from 'node:fs';
|
|
const read = fs.readFileSync;
|
|
fs.readFileSync = function(path, ...args) {
|
|
if (path === ${JSON.stringify(file)}) throw Object.assign(new Error(${JSON.stringify(secret)}), { code: 'EIO' });
|
|
return read.call(this, path, ...args);
|
|
};`;
|
|
const r = spawnSync(process.execPath, ['--import', 'data:text/javascript,' + encodeURIComponent(preload), join(repo, 'scripts/mosaic-task.mjs'), 'validate', file], { encoding: 'utf8', env: { PATH: process.env.PATH, HOME: process.env.HOME, TMPDIR: process.env.TMPDIR } });
|
|
assert.equal(r.status, 2, r.stderr);
|
|
assert.ok(r.stderr.includes(file), r.stderr);
|
|
assert.match(r.stderr, /task file is not valid JSON.*\(EIO\)/);
|
|
assert.doesNotMatch(r.stderr, /sk-test/);
|
|
});
|
|
test('case2 precheck business helper omits parser bytes in its BUSINESS fact', t => {
|
|
const root = scratch(t), f = fixture(root);
|
|
const file = write(join(root, 'config/businesses/acme.json'));
|
|
// Execute the production facts helper, without host or credential checks.
|
|
const shell = readFileSync(join(repo, 'scripts/case2-precheck.sh'), 'utf8');
|
|
const start = shell.indexOf('facts="$(cd "$REPO" && node --input-type=module -e \'') + 'facts="$(cd "$REPO" && node --input-type=module -e \''.length;
|
|
assert.ok(start > 100);
|
|
const end = shell.indexOf("' \"$REPO\" \"$BUSINESS\"", start);
|
|
assert.ok(end > start);
|
|
const r = spawnSync(process.execPath, ['--input-type=module', '-e', shell.slice(start, end), repo, 'acme'], { encoding: 'utf8', env: { ...f.env, HOME: process.env.HOME, TMPDIR: process.env.TMPDIR, PATH: process.env.PATH } });
|
|
assert.equal(r.status, 0, r.stderr);
|
|
assert.ok(r.stdout.includes(`BUSINESS\tbad\tcannot read ${file}:`), r.stdout);
|
|
assert.doesNotMatch(r.stdout + r.stderr, /sk-test|Unexpected token/);
|
|
});
|