feat(affiliate): attribution modes, settlement hardening, trusted proxies, QR and poster
- 归因模式可配置(first_touch/last_touch)、停佣开关、懒结算与佣金基数降级复查 - aksrt_get_client_ip 接入 aksrt_trusted_proxies 信任代理名单,防 CDN 头伪造 - 收益页 Canvas 二维码 + 750x1000 推广海报(本地打包 qrcode.min.js,无 CDN) - 契约测试固化归因/海报/信任代理链路
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
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('QR generator is bundled locally; no external QR CDN in front-end sources', () => {
|
||||
const vendor = path.join(root, 'assets', 'js', 'vendor', 'qrcode.min.js')
|
||||
assert.ok(fs.existsSync(vendor), 'assets/js/vendor/qrcode.min.js must be committed')
|
||||
const script = fs.readFileSync(vendor, 'utf8')
|
||||
assert.ok(script.length > 10000, 'qrcode.min.js must not be a placeholder')
|
||||
// 前端引用面不允许出现外部二维码 CDN。
|
||||
const sources = [
|
||||
read('assets', 'js', 'credits.js'),
|
||||
read('inc', 'helpers', 'user-center-renderers', 'profile-revenue.php'),
|
||||
read('src', 'Core', 'ScriptsPart1b.php'),
|
||||
].join('\n')
|
||||
assert.doesNotMatch(sources, /cdnjs\.cloudflare\.com[^"']*qrcode|cdn\.jsdelivr\.net[^"']*qrcode|unpkg\.com[^"']*qrcode/)
|
||||
})
|
||||
|
||||
test('revenue page renders the QR row and credits.js draws + downloads it', () => {
|
||||
const template = read('inc', 'helpers', 'user-center-renderers', 'profile-revenue.php')
|
||||
assert.match(template, /data-affiliate-qr-row/)
|
||||
assert.match(template, /data-affiliate-qr(?!-)/)
|
||||
assert.match(template, /data-affiliate-qr-download/)
|
||||
assert.match(template, /data-affiliate-poster/)
|
||||
|
||||
const script = read('assets', 'js', 'credits.js')
|
||||
assert.match(script, /renderAffiliateQr\(root, j\.url \|\| ''\)/)
|
||||
// 库缺失时整行隐藏,不抛错。
|
||||
assert.match(script, /typeof window\.qrcode !== 'function'/)
|
||||
assert.match(script, /row\.hidden = true/)
|
||||
// Canvas 绘制(isDark 逐模块填充)+ PNG 下载。
|
||||
assert.match(script, /qr\.isDark\(r, c\)/)
|
||||
assert.match(script, /canvas\.toDataURL\('image\/png'\)/)
|
||||
})
|
||||
|
||||
test('poster canvas is built from admin-configured copy and gated on the poster switch', () => {
|
||||
const runtime = read('src', 'Settings', 'PaymentRuntime.php')
|
||||
assert.match(runtime, /'poster' => \[/)
|
||||
assert.match(runtime, /pay_rebate_poster_s/)
|
||||
assert.match(runtime, /pay_rebate_poster_img/)
|
||||
assert.match(runtime, /pay_rebate_poster_title/)
|
||||
assert.match(runtime, /pay_rebate_poster_desc/)
|
||||
|
||||
const attribution = read('src', 'Payment', 'ReferralAttribution.php')
|
||||
assert.match(attribution, /'poster' => \(array\) \(\$affiliate\['poster'\] \?\? \['enabled' => false\]\)/)
|
||||
assert.match(attribution, /'site_name' => get_bloginfo\('name'\)/)
|
||||
|
||||
const script = read('assets', 'js', 'credits.js')
|
||||
// 海报仅在开关开启时暴露按钮;绘制含顶部图、文案换行、二维码与站点名。
|
||||
assert.match(script, /initAffiliatePoster\(root, j\.url \|\| '', j\.poster \|\| \{\}, j\.site_name \|\| document\.title\)/)
|
||||
assert.match(script, /poster\.enabled !== true\) return/)
|
||||
assert.match(script, /function buildPosterCanvas/)
|
||||
assert.match(script, /function wrapText\(ctx, text, maxWidth\)/)
|
||||
assert.match(script, /image\.crossOrigin = 'anonymous'/)
|
||||
})
|
||||
|
||||
test('invite-code input verifies live against the public REST endpoint', () => {
|
||||
const template = read('templates', 'parts', 'auth-content.php')
|
||||
assert.match(template, /data-invite-verify/)
|
||||
assert.match(template, /data-invite-feedback/)
|
||||
|
||||
const script = read('assets', 'js', 'auth.js')
|
||||
assert.match(script, /function initInviteVerify\(root\)/)
|
||||
assert.match(script, /input\[data-invite-verify\]/)
|
||||
assert.match(script, /initInviteVerify\(root\)/)
|
||||
assert.match(script, /\/wp-json\/aksrt\/v1\/invite-code\/verify\?code=/)
|
||||
})
|
||||
|
||||
test('qrcode asset is enqueued only for profile + affiliate and credits depends on it', () => {
|
||||
const scripts = read('src', 'Core', 'ScriptsPart1b.php')
|
||||
assert.match(scripts, /assets\/js\/vendor\/qrcode\.min\.js/)
|
||||
assert.match(scripts, /'aksrt-qrcode'/)
|
||||
// credits.js 的依赖数组必须在 enqueue 调用内合并 QR 库句柄。
|
||||
assert.match(scripts, /array_merge\(\['loader_js', 'aksrt-vip-coordinator'\], \$affiliate_deps\)/)
|
||||
})
|
||||
|
||||
test('client IP honours the trusted-proxy filter before CDN headers', () => {
|
||||
const captcha = read('inc', 'modules', 'captcha.php')
|
||||
assert.match(captcha, /apply_filters\('aksrt_trusted_proxies', \[\]\)/)
|
||||
assert.match(captcha, /function aksrt_ip_in_list\(string \$ip, array \$list\): bool/)
|
||||
// CIDR 比较必须先检查同族地址长度,避免跨族误判。
|
||||
assert.match(captcha, /strlen\(\$ip_binary\) !== strlen\(\$subnet_binary\)/)
|
||||
// 过滤命中失败时直接回落 REMOTE_ADDR,不读转发头。
|
||||
assert.match(captcha, /!aksrt_ip_in_list\(\$remote, \$trusted\)/)
|
||||
})
|
||||
Reference in New Issue
Block a user