Replace standalone add and export views with accessible dialogs, refine list interactions, and remove obsolete paths and unused counts. Fix filtered totals and reject oversized imports without truncation, with regression coverage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
178 lines
10 KiB
JavaScript
178 lines
10 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 = (...segments) => fs.readFileSync(path.join(root, ...segments), 'utf8')
|
|
|
|
test('invite code table has a unique code key and a usage log table', () => {
|
|
const schema = read('src', 'Core', 'DBMigratorPart2.php')
|
|
assert.match(schema, /CREATE TABLE \{\$prefix\}aksrt_invite_code \([\s\S]*?UNIQUE KEY uniq_invite_code \(code\)/)
|
|
assert.match(schema, /CREATE TABLE \{\$prefix\}aksrt_invite_code \([\s\S]*?\) ENGINE=InnoDB/)
|
|
// 使用记录表:运营需要知道"哪个用户用了哪个码",且 (code_id, user_id) 唯一防重复记账。
|
|
assert.match(schema, /CREATE TABLE \{\$prefix\}aksrt_invite_code_usage \([\s\S]*?UNIQUE KEY uniq_invite_usage \(code_id, user_id\)/)
|
|
const registry = read('src', 'Core', 'DBMigratorPart3.php')
|
|
assert.match(registry, /'invite_code'\s+=>\s+'aksrt_invite_code'/)
|
|
assert.match(registry, /'invite_code_usage'\s+=>\s+'aksrt_invite_code_usage'/)
|
|
})
|
|
|
|
test('consumption is a single conditional UPDATE, not select-then-update', () => {
|
|
const service = read('src', 'Payment', 'InviteCode.php')
|
|
// TOCTOU 防线:消费必须是一条带条件的 UPDATE,且以 affected_rows === 1 判定;
|
|
// 同语句维护 updated_at(列表"更新时间"列与排序依据)。
|
|
assert.match(service, /SET used_count = used_count \+ 1, updated_at = %s WHERE code = %s AND status = 'active' AND \(max_uses = 0 OR used_count < max_uses\)/)
|
|
assert.match(service, /\$updated !== 1/)
|
|
// 消费成功后写入使用记录(使用者视图)。
|
|
assert.match(service, /invite_code_usage/)
|
|
assert.match(service, /recent_users_map/)
|
|
// 码生成必须使用 CSPRNG。
|
|
assert.match(service, /random_int\(/)
|
|
assert.doesNotMatch(service, /str_shuffle|mt_rand|rand\(/)
|
|
// 奖励幂等:确定性操作键。
|
|
assert.match(service, /OperationKey::deterministic\('invite-reward'/)
|
|
})
|
|
|
|
test('registration services gate on the invite-code field mode', () => {
|
|
for (const file of ['src/Auth/ServicePart1a.php', 'src/Auth/ServicePart1b.php']) {
|
|
const service = read(...file.split('/'))
|
|
assert.match(service, /InviteCode::get_instance\(\)/)
|
|
assert.match(service, /请填写邀请码/)
|
|
assert.match(service, /邀请码无效或已用完,请检查后重试/)
|
|
// 校验必须发生在 wp_create_user 之前。
|
|
assert.ok(
|
|
service.indexOf('is_valid($submitted_invite)') < service.indexOf('wp_create_user('),
|
|
`${file}: invite-code validation must run before user creation`
|
|
)
|
|
}
|
|
})
|
|
|
|
test('rewards defer to email verification when signup verification is on', () => {
|
|
const service = read('src', 'Payment', 'InviteCode.php')
|
|
assert.match(service, /signup_verification_enabled\(\)/)
|
|
assert.match(service, /aksrt_email_verified/)
|
|
assert.match(service, /META_PENDING/)
|
|
})
|
|
|
|
test('invite-code surfaces: form field, admin page, menus and both schemas', () => {
|
|
const template = read('templates', 'parts', 'auth-content.php')
|
|
assert.match(template, /name="invite_code"/)
|
|
assert.match(template, /get_invite_code_registration_settings/)
|
|
|
|
const page = read('src', 'Admin', 'Native', 'UsersMenusInviteCode.php')
|
|
const forms = read('src', 'Admin', 'Native', 'UsersMenusInviteForms.php')
|
|
assert.match(page, /render_invite_codes_page/)
|
|
assert.match(page, /use UsersMenusInviteForms;/)
|
|
assert.match(page, /render_invite_codes_list\(\$service, \$this_url, \$nonce\)/)
|
|
assert.match(page, /render_invite_codes_add\(\$nonce\)/)
|
|
assert.match(page, /render_invite_codes_export\(\$nonce\)/)
|
|
assert.match(page, /subsubsub/)
|
|
assert.match(page, /data-clipboard-text/)
|
|
assert.match(page, /orderby-but/)
|
|
assert.match(page, /bulk_action/)
|
|
assert.match(page, /paginate_links/)
|
|
assert.match(page, /aksrt_invite_delete/)
|
|
assert.match(forms, /aksrt_invite_add/)
|
|
assert.match(forms, /aksrt_invite_export/)
|
|
// 列表内不再有列表/添加/导出三视图路由;状态筛选仍是独立的列表筛选器。
|
|
assert.doesNotMatch(page + forms, /page-title-action|[?&](?:tab|view)=(?:add|export)|tab=(?:list|add|export)/)
|
|
// 管理页不再依赖 REST 与 wp_rest nonce。
|
|
assert.doesNotMatch(page + forms, /wp_rest|\$\{endpoint\}/)
|
|
|
|
const menus = read('src', 'Admin', 'Native', 'UsersMenus.php')
|
|
assert.match(menus, /use UsersMenusInviteCode/)
|
|
assert.match(menus, /aksrt_invite_codes/)
|
|
|
|
const bootstrap = read('functions.php')
|
|
assert.match(bootstrap, /InviteCode::get_instance\(\)->init\(\)/)
|
|
|
|
const phpSchema = read('src', 'Admin', 'Schema', 'SchemaUsers.php')
|
|
const jsSchema = read('assets', 'admin', 'src', 'schema', 'sections', 'users', 'auth.js')
|
|
for (const schema of [phpSchema, jsSchema]) {
|
|
assert.match(schema, /invite_code_s/)
|
|
assert.match(schema, /invite_code_open_desc/)
|
|
assert.match(schema, /invite_code_must_desc/)
|
|
}
|
|
})
|
|
|
|
test('admin operations run through admin_post handlers with capability and nonce guards', () => {
|
|
const service = read('src', 'Payment', 'InviteCode.php')
|
|
assert.match(service, /admin_post_aksrt_invite_add/)
|
|
assert.match(service, /admin_post_aksrt_invite_delete/)
|
|
assert.match(service, /admin_post_aksrt_invite_export/)
|
|
assert.match(service, /check_admin_referer\('aksrt_invite_admin', 'aksrt_nonce'\)/)
|
|
assert.match(service, /current_user_can\('edit_users'\)/)
|
|
// 管理端 REST 已移除:除公开 verify 外不再注册邀请码路由(导出文件名除外)。
|
|
assert.doesNotMatch(service, /register_rest_route\('aksrt\/v1', '\/invite-codes/)
|
|
assert.match(service, /register_rest_route\('aksrt\/v1', '\/invite-code\/verify'/)
|
|
// 列表/导出共用状态过滤;批量删除连使用记录一起清。
|
|
assert.match(service, /private function status_where\(string \$status\): array/)
|
|
assert.match(service, /'unused' => \['used_count = 0', \[\]\]/)
|
|
assert.match(service, /public function delete_codes\(array \$ids\): int/)
|
|
assert.match(service, /DELETE FROM \{\$usage_table\} WHERE code_id IN/)
|
|
// 列表支持状态/搜索/标识定位/排序/分页,默认按更新时间倒序。
|
|
assert.match(service, /\$orderby_map = \['code' => 'code', 'created_at' => 'created_at', 'updated_at' => 'updated_at', 'used_count' => 'used_count', 'remark' => 'remark'\]/)
|
|
assert.match(service, /\?\? 'updated_at'/)
|
|
assert.match(service, /'remark' => 'remark'/)
|
|
// 生成支持自定义位数(高级选项),单次上限 1000。
|
|
assert.match(service, /min\(1000, max\(1, \(int\) \(\$args\['count'\] \?\? 1\)\)\)/)
|
|
assert.match(service, /min\(50, max\(4, \(int\) \(\$args\['code_length'\] \?\? self::CODE_LENGTH\)\)\)/)
|
|
})
|
|
|
|
test('import accepts Zibll-compatible lines with per-code rewards', () => {
|
|
const service = read('src', 'Payment', 'InviteCode.php')
|
|
assert.match(service, /public function import_codes\(array \$args\)/)
|
|
// Zibll 行格式:`邀请码<分隔符>奖励<分隔符>标识`,奖励参数串键位兼容。
|
|
assert.match(service, /level_integral/)
|
|
assert.match(service, /vip_time/)
|
|
assert.match(service, /in_array\(\$reward, \['无', 'none', '0'\]/)
|
|
// 空行先剔除,超过 1000 个非空行时拒绝,而不是静默截断。
|
|
assert.match(service, /array_filter\(array_map\('trim', \$lines\), static fn\(\$line\) => \$line !== ''\)/)
|
|
assert.match(service, /count\(\$lines\) > 1000/)
|
|
assert.doesNotMatch(service, /array_slice\(preg_split\('\/\\r\\n\|\\r\|\\n\/', \$raw\) \?: \[\], 0, 1000\)/)
|
|
const adminScript = read('assets', 'js', 'admin-invite-codes.js')
|
|
assert.match(adminScript, /split\(\/\\r\\n\|\\r\|\\n\/\)\.filter\(function \(line\) \{ return line\.trim\(\) !== ''; \}\)/)
|
|
assert.match(service, /\$exists !== null/)
|
|
assert.match(service, /\$failed_codes\[\] = \$code/)
|
|
// 导入表单由独立 Forms trait 渲染,并走添加操作的 add_type=import。
|
|
const forms = read('src', 'Admin', 'Native', 'UsersMenusInviteForms.php')
|
|
assert.match(forms, /name="import_data"/)
|
|
assert.match(forms, /name="import_division"/)
|
|
assert.match(forms, /name="add_type"/)
|
|
})
|
|
|
|
test('per-code remark persists through generate/import and both exports round-trip', () => {
|
|
const schema = read('src', 'Core', 'DBMigratorPart2.php')
|
|
assert.match(schema, /remark\s+VARCHAR\(191\) NOT NULL DEFAULT ''/)
|
|
assert.match(schema, /updated_at\s+DATETIME NOT NULL DEFAULT/)
|
|
|
|
const service = read('src', 'Payment', 'InviteCode.php')
|
|
// 生成与导入都落 remark;导入第三段"标识"→备注。
|
|
assert.match(service, /\$remark = sanitize_text_field\(\(string\) \(\$args\['remark'\] \?\? ''\)\)/)
|
|
assert.match(service, /\$remark = sanitize_text_field\(trim\(\(string\) \(\$parts\[2\] \?\? ''\)\)\)/)
|
|
assert.match(service, /'remark'\s+=>\s+\$remark/)
|
|
// 导出奖励参数串与 parse_reward_string 互逆(balance 分→元,零奖励"无")。
|
|
assert.match(service, /private function reward_param_string\(string \$rewards_json\): string/)
|
|
assert.match(service, /'balance=' \. /)
|
|
assert.match(service, /\$parts === \[\] \? '无'/)
|
|
// 文本导出与导入格式互通;CSV(Excel)含中文表头与状态列。
|
|
assert.match(service, /\$row\['code'\] \. \$division \. \$this->reward_param_string/)
|
|
assert.match(service, /__\('邀请码', 'aksrt'\), __\('使用奖励', 'aksrt'\)/)
|
|
assert.match(service, /\$used > 0 \? __\('已使用', 'aksrt'\) : __\('未使用', 'aksrt'\)/)
|
|
|
|
const forms = read('src', 'Admin', 'Native', 'UsersMenusInviteForms.php')
|
|
// 自动生成标识字段默认 invita_YmdHis;列表标识列可点击定位。
|
|
assert.match(forms, /name="auto_remarks"/)
|
|
assert.match(forms, /invita_' \. current_time\('YmdHis'\)/)
|
|
const page = read('src', 'Admin', 'Native', 'UsersMenusInviteCode.php')
|
|
assert.match(page, /add_query_arg\('remark', \$item\['remark'\], \$this_url\)/)
|
|
})
|
|
|
|
test('settings reader exposes normalized invite-code registration mode', () => {
|
|
const runtime = read('src', 'Settings', 'CreditsRuntime.php')
|
|
assert.match(runtime, /get_invite_code_registration_settings\(\)/)
|
|
assert.match(runtime, /\['close', 'open', 'must'\]/)
|
|
const groups = read('src', 'Settings', 'CreditsPaymentGroups.php')
|
|
assert.match(groups, /CreditsRuntime::get_invite_code_registration_settings\(\)/)
|
|
})
|