'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 = (...segments) => fs.readFileSync(path.join(root, ...segments), 'utf8'); let result; function harness() { if (!result) { const run = spawnSync('php', [path.join(__dirname, 'user-auth-admin-harness.php')], { encoding: 'utf8' }); assert.equal(run.status, 0, `${run.stderr}${run.stdout}`); assert.equal(run.stderr, '', 'the PHP harness must not emit warnings'); result = JSON.parse(run.stdout); } return result; } function template(html, id) { const safeId = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const match = html.match(new RegExp(`]*\\bid="${safeId}"[^>]*>[\\s\\S]*?<\\/template>`)); assert.ok(match, `missing template ${id}`); return match[0]; } const source = { verify: () => read('src', 'Admin', 'Native', 'UsersMenusVerify.php'), dialogs: () => read('src', 'Admin', 'Native', 'UsersMenusVerifyDialogs.php'), menus: () => read('src', 'Admin', 'Native', 'UsersMenus.php'), authApply: () => read('src', 'User', 'AuthApply.php'), applicationRepository: () => read('src', 'Identity', 'ApplicationRepository.php'), }; test('default pending filters, keyword, status views, list and pagination share one card', () => { const { html } = harness(); const cardStart = html.indexOf('class="aksrt-native-card aksrt-native-card--flush aksrt-verify-list-card"'); const dialogStart = html.indexOf('', dialogStart); assert.ok(cardStart >= 0 && cardEnd > cardStart, 'verification list card wraps its content'); const card = html.slice(cardStart, cardEnd + ''.length); assert.equal((html.match(/aksrt-verify-list-card/g) || []).length, 1); for (const marker of ['class="subsubsub"', 'class="aksrt-native-filters"', 'aksrt-verify-table', 'class="tablenav bottom"']) { assert.ok(card.includes(marker), `${marker} should remain inside the list card`); } assert.match(card, //); assert.doesNotMatch(card, /class="wp-list-table/); assert.match(card, /name="status" value="pending"/); assert.match(card, /name="keyword" value="search alert\(1\) &_%"/); assert.match(card, /status=pending&keyword=search\+alert%281%29\+%26_%25/); assert.match(card, /class="current" aria-current="page">待审核/); assert.match(card, /第 3 页,共 3<\/span> 页/); assert.match(card, /name="paged" value="3"/); assert.doesNotMatch(card, /class="aksrt-verify-filter-summary"/); }); test('rendered user/application data is escaped and row review forms exist only in inert templates', () => { const { html } = harness(); assert.match(html, /Applicant <script>alert\(1\)<\/script>/); assert.match(html, /Applicant name <script>alert\(3\)<\/script>/); assert.match(html, /Bio <img src=x onerror=alert\(4\)> & text/); assert.match(html, /Reviewer <img src=x onerror=alert\(2\)>/); assert.doesNotMatch(html, /', }); }); test('delete handler cleans attachments and events only after successful delete, without revoking auth projection', () => { const data = harness(); assert.deepEqual(data.delete_success_redirect, { page: 'aksrt_user_auth', aksrt_users_msg: 'verify_deleted', status: 'rejected', keyword: 'delete search', paged: '6', }); assert.deepEqual(data.delete_success_calls, [ { table: 'wp_aksrt_identity_application', data: { id: 501, type: 'general', channel: 'manual' }, format: ['%d', '%s', '%s'] }, { table: 'wp_aksrt_identity_event', data: { application_id: 501 }, format: ['%d'] }, ]); assert.deepEqual(data.delete_success_attachments, [[1001, true], [1002, true]]); assert.deepEqual(data.delete_auth_projection, { auth: 1, auth_info: { name: 'Already verified', desc: 'kept' } }); assert.deepEqual(data.delete_user_meta_calls, []); assert.deepEqual(data.delete_failure_redirect, { page: 'aksrt_user_auth', aksrt_users_msg: 'verify_failed', status: 'pending', keyword: 'delete failure', paged: '1', aksrt_verify_error: '认证记录删除失败', }); assert.equal(data.delete_failure_calls.length, 1, 'event rows are untouched after repository deletion fails'); assert.match(data.delete_failure_calls[0].table, /identity_application/); assert.deepEqual(data.delete_failure_attachments, []); }); test('proof listing and serving honor both reviewer capabilities without exposing other users’ evidence', () => { const { evidence_permissions: permissions } = harness(); for (const role of ['manager', 'reviewer']) { assert.equal(permissions[role].admin_items, 1); assert.equal(permissions[role].owner_items, 0); assert.equal(permissions[role].serve_result, '证明材料不存在', 'authorized users reach the file check'); } assert.deepEqual(permissions.outsider, { admin_items: 0, owner_items: 0, serve_result: '您没有权限查看该证明材料', }); assert.deepEqual(permissions.owner, { admin_items: 0, owner_items: 1, serve_result: '证明材料不存在', }); }); test('event cleanup failure reports partial deletion instead of a false success', () => { const data = harness(); assert.equal(data.delete_event_failure_calls.length, 2); assert.deepEqual(data.delete_event_failure_redirect, { page: 'aksrt_user_auth', aksrt_users_msg: 'verify_failed', status: 'rejected', keyword: 'delete search', paged: '6', aksrt_verify_error: '认证申请记录已删除,但审核事件清理失败。', }); }); test('assets are restricted to the authentication submenu and the stylesheet follows native UI', () => { const data = harness(); assert.deepEqual(data.wrong_hook_assets, []); assert.equal(data.auth_page_assets.length, 2); assert.deepEqual(data.auth_page_assets[0].slice(0, 4), [ 'style', 'aksrt-admin-user-auth', 'https://example.test/theme/assets/css/admin/user-auth.css', ['aksrt-admin-native-ui'], ]); assert.deepEqual(data.auth_page_assets[1].slice(0, 4), [ 'script', 'aksrt-admin-user-auth', 'https://example.test/theme/assets/js/admin-user-auth.js', [], ]); assert.match(source.verify(), /if \(\$hook !== 'users_page_aksrt_user_auth'\) return/); assert.match(source.menus(), /add_action\('admin_enqueue_scripts', \[\$this, 'enqueue_verify_assets'\]\)/); }); test('the real AuthApply contract requires valid review data and a compare-and-swap from manual_pending', () => { const auth = source.authApply(); const repository = source.applicationRepository(); assert.match(auth, /if \(!in_array\(\$status, \['verified', 'rejected'\], true\)\)/); assert.match(auth, /if \(\$status === 'rejected' && \$note === ''\)/); assert.match(auth, /\(int\) \$application\['user_id'\] !== \$user_id/); assert.match(auth, /\(string\) \$application\['status'\] !== 'manual_pending'/); assert.match(auth, /\$repo->transition\(\(int\) \$application\['id'\], 'manual_pending', \$status/); assert.match(auth, /if \(!\$updated\) \{\s*return new \\WP_Error\('aksrt_auth_conflict'/); assert.match(repository, /\['id' => \$id, 'type' => 'general', 'channel' => 'manual', 'status' => \$expected\]/); }); test('review and deletion nonces use application UUIDs only, while failure notices are escaped by the shell', () => { const verify = source.verify(); const dialogs = source.dialogs(); const menus = source.menus(); assert.match(verify, /check_admin_referer\('aksrt_users_review_verify_' \. \$uuid\)/); assert.match(verify, /check_admin_referer\('aksrt_users_delete_verify_' \. \$uuid\)/); assert.doesNotMatch(verify, /check_admin_referer\('aksrt_users_(?:review|delete)_verify_' \. \$user_id\)/); assert.match(dialogs, /wp_nonce_field\(\$action \. '_' \. \(string\) \$row\['application_uuid'\]\)/); assert.match(menus, /sanitize_text_field\(\(string\) wp_unslash\(\$_GET\['aksrt_verify_error'\]\)\)/); assert.match(menus, /esc_html\(\$message\)/); });