Files
wordpress-theme-aksrt/tests/widget-system/oauth-security-contract.test.cjs
T
kate522 5fd7c212ed fix(runtime): 按参考主题硬切换为 loader/main/auto_fun 单一 DOM 生命周期
- 删除 AksrtFrontend registry/rehydrate 双路架构及 build/public.js 拼包入口,
  前台唯一初始路径为 PHP 本地栈(jQuery 1.9.1 → Bootstrap 3.4.1)
  → loader_js(_win/aksrtData 唯一注入)→ tbquire(['main']) → runtime/main.min.js
- window.auto_fun = debounce(z_auto_fun, 100) 为唯一重扫入口;所有模块改为
  执行一次 + off/on('auto_fun.aksrt-*') 命名空间重扫,data/class 标记幂等
- 局部更新协议统一:HTML 注入 → post_ajax.ed → auto_fun();
  远程 modal → loaded.bs.modal → auto_fun();JSON 成功 → zib_ajax.success
- 灯箱恢复 Miniimgbox/imgbox 唯一 owner(imgbox-container、a[data-imgbox]、
  imgbox_data),删除 AksrtLightbox/data-aksrt-lightbox 第二实现及 CSS/overlay,
  _win.imgbox* 重新接上 post-display 设置
- 死代码清理:runtime 下 11 组被自有实现替代的旧模块副本、无消费 vendored 库、
  顶层 grade.min.js、revenue.js、无调用方的 aksrt_get_win_runtime_config(),
  以及 loader 中 16 条死 mapping
- PHP 入队改为本地栈 + loader_js,routed/widget/submit 依赖统一换绑;
  用户卡片发布按钮恢复 nav_user_newposts_btn 设置门控
- 测试对齐:新增 frontend-lifecycle-contract,重写 navigation/lazyload/toast/
  auth-source/comment/page-display/post-visual 等断言为目标架构契约

验证:node --test widget-system+navigation 223 通过 0 失败;
构建脚本重建 main/imgbox min 与 CSS bundles;改动 PHP php -l、JS node --check
全部通过;git diff --check 干净;credits 重入 harness 通过。
2026-09-03 12:44:31 +08:00

200 lines
7.8 KiB
JavaScript

const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const test = require('node:test')
const vm = require('node:vm')
const { execFileSync } = require('node:child_process')
const root = path.resolve(__dirname, '..', '..')
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8')
const walkFiles = (dir, extensions) => {
const result = []
for (const entry of fs.readdirSync(path.join(root, dir), { withFileTypes: true })) {
const relative = path.join(dir, entry.name)
if (entry.isDirectory()) result.push(...walkFiles(relative, extensions))
else if (extensions.some((ext) => entry.name.endsWith(ext))) result.push(relative)
}
return result
}
const oauthScope = [
'src/Auth/OAuth.php',
'src/Auth/OAuthProviderCatalog.php',
'src/Auth/OAuthProviderBroker.php',
'src/Auth/OAuthAccountService.php',
'src/Auth/OAuthProxyClient.php',
'src/Auth/OAuthProxyServer.php',
'src/Auth/CLoginClient.php',
'src/Admin/Schema/SchemaUsers.php',
'assets/admin/src/schema/sections/users/oauth.js',
].map(read).join('\n')
test('OAuth PHP behavior harness passes', () => {
const output = execFileSync('php', [path.join(root, 'tests/widget-system/oauth-security-harness.php')], { encoding: 'utf8' })
assert.match(output, /OAuth security harness passed/)
})
test('Baidu and Alipay are absent from the OAuth surface', () => {
for (const needle of [
'oauth_baidu_s',
'oauth_alipay_s',
'get_baidu_user_data',
'get_alipay_user_data',
'OAuthLogin\\Baidu',
'OAuthLogin\\Alipay',
"'baidu' => [",
"'alipay' => [",
]) {
assert.equal(oauthScope.includes(needle), false, `unexpected OAuth reference: ${needle}`)
}
})
test('Alipay payment remains available', () => {
const functions = read('functions.php')
const paymentSchema = read('src/Admin/Schema/SchemaPayment.php')
const gateway = read('src/Payment/Gateways/AlipayGateway.php')
assert.match(functions, /AlipayGateway/)
assert.match(paymentSchema, /alipay_appid/)
assert.match(paymentSchema, /alipay_privatekey/)
assert.match(gateway, /class AlipayGateway/)
})
test('OAuth uses one-time transactions and no PHP sessions', () => {
const auth = fs.readdirSync(path.join(root, 'src', 'Auth'))
.filter((name) => name.endsWith('.php'))
.map((name) => read(path.join('src', 'Auth', name)))
.join('\n')
assert.match(auth, /OAuthTransactionStore::issue/)
assert.match(auth, /OAuthTransactionStore::claim/)
assert.equal(auth.includes('session_start('), false)
assert.equal(auth.includes('$_SESSION'), false)
})
test('provider catalog is the canonical provider list', () => {
const catalog = read('src/Auth/OAuthProviderCatalog.php')
for (const provider of ['github', 'gitee', 'qq', 'google', 'weibo', 'weixin']) {
assert.match(catalog, new RegExp(`'${provider}'\\s*=>`))
}
assert.match(catalog, /CLOGIN_TYPES = \['qq', 'weixin', 'weibo'\]/)
assert.match(catalog, /oauth_s/)
assert.match(catalog, /oauth_bind_type/)
})
test('verified email is required for email auto-linking', () => {
const account = read('src/Auth/OAuthAccountService.php')
const broker = read('src/Auth/OAuthProviderBroker.php')
assert.match(account, /!empty\(\$identity\['email_verified'\]\)/)
assert.match(broker, /primary.*verified/s)
assert.match(broker, /email_verified.*Google|Google.*email_verified/s)
})
test('OAuth secrets are marked and masked by the schema provider', () => {
const phpSchema = read('src/Admin/Schema/SchemaUsers.php')
const jsSchema = read('assets/admin/src/schema/sections/users/oauth.js')
const provider = read('src/Admin/SchemaProvider.php')
for (const key of [
'oauth_proxy_secret',
'github_appkey',
'gitee_appkey',
'qq_appkey',
'google_client_secret',
'weibo_appkey',
'weixin_appsecret',
'weixingzh_token',
'weixingzh_appsecret',
'clogin_key',
]) {
assert.match(phpSchema, new RegExp(`${key}[^\\n]+secret`))
assert.match(jsSchema, new RegExp(`${key}[^\\n]+secret`))
}
assert.match(provider, /\*\*\*/)
assert.match(provider, /!empty\(\$field\['secret'\]\)/)
})
test('PHP and JavaScript OAuth schema field keys stay in parity', () => {
const php = read('src/Admin/Schema/SchemaUsers.php')
const js = read('assets/admin/src/schema/sections/users/oauth.js')
const start = php.indexOf("'oauth' => [")
const phpSection = php.slice(start, php.indexOf("'sms' => [", start))
const phpKeys = [...phpSection.matchAll(/\['key' => '([^']+)', 'label'/g)].map((match) => match[1])
const jsKeys = [...js.matchAll(/\{ key: '([^']+)', label:/g)].map((match) => match[1])
assert.deepEqual([...new Set(phpKeys)].sort(), [...new Set(jsKeys)].sort())
})
test('OAuth bind starts require a one-time captcha bind pass', () => {
const oauth = read('src/Auth/OAuth.php')
const weixingzh = read('src/Auth/WeixinGzh.php')
const bindings = read('inc/helpers/user-center-renderers/settings-bindings.php')
assert.match(oauth, /\/oauth\/bind-pass/)
assert.match(oauth, /BIND_PASS_KIND = 'oauth_bind_pass'/)
assert.match(oauth, /OAuthTransactionStore::issue\(self::BIND_PASS_KIND/)
assert.match(oauth, /OAuthTransactionStore::claim\(\$token, self::BIND_PASS_KIND, true\)/)
assert.match(oauth, /validate_bind_pass\(\$bind_pass, \$provider, \$user_id, true\)/)
assert.match(oauth, /validate_bind_pass\(\$bind_pass, 'clogin_' \. \$type, get_current_user_id\(\), true\)/)
assert.match(weixingzh, /OAuth::validate_bind_pass\(\$bind_pass, 'weixingzh'/)
assert.match(bindings, /aksrt_render_captcha\('bind'\)/)
})
test('OAuth result handler maps safe errors and preserves unrelated URL state', () => {
const notices = []
let replaced = ''
const listeners = {}
const location = new URL('https://site.test/sign/?oauth_action=error&oauth_provider=github&oauth_error=%3Cscript%3E&keep=1#section')
const documentElement = { dataset: {}, contains: () => true }
const document = {
readyState: 'complete',
documentElement,
visibilityState: 'visible',
querySelectorAll: () => [],
querySelector: () => null,
addEventListener: (name, fn) => { listeners[name] = fn },
activeElement: null,
body: { classList: { add() {}, remove() {} } },
}
let authInit = null
const jQueryApi = {
off() { return jQueryApi },
on(_name, fn) { authInit = fn; return jQueryApi },
}
const jQueryStub = () => jQueryApi
const window = {
aksrtData: {},
URL,
location,
history: { replaceState: (_state, _title, value) => { replaced = value } },
notyf: (message, type) => notices.push({ message, type }),
matchMedia: () => ({ matches: false }),
setTimeout,
clearTimeout,
setInterval,
clearInterval,
localStorage: { getItem: () => null, setItem() {} },
}
vm.runInNewContext(read('assets/js/auth.js'), {
window,
document,
URL,
FormData: class {},
fetch: () => Promise.reject(new Error('unexpected fetch')),
console,
CustomEvent: class {},
AbortController,
Date,
Math,
Promise,
jQuery: jQueryStub,
})
if (authInit) authInit()
assert.deepEqual(notices, [{ message: '第三方授权失败,请重试', type: 'danger' }])
assert.equal(replaced, '/sign/?keep=1#section')
assert.equal(replaced.includes('script'), false)
})
test('obsolete OAuth admin routes and components are removed', () => {
const controller = read('src/Admin/UserController.php')
const usersApi = read('assets/admin/src/api/modules/users.js')
assert.equal(controller.includes("'/settings/oauth'"), false)
assert.equal(controller.includes("'/settings/oauth-proxy'"), false)
assert.equal(usersApi.includes('getOAuthSettings'), false)
assert.equal(usersApi.includes('saveOAuthSettings'), false)
assert.equal(fs.existsSync(path.join(root, 'assets/admin/src/components/OAuthProviderForm.vue')), false)
assert.equal(fs.existsSync(path.join(root, 'assets/admin/src/pages/users/UsersSectionPage.vue')), false)
})