The "other login methods" block drifted from zibll on three axes. Gitee was mapped to fa-code-branch because FA5 Free ships no Gitee brand glyph, so the button showed a generic branch icon; Google showed the monochrome FA glyph instead of the reference's multi-colour mark. The modal also printed an implicit agreement line under the social buttons, which the reference guest card does not have. And the top-nav user popover built its own .social-login-divider / .oauth-row / .oauth-btn markup and then wrapped that in a second separator and .social_loginbar, so the block rendered nested and unlike the reference. aksrt_brand_icon() now routes zibsvg-/customsvg- values through the shared icon renderer, and the brand map resolves gitee to zibsvg-gitee and google to zibsvg-google-color. Both sprite keys already exist in the runtime symbol sheet, which the loader pulls in on every frontend page. Because get_auth_providers_data() and binding_row() both go through aksrt_brand_icon(), one map change fixes the icon on the modal, the user card, the top-nav popover, the user-center binding list and the profile social links. render_social_buttons() is rewritten to the reference shape: an optional .social-separator, a .social_loginbar, and one .social-login-item <type> toggle-radius anchor per enabled provider with rel="nofollow" and a "<label>登录" title. oauth_button_lg switches to the button-lg variant with a label, and the WeChat entry carries qrcode-signin so auth.js opens the QR panel instead of navigating, matching the reference. The navbar and the link apply modal now append the block verbatim instead of re-wrapping it, and the user-card loop adopts the same item attributes. The retired .social-login-divider / .oauth-row / .oauth-btn rules are dropped from base-1.css, and the binding-list icon rule gains an svg selector so the sprite icons keep the 22px the FA glyphs had. Verification: node --test across widget-system, navigation, admin, asset-ledger, dev, front-submit and visual (613 tests), plus php -l on every changed PHP file. Extended the auth source contract with the agreement-line, brand-icon and popover-structure cases, and pinned the user-card guest markup.
297 lines
16 KiB
JavaScript
297 lines
16 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]*?\['loader_js'\]/);
|
|
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');
|
|
|
|
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(read('src/Settings/CreditsRuntime.php'), /invit_code_s/);
|
|
assert.match(read('src/Settings/CreditsRuntime.php'), /\['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/);
|
|
});
|
|
|
|
test('the social-login area drops the implicit agreement line', () => {
|
|
const shared = read('templates/parts/auth-content.php');
|
|
const userRenderer = read('src/WidgetSystem/Renderers/UserRenderer.php');
|
|
|
|
// The reference shows no agreement copy under the social buttons; only the
|
|
// registration panel keeps its own, and it stays reachable.
|
|
assert.doesNotMatch(shared, /使用社交账号登录即表示/);
|
|
assert.match(shared, /注册即表示/);
|
|
assert.match(shared, /name="agree_terms"[\s\S]*?required/);
|
|
assert.doesNotMatch(userRenderer, /使用社交账号登录即表示|我已阅读并同意/);
|
|
});
|
|
|
|
test('brand icons resolve Gitee and Google to the reference SVG sprite', () => {
|
|
const icons = read('inc/helpers/aksrt-icons.php');
|
|
const sprites = read('assets/js/runtime/svg-icon.js');
|
|
|
|
// FA5 Free ships no Gitee brand glyph and no multi-colour Google mark, so both
|
|
// resolve to the theme's own symbol sheet exactly like the reference does.
|
|
assert.match(icons, /'gitee'\s*=>\s*'zibsvg-gitee'/);
|
|
assert.match(icons, /'google'\s*=>\s*'zibsvg-google-color'/);
|
|
assert.match(icons, /'github'\s*=>\s*'fa fab fa-github'/);
|
|
assert.match(icons, /'wechat'\s*=>\s*'fa fab fa-weixin'/);
|
|
assert.match(sprites, /aksrt_svgs\['gitee'\]/);
|
|
assert.match(sprites, /aksrt_svgs\['google-color'\]/);
|
|
// Sprite values must reach the shared icon renderer instead of being wrapped
|
|
// in a bogus <i class="zibsvg-…">.
|
|
assert.match(icons, /strpos\(\$class, 'zibsvg-'\) === 0[\s\S]*?aksrt_icon_markup\(\$class, \$extra_classes\)/);
|
|
});
|
|
|
|
test('the top-nav user popover reproduces the reference quick-login block', () => {
|
|
const oauth = read('src/Auth/OAuth.php');
|
|
const header = read('inc/helpers/header/aksrt-header-navbar.php');
|
|
const links = read('inc/helpers/aksrt-links.php');
|
|
const legacyCss = read('assets/css/frontend/pages/base-1.css');
|
|
|
|
assert.match(oauth, /social-separator separator muted-3-color em09 mt10/);
|
|
assert.match(oauth, /'<div class="social_loginbar">'/);
|
|
assert.match(oauth, /'social-login-item ' \. \$type/);
|
|
assert.match(oauth, /' button-lg' : ' toggle-radius'/);
|
|
assert.match(oauth, /rel="nofollow"/);
|
|
assert.match(oauth, /qrcode-signin/);
|
|
assert.match(oauth, /oauth_button_lg/);
|
|
|
|
// Callers append the block verbatim — no hand-rolled separator or wrapper.
|
|
assert.match(header, /render_social_buttons\(__\('快速登录', 'aksrt'\)\)/);
|
|
assert.doesNotMatch(header, /social-separator|social_loginbar/);
|
|
assert.match(links, /render_social_buttons\(__\('使用以下方式快速登录', 'aksrt'\)\)/);
|
|
assert.doesNotMatch(links, /social_loginbar/);
|
|
|
|
// The retired .social-login-divider / .oauth-row / .oauth-btn markup and its
|
|
// stylesheet are gone.
|
|
assert.doesNotMatch(oauth, /oauth-row|oauth-btn|social-login-divider/);
|
|
assert.doesNotMatch(legacyCss, /\.oauth-row|\.oauth-btn|\.social-login-divider/);
|
|
});
|