'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('dedicated ban assets enqueue only on the matching native page', () => { const { assets } = phpFixture(); assert.equal(assets.styles.length, 1); assert.deepEqual(assets.styles[0].slice(0, 3), [ 'aksrt-admin-user-ban', 'https://theme.example.test/assets/css/admin/user-ban.css', ['aksrt-admin-native-ui'], ]); assert.equal(assets.scripts.length, 1); assert.deepEqual(assets.scripts[0].slice(0, 3), [ 'aksrt-admin-user-ban', 'https://theme.example.test/assets/js/admin-user-ban.js', [], ]); assert.equal(assets.scripts[0][4], true); }); test('PHP actions reject blank notes and stale versions, and preserve filter roundtrips', () => { const { actions } = phpFixture(); assert.equal(actions.blank_report.message, 'report_failed'); assert.match(actions.blank_report.args.aksrt_ban_error, /必须填写处理说明/); assert.equal(actions.stale_report.message, 'report_failed'); assert.match(actions.stale_report.args.aksrt_ban_error, /已变化或已处理/); assert.equal(actions.valid_report.message, 'report_updated'); assert.deepEqual({ type: actions.valid_report.args.type, status: actions.valid_report.args.status, s: actions.valid_report.args.s, send_user: actions.valid_report.args.send_user, orderby: actions.valid_report.args.orderby, order: actions.valid_report.args.order, paged: actions.valid_report.args.paged, }, { type: 'report', status: 'pending', s: 'needle', send_user: 7, orderby: 'updated', order: 'ASC', paged: 2 }); assert.equal(actions.blank_appeal.message, 'appeal_failed'); assert.match(actions.blank_appeal.args.aksrt_ban_error, /必须填写拒绝原因/); assert.equal(actions.valid_appeal.message, 'appeal_rejected'); assert.equal(actions.appeal_status, 'rejected'); assert.equal(actions.appeal_payload.admin_note, 'Missing evidence'); assert.equal(actions.approve_appeal.message, 'appeal_approved'); assert.equal(actions.approved_status, 'approved'); assert.equal(actions.restrictions_lifted, 1); assert.deepEqual(actions.report_calls, [[9001, 'resolved', 'Reviewed']]); assert.equal(actions.messages.length, 2); }); test('ban admin contract keeps the single-dialog flow and separate native operations', () => { const users = read('src/Admin/Native/UsersMenus.php'); const page = read('src/Admin/Native/UsersMenusBan.php'); const query = read('src/Admin/Native/UsersMenusBanQuery.php'); const actions = read('src/Admin/Native/UsersMenusBanActions.php'); const dialogs = read('src/Admin/Native/UsersMenusBanDialogs.php'); const script = read('assets/js/admin-user-ban.js'); const css = read('assets/css/admin/user-ban.css'); assert.match(users, /use UsersMenusBan;/); assert.match(users, /admin_post_aksrt_users_update_report/); assert.match(users, /admin_post_aksrt_users_process_appeal/); assert.doesNotMatch(users, /use UsersMenusTickets;/); assert.ok(!fs.existsSync(path.join(root, 'src/Admin/Native/UsersMenusTickets.php'))); assert.match(page, /if \(\$hook !== 'users_page_aksrt_user_ban'\) return/); assert.match(page, /ban_record_version\(\$row\), \$submitted\)/); assert.match(actions, /\$this->ban_request_is_current\(\$row, 'pending'\)/); assert.match(actions, /\$decision === 'reject' && \$note === ''/); assert.match(actions, /\$new_status === 'dismissed' && \$note === ''/); assert.match(script, /dialog\.showModal\(\)/); assert.match(script, /dialog\.replaceChildren\(\)/); assert.match(script, /previousOpener\.focus\(\)/); assert.match(script, /window\.addEventListener\('pageshow'/); assert.match(css, /html\.aksrt-ban-modal-open \{ overflow: hidden/); });