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/) // 管理页为服务端渲染三视图(列表/添加/导出),通过 page-title-action 切换。 const page = read('src', 'Admin', 'Native', 'UsersMenusInviteCode.php') assert.match(page, /render_invite_codes_page/) assert.match(page, /page-title-action/) assert.match(page, /subsubsub/) assert.match(page, /data-clipboard-text/) assert.match(page, /orderby-but/) assert.match(page, /bulk_action/) assert.match(page, /aksrt_invite_delete/) assert.match(page, /aksrt_invite_add/) assert.match(page, /aksrt_invite_export/) assert.match(page, /paginate_links/) // 管理页不再依赖 REST 与 wp_rest nonce。 assert.doesNotMatch(page, /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'\]/) // 单次导入上限与重复码不计入成功。 assert.match(service, /array_slice\(preg_split\('\/\\r\\n\|\\r\|\\n\/', \$raw\) \?: \[\], 0, 1000\)/) assert.match(service, /\$exists !== null/) assert.match(service, /\$failed_codes\[\] = \$code/) // 导入走添加视图表单提交(add_type=import),textarea + 分隔符。 const page = read('src', 'Admin', 'Native', 'UsersMenusInviteCode.php') assert.match(page, /name="import_data"/) assert.match(page, /name="import_division"/) assert.match(page, /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 page = read('src', 'Admin', 'Native', 'UsersMenusInviteCode.php') // 添加表单标识字段(默认 invita_YmdHis);列表标识列可点击定位。 assert.match(page, /name="auto_remarks"/) assert.match(page, /invita_' \. current_time\('YmdHis'\)/) 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\(\)/) })