fix(profile): align account dialogs and contracts
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
'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, /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.doesNotMatch(template + javascript + css, /pwd-strength|pwd-send-code|email-send-code/);
|
||||
|
||||
const emailConditionStart = template.indexOf('<?php if ($email_verify_required) : ?>');
|
||||
const emailElse = template.indexOf('<?php else : ?>', emailConditionStart);
|
||||
const emailEnd = template.indexOf('<?php endif; ?>', emailElse);
|
||||
const emailSteps = template.slice(emailConditionStart, emailElse);
|
||||
const emailDirect = template.slice(emailElse, emailEnd);
|
||||
const phoneConditionStart = template.indexOf('<?php if ($account_phone !== \'\') : ?>');
|
||||
const phoneElse = template.indexOf('<?php else : ?>', phoneConditionStart);
|
||||
const phoneEnd = template.indexOf('<?php endif; ?>', 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(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(code, /verify_old_phone/);
|
||||
assert.match(code, /bind_new_phone/);
|
||||
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, /aksrt_account_rate_limited\(\$user_id, 'change_password'\)/);
|
||||
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/);
|
||||
});
|
||||
@@ -0,0 +1,281 @@
|
||||
<?php
|
||||
/** Account email and password transition contract harness. */
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace {
|
||||
define('ABSPATH', __DIR__ . '/');
|
||||
define('MINUTE_IN_SECONDS', 60);
|
||||
$GLOBALS['aksrt_account_settings'] = ['email_set_captch' => true];
|
||||
$GLOBALS['aksrt_account_user_id'] = 7;
|
||||
$GLOBALS['aksrt_account_user'] = null;
|
||||
$GLOBALS['aksrt_account_meta'] = [];
|
||||
$GLOBALS['aksrt_account_transients'] = [];
|
||||
$GLOBALS['aksrt_account_actions'] = [];
|
||||
$GLOBALS['aksrt_account_mail'] = [];
|
||||
$GLOBALS['aksrt_account_existing_emails'] = [];
|
||||
$GLOBALS['aksrt_account_captcha_calls'] = [];
|
||||
$GLOBALS['aksrt_account_captcha_fail'] = false;
|
||||
$GLOBALS['aksrt_account_update_fail'] = false;
|
||||
|
||||
final class WP_User {
|
||||
public function __construct(
|
||||
public int $ID,
|
||||
public string $user_email,
|
||||
public string $display_name,
|
||||
public string $user_pass
|
||||
) {}
|
||||
}
|
||||
|
||||
final class WP_Error {
|
||||
public function __construct(
|
||||
private string $code = '',
|
||||
private string $message = '',
|
||||
private mixed $data = null
|
||||
) {}
|
||||
public function get_error_code(): string { return $this->code; }
|
||||
public function get_error_message(): string { return $this->message; }
|
||||
public function get_error_data(): mixed { return $this->data; }
|
||||
}
|
||||
|
||||
final class JsonResponseException extends \RuntimeException {
|
||||
public function __construct(public array $payload) { parent::__construct('json_response'); }
|
||||
}
|
||||
|
||||
function add_action(string $hook, callable $callback): void {
|
||||
$GLOBALS['aksrt_account_actions'][$hook] = $callback;
|
||||
}
|
||||
function __(string $text, string $domain = ''): string { return $text; }
|
||||
function is_wp_error(mixed $value): bool { return $value instanceof WP_Error; }
|
||||
function is_email(string $email): bool { return (bool) filter_var($email, FILTER_VALIDATE_EMAIL); }
|
||||
function sanitize_email(string $email): string { return trim($email); }
|
||||
function sanitize_key(string $key): string { return preg_replace('/[^a-z0-9_\-]/', '', strtolower($key)) ?? ''; }
|
||||
function sanitize_text_field(string $value): string { return trim(strip_tags($value)); }
|
||||
function wp_unslash(mixed $value): mixed { return $value; }
|
||||
function wp_verify_nonce(string $nonce, string $action): bool { return $nonce === 'valid' && $action === 'aksrt_account_settings'; }
|
||||
function get_current_user_id(): int { return (int) $GLOBALS['aksrt_account_user_id']; }
|
||||
function wp_get_current_user(): mixed { return $GLOBALS['aksrt_account_user']; }
|
||||
function get_userdata(int $user_id): ?WP_User { return $user_id === 7 ? $GLOBALS['aksrt_account_user'] : null; }
|
||||
function get_bloginfo(string $show = ''): string { return 'AKSRT Test'; }
|
||||
function wp_hash_password(string $password): string { return 'hash:' . $password; }
|
||||
function wp_check_password(string $password, string $hash, int $user_id = 0): bool { return hash_equals('hash:' . $password, $hash); }
|
||||
function email_exists(string $email): int|false { return $GLOBALS['aksrt_account_existing_emails'][strtolower($email)] ?? false; }
|
||||
function set_transient(string $key, mixed $value, int $expiration = 0): bool {
|
||||
$GLOBALS['aksrt_account_transients'][$key] = [
|
||||
'value' => $value,
|
||||
'expires' => $expiration > 0 ? time() + $expiration : 0,
|
||||
];
|
||||
return true;
|
||||
}
|
||||
function get_transient(string $key): mixed {
|
||||
$row = $GLOBALS['aksrt_account_transients'][$key] ?? null;
|
||||
if (!is_array($row)) return false;
|
||||
if (!empty($row['expires']) && $row['expires'] <= time()) {
|
||||
unset($GLOBALS['aksrt_account_transients'][$key]);
|
||||
return false;
|
||||
}
|
||||
return $row['value'] ?? false;
|
||||
}
|
||||
function delete_transient(string $key): bool {
|
||||
unset($GLOBALS['aksrt_account_transients'][$key]);
|
||||
return true;
|
||||
}
|
||||
function get_user_meta(int $user_id, string $key, bool $single = false): mixed {
|
||||
return $GLOBALS['aksrt_account_meta'][$user_id][$key] ?? '';
|
||||
}
|
||||
function update_user_meta(int $user_id, string $key, mixed $value): bool {
|
||||
$GLOBALS['aksrt_account_meta'][$user_id][$key] = $value;
|
||||
return true;
|
||||
}
|
||||
function delete_user_meta(int $user_id, string $key): bool {
|
||||
unset($GLOBALS['aksrt_account_meta'][$user_id][$key]);
|
||||
return true;
|
||||
}
|
||||
function wp_update_user(array $data): int|WP_Error {
|
||||
if ($GLOBALS['aksrt_account_update_fail']) {
|
||||
return new WP_Error('update_failed', 'update failed');
|
||||
}
|
||||
$user = $GLOBALS['aksrt_account_user'];
|
||||
if (isset($data['user_email'])) $user->user_email = (string) $data['user_email'];
|
||||
if (isset($data['user_pass'])) $user->user_pass = wp_hash_password((string) $data['user_pass']);
|
||||
return (int) $user->ID;
|
||||
}
|
||||
function aksrt_require_captcha(string $scene, array $payload): true|WP_Error {
|
||||
$GLOBALS['aksrt_account_captcha_calls'][] = $scene;
|
||||
return $GLOBALS['aksrt_account_captcha_fail'] ? new WP_Error('captcha_failed', 'captcha failed') : true;
|
||||
}
|
||||
function aksrt_get_auth_agreement_view(): array { return ['enabled' => true, 'required' => true]; }
|
||||
function wp_send_json_error(array $data): never { throw new JsonResponseException(['success' => false, 'data' => $data]); }
|
||||
function wp_send_json_success(array $data): never { throw new JsonResponseException(['success' => true, 'data' => $data]); }
|
||||
|
||||
function account_settings_request(string $action, array $data = []): array {
|
||||
$callback = $GLOBALS['aksrt_account_actions'][$action] ?? null;
|
||||
if (!is_callable($callback)) throw new \RuntimeException('Missing AJAX action: ' . $action);
|
||||
$_POST = array_merge(['_settings_nonce' => 'valid'], $data);
|
||||
try {
|
||||
$callback();
|
||||
} catch (JsonResponseException $response) {
|
||||
return $response->payload;
|
||||
}
|
||||
throw new \RuntimeException('AJAX action returned without a JSON response: ' . $action);
|
||||
}
|
||||
|
||||
function account_settings_check(bool $condition, string $message): void {
|
||||
if (!$condition) throw new \RuntimeException($message);
|
||||
}
|
||||
|
||||
function account_settings_code_key(string $scene, string $target): string {
|
||||
return 'aksrt_account_code_7_' . $scene . '_' . md5(strtolower(trim($target)));
|
||||
}
|
||||
|
||||
function account_settings_rate_key(string $scene, string $target): string {
|
||||
return 'aksrt_account_email_rate_7_' . $scene . '_' . md5(strtolower(trim($target)));
|
||||
}
|
||||
|
||||
function account_settings_code(string $scene, string $target): string {
|
||||
$stored = get_transient(account_settings_code_key($scene, $target));
|
||||
account_settings_check(is_array($stored), 'expected verification code was not stored for ' . $scene);
|
||||
return substr((string) ($stored['hash'] ?? ''), strlen('hash:'));
|
||||
}
|
||||
|
||||
function account_settings_proof_key(): string {
|
||||
return 'aksrt_account_proof_email_old_7';
|
||||
}
|
||||
|
||||
require dirname(__DIR__, 2) . '/inc/modules/user-parts/account-ajax.php';
|
||||
}
|
||||
|
||||
namespace AKSRT\Settings {
|
||||
final class Settings {
|
||||
public static function get_all(): array { return $GLOBALS['aksrt_account_settings']; }
|
||||
public static function send_event_email(string $event, string $to, array $values): bool {
|
||||
$GLOBALS['aksrt_account_mail'][] = ['event' => $event, 'to' => $to, 'values' => $values];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace AKSRT\Auth {
|
||||
final class Service {
|
||||
public static function validate_password(string $password, string $confirm = ''): true|string {
|
||||
if (strlen($password) < 6) return 'password too short';
|
||||
if ($confirm !== '' && $password !== $confirm) return 'password mismatch';
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
$GLOBALS['aksrt_account_user'] = new WP_User(7, 'old@example.test', 'Test User', 'hash:OldPass123');
|
||||
$send_action = 'wp_ajax_aksrt_send_account_email_code';
|
||||
$verify_action = 'wp_ajax_aksrt_verify_account_email';
|
||||
$change_email_action = 'wp_ajax_aksrt_change_email';
|
||||
$change_password_action = 'wp_ajax_aksrt_change_password';
|
||||
$saved_user = $GLOBALS['aksrt_account_user'];
|
||||
$GLOBALS['aksrt_account_user'] = null;
|
||||
account_settings_check(empty(account_settings_request($send_action, ['scene' => 'verify_email'])['success']), 'email-code request did not handle a missing user object');
|
||||
account_settings_check(empty(account_settings_request($verify_action, ['code' => '123456'])['success']), 'old-email verification did not handle a missing user object');
|
||||
account_settings_check(empty(account_settings_request($change_email_action, ['new_email' => 'new@example.test'])['success']), 'email update did not handle a missing user object');
|
||||
account_settings_check(empty(account_settings_request($change_password_action, ['password' => 'NewPass123', 'password2' => 'NewPass123'])['success']), 'password update did not handle a missing user object');
|
||||
$GLOBALS['aksrt_account_user'] = $saved_user;
|
||||
|
||||
$old_send = account_settings_request($send_action, ['scene' => 'verify_email']);
|
||||
account_settings_check(!empty($old_send['success']), 'old email verification code could not be sent');
|
||||
$old_code = account_settings_code('verify_email', 'old@example.test');
|
||||
$bad_old_code = account_settings_request($verify_action, ['code' => 'wrong']);
|
||||
account_settings_check(empty($bad_old_code['success']), 'email verification accepted an incorrect old-email code');
|
||||
$old_code_row = get_transient(account_settings_code_key('verify_email', 'old@example.test'));
|
||||
account_settings_check(($old_code_row['attempts'] ?? 0) === 1, 'incorrect old-email attempt was not recorded');
|
||||
$GLOBALS['aksrt_account_transients'][account_settings_code_key('verify_email', 'old@example.test')]['expires'] = time() - 1;
|
||||
$expired_old_code = account_settings_request($verify_action, ['code' => $old_code]);
|
||||
account_settings_check(empty($expired_old_code['success']), 'email verification accepted an expired old-email code');
|
||||
account_settings_check(get_transient(account_settings_code_key('verify_email', 'old@example.test')) === false, 'expired old-email code was not discarded');
|
||||
account_settings_check(!aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'expired old-email code issued an old-email proof');
|
||||
delete_transient(account_settings_rate_key('verify_email', 'old@example.test'));
|
||||
$old_send = account_settings_request($send_action, ['scene' => 'verify_email']);
|
||||
account_settings_check(!empty($old_send['success']), 'old email code could not be resent after expiry');
|
||||
$old_code = account_settings_code('verify_email', 'old@example.test');
|
||||
$verified_old = account_settings_request($verify_action, ['code' => $old_code]);
|
||||
account_settings_check(!empty($verified_old['success']), 'email verification rejected the correct old-email code');
|
||||
account_settings_check(aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'old-email proof was not issued');
|
||||
account_settings_check(get_transient(account_settings_code_key('verify_email', 'old@example.test')) === false, 'valid old-email code was not consumed');
|
||||
|
||||
$new_email = 'new@example.test';
|
||||
$new_send = account_settings_request($send_action, ['scene' => 'change_email', 'new_email' => $new_email]);
|
||||
account_settings_check(!empty($new_send['success']), 'new email verification code could not be sent');
|
||||
$new_code = account_settings_code('change_email', $new_email);
|
||||
$GLOBALS['aksrt_account_existing_emails']['taken@example.test'] = 19;
|
||||
$duplicate = account_settings_request($change_email_action, ['new_email' => 'taken@example.test', 'code' => $new_code, 'agree_terms' => '1']);
|
||||
account_settings_check(empty($duplicate['success']), 'email update accepted an address already owned by another account');
|
||||
account_settings_check(aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'duplicate-email validation consumed the old-email proof');
|
||||
account_settings_check(get_transient(account_settings_code_key('change_email', $new_email)) !== false, 'duplicate-email validation consumed the new-email code');
|
||||
unset($GLOBALS['aksrt_account_existing_emails']['taken@example.test']);
|
||||
|
||||
$bad_new_code = account_settings_request($change_email_action, ['new_email' => $new_email, 'code' => 'wrong', 'agree_terms' => '1']);
|
||||
account_settings_check(empty($bad_new_code['success']), 'email update accepted an incorrect new-email code');
|
||||
account_settings_check(aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'incorrect-code validation consumed the old-email proof');
|
||||
account_settings_check(get_transient(account_settings_code_key('change_email', $new_email)) !== false, 'incorrect-code validation consumed the new-email code');
|
||||
$GLOBALS['aksrt_account_transients'][account_settings_code_key('change_email', $new_email)]['expires'] = time() - 1;
|
||||
$expired_new_code = account_settings_request($change_email_action, ['new_email' => $new_email, 'code' => $new_code, 'agree_terms' => '1']);
|
||||
account_settings_check(empty($expired_new_code['success']), 'email update accepted an expired new-email code');
|
||||
account_settings_check(aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'expired new-email code validation consumed the old-email proof');
|
||||
account_settings_check(get_transient(account_settings_code_key('change_email', $new_email)) === false, 'expired new-email code was not discarded');
|
||||
delete_transient(account_settings_rate_key('change_email', $new_email));
|
||||
$new_send = account_settings_request($send_action, ['scene' => 'change_email', 'new_email' => $new_email]);
|
||||
account_settings_check(!empty($new_send['success']), 'new email code could not be resent after expiry');
|
||||
$new_code = account_settings_code('change_email', $new_email);
|
||||
|
||||
$GLOBALS['aksrt_account_update_fail'] = true;
|
||||
$write_failure = account_settings_request($change_email_action, ['new_email' => $new_email, 'code' => $new_code, 'agree_terms' => '1']);
|
||||
account_settings_check(empty($write_failure['success']), 'email update reported success after the user write failed');
|
||||
account_settings_check(aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'failed database write consumed the old-email proof');
|
||||
account_settings_check(get_transient(account_settings_code_key('change_email', $new_email)) !== false, 'failed database write consumed the new-email code');
|
||||
$GLOBALS['aksrt_account_update_fail'] = false;
|
||||
$changed = account_settings_request($change_email_action, ['new_email' => $new_email, 'code' => $new_code, 'agree_terms' => '1']);
|
||||
account_settings_check(!empty($changed['success']), 'email update rejected valid old and new proofs');
|
||||
account_settings_check($GLOBALS['aksrt_account_user']->user_email === $new_email, 'email update did not persist the new address');
|
||||
account_settings_check(!aksrt_account_consume_proof(7, 'email_old', 'old@example.test', false), 'successful update retained the old-email proof');
|
||||
account_settings_check(get_transient(account_settings_code_key('change_email', $new_email)) === false, 'successful update retained the new-email code');
|
||||
account_settings_check((int) get_user_meta(7, 'aksrt_email_verified', true) === 1, 'successful update did not mark the email verified');
|
||||
account_settings_check(end($GLOBALS['aksrt_account_mail'])['event'] === 'change_notice', 'successful update did not notify the previous email');
|
||||
|
||||
$GLOBALS['aksrt_account_settings']['email_set_captch'] = false;
|
||||
$direct_email = account_settings_request($change_email_action, ['new_email' => 'direct@example.test', 'agree_terms' => '1']);
|
||||
account_settings_check(!empty($direct_email['success']), 'email update without configured verification still required a code');
|
||||
account_settings_check($GLOBALS['aksrt_account_user']->user_email === 'direct@example.test', 'direct email update did not persist');
|
||||
|
||||
$GLOBALS['aksrt_account_settings']['email_set_captch'] = true;
|
||||
$GLOBALS['aksrt_account_user']->user_email = '';
|
||||
$first_email = 'first@example.test';
|
||||
$first_send = account_settings_request($send_action, ['scene' => 'change_email', 'new_email' => $first_email]);
|
||||
account_settings_check(!empty($first_send['success']), 'first email binding did not send a new-address code');
|
||||
$first_code = account_settings_code('change_email', $first_email);
|
||||
$first_bind = account_settings_request($change_email_action, ['new_email' => $first_email, 'code' => $first_code, 'agree_terms' => '1']);
|
||||
account_settings_check(!empty($first_bind['success']), 'first email binding incorrectly required old-email verification');
|
||||
account_settings_check($GLOBALS['aksrt_account_user']->user_email === $first_email, 'first email binding did not persist');
|
||||
|
||||
$GLOBALS['aksrt_account_captcha_fail'] = true;
|
||||
$captcha_rejected = account_settings_request($change_password_action, ['password' => 'NewPass123', 'password2' => 'NewPass123', 'passwordold' => 'OldPass123']);
|
||||
account_settings_check(empty($captcha_rejected['success']), 'password change bypassed human verification');
|
||||
account_settings_check($GLOBALS['aksrt_account_user']->user_pass === 'hash:OldPass123', 'captcha rejection changed the password');
|
||||
$GLOBALS['aksrt_account_captcha_fail'] = false;
|
||||
|
||||
for ($attempt = 0; $attempt < 8; $attempt++) {
|
||||
$wrong_password = account_settings_request($change_password_action, ['password' => 'NewPass123', 'password2' => 'NewPass123', 'passwordold' => 'wrong']);
|
||||
account_settings_check(empty($wrong_password['success']), 'password change accepted an incorrect old password');
|
||||
}
|
||||
$limited_password = account_settings_request($change_password_action, ['password' => 'NewPass123', 'password2' => 'NewPass123', 'passwordold' => 'OldPass123']);
|
||||
account_settings_check(empty($limited_password['success']), 'password change did not apply its old-password attempt limit');
|
||||
delete_transient('aksrt_account_rate_change_password_7');
|
||||
$changed_password = account_settings_request($change_password_action, ['password' => 'NewPass123', 'password2' => 'NewPass123', 'passwordold' => 'OldPass123']);
|
||||
account_settings_check(!empty($changed_password['success']), 'password change rejected the correct old password');
|
||||
account_settings_check($GLOBALS['aksrt_account_user']->user_pass === 'hash:NewPass123', 'password change did not persist the new password');
|
||||
account_settings_check(get_transient('aksrt_account_rate_change_password_7') === false, 'successful password change did not reset its error limit');
|
||||
|
||||
update_user_meta(7, 'oauth_new', 1);
|
||||
$oauth_password = account_settings_request($change_password_action, ['password' => 'OAuthPass123', 'password2' => 'OAuthPass123']);
|
||||
account_settings_check(!empty($oauth_password['success']), 'OAuth first-time password setup incorrectly required an old password');
|
||||
account_settings_check(get_user_meta(7, 'oauth_new', true) === '', 'OAuth first-time password setup retained the new-account marker');
|
||||
|
||||
echo "Account settings harness passed\n";
|
||||
}
|
||||
@@ -42,16 +42,16 @@ test('password and email account flows verify captcha once per distinct HTTP req
|
||||
return account.slice(start, end === -1 ? account.length : end);
|
||||
};
|
||||
|
||||
const send = methodBody('wp_ajax_aksrt_send_email_code', 'wp_ajax_aksrt_change_password');
|
||||
const send = methodBody('wp_ajax_aksrt_send_account_email_code', 'wp_ajax_aksrt_verify_account_email');
|
||||
const password = methodBody('wp_ajax_aksrt_change_password', 'wp_ajax_aksrt_change_email');
|
||||
const email = methodBody('wp_ajax_aksrt_change_email');
|
||||
|
||||
assert.equal((send.match(/aksrt_require_captcha\(/g) || []).length, 1);
|
||||
assert.match(send, /aksrt_require_captcha\( \$scene,/);
|
||||
assert.match(send, /aksrt_require_captcha\(\s*'change_email'/);
|
||||
assert.equal((password.match(/aksrt_require_captcha\(/g) || []).length, 1);
|
||||
assert.match(password, /aksrt_require_captcha\( 'change_password'/);
|
||||
assert.match(password, /aksrt_require_captcha\(\s*'change_password'/);
|
||||
assert.equal((email.match(/aksrt_require_captcha\(/g) || []).length, 1);
|
||||
assert.match(email, /aksrt_require_captcha\( 'change_email'/);
|
||||
assert.match(email, /aksrt_require_captcha\(\s*'change_email'/);
|
||||
});
|
||||
|
||||
test('legacy public SMS-code AJAX surface is removed while SMS service remains', () => {
|
||||
@@ -59,6 +59,5 @@ test('legacy public SMS-code AJAX surface is removed while SMS service remains',
|
||||
|
||||
assert.doesNotMatch(sms, /aksrt_send_sms_code/);
|
||||
assert.doesNotMatch(sms, /function ajax_send_code\s*\(/);
|
||||
assert.match(sms, /public function send_code\(string \$phone\): array/);
|
||||
assert.match(sms, /public function verify_code\(string \$phone, string \$code\): array/);
|
||||
assert.match(sms, /public function send_code\(string \$phone, string \$code\): array/);
|
||||
});
|
||||
|
||||
@@ -193,7 +193,7 @@ test('auth and account security flows consume fresh captcha tickets', () => {
|
||||
const center = read('assets/js/routes/user-center.js');
|
||||
assert.match(center, /runCaptcha\('bind_phone', form\)[\s\S]*\/user\/phone-code/);
|
||||
assert.match(center, /runCaptcha\('bind_phone', form\)[\s\S]*\/user\/phone/);
|
||||
assert.match(center, /runCaptcha\(scene, fm\)[\s\S]*resetCaptcha\(scene, fm\)/);
|
||||
assert.match(center, /function postAjaxForm\(form, action, scene, success\)[\s\S]*?runCaptcha\(scene, form\)[\s\S]*?finally\(function \(\) \{ resetCaptcha\(scene, form\); \}\)/);
|
||||
assert.match(center, /\/oauth\/bind-pass/);
|
||||
assert.match(center, /bind_verification_required:\s*'请先完成人机验证后再绑定'/);
|
||||
});
|
||||
|
||||
@@ -104,6 +104,7 @@ test('wallet reward content opens in the shared modal runtime once', () => {
|
||||
const authorBar = read('templates/parts/single-author-bar.php');
|
||||
const singleActions = read('inc/helpers/aksrt-single-render.php');
|
||||
const scripts = read('src/Core/ScriptsPart1b.php');
|
||||
const scriptsPart1c = read('src/Core/ScriptsPart1c.php');
|
||||
|
||||
assert.match(reward, /runtime\.open\(\{/);
|
||||
assert.match(reward, /className: 'aksrt-reward-modal modal-mini full-sm'/);
|
||||
@@ -122,7 +123,8 @@ test('wallet reward content opens in the shared modal runtime once', () => {
|
||||
assert.match(helper, /data-aksrt-reward-submit/);
|
||||
assert.match(authorBar, /aksrt_render_rewards_template_for_user/);
|
||||
assert.match(singleActions, /aksrt_render_rewards_template_for_user/);
|
||||
assert.match(scripts, /'needs_reward'\s*=>\s*'single-post\/rewards'/);
|
||||
assert.match(scriptsPart1c, /\$requirements\['needs_reward'\]\s*=\s*\$is_article && !empty\(\$settings\['rewards_enabled'\]\)/);
|
||||
assert.match(scriptsPart1c, /\$singular_assets\['needs_reward'\][\s\S]*?\$reward_js[\s\S]*?'aksrt-reward'/);
|
||||
assert.doesNotMatch(helper + authorBar + singleActions, /aksrt_render_rewards_modal_for_user|reward-modal__qr/);
|
||||
});
|
||||
|
||||
|
||||
@@ -490,8 +490,9 @@ test('profile routes load versioned leaf styles and settings dialogs use the sha
|
||||
assert.match(settingsAccount, /template data-aksrt-settings-modal-template="password"/);
|
||||
assert.match(settingsAccount, /template data-aksrt-settings-modal-template="email"/);
|
||||
assert.match(settingsAccount, /template data-aksrt-settings-modal-template="phone"/);
|
||||
const settingsSubmitButtons = settingsAccount.match(/<button type="submit" class="but jb-blue padding-lg btn-block"[^>]*><i class="fa fas fa-check" aria-hidden="true"><\/i> <span data-aksrt-settings-submit-label><\?php esc_html_e\('确认提交', 'aksrt'\); \?><\/span><\/button>/g) || [];
|
||||
assert.equal(settingsSubmitButtons.length, 3, 'password, email, and phone dialogs use the reference submit button with the FA5 icon');
|
||||
assert.match(settingsAccount, /id="pwd-form"[\s\S]*?data-aksrt-settings-submit-label/);
|
||||
assert.match(settingsAccount, /id="email-new-form"[\s\S]*?but jb-blue padding-lg btn-block/);
|
||||
assert.match(settingsAccount, /id="aksrt-bind-phone-form"[\s\S]*?but jb-blue padding-lg btn-block/);
|
||||
assert.doesNotMatch(settingsAccount, /aksrt-settings-modal__footer|确认修改|确认更改|确认绑定/);
|
||||
assert.ok(settingsAccount.indexOf('绑定邮箱') < settingsAccount.indexOf('绑定手机'));
|
||||
assert.ok(settingsAccount.indexOf('绑定手机') < settingsAccount.indexOf("require __DIR__ . '/settings-bindings.php'"));
|
||||
@@ -504,13 +505,20 @@ test('profile routes load versioned leaf styles and settings dialogs use the sha
|
||||
assert.match(userCenterJs, /className: 'aksrt-user-settings-modal modal-mini'/);
|
||||
assert.match(userCenterJs, /mobileBottom: true/);
|
||||
assert.match(userCenterJs, /onContent: function \(_, session\)/);
|
||||
assert.match(userCenterJs, /aksrt:modal:close[\s\S]*?resetSettingsModal\(shell\)/);
|
||||
assert.match(userCenterJs, /aksrt:modal:close[\s\S]*?resetSettingsModal\(event\.detail && event\.detail\.shell\)/);
|
||||
assert.match(userCenterJs, /resetCaptcha\(scene, form\)/);
|
||||
assert.match(userCenterJs, /function setSettingsSubmitLabel\(button, text\)[\s\S]*?label\.textContent = text/);
|
||||
assert.match(userCenterJs, /setSettingsSubmitLabel\(submitBtn, submitIdleText\)/);
|
||||
assert.match(userCenterJs, /aksrt_change_password/);
|
||||
assert.match(userCenterJs, /aksrt_change_email/);
|
||||
assert.match(userCenterJs, /AksrtModalRuntime\.close\('settings-submit'\)/);
|
||||
assert.match(settingsAccount, /data-account-step="email-old"[\s\S]*?data-account-step="email-new"/);
|
||||
assert.match(settingsAccount, /\$email_verify_required/);
|
||||
assert.match(settingsAccount, /data-account-step="phone-old"[\s\S]*?data-account-step="phone-new"/);
|
||||
assert.match(settingsAccount, /data-account-recovery-open[\s\S]*?data-account-password-pane="recovery"/);
|
||||
assert.match(settingsAccount, /aksrt_render_captcha\('change_password'\)/);
|
||||
assert.match(userCenterJs, /\/auth\/code/);
|
||||
assert.match(userCenterJs, /\/auth\/reset-password/);
|
||||
assert.match(userCenterJs, /\/user\/phone-verify/);
|
||||
assert.doesNotMatch(settingsAccount + userCenterJs + settingsCss, /pwd-strength|pwd-send-code|email-send-code|aksrt_send_email_code/);
|
||||
assert.match(settingsCss, /\.aksrt-modal-runtime\.aksrt-user-settings-modal:not\(\.aksrt-modal-runtime--bottom\)[\s\S]*?width: 400px/);
|
||||
assert.match(settingsCss, /\.aksrt-settings-modal \.modal-colorful-header[\s\S]*?height: 120px/);
|
||||
assert.match(settingsAccount, /class="colorful-make"/);
|
||||
|
||||
@@ -340,30 +340,25 @@ test('settings actions use rehydrated fixed-id notyf flows', () => {
|
||||
const context = read('inc/helpers/aksrt-user-center.php');
|
||||
const page = read('templates/pages/page-profile.php');
|
||||
|
||||
[
|
||||
'aksrt-settings-password-code-toast',
|
||||
'aksrt-settings-email-code-toast',
|
||||
'aksrt-settings-password-toast',
|
||||
'aksrt-settings-email-toast',
|
||||
'aksrt-settings-oauth-unbind-toast',
|
||||
'aksrt-settings-phone-bind-toast',
|
||||
].forEach((id) => assert.match(userCenter, new RegExp("['\"]" + id + "['\"]")));
|
||||
assert.match(userCenter, /aksrt-settings-oauth-unbind-toast/);
|
||||
assert.doesNotMatch(userCenter, /aksrt-settings-(?:password|email|phone)-(?:code-)?toast/);
|
||||
|
||||
assert.match(userCenter, /notyf\([^;]+?'load'[^;]+?toastId\);/);
|
||||
assert.match(userCenter, /notyf\([^;]+?'success'[^;]+?toastId\);/);
|
||||
assert.match(userCenter, /notyf\([^;]+?'danger'[^;]+?toastId\);/);
|
||||
assert.match(userCenter, /auto_fun\.aksrt-user-center'[\s\S]*?initSettings\(\);[\s\S]*?initBindPhone\(\);/);
|
||||
assert.match(userCenter, /auto_fun\.aksrt-user-center'[\s\S]*?initSettings\(\);[\s\S]*?initSettingsCallbackToast\(\);/);
|
||||
assert.doesNotMatch(userCenter, /initBindPhone\(/);
|
||||
assert.match(userCenter, /aksrtSettingsInitialized/);
|
||||
assert.match(userCenter, /oauth_action[\s\S]*?notyf\(message, type\)[\s\S]*?history\.replaceState/);
|
||||
assert.doesNotMatch(userCenter, /oauth_unbound/);
|
||||
assert.match(auth, /\[data-identity-root\], \[data-aksrt-settings-root\]/);
|
||||
assert.doesNotMatch(auth, /\.settings-section/);
|
||||
|
||||
assert.match(userCenter, /'aksrt-settings-phone-code-toast'/);
|
||||
assert.match(userCenter, /notyf\('正在发送验证码…', 'load', '', toastId\)/);
|
||||
assert.match(userCenter, /notyf\([^;]+?'success', 5, toastId\)/);
|
||||
assert.match(userCenter, /notyf\([^;]+?'danger', 5, toastId\)/);
|
||||
assert.match(userCenter, /\/user\/phone-code/);
|
||||
assert.match(userCenter, /\/user\/phone-verify/);
|
||||
const accountSettings = read('inc/helpers/user-center-renderers/settings-account.php');
|
||||
assert.match(accountSettings, /data-aksrt-sms-scene="verify_old_phone"/);
|
||||
assert.match(accountSettings, /data-aksrt-sms-scene="bind_new_phone"/);
|
||||
|
||||
assert.match(settings, /require \$dir \. '\/settings-account\.php';/);
|
||||
assert.doesNotMatch(context, /account_modals_html|'account-modals'/);
|
||||
|
||||
@@ -198,27 +198,31 @@ test('native user profile omits ledger tables and renders canonical images eager
|
||||
assert.doesNotMatch(level, /Level::get_badge_html/);
|
||||
});
|
||||
|
||||
test('identity status, type, and name are directly editable on the native profile', () => {
|
||||
test('identity marker and profile are editable on the native profile while applications use the review queue', () => {
|
||||
const auth = read('src/User/AuthApply.php');
|
||||
const verify = read('src/Admin/Native/UsersMenusVerify.php');
|
||||
const transitions = read('src/Identity/TransitionService.php');
|
||||
|
||||
assert.match(auth, /add_action\('edit_user_profile_update', \[\$this, 'handle_admin_profile_update'\]\)/);
|
||||
assert.match(auth, /add_action\('edit_user_profile_update', \[\$this, 'save_admin_profile'\]\)/);
|
||||
assert.match(auth, /current_user_can\('aksrt_review_identity'\)/);
|
||||
assert.match(auth, /wp_nonce_field\('aksrt_identity_profile_' \.[\s\S]*?'aksrt_identity_profile_nonce'\)/);
|
||||
assert.match(auth, /name="aksrt_identity_application_uuid"/);
|
||||
assert.match(auth, /name="aksrt_identity_type"/);
|
||||
assert.match(auth, /value="personal"[\s\S]*?个人认证/);
|
||||
assert.match(auth, /value="enterprise"[\s\S]*?企业认证/);
|
||||
assert.match(auth, /name="aksrt_identity_public_name"/);
|
||||
assert.match(auth, /姓名 \/ 主体全称/);
|
||||
assert.match(auth, /name="aksrt_identity_status"/);
|
||||
assert.match(auth, /'manual_pending' => __\('待审核'/);
|
||||
assert.match(auth, /'verified' => __\('已认证'/);
|
||||
assert.match(auth, /'rejected' => __\('已驳回'/);
|
||||
assert.match(auth, /create_admin_application\(\$repo, \$user_id, \$fields\)/);
|
||||
assert.match(auth, /'type' => \$fields\['type'\]/);
|
||||
assert.match(auth, /self::review\(\$user_id, \$application_uuid, \$target_status, \$note, get_current_user_id\(\), \$fields\)/);
|
||||
assert.match(transitions, /public function admin_transition/);
|
||||
assert.match(transitions, /\? in_array\(\$next, \['manual_pending', 'verified', 'rejected', 'cancelled'\], true\)/);
|
||||
assert.match(transitions, /'verified' => \['cancelled'\]/);
|
||||
assert.match(auth, /wp_nonce_field\('aksrt_auth_profile_' \.[\s\S]*?'aksrt_auth_profile_nonce'\)/);
|
||||
assert.match(auth, /name="aksrt_auth_enabled"/);
|
||||
assert.match(auth, /name="aksrt_auth_name"/);
|
||||
assert.match(auth, /name="aksrt_auth_desc"/);
|
||||
assert.match(auth, /name="aksrt_auth_time"/);
|
||||
assert.match(auth, /update_user_meta\(\$user_id, 'auth', 1\)/);
|
||||
assert.match(auth, /update_user_meta\(\$user_id, 'auth_info'/);
|
||||
assert.doesNotMatch(auth, /name="aksrt_identity_(?:application_uuid|type|status|public_name)"/);
|
||||
|
||||
assert.match(verify, /'general',[\s\S]*?'manual'/);
|
||||
assert.match(verify, /if \(\$status === 'manual_pending'\)/);
|
||||
assert.match(verify, /'verified' => __\('通过申请'/);
|
||||
assert.match(verify, /'rejected' => __\('拒绝申请'/);
|
||||
assert.match(verify, /AuthApply::review\(\$user_id, \$uuid, \$status, \$note, get_current_user_id\(\), \$fields\)/);
|
||||
assert.match(auth, /\['verified', 'rejected'\]/);
|
||||
assert.match(auth, /\(string\) \$application\['status'\] !== 'manual_pending'/);
|
||||
assert.match(transitions, /'manual_pending' => 'pending'/);
|
||||
assert.match(transitions, /'verified' => 'verified'/);
|
||||
assert.match(transitions, /'rejected' => 'rejected'/);
|
||||
assert.match(transitions, /default => ''/);
|
||||
});
|
||||
|
||||
@@ -5,10 +5,13 @@ declare(strict_types=1);
|
||||
|
||||
namespace {
|
||||
define('ABSPATH', __DIR__ . '/');
|
||||
define('MINUTE_IN_SECONDS', 60);
|
||||
define('HOUR_IN_SECONDS', 3600);
|
||||
$GLOBALS['aksrt_profile_meta'] = [];
|
||||
$GLOBALS['aksrt_profile_users'] = [];
|
||||
$GLOBALS['aksrt_nonce_valid'] = true;
|
||||
$GLOBALS['aksrt_can_edit_user'] = true;
|
||||
$GLOBALS['aksrt_current_user_id'] = 7;
|
||||
$GLOBALS['aksrt_transients'] = [];
|
||||
$GLOBALS['aksrt_expected_sms_code'] = '246810';
|
||||
|
||||
@@ -34,7 +37,7 @@ namespace {
|
||||
function is_admin(): bool { return true; }
|
||||
function current_user_can(string $capability, ...$args): bool { return $GLOBALS['aksrt_can_edit_user']; }
|
||||
function is_user_logged_in(): bool { return true; }
|
||||
function get_current_user_id(): int { return 7; }
|
||||
function get_current_user_id(): int { return (int) ($GLOBALS['aksrt_current_user_id'] ?? 7); }
|
||||
function get_user_meta(int $user_id, string $key, bool $single = false) {
|
||||
$value = $GLOBALS['aksrt_profile_meta'][$user_id][$key] ?? '';
|
||||
return $single ? $value : ($value === '' ? [] : [$value]);
|
||||
@@ -48,7 +51,7 @@ namespace {
|
||||
foreach ($GLOBALS['aksrt_profile_meta'] as $user_id => $meta) {
|
||||
if (in_array((int) $user_id, array_map('intval', $args['exclude'] ?? []), true)) continue;
|
||||
if (($meta[$args['meta_key'] ?? ''] ?? null) !== ($args['meta_value'] ?? null)) continue;
|
||||
$results[] = ($args['fields'] ?? '') === 'ids' ? (int) $user_id : new WP_User((int) $user_id);
|
||||
$results[] = in_array(($args['fields'] ?? ''), ['ids', 'ID'], true) ? (int) $user_id : new WP_User((int) $user_id);
|
||||
if (count($results) >= (int) ($args['number'] ?? 1)) break;
|
||||
}
|
||||
return $results;
|
||||
@@ -99,6 +102,12 @@ namespace AKSRT\Sms {
|
||||
}
|
||||
}
|
||||
|
||||
namespace AKSRT\Settings {
|
||||
final class Settings {
|
||||
public static function get_all(): array { return ['bind_phone' => true]; }
|
||||
}
|
||||
}
|
||||
|
||||
namespace AKSRT\Auth {
|
||||
require dirname(__DIR__, 2) . '/src/Auth/ServicePart1b.php';
|
||||
final class PhoneHarnessService { use ServicePart1b; }
|
||||
@@ -150,10 +159,25 @@ namespace {
|
||||
check(empty($same_phone['success']), 'binding accepted the current phone as a change');
|
||||
|
||||
$phone_to_bind = '13800000010';
|
||||
$otp_key = 'aksrt_auth_code_' . md5('bind_phone|' . strtolower(trim($phone_to_bind)));
|
||||
$otp_key = 'aksrt_auth_code_' . md5('account_bind_new_phone_7|' . strtolower(trim($phone_to_bind)));
|
||||
$operations = new \AKSRT\Admin\PhoneOperationsHarness();
|
||||
$GLOBALS['aksrt_transients'][$otp_key] = ['hash' => 'fixture-hash', 'attempts' => 0, 'expires' => time() + 300];
|
||||
$old_phone = '13800000007';
|
||||
$old_otp_key = 'aksrt_auth_code_' . md5('account_verify_old_phone_7|' . strtolower(trim($old_phone)));
|
||||
$GLOBALS['aksrt_transients'][$old_otp_key] = ['hash' => 'fixture-hash', 'attempts' => 0, 'expires' => time() + 300];
|
||||
$GLOBALS['aksrt_expected_sms_code'] = '246810';
|
||||
$wrong_old_code = $operations->user_verify_phone(new WP_REST_Request(['phone' => $old_phone, 'code' => '000000']));
|
||||
check(empty($wrong_old_code->data['success']), 'REST phone replacement accepted an incorrect old-number code');
|
||||
check(($GLOBALS['aksrt_transients'][$old_otp_key]['attempts'] ?? 0) === 1, 'incorrect old-number code attempt was not recorded');
|
||||
$GLOBALS['aksrt_transients'][$old_otp_key] = ['hash' => 'fixture-hash', 'attempts' => 0, 'expires' => time() - 1];
|
||||
$expired_old_code = $operations->user_verify_phone(new WP_REST_Request(['phone' => $old_phone, 'code' => '246810']));
|
||||
check(empty($expired_old_code->data['success']), 'REST phone replacement accepted an expired old-number code');
|
||||
check(!isset($GLOBALS['aksrt_transients'][$old_otp_key]), 'expired old-number code was not discarded');
|
||||
$GLOBALS['aksrt_transients'][$old_otp_key] = ['hash' => 'fixture-hash', 'attempts' => 0, 'expires' => time() + 300];
|
||||
$verified_old_phone = $operations->user_verify_phone(new WP_REST_Request(['phone' => $old_phone, 'code' => '246810']));
|
||||
check(!empty($verified_old_phone->data['success']), 'REST phone replacement rejected the correct old-number code');
|
||||
check(!isset($GLOBALS['aksrt_transients'][$old_otp_key]), 'valid old-number code was not consumed');
|
||||
check(($GLOBALS['aksrt_transients']['aksrt_account_phone_old_7']['phone'] ?? '') === $old_phone, 'old-number proof was not issued for the current account');
|
||||
$GLOBALS['aksrt_transients'][$otp_key] = ['hash' => 'fixture-hash', 'attempts' => 0, 'expires' => time() + 300];
|
||||
$wrong_code = $operations->user_bind_phone(new WP_REST_Request(['phone' => $phone_to_bind, 'code' => '000000']));
|
||||
check(empty($wrong_code->data['success']), 'REST phone binding accepted an incorrect SMS code');
|
||||
check(($GLOBALS['aksrt_transients'][$otp_key]['attempts'] ?? 0) === 1, 'incorrect SMS code attempt was not recorded');
|
||||
@@ -161,11 +185,29 @@ namespace {
|
||||
$expired_code = $operations->user_bind_phone(new WP_REST_Request(['phone' => $phone_to_bind, 'code' => '246810']));
|
||||
check(empty($expired_code->data['success']), 'REST phone binding accepted an expired SMS code');
|
||||
check(!isset($GLOBALS['aksrt_transients'][$otp_key]), 'expired SMS code was not discarded');
|
||||
$duplicate_phone = '13800000008';
|
||||
$duplicate_otp_key = 'aksrt_auth_code_' . md5('account_bind_new_phone_7|' . strtolower(trim($duplicate_phone)));
|
||||
$GLOBALS['aksrt_transients'][$duplicate_otp_key] = ['hash' => 'fixture-hash', 'attempts' => 0, 'expires' => time() + 300];
|
||||
$duplicate_code = $operations->user_bind_phone(new WP_REST_Request(['phone' => $duplicate_phone, 'code' => '246810']));
|
||||
check(empty($duplicate_code->data['success']), 'REST phone binding accepted a number owned by another user');
|
||||
check(isset($GLOBALS['aksrt_transients'][$duplicate_otp_key]), 'duplicate-number validation consumed the valid SMS code');
|
||||
$GLOBALS['aksrt_transients'][$otp_key] = ['hash' => 'fixture-hash', 'attempts' => 0, 'expires' => time() + 300];
|
||||
$valid_code = $operations->user_bind_phone(new WP_REST_Request(['phone' => $phone_to_bind, 'code' => '246810']));
|
||||
check(!empty($valid_code->data['success']), 'REST phone binding rejected the correct SMS code');
|
||||
check(get_user_meta(7, 'phone_number', true) === $phone_to_bind, 'REST phone binding did not store the canonical phone meta');
|
||||
check(!isset($GLOBALS['aksrt_transients'][$otp_key]), 'valid SMS code was not consumed');
|
||||
check(!isset($GLOBALS['aksrt_transients']['aksrt_account_phone_old_7']), 'successful phone change did not consume the old-number proof');
|
||||
|
||||
$initial_bind_user_id = 9;
|
||||
$initial_bind_phone = '13800000011';
|
||||
$initial_bind_code_key = 'aksrt_auth_code_' . md5('account_bind_new_phone_9|' . strtolower(trim($initial_bind_phone)));
|
||||
$GLOBALS['aksrt_current_user_id'] = $initial_bind_user_id;
|
||||
$GLOBALS['aksrt_transients'][$initial_bind_code_key] = ['hash' => 'fixture-hash', 'attempts' => 0, 'expires' => time() + 300];
|
||||
$initial_bind = $operations->user_bind_phone(new WP_REST_Request(['phone' => $initial_bind_phone, 'code' => '246810']));
|
||||
check(!empty($initial_bind->data['success']), 'first-time phone binding incorrectly required an old-number proof');
|
||||
check(get_user_meta($initial_bind_user_id, 'phone_number', true) === $initial_bind_phone, 'first-time phone binding did not store the canonical phone meta');
|
||||
check(!isset($GLOBALS['aksrt_transients'][$initial_bind_code_key]), 'successful first-time phone binding did not consume its SMS code');
|
||||
$GLOBALS['aksrt_current_user_id'] = 7;
|
||||
|
||||
$_POST = [
|
||||
'aksrt_profile_admin_nonce' => 'ok',
|
||||
|
||||
Reference in New Issue
Block a user