对照参考主题(Zibll)修正四项偏差,申诉链路从页面整体迁移至弹窗:
- feat(ban): 新增三个 1:1 复刻弹窗(REST /user-ban/{info,appeal,log}-modal):
查看禁封状态(多彩头 c-gray + 信息行 + 申诉按钮)、禁封申诉(edit-header
gavel/c-blue + 信息框 + 资料项 data[key] 输入 + 审核中态 + 禁止申诉态)、
禁封记录(多彩头 c-yellow + 计数 + 逐条卡片);user-ban.js 增加
data-aksrt-ban-info/appeal/log 委托与 admin-ajax 申诉提交
- feat(ban): 禁封/解封弹窗的「当前用户已有N次禁封记录」静态文案改为可点击
黄链(N>1),打开禁封记录弹窗;解封视图信息行改复用统一数据源
- refactor(ban): 治理展示态收敛至 Restriction::state_badges()(封号=c-red
违规已封号 / 小黑屋=c-yellow 小黑屋禁封中 / 禁言=c-gray 禁言中,颜色与
字段统一管理)+ get_ban_info()(状态/起止时间/原因/说明/申诉权限,
三弹窗与禁封页共用)
- feat(ban): 登录行为对齐参考主题——移除登录期拦截(enforce_login_ban)
与 complete_login 禁封闸门,封号用户可正常登录;新增 template_redirect
前台禁封页(1:1 zib_user_ban_restrict_login/zib_die_page:红盒提示 +
信息行 + 申诉权限 + 退出登录/申诉按钮,模板 templates/parts/user/ban-block.php)
- feat(profile): 公开主页禁封状态从资料卡下方提示框改为占据身份行元信息位
的 badg 徽章(参考主题 author_header_identity 语义);用户中心元信息改为
but 按钮(徽章+箭头)打开查看禁封状态弹窗;评论拒绝态链接同源
- refactor: 移除申诉页面模板(page-ban-appeal.php)及其资产/注册/入队、
get_profile_notice/profile_notice 链、state_labels/state_notices/
login_ban_message、UserRuntime 的 public_notice/ban_notice_text 死配置、
report_settings 死键、无消费方的 aksrt_ban_appeal_status 端点
- feat(ban): BanAppeal::ajax_submit 兼容弹窗 data[key] 表单结构
(逐项清洗入库,申诉说明取 data['申诉说明'],旧平铺字段保持兼容)
- chore: 重新构建 public.css/public.min.css(三个新弹窗几何与头部样式)
- test: 契约测试重写(徽章元信息位、三弹窗与记录链接、禁封页、允许登录 +
前台拦截、死代码移除断言),全量 362 项通过
532 lines
36 KiB
JavaScript
532 lines
36 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')
|
||
const userBanHelper = read('inc/helpers/user-ban.php')
|
||
const userBanJs = read('assets/js/user-ban.js')
|
||
const authorHero = read('templates/parts/user/author-hero.php')
|
||
const banBlock = read('templates/parts/user/ban-block.php')
|
||
const ucHeader = read('inc/helpers/user-center-renderers/profile-header.php')
|
||
const commentsHelper = read('inc/helpers/aksrt-comments.php')
|
||
const identityHelper = read('inc/helpers/aksrt-identity.php')
|
||
const avatarHelper = read('inc/helpers/aksrt-avatar.php')
|
||
const singleComponents = read('inc/helpers/aksrt-single-components.php')
|
||
const banAppeal = read('src/User/BanAppeal.php')
|
||
const mainCss = read('assets/css/main.css')
|
||
const mainMinCss = read('assets/css/main.min.css')
|
||
const userRuntime = read('src/Settings/UserRuntime.php')
|
||
const phoneOps = read('src/Admin/UserControllerAuthPhoneOperations.php')
|
||
const authService = read('src/Auth/Service.php')
|
||
const oauthAccountService = read('src/Auth/OAuthAccountService.php')
|
||
const weixinGzh = read('src/Auth/WeixinGzh.php')
|
||
const oauthFlow = read('src/Auth/OAuth.php')
|
||
const oauthProxy = read('src/Auth/OAuthProxyClient.php')
|
||
const authJs = read('assets/js/auth.js')
|
||
const userCenterJs = read('assets/js/routes/user-center.js')
|
||
const modalRuntimeCss = read('assets/css/frontend/modal-runtime.css')
|
||
const templatesMap = read('src/Core/Templates.php')
|
||
const scriptsPart1b = read('src/Core/ScriptsPart1b.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('ban identity badge occupies the public profile meta row', () => {
|
||
// 公开主页:禁封状态占据身份行元信息位(参考主题 author_header_identity),
|
||
// 不再是资料卡下方提示框(notice 链已移除)。
|
||
assert.match(userBanHelper, /function aksrt_get_user_ban_identity_badge/)
|
||
assert.match(authorHero, /aksrt_get_user_ban_identity_badge\(\$user_id\)/)
|
||
assert.match(authorHero, /aksrt-author-hero__identity/)
|
||
assert.ok(!authorHero.includes('aksrt-author-hero__notice'), 'profile notice box must stay removed')
|
||
assert.ok(!authorHero.includes('profile_notice'), 'dashboard must not carry profile_notice anymore')
|
||
assert.ok(!home.includes('get_profile_notice'), 'Home::get_profile_notice must stay removed')
|
||
assert.ok(!mainCss.includes('aksrt-author-hero__notice'), 'notice box CSS must stay removed')
|
||
// 徽章字段(文案/颜色/图标)由 Restriction::state_badges() 统一管理。
|
||
assert.match(userBanHelper, /state_badges\(\)/)
|
||
// 死配置清理:公开提示开关与提示文案键不再进入运行时。
|
||
assert.ok(!userRuntime.includes('public_notice'), 'dead public_notice setting must stay removed')
|
||
assert.ok(!userRuntime.includes('ban_notice_text'), 'dead ban_notice_text setting must stay removed')
|
||
// 用户中心元信息入口:but 按钮(徽章 + 箭头)→ 查看禁封状态弹窗。
|
||
assert.match(userBanHelper, /function aksrt_get_user_ban_info_link/)
|
||
assert.match(userBanHelper, /data-aksrt-ban-info=/)
|
||
assert.match(ucHeader, /aksrt_get_user_ban_info_link/)
|
||
})
|
||
|
||
test('governance display state is a single source shared by every prompt surface', () => {
|
||
// 展示态与徽章字段集中在 Restriction(身份行/用户中心/评论拒绝态/昵称徽章共用)。
|
||
for (const state of ['STATE_BANNED', 'STATE_RESTRICTED', 'STATE_MUTED']) {
|
||
assert.match(restriction, new RegExp(`const ${state}\\s+=`), `missing display state: ${state}`)
|
||
}
|
||
assert.match(restriction, /public function get_display_state\(int \$user_id\): string/)
|
||
assert.match(restriction, /public static function state_badges\(\): array/)
|
||
// 颜色统一管理:封号=红、小黑屋=黄、禁言=灰。
|
||
assert.match(restriction, /'label' => __\('违规已封号', 'aksrt'\),\s*'tone' => 'c-red'/)
|
||
assert.match(restriction, /'label' => __\('小黑屋禁封中', 'aksrt'\),\s*'tone' => 'c-yellow'/)
|
||
assert.match(restriction, /'label' => __\('禁言中', 'aksrt'\),\s*'tone' => 'c-gray'/)
|
||
// 禁封信息行单一数据源(查看禁封状态弹窗/申诉弹窗/禁封页/管理端弹窗共用)。
|
||
assert.match(restriction, /public function get_ban_info\(int \$user_id\): array/)
|
||
assert.match(restriction, /'status'\s*=>/)
|
||
assert.match(restriction, /'no_appeal_desc'/)
|
||
assert.match(restriction, /is_banned\(\$user_id\)[\s\S]*?is_suspended\(\$user_id\)[\s\S]*?is_restricted\(\$user_id, self::TYPE_MUTE\)/)
|
||
// 请求级缓存:评论列表逐条渲染昵称徽章不得反复查库。
|
||
assert.match(userBanHelper, /static \$cache = \[\]/)
|
||
})
|
||
|
||
test('ban prompt surface mirrors the reference theme modals and block page', () => {
|
||
// 查看禁封状态弹窗(1:1 zib_get_ban_info_modal:多彩头 + 信息行 + 申诉按钮)。
|
||
assert.match(userBanHelper, /function aksrt_get_user_ban_info_modal/)
|
||
assert.match(userBanHelper, /aksrt-user-ban-info-modal/)
|
||
assert.match(userBanHelper, /您已解封,请刷新页面/)
|
||
assert.match(userBanHelper, /您当前已无法进行申诉/)
|
||
// 禁封申诉弹窗(1:1 zib_get_user_ban_appeal_modal:edit-header + 信息框 +
|
||
// 资料项 inputs + 审核中态 + 禁止申诉态;仅本人)。
|
||
assert.match(userBanHelper, /function aksrt_get_user_ban_appeal_modal/)
|
||
assert.match(userBanHelper, /aksrt-user-ban-appeal-modal/)
|
||
assert.match(userBanHelper, /function aksrt_get_user_ban_appeal_link/)
|
||
assert.match(userBanHelper, /data-aksrt-ban-appeal=/)
|
||
assert.match(userBanHelper, /抱歉!您当前已无法进行申诉/)
|
||
assert.match(userBanHelper, /您的申诉正在审核中,请耐心等待/)
|
||
assert.match(userBanHelper, /name="data\[/)
|
||
assert.match(userBanHelper, /详细的说明更容易通过审核喔/)
|
||
// 申诉表单 data[key] 结构与后端兼容。
|
||
assert.match(banAppeal, /is_array\(\$_POST\['data'\]\)/)
|
||
assert.match(banAppeal, /申诉说明/)
|
||
// 禁封记录弹窗 + 记录链接(1:1 zib_get_ban_log_modal / zib_get_user_ban_log_link)。
|
||
assert.match(userBanHelper, /function aksrt_get_user_ban_log_modal/)
|
||
assert.match(userBanHelper, /function aksrt_get_user_ban_log_link/)
|
||
assert.match(userBanHelper, /data-aksrt-ban-log=/)
|
||
assert.match(userBanHelper, /当前用户已有%d次禁封记录,请谨慎操作/)
|
||
assert.match(userBanHelper, /该用户共有%d次禁封记录/)
|
||
// 登录后禁封状态页(1:1 zib_user_ban_restrict_login / zib_die_page)。
|
||
assert.match(userBanHelper, /function aksrt_render_user_ban_block_page/)
|
||
assert.match(banBlock, /抱歉,您的账号当前处于禁封状态,暂时无法登录/)
|
||
assert.match(banBlock, /退出登录/)
|
||
assert.match(banBlock, /aksrt_get_user_ban_appeal_link/)
|
||
for (const row of ['用户名', '状态', '开始时间', '结束时间', '原因', '说明', '申诉权限']) {
|
||
assert.ok(banBlock.includes(`__('${row}', 'aksrt')`), `ban block page missing row: ${row}`)
|
||
}
|
||
// Restriction 以 template_redirect 收口(不禁登录)。
|
||
assert.match(restriction, /add_action\('template_redirect', \[\$this, 'restrict_frontend_access'\], 1\)/)
|
||
assert.match(restriction, /public function restrict_frontend_access\(\): void/)
|
||
assert.ok(!restriction.includes('enforce_login_ban'), 'login-time ban block must stay removed')
|
||
// 评论拒绝态(参考主题 zib_get_user_ban_nocan_info)。
|
||
assert.match(userBanHelper, /function aksrt_get_user_ban_nocan_html/)
|
||
assert.match(commentsHelper, /aksrt_get_user_ban_nocan_html/)
|
||
assert.match(commentsHelper, /暂时无法发表评论/)
|
||
// 昵称徽章 + 头像角标(参考主题 user_name_badge / user_avatar_badge)。
|
||
assert.match(userBanHelper, /function aksrt_get_user_ban_name_badge/)
|
||
assert.match(userBanHelper, /function aksrt_get_user_ban_avatar_badge/)
|
||
assert.match(identityHelper, /aksrt_get_user_ban_name_badge\(\$user_id\)/)
|
||
assert.match(avatarHelper, /aksrt_get_user_ban_avatar_badge\(\$user_id\)/)
|
||
// 三个弹窗 REST 路由(modal-mini 几何 / 每次刷新)。
|
||
for (const route of ['/user-ban/info-modal', '/user-ban/appeal-modal', '/user-ban/log-modal']) {
|
||
assert.ok(userController.includes(`'${route}'`), `missing REST route: ${route}`)
|
||
}
|
||
// 前端委托与 admin-ajax 提交。
|
||
for (const attr of ['data-aksrt-ban-info', 'data-aksrt-ban-appeal', 'data-aksrt-ban-log']) {
|
||
assert.ok(userBanJs.includes(attr), `missing JS delegation: ${attr}`)
|
||
}
|
||
assert.match(userBanJs, /aksrt_ban_appeal_submit/)
|
||
assert.match(userBanJs, /new FormData\(form\)/)
|
||
// modal-mini 几何(400px)与头部样式覆盖三个新弹窗类。
|
||
for (const cls of ['aksrt-user-ban-info-modal', 'aksrt-user-ban-appeal-modal', 'aksrt-user-ban-log-modal']) {
|
||
assert.ok(modalRuntimeCss.includes(cls), `missing modal geometry for ${cls}`)
|
||
}
|
||
// 头像角标样式:源文件与入队的 min 产物同步。
|
||
for (const css of [mainCss, mainMinCss]) {
|
||
assert.ok(css.includes('aksrt-avatar-ban-badge'), 'ban avatar badge CSS must exist in both main.css and main.min.css')
|
||
}
|
||
// 申诉页面模板与专属资产已移除(死代码清理)。
|
||
assert.ok(!templatesMap.includes('page-ban-appeal'), 'ban appeal page template registration must stay removed')
|
||
assert.ok(!scriptsPart1b.includes('page-ban-appeal'), 'ban appeal page assets must stay removed')
|
||
})
|
||
|
||
test('report entry hides targets already banned or suspended', () => {
|
||
// 参考主题 zib_get_report_link:已封禁/小黑屋用户不展示举报入口(禁言不隐藏)。
|
||
assert.match(userBanHelper, /function aksrt_is_user_ban_restricted/)
|
||
assert.match(commentItem, /aksrt_is_user_ban_restricted\(\$author_id\)/)
|
||
assert.match(singleComponents, /aksrt_is_user_ban_restricted\(\$post_author_id\)/)
|
||
assert.match(authorHero, /aksrt_is_user_ban_restricted\(\$user_id\)/)
|
||
})
|
||
|
||
test('banned users may log in and meet the frontend block page', () => {
|
||
// 对齐参考主题:封号用户可正常登录(不禁登录),登录后前台任何页面展示禁封页。
|
||
assert.ok(!restriction.includes("add_filter('authenticate'"), 'login-time ban block must stay removed')
|
||
assert.ok(!authService.includes('user_banned'), 'dead user_banned error branch must stay removed')
|
||
assert.ok(!authService.includes('get_error_code'), 'friendly_login_error must not carry the dead code param')
|
||
assert.ok(!oauthAccountService.includes('is_banned'), 'complete_login must not gate login')
|
||
const completeLoginFn = oauthAccountService.slice(oauthAccountService.indexOf('function complete_login'))
|
||
const completeLoginBody = completeLoginFn.slice(0, completeLoginFn.indexOf('\n}'))
|
||
assert.ok(!completeLoginBody.includes('WP_Error'), 'complete_login must stay void')
|
||
assert.ok(!phoneOps.includes('login_ban_message'), 'SMS path must not carry the removed ban gate')
|
||
assert.ok(!weixinGzh.includes('account_banned'), 'weixin gzh must not carry the removed ban error code')
|
||
assert.ok(!oauthFlow.includes('account_banned'), 'oauth flow must not carry the removed ban error code')
|
||
assert.ok(!oauthProxy.includes('account_banned'), 'oauth proxy must not carry the removed ban error code')
|
||
for (const js of [authJs, userCenterJs]) {
|
||
assert.ok(!js.includes('account_banned'), 'removed account_banned JS message must stay gone')
|
||
}
|
||
// Toast 透传(一般性修复,保留):REST 层不得把失败原因硬编码为凭证错误。
|
||
assert.match(phoneOps, /\$result\['error'\]/)
|
||
assert.ok(
|
||
phoneOps.includes("$login_error !== '' ? $login_error : __('账号或密码错误', 'aksrt')"),
|
||
'auth_login must pass through the real failure reason'
|
||
)
|
||
})
|
||
|
||
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')
|
||
// 处理说明必须随 aksrt_report_status_changed 传入邮件载荷(此前硬编码空串,
|
||
// 举报人收到的“说明:”永远为空)。
|
||
assert.match(reportCenter, /do_action\('aksrt_report_status_changed', \$report_id[\s\S]*?new_status, \$note_clean\)/)
|
||
assert.match(mailNotifications, /function report_status_changed\(int \$report_id, int \$reporter_id, string \$status, string \$note = ''\)/)
|
||
assert.match(mailNotifications, /'note'\s*=>\s*\$note !== '' \? \$note :/)
|
||
// 举报通知必须入站内消息系统:提交→管理员(直达工单列表)、
|
||
// 处理结果→举报人(与结果邮件同一时机、同一说明)。
|
||
assert.match(reportCenter, /notify_admins_new_report\(/)
|
||
assert.match(reportCenter, /notify_reporter_status_changed\(/)
|
||
assert.match(reportCenter, /Message::get_instance\(\)->send\(/)
|
||
assert.match(reportCenter, /users\.php\?page=aksrt_user_ban&type=report&status=pending/)
|
||
})
|
||
|
||
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')`]) {
|
||
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}`)
|
||
}
|
||
// 处理表单必须携带隐藏 action 字段:admin-post.php 靠它触发
|
||
// admin_post_{action} 钩子,缺失时 WordPress 不输出任何内容(白屏)。
|
||
// value 按工单类型分发(report → update_report,appeal → process_appeal)。
|
||
assert.match(tickets, /name="action" value="' \. esc_attr\(\$kind === 'report' \? 'aksrt_users_update_report' : 'aksrt_users_process_appeal'\)/)
|
||
assert.match(usersMenus, /admin_post_aksrt_users_update_report/)
|
||
assert.match(usersMenus, /admin_post_aksrt_users_process_appeal/)
|
||
// 内联处理视图(参考主题方向):表单以合法的 colspan=6 单元格随待处理行
|
||
// 预渲染;展开/取消走 JS 行↔表单互换(无整页刷新),信息只出现一次,
|
||
// 不再裸嵌 tbody 被 foster-parent 错位。
|
||
assert.match(tickets, /<td colspan="6">';\s*\$this->render_ticket_process_form/)
|
||
assert.match(tickets, /data-aksrt-ticket-toggle="/)
|
||
assert.match(tickets, /data-aksrt-ticket-cancel="/)
|
||
assert.match(tickets, /data-aksrt-ticket-row="/)
|
||
assert.match(tickets, /e\.preventDefault\(\);/)
|
||
// 展开的处理表单提供“取消”按钮(确认提交右侧)。
|
||
assert.match(tickets, /class="button button-primary process-submit"/)
|
||
assert.match(tickets, /<a class="button" data-aksrt-ticket-cancel="' \. esc_attr\(\$row\['kind'\] \. '-' \. \(string\) \$row\['id'\]\) . '" href="' \. esc_url\(remove_query_arg\(\['action', 'id'\]\)\) . '">' \. esc_html__\('取消', 'aksrt'\)/)
|
||
|
||
// 分页对齐参考主题 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')
|
||
// 禁封限制 Tab 已移除(参考主题无此后台 Tab):禁封改走前台封禁弹窗。
|
||
assert.ok(!usersMenus.includes('tab=restrictions'), 'restrictions tab is gone')
|
||
assert.ok(!usersMenus.includes('render_restrictions_tab'), 'restrictions tab renderer is gone')
|
||
assert.ok(!usersMenus.includes('handle_add_restriction'), 'backend ban form handler is gone')
|
||
assert.ok(!usersMenus.includes('handle_remove_restriction'), 'backend unban form handler 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')
|
||
// 工单处理页「点击此处」跳转前台用户公开主页(封面三点菜单 → 封禁弹窗)。
|
||
assert.match(tickets, /aksrt_get_user_home_url\(\$target_id\)/)
|
||
assert.ok(!tickets.includes('tab=restrictions'), 'ticket ban link must not point at the removed backend tab')
|
||
})
|
||
|
||
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_ban_or_report_item/)
|
||
// 禁封项(管理员可见):参考主题 comments_action_lists 钩子语义,渲染于 user-ban.php。
|
||
assert.match(userBanHelper, /comment-ban-link/)
|
||
assert.match(userBanHelper, /data-aksrt-ban="/)
|
||
// 举报不作为 comt-meta 独立按钮直接渲染。
|
||
assert.doesNotMatch(commentItem, /\$report\s*=\s*aksrt_get_comment_report_html\(\$comment\)/)
|
||
})
|
||
|
||
test('frontend ban modal mirrors the reference theme zib_get_set_user_ban_modal 1:1', () => {
|
||
// 禁封方式/禁封时间/预置原因徽章/其它原因/禁封说明/删除全部内容/禁止用户申诉,
|
||
// 以及已禁封用户的状态信息与解封留言表单(弹窗经 REST /user-ban/modal 输出)。
|
||
for (const label of ['拉入小黑屋', '禁封用户', '禁封方式', '禁封时间', '选择禁封原因', '其它原因', '禁封说明', '删除全部内容', '禁止用户申诉', '请填写禁止申诉说明', '解封用户', '移出小黑屋', '给用户的留言', '开始时间', '结束时间', '原因', '说明', '禁封记录', '确认禁封']) {
|
||
assert.ok(userBanHelper.includes(label), `ban modal missing reference label: ${label}`)
|
||
}
|
||
// 状态文案由 Restriction::get_ban_info() 单一数据源产出(各提示面共用)。
|
||
for (const label of ['禁封中(禁止登录)', '小黑屋禁封中']) {
|
||
assert.ok(restriction.includes(label), `ban status label missing from Restriction: ${label}`)
|
||
}
|
||
// 时长选项 1/7/14/30/100/永久(默认 7 天);类型映射:小黑屋→suspend,禁封→ban。
|
||
assert.match(userBanHelper, /1 => __\('1天', 'aksrt'\)/)
|
||
assert.match(userBanHelper, /7 => __\('7天', 'aksrt'\)/)
|
||
assert.match(userBanHelper, /100 => __\('100天', 'aksrt'\)/)
|
||
assert.match(userBanHelper, /7 === \$days \? ' checked="checked"' : ''/)
|
||
// 预置原因来自 UserRuntime::ban_settings(与后台同一数据源)。
|
||
assert.match(userBanHelper, /UserRuntime::ban_settings\(\)\['reasons'\]/)
|
||
// 勾选禁止申诉必须填写说明(参考主题同校验),落用户 meta 供申诉入口拦截。
|
||
assert.match(userBanHelper, /_aksrt_no_appeal|no_appeal/)
|
||
const appeal = read('src/User/BanAppeal.php')
|
||
assert.match(appeal, /_aksrt_no_appeal/)
|
||
// 弹窗入口按钮契约:[data-aksrt-ban] 委托(封面按钮 / 评论菜单项 / 文章作者菜单项)。
|
||
assert.match(userBanHelper, /data-aksrt-ban="/)
|
||
assert.match(userBanHelper, /function aksrt_get_user_cover_menu_action_html/)
|
||
assert.match(userBanHelper, /function aksrt_get_comment_ban_or_report_item/)
|
||
assert.match(userBanHelper, /function aksrt_get_post_author_ban_item/)
|
||
// 权限模型(参考主题 zib_current_user_can('set_user_ban') 语义):
|
||
// manage_options + 非本人 + 非超级管理员 + 功能开关。
|
||
assert.match(userBanHelper, /current_user_can\('manage_options'\)/)
|
||
assert.match(userBanHelper, /\$current_id === \$user_id/)
|
||
assert.match(userBanHelper, /is_super_admin\(\$user_id\)/)
|
||
assert.match(userBanHelper, /user_ban_s/)
|
||
// REST 路由:modal 拉取 + set 提交(permission_callback 校验)。
|
||
assert.match(userController, /'\/user-ban\/modal'/)
|
||
assert.match(userController, /'\/user-ban\/set'/)
|
||
// 解封视图多彩横幅(参考主题 zib_get_modal_colorful_header('c-yellow', ...))。
|
||
assert.match(userBanHelper, /modal-colorful-header colorful-bg c-yellow/)
|
||
// 提交后整页刷新(禁封 reload:true)/ 仅关闭弹窗(解封 hide_modal:true)。
|
||
assert.match(userBanJs, /window\.location\.reload\(\)/)
|
||
assert.match(userBanJs, /AksrtModalRuntime\.close\('submit'\)/)
|
||
assert.match(userBanJs, /data-aksrt-ban-submit/)
|
||
assert.match(userBanJs, /data-aksrt-unban-submit/)
|
||
// 桌面 600px(参考主题未传 data-class → Bootstrap .modal-dialog 默认宽)
|
||
// + min-height 360px(data-height=360);移动端底部抽屉。
|
||
assert.match(userBanJs, /MODAL_HEIGHT = 360/)
|
||
assert.match(userBanJs, /mobileBottom: true/)
|
||
const css = read('assets/css/frontend/modal-runtime.css')
|
||
assert.match(css, /\.aksrt-user-ban-modal:not\(\.aksrt-modal-runtime--bottom\) \.aksrt-modal-runtime__dialog/)
|
||
assert.match(css, /600px/)
|
||
// textarea autoHeight 1:1(参考主题 maxHeight 锁高 + 边框补偿)。
|
||
assert.match(userBanJs, /getAttribute\('maxHeight'\)/)
|
||
// 三个入口模板接线:封面三点菜单 / 评论区三点菜单 / 文章作者 metabox 三点菜单。
|
||
const authorHero = read('templates/parts/user/author-hero.php')
|
||
assert.match(authorHero, /aksrt_get_user_cover_menu_action_html\(\$user_id\)/)
|
||
assert.match(commentItem, /aksrt_get_comment_ban_or_report_item\(\$comment\)/)
|
||
const singleComponents = read('inc/helpers/aksrt-single-components.php')
|
||
assert.match(singleComponents, /aksrt_get_post_author_ban_item\(/)
|
||
// 脚本入队(singular + profile,与 report.js 同组)。
|
||
const scripts = read('src/Core/ScriptsPart1b.php')
|
||
assert.match(scripts, /'aksrt-user-ban'/)
|
||
})
|
||
|
||
test('uninstall cleans up every ban/report meta key written by the theme', () => {
|
||
for (const key of ['_aksrt_active_restrictions', '_aksrt_is_banned', '_aksrt_no_appeal', '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\(/)
|
||
})
|
||
|
||
test('suspend (小黑屋) enforcement blocks publishing end to end', () => {
|
||
// 服务层提供 is_suspended / get_active_suspend(对齐参考主题:小黑屋
|
||
// 失去所有发布权限——发帖、评论等;此前 suspend 只在公开主页显示提示)。
|
||
assert.match(restriction, /public function is_suspended\(int \$user_id\): bool/)
|
||
assert.match(restriction, /public function get_active_suspend\(int \$user_id\): \?array/)
|
||
// 前台发帖入口(FrontSubmitSave::persist)拦截发帖/草稿/编辑。
|
||
const frontSave = read('src/Core/FrontSubmitSave.php')
|
||
assert.match(frontSave, /Restriction::get_instance\(\)->is_suspended\(\$user_id\)/)
|
||
assert.match(frontSave, /您已被关入小黑屋/)
|
||
// 评论提交(pre_comment_on_post)同时覆盖禁言(mute)与小黑屋(suspend)。
|
||
assert.match(restriction, /enforce_comment_mute[\s\S]*?is_suspended\(\$user_id\)/)
|
||
assert.match(restriction, /您已被关入小黑屋,暂时无法评论/)
|
||
})
|
||
|
||
test('report modal close icon and textarea autoHeight match the reference theme', () => {
|
||
const reportJs = read('assets/js/report.js')
|
||
// 关闭按钮走 SVG 雪碧图 icon-close(与参考主题 .ic-close 同构,fa-times 退役)。
|
||
assert.match(reportJs, /<svg class="ic-close" aria-hidden="true"><use xlink:href="#icon-close"><\/use><\/svg>/)
|
||
assert.ok(!reportJs.includes('fa-times'), 'close icon must use the SVG sprite, not FontAwesome')
|
||
// textarea 自动增高(参考主题 autoHeight 行为),隐藏中的“其它原因”跳过首次计算。
|
||
assert.match(reportJs, /function bindAutoHeight\(/)
|
||
assert.match(reportJs, /querySelectorAll\('textarea\.form-control'\)\.forEach\(bindAutoHeight\)/)
|
||
assert.match(reportJs, /offsetHeight > 0\) grow\(\)/)
|
||
})
|
||
|
||
test('wechat template messages cover report and account restriction types', () => {
|
||
const wechat = read('src/Payment/WeChatNotify.php')
|
||
assert.match(wechat, /const NOTIFY_TYPES = \['comment_reply', 'system_notice', 'order_paid', 'credits_change', 'report', 'system'\]/)
|
||
assert.match(wechat, /in_array\(\$type, self::NOTIFY_TYPES, true\)/)
|
||
// 订阅白名单与状态输出复用同一常量,不允许再出现硬编码子集。
|
||
assert.ok(!wechat.includes("['comment_reply', 'system_notice', 'order_paid', 'credits_change']"), 'type list must come from NOTIFY_TYPES')
|
||
// 站内通知缺省模板字段时用 title/description 兜底合成。
|
||
assert.match(wechat, /\$data\['description'\] \?\? ''/)
|
||
})
|