'use strict'; const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const { spawnSync } = require('node:child_process'); const test = require('node:test'); const root = path.resolve(__dirname, '..', '..'); const read = (file) => fs.readFileSync(path.join(root, file), 'utf8'); let fixture; function phpFixture() { if (fixture) return fixture; const result = spawnSync(process.env.PHP_BINARY || 'php', [path.join(__dirname, 'ban-admin-harness.php')], { cwd: root, encoding: 'utf8', }); assert.equal(result.status, 0, result.stderr || result.stdout); fixture = JSON.parse(result.stdout); return fixture; } function inputValue(html, name) { const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const match = html.match(new RegExp(`]*name="${escaped}"[^>]*value="([^"]*)"`)); return match ? match[1] : null; } test('ban queue merges all records, reports status counts, and clamps pages above 100 rows', () => { const result = phpFixture(); assert.equal(result.queue.total, 132); assert.equal(result.queue.page, 2); assert.equal(result.queue.items, 20); assert.deepEqual(result.queue.counts, { all: 132, pending: 128, resolved: 2, dismissed: 2 }); assert.equal(result.queue.overlarge_page, 7); assert.equal(result.queue.overlarge_items, 12); assert.deepEqual(result.filters, { type: 'report', status: '', s: 'needle', send_user: 7, orderby: 'updated', order: 'ASC', }); }); test('real PHP rendering keeps review forms in templates and completed records read-only', () => { const { html, filtered_html: filteredHtml } = phpFixture(); assert.equal((html.match(/\s*
\s*([\s\S]*?)<\/template>/)?.[1] || ''; assert.notEqual(resolved, ''); assert.doesNotMatch(resolved, /]*required>/); assert.doesNotMatch(html, /]*checked/); }); test('PHP templates escape all appeal data and report evidence while preserving safe links', () => { const { html } = phpFixture(); assert.match(html, /<script>alert\(1\)<\/script>/); assert.match(html, /<img src=x onerror=alert\(1\)>/); assert.match(html, /Name <img src=x onerror=alert\(4\)>/); assert.match(html, /Applicant <b>value<\/b>/); assert.match(html, /reason <svg onload=alert\(1\)>/); assert.match(html, /href="https:\/\/evidence\.example\.test\/proof\.jpg" target="_blank" rel="noopener noreferrer"/); assert.match(html, /href="https:\/\/appeal\.example\.test\/file\.pdf" target="_blank" rel="noopener noreferrer"/); assert.match(html, /
https:\/\/appeal\.example\.test\/document\.pdf<\/dd>/); assert.doesNotMatch(html, /href="javascript:/i); assert.doesNotMatch(html, /alert\(/i); }); test('report details display readable categories separately from optional content and evidence', () => { const { html } = phpFixture(); const detail = html.match(/