Files
wordpress-theme-aksrt/tests/widget-system/auth-source-contract.test.cjs
T
kate522 ff4ec1eb43 feat(captcha): 打通人机验证 11 个场景的前后端完整链路
- 统一验证入口 aksrt_require_captcha(),返回 true|WP_Error,启用场景配置缺失时 fail-closed 拒绝
- 前端公共 API 重构为 window.AKSRTCaptcha(verify/collect/reset/run),单飞锁防并发重复验证,一次性 proof 绑定浏览器 cookie+场景+服务商+主机名
- challenge/exchange REST 端点:challenge 原子占用防票据放大厂商调用,增加频率限制
- 新增 OAuth bind pass:服务端签发 5 分钟一次性令牌,绑定用户+服务商+浏览器,微信扫码延迟消费
- 覆盖场景:评论、登录、注册、绑定第三方、绑定手机、找回密码、修改密码、更改邮箱、资料下载、签到、前台投稿
- 移除 Service 层重复验证与 _auth_code_verified/_defer_login 等客户端可伪造标志,删除下载共享 pass 死代码
- 修复腾讯 TC3 签名 scope、Cloudflare action/cdata/hostname 严格校验、阿里云验签错误码映射
- 后台 Schema 保存对 secret 字段做掩码/保留合并;导入导出过滤 aksrt_captcha_config
- 重建 public bundle 与 admin SPA;新增 captcha 运行时/安全/业务契约测试与 harness
2026-09-02 13:03:24 +08:00

243 lines
13 KiB
JavaScript

const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const root = path.resolve(__dirname, '../..');
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
test('modal and sign page use one renderer and one state machine', () => {
const modal = read('templates/parts/modal-auth.php');
const page = read('templates/pages/page-sign.php');
const shared = read('templates/parts/auth-content.php');
const scripts = read('src/Core/ScriptsPart1c.php');
const runtime = read('assets/js/runtime/main.js') + read('assets/js/runtime/main.min.js');
const loader = read('assets/js/runtime/loader.js');
assert.match(modal, /templates\/parts\/auth-content\.php/);
assert.match(page, /templates\/parts\/auth-content\.php/);
assert.match(shared, /data-auth-panel="signin"/);
assert.match(shared, /data-auth-panel="signup"/);
assert.match(shared, /data-auth-panel="resetpassword"/);
assert.match(shared, /data-auth-panel="bind"/);
assert.match(shared, /data-auth-panel="oauth"/);
assert.match(scripts, /assets\/js\/auth\.js/);
assert.match(scripts, /'aksrt-auth',[\s\S]*?assets\/js\/auth\.js[\s\S]*?\['aksrt-runtime'\]/);
assert.doesNotMatch(scripts, /modal-auth\.js|sign-register\.js/);
assert.doesNotMatch(runtime, /signsubmit-loader|tbquire\(\['sign-register'\]\)|\.on\('click', '\.(?:signin|signup)-loader'/);
assert.doesNotMatch(loader, /sign-register/);
});
test('static Zibll authentication dimensions and responsive contract are retained', () => {
const css = read('assets/css/routes/sign.css');
assert.match(css, /\.sign\s*\{[\s\S]*?max-width:\s*330px/);
assert.match(css, /#u_sign\.has-sign-img \.modal-dialog\s*\{\s*width:\s*800px/);
assert.match(css, /#u_sign \.sign-img\s*\{[^}]*inset:\s*0 45% 0 0;[^}]*padding:\s*0/);
assert.match(css, /#u_sign \.sign-img \+ \.sign\s*\{[^}]*margin-left:\s*55%/);
assert.match(css, /\.social-login-item\s*\{[\s\S]*?width:\s*32px;[\s\S]*?height:\s*32px/);
assert.match(css, /\.auth-qrcode\s*\{[\s\S]*?width:\s*180px;[\s\S]*?min-height:\s*180px/);
assert.match(css, /@media \(max-width:\s*767px\)[\s\S]*?\.sign-img\s*\{\s*display:\s*none/);
});
test('authentication settings use reference defaults and canonical keys', () => {
const js = read('assets/admin/src/schema/sections/users/auth.js');
const php = read('src/Admin/Schema/SchemaUsers.php');
const all = js + '\n' + php;
assert.match(all, /user_sign_type['"]?\s*(?:=>|:)\s*['"]modal/);
assert.match(all, /user_signup_captch['"]?\s*(?:=>|:)\s*false/);
assert.match(all, /sign_card_position['"]?\s*(?:=>|:)\s*['"]right/);
assert.match(all, /sign_show_logo['"]?\s*(?:=>|:)\s*false/);
assert.match(all, /sign_modal_show_logo['"]?\s*(?:=>|:)\s*false/);
assert.match(all, /invit_code_s/);
assert.match(all, /only_nopas/);
assert.match(all, /user_signup_no_repas[\s\S]*?dependsOn[\s\S]*?user_signup_captch/);
assert.doesNotMatch(all, /user_close_login|user_close_signup|nopas_first|password_first|nopas_only|referral_registration_enabled|referral_register_field/);
});
test('authentication galleries save attachment IDs and are consumed by both surfaces', () => {
const field = read('assets/admin/src/components/form/AksField.vue');
const gallery = read('assets/admin/src/components/form/AksGalleryField.vue');
const provider = read('src/Admin/SchemaProvider.php');
const reader = read('src/Settings/AuthReader.php');
const modal = read('templates/parts/modal-auth.php');
const page = read('templates/pages/page-sign.php');
const css = read('assets/css/routes/sign.css');
assert.match(field, /<AksGalleryField[\s\S]*?schema\.type === 'gallery'/);
assert.match(gallery, /multiple:\s*true/);
assert.match(gallery, /Number\.parseInt\(attachment\.id/);
assert.match(provider, /'gallery'\s*=>\s*'attachment_ids'/);
assert.match(provider, /case 'attachment_ids':[\s\S]*?array_map\('absint'/);
assert.match(reader, /auth_gallery_ids\('sign_modal_background'\)/);
assert.match(reader, /auth_gallery_ids\('sign_page_background'\)/);
assert.match(modal, /wp_get_attachment_image\(\$image_id, 'full'/);
assert.match(modal, /<div class="sign-img"><\?php echo \$image_html/);
assert.match(page, /wp_get_attachment_image_url\(\$background_id, 'full'\)/);
assert.match(page, /<div class="sign-page-background" style="background-image:url/);
assert.match(css, /\.sign-img img\s*\{[\s\S]*?object-fit:\s*cover/);
assert.doesNotMatch(modal + page, /is_string\(\$images\)|explode\(',', \$images\)/);
});
test('only the shared authentication REST routes and /sign/ renderer are registered', () => {
const controller = read('src/Admin/UserController.php');
const router = read('src/Auth/Router.php');
const urls = read('inc/modules/user-parts/urls.php');
['/auth/code', '/auth/login', '/auth/register', '/auth/reset-password'].forEach((route) => {
assert.match(controller, new RegExp(route.replace('/', '\\/')));
});
assert.doesNotMatch(controller, /auth\/(?:modal-|phone-|send-sms-code|bind-phone)/);
assert.match(router, /'sign'\s*=>\s*'index\.php\?aksrt_sign=1'/);
assert.doesNotMatch(router, /'login'\s*=>|'register'\s*=>|'reset-password'\s*=>|'verify-email'\s*=>|'bind-account'\s*=>/);
assert.match(router, /aksrt_bypass/);
assert.match(router, /\$action === 'logout'[\s\S]*?aksrt_get_logout_url/);
assert.doesNotMatch(router, /is_user_logged_in\(\) && current_user_can\('manage_options'\)|wp_doing_ajax\(\)/);
assert.match(urls, /home_url\('\/sign\/'\)/);
});
test('legacy authentication templates and scripts are removed', () => {
[
'assets/js/modal-auth.js',
'assets/js/sign-register.js',
'assets/js/runtime/sign-register.js',
'assets/js/runtime/sign-register.min.js',
'assets/css/routes/auth.css',
'assets/css/frontend/enhancements/modal-motion.css',
'templates/pages/page-login.php',
'templates/pages/page-register.php',
'templates/pages/page-reset-password.php',
'templates/pages/page-bind-account.php',
'templates/pages/page-verify-email.php',
].forEach((file) => assert.equal(fs.existsSync(path.join(root, file)), false, file));
});
test('unified handlers enforce nonce, captcha, rate limits, configured channels, and generic errors', () => {
const handlers = read('src/Admin/UserControllerAuthPhoneOperations.php');
const service = read('src/Auth/Service.php');
const serviceA = read('src/Auth/ServicePart1a.php');
const serviceB = read('src/Auth/ServicePart1b.php');
assert.match(handlers, /auth_verify_rest_nonce/);
assert.match(handlers, /private function auth_require_captcha/);
assert.match(handlers, /get_error_code\(\)/);
assert.match(handlers, /get_error_message\(\)/);
assert.match(handlers, /auth_rate_limited/);
assert.match(handlers, /auth_channel_allowed/);
['auth_code', 'auth_login', 'auth_register', 'auth_reset_password'].forEach((method) => {
const start = handlers.indexOf(`public function ${method}(`);
const end = handlers.indexOf('\n\tpublic function ', start + 1);
const body = handlers.slice(start, end === -1 ? handlers.length : end);
assert.match(body, /auth_require_captcha/, method);
});
assert.match(handlers, /passwordless_login_type/);
assert.match(handlers, /signup_verification_type/);
assert.match(handlers, /reset_password_verification_type/);
assert.match(handlers, /账号或密码错误/);
assert.match(handlers, /账号或验证码错误/);
assert.match(service, /signin_phone_enabled/);
assert.doesNotMatch(service + serviceA + serviceB, /aksrt_require_captcha/);
assert.doesNotMatch(service + serviceA + serviceB, /_auth_code_verified|_defer_login/);
assert.doesNotMatch(service, /captcha_scene/);
assert.match(handlers, /public function user_bind_phone[\s\S]*?auth_require_captcha\('bind_phone'/);
assert.doesNotMatch(service, /login_by_phone_password|login_by_phone_code/);
});
test('OAuth bind and reset flows stay inside the shared page and four auth endpoints', () => {
const shared = read('templates/parts/auth-content.php');
const page = read('templates/pages/page-sign.php');
const handlers = read('src/Admin/UserControllerAuthPhoneOperations.php');
const serviceA = read('src/Auth/ServicePart1a.php');
const serviceB = read('src/Auth/ServicePart1b.php');
assert.match(page, /'obk'\s*=>\s*\$obk/);
assert.match(shared, /data-auth-panel="bind"[\s\S]*?data-auth-form="login"/);
assert.match(shared, /name="obk"/);
assert.match(handlers, /finalize_oauth_binding/);
assert.match(serviceA, /finalize_oauth_binding/);
assert.doesNotMatch(serviceA + serviceB, /bind_oauth_account|get_reset_context|request_password_reset|reset_password_with_token/);
});
test('shared renderer contracts every configured authentication state', () => {
const shared = read('templates/parts/auth-content.php');
const modal = read('templates/parts/modal-auth.php');
const client = read('assets/js/auth.js');
const settings = read('src/Settings/AuthReader.php');
assert.match(shared, /data-auth-password-fields/);
assert.match(shared, /data-auth-code-fields/);
assert.match(shared, /data-login-mode/);
['login', 'register', 'resetpassword'].forEach((scene) => {
assert.match(shared, new RegExp(`data-send-code="${scene}"`));
});
assert.match(shared, /\$omit_confirm/);
assert.match(shared, /\$ref_mode !== 'hidden'/);
assert.match(shared, /\$ref_mode === 'required'/);
assert.match(shared, /sign-agreement-implicit/);
assert.match(shared, /name="agree_terms"[\s\S]*?required/);
assert.match(shared, /oauth-button-lg/);
assert.match(shared, /data-auth-qrcode/);
assert.match(modal, /data-auto=/);
assert.match(client, /aksrtAuthAutoUntil/);
assert.match(client, /data-password-show/);
assert.match(client, /pollQr/);
assert.match(client, /AbortController/);
assert.match(client, /refreshInFlight|inFlight/);
assert.match(client, /visibilitychange/);
assert.match(client, /Math\.pow\(2/);
assert.match(client, /QR_TIMEOUT/);
assert.match(client, /stopQr\(root\)/);
assert.match(client, /window\.notyf\(escapeHtml\(text\), type \|\| 'info'\)/);
assert.match(client, /notify\('请先完成人机验证', 'warning'\)/);
assert.match(client, /form\.noValidate = true/);
assert.match(client, /form\.checkValidity\(\)[\s\S]*?field\.validationMessage/);
assert.match(client, /window\.location\.assign\(authPageUrl/);
assert.doesNotMatch(client + shared, /showMessage|data-auth-message|sign-message/);
assert.match(settings, /invitation_mode/);
assert.match(settings, /\['close', 'open', 'must'\]/);
});
test('OAuth frontend is accessible, redirect-aware, and removes retired providers', () => {
const shared = read('templates/parts/auth-content.php');
const modal = read('templates/parts/modal-auth.php');
const client = read('assets/js/auth.js');
const userCenter = read('assets/js/routes/user-center.js');
const bindings = read('inc/helpers/user-center-renderers/settings-bindings.php');
const signCss = read('assets/css/routes/sign.css');
const mainCss = read('assets/css/main.css');
assert.match(modal, /role="dialog"[^>]*aria-modal="true"[^>]*aria-labelledby="aksrt-auth-modal-title"/);
assert.match(modal, /id="aksrt-auth-modal-title"/);
assert.match(client, /focusableModalItems/);
assert.match(client, /lastModalTrigger/);
assert.match(client, /restore\.focus\(\)/);
assert.match(shared, /role="tabpanel"[^>]*data-auth-panel="signin"/);
assert.match(shared, /role="tabpanel"[^>]*data-auth-panel="oauth"/);
assert.match(client, /setAttribute\('role', 'tab'\)/);
assert.match(client, /aria-controls/);
assert.match(client, /aria-selected/);
assert.match(shared, /data-password-show[^>]*aria-label=/);
assert.match(shared, /aria-pressed="false"/);
assert.match(client, /setAttribute\('aria-label', visible \? '隐藏密码' : '显示密码'\)/);
assert.match(client, /setAttribute\('aria-pressed', visible \? 'true' : 'false'\)/);
assert.match(shared, /data-auth-qr-status[^>]*role="status"[^>]*aria-live="polite"/);
assert.match(shared, /data-provider=/);
assert.match(shared, /aria-label="<\?php echo esc_attr\(sprintf/);
assert.match(client, /searchParams\.set\('redirect_to', redirect\)/);
assert.match(client, /oauth_action/);
assert.match(client, /oauth_provider/);
assert.match(client, /oauth_error/);
assert.match(client, /oauthErrorMessages/);
assert.doesNotMatch(client, /notify\([^\n;]*searchParams\.get\('oauth_error'/);
assert.match(userCenter, /errorMessages\[currentUrl\.searchParams\.get\('oauth_error'\)/);
assert.match(userCenter, /result\.status === 409/);
assert.match(userCenter, /d\.message \|\| \(d\.data && d\.data\.message\)/);
assert.match(bindings, /\$binding\['provider'\] \?\? \$binding\['key'\]/);
assert.match(bindings, /\$binding\['start_url'\] \?\? \$binding\['url'\]/);
assert.match(bindings, /data-provider=/);
assert.doesNotMatch(signCss + mainCss, /social-login-item[^\n{]*(?:baidu|alipay)|social-login-item\.(?:baidu|alipay)/i);
assert.match(mainCss, /\.pay-box \.but\.baidu/);
});