Files
meshcore-analyzer/tests/unit/test-preflight-xss-gate.js
T
Alex B 893773338e chore(tests): move root test-*.js into tests/unit and tests/e2e (#2036)
Moves 290 root test-*.js into tests/unit (177, listed in test-all.sh) and tests/e2e (113, classified in scripts/non-unit-tests.json), per #1981 and PR-D of #1385. Root goes from 348 entries to 48. test-all.sh and test-fixtures/ stay put. The inventory guard now fails if a test reappears in the root or sits in the wrong folder.

Verified independently of the diff: the invoked sets are unchanged (test-all.sh 177 before and after, deploy.yml 96 before and after, both identical as sets), and a full local run of test-all.sh on master and on the branch produced 4702 output lines each whose only differences are absolute paths, stack-trace line numbers shifted by the REPO_ROOT line, the inventory wording and two perf ratios. The guard was mutation-checked: a test back in the root, a unit suite in tests/e2e, and a suite dropped from test-all.sh each make it exit 1. CI run 35246304316 ran 97 suites from tests/e2e and is green.

Follow-up 9335c51d finished the instruction files: no bare root test command is left in AGENTS.md, the squad charters, .github or docs, and every tests/ path they name resolves.

Merged by the interim maintainer without a second human reviewer: CI and the local runs above are the independent checks.

Known and deliberately out of scope: 18 of the 113 files in tests/e2e are invoked by no runner at all, and one of them cannot run anywhere because it requires jsdom, which is not a declared dependency. Tracked separately.
2026-09-17 19:06:07 +02:00

106 lines
4.6 KiB
JavaScript

#!/usr/bin/env node
// test-preflight-xss-gate.js — exercises scripts/check-xss-sinks.sh against
// the testdata/preflight-xss fixtures. Asserts the bad fixtures HARD-FAIL
// (exit 1) and the good fixtures pass (exit 0).
//
// This is the repo-side validation of the canonical pr-preflight gate
// documented at ~/.openclaw/skills/pr-preflight/scripts/check-xss-sinks.sh.
// The skill-side script enforces the gate at PR-creation time; this test
// guards against regressions in the local mirror at scripts/check-xss-sinks.sh.
//
// Each fixture line is a behavioral assertion:
// bad-* MUST fail — proves the gate catches the unescaped sink class.
// good-* MUST pass — proves the gate doesn't false-positive on escaped
// or test-covered sinks.
//
// Exit 1 on any assertion failure.
'use strict';
const REPO_ROOT = require('path').resolve(__dirname, '..', '..');
const { spawnSync } = require('child_process');
const path = require('path');
const fs = require('fs');
const SCRIPT = path.resolve(REPO_ROOT, 'scripts/check-xss-sinks.sh');
const FIXTURE_DIR = path.resolve(REPO_ROOT, 'testdata/preflight-xss');
if (!fs.existsSync(SCRIPT)) {
console.error(`FAIL: ${SCRIPT} missing`);
process.exit(1);
}
if (!fs.existsSync(FIXTURE_DIR)) {
console.error(`FAIL: ${FIXTURE_DIR} missing`);
process.exit(1);
}
// Each case may specify which test file(s) to expose via PREFLIGHT_TEST_FILES.
const cases = [
// bad fixtures: gate MUST flag (exit 1)
{ file: 'bad-1-template-literal.js', expect: 1, label: 'innerHTML template literal with ${name}' },
{ file: 'bad-2-setAttribute-href.js', expect: 1, label: "setAttribute('href', `…${hash}…`)" },
{ file: 'bad-3-bindPopup.js', expect: 1, label: 'Leaflet bindPopup(`…${observer}…`)' },
{ file: 'bad-4-bare-ident.js', expect: 1, label: 'innerHTML = name (bare ident, no quote/backtick)' },
{ file: 'bad-5-string-concat.js', expect: 1, label: 'innerHTML = name + "<b>" (concat)' },
{ file: 'bad-6-bindPopup-concat.js', expect: 1, label: 'bindPopup("Name: " + observer) (concat)' },
{ file: 'bad-7-outerHTML.js', expect: 1, label: 'outerHTML sink' },
{ file: 'bad-8-document-write.js', expect: 1, label: 'document.write template literal' },
{ file: 'bad-9-shamtest-fixture.js', expect: 1, label: 'sham test (markers only in comments)',
tests: ['sham-test-fixture-9.js'] },
{ file: 'bad-10-line-level-escape-bypass.js', expect: 1,
label: '${escapeHtml(role)} ${name} — one wrapped, one raw' },
{ file: 'bad-11-comment-rubber-stamp.js', expect: 1,
label: 'escapeHtml mentioned only in // comment' },
{ file: 'bad-12-setattr-concat.js', expect: 1,
label: 'setAttribute("href", "javascript:" + payload) (concat, no $)' },
// good fixtures: gate MUST pass (exit 0)
{ file: 'good-1-escaped.js', expect: 0, label: 'escapeHtml(${name}) wrapper' },
{ file: 'good-2-tested.js', expect: 0, label: 'unescaped but DOM-grep-tested in same PR',
tests: ['test-good-2.js'] },
{ file: 'good-3-catch-block-error.js', expect: 0,
label: 'catch (e) { ${e.message} } — exception property' },
{ file: 'good-3b-chained-cause.js', expect: 0,
label: '${error.cause.message} / ${parseError.message} chained error props' },
{ file: 'good-4-tested.js', expect: 0,
label: 'unescaped sink, REAL test (markers in executable code)',
tests: ['test-good-4.js'] },
];
let failed = 0;
for (const c of cases) {
const target = path.join(FIXTURE_DIR, c.file);
if (!fs.existsSync(target)) {
console.error(`FAIL: fixture missing: ${target}`);
failed++;
continue;
}
// The shell protocol uses ':' separators. Relative paths avoid Windows
// drive-letter colons being mistaken for separators by its Python reader.
const tests = (c.tests || []).map(t => path.relative(REPO_ROOT, path.join(FIXTURE_DIR, t)));
const env = Object.assign({}, process.env);
if (tests.length > 0) {
env.PREFLIGHT_TEST_FILES = tests.join(':');
} else {
delete env.PREFLIGHT_TEST_FILES;
}
const res = spawnSync('bash', [SCRIPT, '--file', target], {
cwd: REPO_ROOT,
env,
encoding: 'utf8',
});
const got = res.status;
if (got !== c.expect) {
console.error(`FAIL: ${c.file} (${c.label}) — expected exit ${c.expect}, got ${got}`);
if (res.stdout) console.error(' stdout:', res.stdout.trim());
if (res.stderr) console.error(' stderr:', res.stderr.trim());
failed++;
} else {
console.log(`PASS: ${c.file} — ${c.label} (exit ${got})`);
}
}
if (failed > 0) {
console.error(`\n${failed} assertion(s) failed.`);
process.exit(1);
}
console.log('\nAll preflight-xss-gate assertions passed.');