The board API returns a scan refusal in its JSON error body. The test starts a board on port 0 over a seen.json that is not an object and checks the body names seen.json by label, with no sentinel or scratch path. It fails on BASE, where the body quotes the host path.
99 lines
4.9 KiB
JavaScript
99 lines
4.9 KiB
JavaScript
// #1873 (redact6): a control-board refusal names the config, the seen marks file or the seat
|
|
// by label, never a host path. The sentinel is a component of every scratch path, so a refusal
|
|
// that quotes a path quotes the sentinel too; each test also checks the scratch root and the label.
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { dirname, join } from 'node:path';
|
|
import { ConfigError, loadConfig, loadRegistrations, loadSeen } from '../src/scan.mjs';
|
|
import { readRegistration, registrationPath, SeatError } from '../../seat/src/seat.mjs';
|
|
import { startServer } from '../src/serve.mjs';
|
|
|
|
const secret = 'sk-test-NOT-A-SECRET-1234';
|
|
function scratch(t) {
|
|
const root = mkdtempSync(join(tmpdir(), 'r6-'));
|
|
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
const dir = join(root, secret);
|
|
mkdirSync(dir);
|
|
return { root, dir };
|
|
}
|
|
function clean(text, root) {
|
|
assert.ok(!text.includes(root), text);
|
|
assert.doesNotMatch(text, /sk-test|NOT-A-SECRET/);
|
|
}
|
|
function labelled(label, root) {
|
|
return error => {
|
|
assert.ok(error instanceof ConfigError, error.stack);
|
|
assert.equal(error.message, label);
|
|
clean(error.message, root);
|
|
return true;
|
|
};
|
|
}
|
|
function write(file, text) {
|
|
mkdirSync(dirname(file), { recursive: true });
|
|
writeFileSync(file, text);
|
|
return file;
|
|
}
|
|
|
|
test('config refusals name the config, not its path', t => {
|
|
const { root, dir } = scratch(t);
|
|
assert.throws(() => loadConfig(join(dir, 'config.json')), labelled('config not found', root));
|
|
assert.throws(() => loadConfig(write(join(dir, 'array.json'), '[]')), labelled('config is not an object', root));
|
|
assert.throws(() => loadConfig(write(join(dir, 'relative.json'), '{"dataRoot":"data"}')), labelled('config.dataRoot must be an absolute path', root));
|
|
});
|
|
|
|
test('seen marks refusals name seen.json, not its path', t => {
|
|
const { root, dir } = scratch(t);
|
|
write(join(dir, 'seen.json'), '[]');
|
|
assert.throws(() => loadSeen(dir), labelled('seen marks file seen.json must be a JSON object', root));
|
|
write(join(dir, 'seen.json'), '{"a":1}');
|
|
assert.throws(() => loadSeen(dir), labelled('seen marks file seen.json has a non-string value for "a"', root));
|
|
});
|
|
|
|
test('a bad registration is reported by layout and seat, never by its path', t => {
|
|
const { root, dir } = scratch(t);
|
|
write(registrationPath(dir, 'builder', 'repo'), '{}');
|
|
const { registrations, errors } = loadRegistrations(dir);
|
|
assert.deepEqual(registrations, []);
|
|
assert.equal(errors.length, 1);
|
|
assert.match(errors[0], /^seat repo\/builder: .*: registration repo\/builder$/);
|
|
clean(errors[0], root);
|
|
});
|
|
|
|
// Review round (#1873): a key or name that could be a host path is described, never echoed.
|
|
test('a seen marks key or a registration field that is not a plain name is described, never echoed', t => {
|
|
const { root, dir } = scratch(t);
|
|
writeFileSync(join(dir, 'seen.json'), JSON.stringify({ [join(dir, 'host')]: 1 }));
|
|
assert.throws(() => loadSeen(dir), labelled('seen marks file seen.json has a non-string value for that is not a plain name', root));
|
|
const f = registrationPath(dir, 'builder', 'repo');
|
|
mkdirSync(dirname(f), { recursive: true });
|
|
writeFileSync(f, JSON.stringify({ [join(dir, 'host')]: 1 }));
|
|
assert.throws(() => readRegistration(dir, 'builder', 'repo'), e => {
|
|
assert.ok(e instanceof SeatError, e.stack);
|
|
assert.equal(e.exitCode, 2);
|
|
assert.equal(e.message, 'registration has an unknown field: that is not a plain name: registration repo/builder');
|
|
clean(e.message, root);
|
|
return true;
|
|
});
|
|
});
|
|
|
|
test('an invalid seat name or layout states the constraint, never the value, and keeps exit 4', t => {
|
|
const { root, dir } = scratch(t);
|
|
assert.throws(() => registrationPath(dir, join(dir, 'x'), 'repo'), e => e instanceof SeatError && e.exitCode === 4 && e.message === 'invalid seat name (lowercase letters, digits, ., _ or -, at most 64)' && (clean(e.message, root), true));
|
|
assert.throws(() => registrationPath(dir, 'builder', join(dir, 'x')), e => e instanceof SeatError && e.exitCode === 4 && e.message === 'invalid layout (one of repo, fleet, unknown)' && (clean(e.message, root), true));
|
|
});
|
|
|
|
// The board API returns a scan refusal in its JSON body, so the HTTP body carries the label too.
|
|
test('a seen marks refusal reaches the board API body by label, never by path', async t => {
|
|
const { root, dir } = scratch(t);
|
|
write(join(dir, 'seen.json'), '[]');
|
|
const server = await startServer({ host: '127.0.0.1', port: 0, specs: [], boardDir: dir, isAlive: () => true, page: '<html></html>' });
|
|
t.after(() => new Promise(done => server.close(done)));
|
|
const res = await fetch(`http://127.0.0.1:${server.address().port}/api/board`);
|
|
const body = await res.text();
|
|
assert.equal(res.status, 500);
|
|
assert.deepEqual(JSON.parse(body), { error: 'seen marks file seen.json must be a JSON object' });
|
|
clean(body, root);
|
|
});
|