ban_settings() derived each reason slug via sanitize_key(label), which strips CJK text to an empty string; the frontend filter then discarded every reason (including the schema defaults) and fell back to the hard-coded list. Derive the slug from an ASCII key when possible and from a stable md5 short hash of the label otherwise, and relax the frontend filter to the label only. Adds a WP-stub harness that verifies the defaults path end to end (tests/dev/verify-ban-settings-defaults.php).
236 lines
15 KiB
JavaScript
236 lines
15 KiB
JavaScript
const test = require('node:test')
|
||
const assert = require('node:assert/strict')
|
||
const fs = require('node:fs')
|
||
const path = require('node:path')
|
||
|
||
const root = path.resolve(__dirname, '..', '..')
|
||
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8')
|
||
|
||
const userController = read('src/Admin/UserController.php')
|
||
const reportOps = read('src/Admin/UserControllerReportOperations.php')
|
||
const restrictionOps = read('src/Admin/UserControllerRestrictionOperations.php')
|
||
const restriction = read('src/User/Restriction.php')
|
||
const reportCenter = read('src/Messages/ReportCenter.php')
|
||
const usersMenus = read('src/Admin/Native/UsersMenus.php')
|
||
const mailNotifications = read('src/Settings/MailNotifications.php')
|
||
const home = read('src/User/Home.php')
|
||
const commentItem = read('inc/helpers/comments/item.php')
|
||
const functions = read('functions.php')
|
||
const uninstall = read('uninstall.php')
|
||
|
||
test('report REST surface is canonical: submit + users/reports only', () => {
|
||
assert.match(userController, /'\/report\/submit'/)
|
||
assert.match(userController, /'\/users\/reports'/)
|
||
assert.match(userController, /'\/users\/reports\/\(\?P<id>\\d\+\)'/)
|
||
assert.match(userController, /'\/users\/reports\/\(\?P<id>\\d\+\)\/process'/)
|
||
for (const dead of ["'/report/list'", "'/report/update-status'", "'/report/counts'"]) {
|
||
assert.ok(!userController.includes(dead), `alias route should be removed: ${dead}`)
|
||
}
|
||
assert.ok(!reportOps.includes('public function update_report_status'), 'dead update_report_status method')
|
||
assert.ok(!reportOps.includes('public function get_report_counts'), 'dead get_report_counts method')
|
||
})
|
||
|
||
test('report center has no legacy ajax handlers or option migration', () => {
|
||
assert.ok(!reportCenter.includes('ajax_update_status'))
|
||
assert.ok(!reportCenter.includes('ajax_list'))
|
||
assert.ok(!reportCenter.includes('ajax_counts'))
|
||
assert.ok(!reportCenter.includes('migrate_from_option'))
|
||
assert.ok(!reportCenter.includes('maybe_migrate'))
|
||
assert.ok(!reportCenter.includes("get_option('aksrt_reports'"))
|
||
assert.ok(!functions.includes('ReportCenter::get_instance()->init()'), 'ReportCenter init() is gone')
|
||
assert.match(reportCenter, /public function has_pending_report/)
|
||
assert.match(reportOps, /has_pending_report\(\$reporter_id, \$target_type, \$target_id\)/)
|
||
})
|
||
|
||
test('report processing delegates actions to the shared ReportCenter entry', () => {
|
||
assert.match(reportOps, /ReportCenter::get_instance\(\)->apply_action\(\$target_type, \$target_id, \$action_taken, \$reason\)/)
|
||
assert.match(reportCenter, /public function apply_action\(/)
|
||
assert.match(reportCenter, /->restrict\(\s*\$target_id,\s*\$action_taken,/)
|
||
assert.match(reportCenter, /lift_user_restriction\(\$target_id\)/)
|
||
assert.match(reportCenter, /in_array\(\$action_taken, \\AKSRT\\User\\Restriction::TYPES, true\)/)
|
||
assert.ok(!reportCenter.includes('aksrt_user_ban_state'), 'dual-track ban state meta must not be written or read')
|
||
for (const deadAction of ["'value' => 'warned'", "'value' => 'muted'", "'value' => 'restricted'", "'value' => 'banned'"]) {
|
||
assert.ok(!reportOps.includes(deadAction), `action value must map to Restriction types: ${deadAction}`)
|
||
}
|
||
for (const action of ["'value' => 'warn'", "'value' => 'mute'", "'value' => 'suspend'", "'value' => 'ban'", "'value' => 'clear'"]) {
|
||
assert.ok(reportOps.includes(action), `missing action option: ${action}`)
|
||
}
|
||
assert.match(reportOps, /get_user_restriction_state/)
|
||
assert.ok(!usersMenus.includes('$svc->apply_action('), 'native page follows the reference theme flow (status-only update)')
|
||
})
|
||
|
||
test('restriction class supports lifting and kicks sessions on ban', () => {
|
||
assert.match(restriction, /public function lift_user_restriction\(int \$user_id/)
|
||
assert.match(restriction, /public function lift_user_restriction\(int \$user_id, array \$types = \[self::TYPE_BAN, self::TYPE_SUSPEND, self::TYPE_MUTE\]\): int/)
|
||
assert.match(restriction, /WP_Session_Tokens::get_instance\(\$user_id\)->destroy_all\(\)/)
|
||
assert.ok(!restriction.includes('sync_user_restriction_meta'), 'write-only meta sync is dead code')
|
||
assert.ok(!restriction.includes('_aksrt_is_banned'))
|
||
})
|
||
|
||
test('ban admin REST endpoints stay in sync with the SPA client', () => {
|
||
assert.match(userController, /'\/users\/\(\?P<user_id>\\d\+\)\/restrictions'/)
|
||
assert.match(userController, /'\/users\/\(\?P<user_id>\\d\+\)\/restrict'/)
|
||
// The self-built SPA ban-manager page is removed: ban enforcement lives on
|
||
// the native reference-theme page, and the SPA hosts the settings schema.
|
||
assert.ok(!fs.existsSync(path.join(root, 'assets/admin/src/pages/users/UsersBan.vue')), 'self-built SPA ban page must stay deleted')
|
||
assert.ok(!fs.existsSync(path.join(root, 'assets/admin/src/pages/users/UsersReport.vue')), 'self-built SPA report queue must stay deleted')
|
||
})
|
||
|
||
test('ban/report settings page mirrors the reference theme schema', () => {
|
||
const schemaPhp = read('src/Admin/Schema/SchemaUsers.php')
|
||
const schemaJs = read('assets/admin/src/schema/sections/users/report.js')
|
||
const sections = read('assets/admin/src/constants/sections/users.js')
|
||
|
||
// SPA 侧边栏“用户互动 → 禁封/举报”挂载为 schema 设置页(非跳转页)
|
||
assert.match(sections, /schemaPage\(\{\s*\n\s*key: 'users-ban',/)
|
||
assert.match(sections, /label: '禁封\/举报'/)
|
||
assert.match(sections, /section: 'report'/)
|
||
assert.ok(!sections.includes("component: 'NativeOpsRedirect'"), 'ban page must not be a redirect stub')
|
||
|
||
// 字段键 1:1 对齐参考主题:禁封/举报开关、预置原因、申诉说明、申诉资料、图片举报、举报说明
|
||
for (const key of ['user_ban_s', 'user_report_s', 'ban_preset_reason', 'ban_appeal_desc', 'ban_appeal_keys', 'user_report_img_s', 'user_report_desc']) {
|
||
assert.ok(schemaPhp.includes(`'${key}'`), `PHP schema missing reference key: ${key}`)
|
||
assert.ok(schemaJs.includes(key), `SPA schema missing reference key: ${key}`)
|
||
}
|
||
// 参考主题文案与 group 结构
|
||
for (const label of ['禁封功能', '举报功能', '预置原因', '申诉说明', '申诉所需资料', '举报可上传图片', '举报说明']) {
|
||
assert.ok(schemaJs.includes(`label: '${label}'`), `SPA schema missing reference label: ${label}`)
|
||
}
|
||
assert.match(schemaJs, /type: 'group'/)
|
||
assert.match(schemaJs, /添加原因/)
|
||
assert.match(schemaJs, /添加申诉资料/)
|
||
|
||
// 运行时读取兼容 group 数组 [{t}] 并保留旧格式回退
|
||
const runtime = read('src/Settings/UserRuntime.php')
|
||
assert.match(runtime, /ban_settings\(\): array/)
|
||
assert.match(runtime, /report_settings\(\): array/)
|
||
assert.match(runtime, /user_report_s/)
|
||
// 缺键回退 schema 默认值(参考主题 _pz 语义):运行时经 read_domain 读取,
|
||
// 裸 get_all() 会让未保存过分区的站点拿不到 ban_preset_reason 默认值。
|
||
assert.match(runtime, /read_domain\('users', 'report'\)/)
|
||
// 预置原因 slug 不得由 sanitize_key(标签) 直接派生:中文标签会被清洗成
|
||
// 空串,前端按 slug 非空过滤后整张原因列表会被丢弃。ASCII 键优先,
|
||
// 否则用标签内容的稳定 md5 短哈希。
|
||
assert.match(runtime, /md5\(\$text\)/)
|
||
// 死链清理:user_report_categories 从不是真实存储键,解析器必须删除。
|
||
assert.ok(!runtime.includes('parse_report_categories'), 'dead parse_report_categories must stay removed')
|
||
assert.ok(!runtime.includes('user_report_categories'), 'dead user_report_categories key must stay removed')
|
||
const appeal = read('src/User/BanAppeal.php')
|
||
assert.match(appeal, /ban_appeal_keys/)
|
||
assert.match(appeal, /ban_appeal_desc/)
|
||
})
|
||
|
||
test('ban appeal approval actually lifts restrictions', () => {
|
||
assert.match(usersMenus, /lift_user_restriction\(\$user_id\)/)
|
||
assert.ok(!usersMenus.includes("SET status = 'lifted'"), 'restriction table has no status column')
|
||
assert.ok(!usersMenus.includes('clear_user_bans'))
|
||
assert.ok(!usersMenus.includes('lift_ban'))
|
||
})
|
||
|
||
test('profile governance notice reads from the restriction table', () => {
|
||
assert.match(home, /->is_banned\(\$user_id\)/)
|
||
assert.match(home, /\\AKSRT\\User\\Restriction::get_instance\(\)/)
|
||
assert.ok(!home.includes('aksrt_user_ban_state'))
|
||
})
|
||
|
||
test('admin report notification links to the real page', () => {
|
||
assert.match(mailNotifications, /users\.php\?page=aksrt_user_ban'/)
|
||
assert.ok(!mailNotifications.includes('page=aksrt_reports&'), 'dead admin page link')
|
||
assert.ok(!mailNotifications.includes('tab=reports'), 'merged ticket page has no reports tab')
|
||
})
|
||
|
||
test('native ban page mirrors the reference theme layout', () => {
|
||
const tickets = read('src/Admin/Native/UsersMenusTickets.php')
|
||
|
||
// 菜单与页面标题对齐参考主题(page title=处理举报及禁封申诉,menu title=举报&禁封申诉)
|
||
assert.ok(usersMenus.includes(`__('处理举报及禁封申诉', 'aksrt'),\n __('举报&禁封申诉', 'aksrt')`), 'submenu titles should match the reference theme')
|
||
assert.ok(usersMenus.includes('用户禁封申诉及用户举报管理'), 'page heading should match the reference theme')
|
||
|
||
// 类型筛选按钮组 + 状态 subsubsub + 搜索
|
||
for (const label of [`__('禁封申诉', 'aksrt')`, `__('用户举报', 'aksrt')`, `__('待处理', 'aksrt')`, `__('已批准', 'aksrt')`, `__('已拒绝', 'aksrt')`, `__('搜索记录', 'aksrt')`, `__('查看全部', 'aksrt')`]) {
|
||
assert.ok(tickets.includes(label), `missing reference-theme label: ${label}`)
|
||
}
|
||
|
||
// 表头列与参考主题一致:状态/类型/提交用户/详情/提交时间/更新时间(带排序)
|
||
for (const head of ['状态', '类型', '提交用户', '详情', '提交时间', '更新时间']) {
|
||
assert.ok(tickets.includes(`__('${head}', 'aksrt')`), `missing table head: ${head}`)
|
||
}
|
||
assert.match(tickets, /orderby-but/)
|
||
assert.match(tickets, /dashicons-arrow-up/)
|
||
|
||
// 行渲染与处理表单对齐参考主题
|
||
for (const label of [`__('立即处理', 'aksrt')`, `__('正在处理', 'aksrt')`, `__('被举报用户', 'aksrt')`, `__('被举报用户禁封信息', 'aksrt')`, `__('申诉用户', 'aksrt')`, `__('禁封信息', 'aksrt')`, `__('申诉信息', 'aksrt')`, `__('处理方式', 'aksrt')`, `__('处理留言', 'aksrt')`, `__('申诉通过(解封该用户)', 'aksrt')`, `__('申诉拒绝(保持原禁封状态)', 'aksrt')`, `__('此处内容会反馈给举报人', 'aksrt')`, `__('确认提交', 'aksrt')`, `__('用户状态正常', 'aksrt')`, `__('当前用户已解封', 'aksrt')`, `__('此用户已解封', 'aksrt')`, `__('暂无申请记录', 'aksrt')`]) {
|
||
assert.ok(tickets.includes(label), `missing process form label: ${label}`)
|
||
}
|
||
|
||
// 分页对齐参考主题 zibpay_admin_pagenavi(每页 20 条)
|
||
assert.match(tickets, /TICKETS_PER_PAGE = 20/)
|
||
assert.match(tickets, /ticket_pagenavi\(int \$total_count, int \$per_page\)/)
|
||
for (const label of ['前往第一页', '前往上一页', '前往下一页', '前往最后一页', 'displaying-num', 'paging-input', 'total-pages']) {
|
||
assert.ok(tickets.includes(label), `missing pagenavi part: ${label}`)
|
||
}
|
||
|
||
// 旧的三 tab 结构已收敛;举报处理表单不包含处罚下拉(对齐参考主题)
|
||
assert.ok(!usersMenus.includes('tab=reports'), 'old reports tab is gone')
|
||
assert.ok(!usersMenus.includes('tab=appeals'), 'old appeals tab is gone')
|
||
assert.ok(!usersMenus.includes('render_reports_tab'))
|
||
assert.ok(!usersMenus.includes('render_appeals_tab'))
|
||
assert.match(usersMenus, /use UsersMenusTickets;/)
|
||
assert.ok(!tickets.includes('name="action_taken"'), 'report process form should be status-only like the reference theme')
|
||
})
|
||
|
||
test('report modal mirrors the reference theme: preset reasons, image evidence, notice, geometry', () => {
|
||
const scripts = read('src/Core/ScriptsPart1c.php')
|
||
// 预置原因真实数据源是 ban_preset_reason(UserRuntime::ban_settings),
|
||
// 不再读取 report_settings 中不存在的 categories 键(短路已修)。
|
||
assert.match(scripts, /UserRuntime::ban_settings\(\)\['reasons'\]/)
|
||
assert.ok(!scripts.includes("$report['categories']"), 'dead report.categories source must be removed')
|
||
// 图片举证开关与举报说明随 aksrtData 注入。
|
||
assert.match(scripts, /imgEnabled/)
|
||
assert.match(scripts, /report_settings\(\)/)
|
||
|
||
const reportJs = read('assets/js/report.js')
|
||
// Zibll 同构标记:edit-header 警示头、原因徽章 active 焦点色、其它原因、
|
||
// 图片举证入口(后台开启时)、举报说明注入、but-average modal-buts 底栏。
|
||
for (const part of ['edit-header', 'toggle-radius mr10 c-red', 'data-aksrt-report-reason', 'form-but-radio', '其它原因', '违规链接', '详细描述', '添加图片举证', 'but-average modal-buts']) {
|
||
assert.ok(reportJs.includes(part), `report.js missing reference-theme part: ${part}`)
|
||
}
|
||
assert.match(reportJs, /report\/upload-image/)
|
||
assert.ok(!reportJs.includes('is-active'), 'chips must toggle the reference .active class, not a custom one')
|
||
|
||
// 举报图片上传端点存在,复用评论上传回调、独立权限(登录 + 举报 + 图片开关)。
|
||
assert.match(userController, /'\/report\/upload-image'/)
|
||
|
||
// 几何 1:1:桌面端 600px(参考主题 Bootstrap 默认模态宽)。
|
||
const css = read('assets/css/frontend/modal-runtime.css')
|
||
assert.match(css, /\.aksrt-report-modal:not\(\.aksrt-modal-runtime--bottom\) \.aksrt-modal-runtime__dialog/)
|
||
assert.match(css, /600px/)
|
||
// 加号图标单一来源:main.css 的 .preview .add:before/:after(inset:0 + margin:auto),
|
||
// modal-runtime 不得重复定义(否则过约束冲突,加号偏移只剩“L”形)。
|
||
assert.ok(!/form-upload \.preview \.add::?(before|after)/.test(css), 'duplicated add plus rules must stay out of modal-runtime.css')
|
||
})
|
||
|
||
test('comments expose a report entry for other users when reporting is enabled', () => {
|
||
// 举报位于“回复”右侧三点下拉菜单内(参考主题同位置),载荷走 wp_json_encode。
|
||
assert.match(commentItem, /data-aksrt-report="/)
|
||
assert.match(commentItem, /function aksrt_get_comment_report_html/)
|
||
assert.match(commentItem, /comment-report-link/)
|
||
assert.match(commentItem, /report'\]\['enabled'\]/)
|
||
assert.match(commentItem, /\$author_id === \$reporter_id/)
|
||
// 下拉菜单(置顶/编辑/批准/删除 + 举报)包含举报项。
|
||
assert.match(commentItem, /function aksrt_get_comments_action_dropdown_html[\s\S]*?\$lists \.= aksrt_get_comment_report_html/)
|
||
// 举报不作为 comt-meta 独立按钮直接渲染。
|
||
assert.doesNotMatch(commentItem, /\$report\s*=\s*aksrt_get_comment_report_html\(\$comment\)/)
|
||
})
|
||
|
||
test('uninstall cleans up every ban/report meta key written by the theme', () => {
|
||
for (const key of ['_aksrt_active_restrictions', '_aksrt_is_banned', 'aksrt_user_ban_state']) {
|
||
assert.ok(uninstall.includes(`'${key}'`), `missing uninstall cleanup: ${key}`)
|
||
}
|
||
})
|
||
|
||
test('restriction REST operations pass expiry through to the service', () => {
|
||
assert.match(restrictionOps, /\$params\['expires_at'\] \?\? ''/)
|
||
assert.match(restrictionOps, /Restriction::get_instance\(\)->restrict\(/)
|
||
})
|