'use strict'; const assert = require('node:assert/strict'); const { execFileSync } = require('node:child_process'); 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('account dialogs expose configured email steps, phone steps, and reference password branches', () => { const template = read('inc/helpers/user-center-renderers/settings-account.php'); const javascript = read('assets/js/routes/user-center.js'); const css = read('assets/css/routes/user-center/settings.css'); assert.match(template, /\$email_verify_required = \$account_email !== '' && !empty\(\$user_settings\['email_set_captch'\]\)/); assert.match(template, /data-account-step="email-old"[\s\S]*?data-account-step="email-new"[\s\S]*?data-account-step="email-success"/); assert.match(template, /if \(\$email_verify_required\)/); assert.match(template, /if \(\$email_verify_required\)[\s\S]*?else : \?>[\s\S]*?aksrt-settings-modal__pane/); assert.match(template, /if \(!empty\(\$user_settings\['email_set_captch'\]\)\)/); assert.match(template, /data-account-step="phone-old"[\s\S]*?data-account-step="phone-new"[\s\S]*?data-account-step="phone-success"/); assert.match(template, /if \(\$account_phone !== ''\)[\s\S]*?else : \?>[\s\S]*?aksrt-settings-modal__pane/); assert.match(template, /data-aksrt-sms-scene="verify_old_phone"/); assert.match(template, /data-aksrt-sms-scene="bind_new_phone"/); assert.match(template, /\$account_close_icon = function_exists\('aksrt_icon_markup'\)/); assert.match(template, /' \. esc_html\(\$account_phone_display\)/); assert.doesNotMatch(template, /name="phone" value="/); assert.match(template, /data-aksrt-phone-digits data-hidden-length/); assert.match(template, /class="aksrt-phone-digit" inputmode="numeric" maxlength="1"/); assert.match(template, /aria-label="<\?php echo esc_attr\(sprintf\(__\('手机号第 %d 位数字', 'aksrt'\), 4 \+ \$phone_digit_index\)\); \?>"/); assert.match(template, /data-aksrt-phone-code-area<\?php echo \$phone_mask !== null \? ' hidden' : ''; \?>/); assert.match(template, /data-aksrt-phone-digits-error/); assert.match(javascript, /\/user\/phone-match/); assert.match(javascript, /middle: middle/); assert.match(javascript, /stage: 'reset_old_phone'/); assert.match(javascript, /function initPhoneDigits\(/); assert.match(css, /\.aksrt-settings-modal \.aksrt-phone-digit \{/); assert.match(css, /\.aksrt-settings-modal \.aksrt-phone-digits\.has-error \.aksrt-phone-digit \{/); assert.match(template, /aksrt_render_captcha\('change_email'\)[\s\S]*?id="email-code"/); assert.match(template, /aksrt_render_captcha\('bind_phone'\)[\s\S]*?id="bind-phone-code"/); assert.match(template, /if \(!\$account_oauth_new\)[\s\S]*?name="passwordold"/); assert.match(template, /if \(!\$account_oauth_new && \$can_recover_password\)/); assert.match(template, /data-account-password-pane="recovery"/); assert.match(javascript, /\/auth\/code/); assert.match(javascript, /\/auth\/reset-password/); assert.match(javascript, /className: 'aksrt-user-settings-modal modal-mini full-sm'/); assert.doesNotMatch(template + javascript + css, /pwd-strength|pwd-send-code|email-send-code/); assert.match(css, /\.aksrt-modal-runtime\.aksrt-user-settings-modal\.aksrt-modal-runtime--bottom \.aksrt-modal-runtime__dialog \{[\s\S]*?width: 400px/); assert.match(css, /@media \(max-width: 640px\)[\s\S]*?width: 100%/); assert.match(css, /modal-line-field--code \.yztx\.abs-right[\s\S]*?top: 50%[\s\S]*?right: \.7em/); assert.doesNotMatch(css, /\.code-send-btn \{ min-width:/); const emailConditionStart = template.indexOf(''); const emailElse = template.indexOf('', emailConditionStart); const emailEnd = template.indexOf('', emailElse); const emailSteps = template.slice(emailConditionStart, emailElse); const emailDirect = template.slice(emailElse, emailEnd); const phoneConditionStart = template.indexOf(''); const phoneElse = template.indexOf('', phoneConditionStart); const phoneEnd = template.indexOf('', phoneElse); const phoneSteps = template.slice(phoneConditionStart, phoneElse); const phoneDirect = template.slice(phoneElse, phoneEnd); assert.match(emailSteps, /account-step-list/); assert.doesNotMatch(emailDirect, /account-step-list|data-account-step=/); assert.match(phoneSteps, /account-step-list/); assert.doesNotMatch(phoneDirect, /account-step-list|data-account-step=/); }); test('account PHP harness exercises email and password state transitions', () => { const harness = path.join(root, 'tests', 'widget-system', 'account-settings-harness.php'); const output = execFileSync('php', [harness], { encoding: 'utf8' }); assert.match(output, /Account settings harness passed/); }); test('email update keeps both proofs through validation failures and consumes them only after the write', () => { const ajax = read('inc/modules/user-parts/account-ajax.php'); const send = ajax.slice(ajax.indexOf("add_action('wp_ajax_aksrt_send_account_email_code'"), ajax.indexOf("add_action('wp_ajax_aksrt_verify_account_email'")); const verify = ajax.slice(ajax.indexOf("add_action('wp_ajax_aksrt_verify_account_email'"), ajax.indexOf("add_action('wp_ajax_aksrt_change_password'")); const change = ajax.slice(ajax.indexOf("add_action('wp_ajax_aksrt_change_email'")); assert.match(send, /\['verify_email', 'change_email'\]/); assert.match(send, /email_exists\(\$target\)/); assert.match(send, /SendLimit::check\(\$target\)/); assert.match(send, /SendLimit::record\(\$target\)/); assert.doesNotMatch(send, /aksrt_account_email_rate_/); assert.doesNotMatch(ajax, /aksrt_account_email_rate_/); assert.match(verify, /aksrt_require_captcha\(\s*'change_email'/); assert.match(verify, /aksrt_account_set_proof\(\$user_id, 'email_old'/); assert.match(change, /aksrt_account_email_verification_required\(\)/); assert.match(change, /aksrt_account_consume_proof\([^;]+false\)/); assert.match(change, /aksrt_account_consume_code\([\s\S]*?false\s*\)/); assert.ok(change.indexOf('wp_update_user(') < change.indexOf("delete_transient(aksrt_account_proof_key")); assert.ok(change.indexOf('wp_update_user(') < change.indexOf("delete_transient(aksrt_account_code_key")); assert.match(change, /\$agreement\['required'\]/); assert.doesNotMatch(ajax, /wp_ajax_aksrt_send_email_code/); }); test('phone replacement requires old-number proof and a new-number code while reusing bind side effects', () => { const controller = read('src/Admin/UserControllerAuthPhoneOperations.php'); const routes = read('src/Admin/UserController.php'); const binding = read('src/Auth/ServicePart1b.php'); const code = controller.slice(controller.indexOf('public function user_phone_code('), controller.indexOf('public function user_verify_phone(')); const verify = controller.slice(controller.indexOf('public function user_verify_phone('), controller.indexOf('public function user_bind_phone(')); const bind = controller.slice(controller.indexOf('public function user_bind_phone('), controller.indexOf('public function test_sms_send(')); assert.match(routes, /\/user\/phone-verify[\s\S]*?user_verify_phone/); assert.match(routes, /\/user\/phone-match[\s\S]*?user_phone_match/); assert.match(code, /verify_old_phone/); assert.match(code, /bind_new_phone/); assert.match(code, /public function user_phone_match\(/); assert.match(code, /auth_middle_matches\(\$parts, \$middle\)/); assert.match(code, /reset_old_phone/); assert.match(code, /填写的号码与绑定手机号不一致/); assert.match(code, /\$phone = \$current_phone;/); assert.doesNotMatch(code, /auth_rate_limited\('account-phone-code'/); assert.match(verify, /auth_consume_code\('account_verify_old_phone_' \./); assert.match(verify, /aksrt_account_phone_old_/); assert.match(bind, /aksrt_account_phone_old_/); assert.match(bind, /auth_consume_code\(\$code_scene, \$phone, \$code, false\)/); assert.ok(bind.indexOf("Auth\\Service::bind_phone") < bind.indexOf("delete_transient($this->auth_code_key($code_scene, $phone))")); assert.match(bind, /aksrt_get_auth_agreement_view/); assert.match(bind, /Auth\\Service::bind_phone\(\$payload, true\)/); assert.match(binding, /aksrt_user_phone_changed/); assert.doesNotMatch(controller, /auth_consume_code\('bind_phone'/); }); test('password change retains old-password limits and captcha; recovery uses shared auth endpoints', () => { const ajax = read('inc/modules/user-parts/account-ajax.php'); const javascript = read('assets/js/routes/user-center.js'); const password = ajax.slice(ajax.indexOf("add_action('wp_ajax_aksrt_change_password'"), ajax.indexOf("add_action('wp_ajax_aksrt_change_email'")); assert.match(password, /get_user_meta\(\$user_id, 'oauth_new', true\)/); assert.match(password, /wp_check_password\(\$old/); assert.match(password, /SecurityHardening::error_frequency_wait\(\)/); assert.match(password, /SecurityHardening::error_frequency_record\(\)/); assert.doesNotMatch(password, /aksrt_account_rate_limited/); assert.doesNotMatch(ajax, /function aksrt_account_rate_limited/); assert.doesNotMatch(ajax, /function aksrt_account_reset_rate/); assert.match(password, /aksrt_require_captcha\('change_password'/); assert.match(password, /Auth\\Service::validate_password\(\$password, \$password2\)/); assert.doesNotMatch(password, /email.*code|password.*strength/i); assert.match(javascript, /scene: 'resetpassword', target: target, channel:/); assert.match(javascript, /\/auth\/code/); assert.match(javascript, /\/auth\/reset-password/); assert.match(javascript, /data-recovery-channel/); });