Files
meshcore-analyzer/tests/unit/test-rx-coverage-escape.js
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

63 lines
3.1 KiB
JavaScript

'use strict';
const REPO_ROOT = require('path').resolve(__dirname, '..', '..');
// Unit test for #14: the mobile RX coverage leaderboard must HTML-escape the
// pubkey it interpolates into the row markup (data-rx="..." and the truncated
// fallback label), not only the name. A no-ACL broker / pre-validation rows
// could carry a non-hex pubkey, and the rest of the row is built by string
// concatenation, so an unescaped pubkey is an HTML-injection vector.
//
// Like test-coverage-gate.js we slice the real row-building expression out of
// public/rx-coverage.js and evaluate it in a vm sandbox — no hand-copied
// duplicate — so the test tracks the actual source.
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const src = fs.readFileSync(path.join(REPO_ROOT, 'public', 'rx-coverage.js'), 'utf8');
// Slice from `var nm = o.name ...` through the end of the returned row string.
const startMarker = 'var nm = o.name ? escapeHtml(o.name)';
const endMarker = "}).join('');";
const startIdx = src.indexOf(startMarker);
assert.ok(startIdx >= 0, 'could not locate row-builder start in rx-coverage.js');
const endIdx = src.indexOf(endMarker, startIdx);
assert.ok(endIdx >= 0, 'could not locate row-builder end in rx-coverage.js');
const block = src.slice(startIdx, endIdx);
// Canonical escapeHtml (public/app.js).
function escapeHtml(s) {
if (s == null) return '';
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
function renderRow(o) {
const sandbox = { o: o, i: 0, selectedRx: '', escapeHtml: escapeHtml };
vm.createContext(sandbox);
return vm.runInContext('(function () { ' + block + ' })()', sandbox);
}
// Malicious pubkey that would break out of the data-rx attribute and inject a
// tag if interpolated raw. With escaping, no raw '<', '>' or attribute-closing
// '"' survives.
const evil = '"><img src=x onerror=alert(1)>';
// Case 1: no name → pubkey used as the visible label fallback too.
const row1 = renderRow({ pubkey: evil, name: '', receptions: 1, nodes: 1 });
assert.ok(row1.indexOf('<img') === -1, 'raw <img must not appear in row (label fallback): ' + row1);
assert.ok(row1.indexOf('data-rx="' + evil + '"') === -1, 'raw pubkey must not appear unescaped in data-rx');
assert.ok(row1.indexOf('&lt;img') !== -1 || row1.indexOf('&quot;&gt;') !== -1, 'pubkey should be HTML-escaped: ' + row1);
// Case 2: name present → label is the (escaped) name, but data-rx still carries
// the pubkey and must be escaped.
const row2 = renderRow({ pubkey: evil, name: 'Mob', receptions: 2, nodes: 3 });
assert.ok(row2.indexOf('<img') === -1, 'raw <img must not appear in row (named): ' + row2);
// #a11y: the clickable row must be keyboard-operable (role/tabindex) and expose
// pressed state, so it isn't a mouse-only <div>.
assert.ok(/role="button"/.test(row2), 'row must have role="button"');
assert.ok(/tabindex="0"/.test(row2), 'row must be focusable (tabindex)');
assert.ok(/aria-pressed="(true|false)"/.test(row2), 'row must expose aria-pressed');
console.log('rx-coverage pubkey escaping + row a11y OK');