- 一次性邀请码:批量生成(次数/有效期/备注/四类奖励)、原子消费防超发、 开启注册验证时奖励延迟发放(账本幂等键) - Zibll 兼容文本导入(points/balance/vip/vip_time/level_integral,分隔符可自定义, 第三段标识存为备注)、CSV 全字段导出与可回流文本导出 - 使用记录表 aksrt_invite_code_usage + 后台使用者列;注册表单 400ms 防抖实时校验 - 注册门控三模式(close/open/must)设置链路(PHP schema + admin SPA schema) - 契约测试覆盖建表/消费竞态/导入导出/备注全链路
133 lines
6.7 KiB
JavaScript
133 lines
6.7 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 判定。
|
|
assert.match(service, /SET used_count = used_count \+ 1 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')
|
|
assert.match(page, /render_invite_codes_page/)
|
|
assert.match(page, /\$\{endpoint\}\/generate/)
|
|
assert.match(page, /_wpnonce/)
|
|
|
|
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('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/)
|
|
// REST 写入口 + 后台导入面板。
|
|
assert.match(service, /invite-codes\/import/)
|
|
const page = read('src', 'Admin', 'Native', 'UsersMenusInviteCode.php')
|
|
assert.match(page, /aksrt-inv-import-data/)
|
|
assert.match(page, /\$\{endpoint\}\/import/)
|
|
assert.match(page, /aksrt-inv-import-division/)
|
|
})
|
|
|
|
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 ''/)
|
|
|
|
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 含 remark+奖励列;文本导出与导入格式互通。
|
|
assert.match(service, /code,batch_id,remark,rewards,max_uses/)
|
|
assert.match(service, /invite-codes\/export-text/)
|
|
assert.match(service, /\$row\['code'\] \. \$division \. \$this->reward_param_string/)
|
|
|
|
const page = read('src', 'Admin', 'Native', 'UsersMenusInviteCode.php')
|
|
assert.match(page, /aksrt-inv-remark/)
|
|
assert.match(page, /remark: root\.querySelector\('#aksrt-inv-remark'\)/)
|
|
assert.match(page, /invite-codes\/export-text/)
|
|
assert.match(page, /item\.remark \|\| '—'/)
|
|
})
|
|
|
|
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\(\)/)
|
|
})
|