Files
wordpress-theme-aksrt/tests/widget-system/user-auth-admin-harness.php
T
kate522andClaude Fable 5 bc26d62602 feat(admin): redesign identity review with dialogs
Replace inline identity review forms with focused detail and review dialogs, improve list filters and caching, and align evidence permissions and deletion feedback. Add contract and runtime regression coverage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-30 20:05:21 +08:00

519 lines
22 KiB
PHP

<?php
/** WordPress-free harness for the native identity admin render and POST handlers. */
namespace AKSRT\Core {
final class DBMigrator {
public static function table($key) { return 'wp_aksrt_' . $key; }
}
}
namespace AKSRT\User {
final class AuthApply {
public static $next_result = null;
public static function review($user_id, $application_uuid, $status, $note, $reviewer_id, $profile_fields) {
$GLOBALS['review_calls'][] = compact('user_id', 'application_uuid', 'status', 'note', 'reviewer_id', 'profile_fields');
return self::$next_result;
}
}
}
namespace {
define('ABSPATH', __DIR__);
define('ARRAY_A', 'ARRAY_A');
$root = dirname(__DIR__, 2);
define('AKSRT_DIR', $root);
define('AKSRT_URI', 'https://example.test/theme');
class WP_Error {
public function __construct(public $code, public $message) {}
public function get_error_message() { return $this->message; }
}
class HarnessWpDie extends \RuntimeException {}
class HarnessRedirect extends \RuntimeException {}
class WP_User {
public function __construct(public $ID, public $display_name) {}
}
function __($text, $domain = '') { return (string) $text; }
function esc_html($text) { return htmlspecialchars((string) $text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
function esc_attr($text) { return esc_html($text); }
function esc_textarea($text) { return esc_html($text); }
function esc_html__($text, $domain = '') { return esc_html($text); }
function esc_attr__($text, $domain = '') { return esc_attr($text); }
function esc_url($url) {
$url = (string) $url;
return preg_match('/^\s*javascript:/i', $url) ? '' : esc_attr($url);
}
function admin_url($path = '') { return 'https://example.test/wp-admin/' . ltrim((string) $path, '/'); }
function get_edit_user_link($id) { return admin_url('user-edit.php?user_id=' . abs((int) $id)); }
function get_avatar_url($id, $args = []) { return 'https://example.test/avatar.png?user=' . abs((int) $id); }
function get_userdata($id) {
$id = (int) $id;
return isset($GLOBALS['users'][$id]) ? new WP_User($id, $GLOBALS['users'][$id]) : false;
}
function current_user_can($capability, ...$args) { return !empty($GLOBALS['caps'][$capability]); }
function get_current_user_id() { return $GLOBALS['current_user_id'] ?? 7; }
function is_user_logged_in() { return get_current_user_id() > 0; }
function get_attached_file($id) { return false; }
function get_post_mime_type($id) { return 'image/png'; }
function delete_user_meta($user_id, $key) {
$GLOBALS['delete_user_meta_calls'][] = [(int) $user_id, (string) $key];
unset($GLOBALS['user_meta'][(int) $user_id][(string) $key]);
return true;
}
function update_user_meta($user_id, $key, $value) {
$GLOBALS['user_meta'][(int) $user_id][(string) $key] = $value;
return true;
}
function wp_die($message = '') { throw new HarnessWpDie((string) $message); }
function wp_unslash($value) { return $value; }
function absint($value) { return abs((int) $value); }
function sanitize_key($text) { return preg_replace('/[^a-z0-9_\-]/', '', strtolower((string) $text)) ?? ''; }
function sanitize_text_field($text) {
$text = strip_tags((string) $text);
$text = preg_replace('/[\r\n\t ]+/', ' ', $text) ?? '';
return trim($text);
}
function sanitize_textarea_field($text) {
$text = strip_tags((string) $text);
$text = preg_replace('/[\r\t ]+/', ' ', $text) ?? '';
$text = preg_replace('/\n{3,}/', "\n\n", $text) ?? '';
return trim($text);
}
function is_wp_error($value) { return $value instanceof WP_Error; }
function number_format_i18n($number) { return number_format((float) $number); }
function get_date_from_gmt($date, $format = 'Y-m-d H:i:s') {
if ((string) $date === '') return '';
$utc = new DateTimeImmutable((string) $date, new DateTimeZone('UTC'));
return $utc->setTimezone(new DateTimeZone('Asia/Shanghai'))->format($format);
}
function wp_create_nonce($action) { return 'nonce:' . (string) $action; }
function wp_nonce_field($action, $name = '_wpnonce', $referer = true, $display = true) {
$html = '<input type="hidden" name="' . esc_attr($name) . '" value="' . esc_attr(wp_create_nonce($action)) . '">';
if ($display) echo $html;
return $html;
}
function check_admin_referer($action, $query_arg = '_wpnonce') {
$GLOBALS['checked_nonces'][] = [$action, $query_arg];
$provided = (string) ($_POST[$query_arg] ?? '');
if (!hash_equals(wp_create_nonce($action), $provided)) {
throw new HarnessWpDie('nonce verification failed');
}
return 1;
}
function add_query_arg($key, $value = '', $url = '') {
if (is_array($key)) {
$args = $key;
$url = (string) $value;
} else {
$args = [$key => $value];
if ($url === '') $url = (string) ($_SERVER['REQUEST_URI'] ?? '');
}
$parts = parse_url((string) $url) ?: [];
parse_str($parts['query'] ?? '', $query);
foreach ($args as $name => $item) $query[$name] = $item;
$base = '';
if (isset($parts['scheme'])) $base .= $parts['scheme'] . '://';
if (isset($parts['host'])) $base .= $parts['host'];
if (isset($parts['port'])) $base .= ':' . $parts['port'];
$base .= $parts['path'] ?? '';
return $base . ($query !== [] ? '?' . http_build_query($query) : '') . (isset($parts['fragment']) ? '#' . $parts['fragment'] : '');
}
function cache_users($ids) { $GLOBALS['cache_calls'][] = array_values($ids); }
function update_meta_cache($type, $ids) { $GLOBALS['meta_cache_calls'][] = [$type, array_values($ids)]; }
function get_post_meta($id, $key, $single = false) { return $GLOBALS['post_meta'][(int) $id][$key] ?? ''; }
function get_post_type($id) { return in_array((int) $id, $GLOBALS['attachment_ids'], true) ? 'attachment' : false; }
function wp_delete_attachment($id, $force = false) {
$GLOBALS['deleted_attachments'][] = [(int) $id, (bool) $force];
return true;
}
function wp_enqueue_style(...$args) { $GLOBALS['assets'][] = ['style', ...$args]; }
function wp_enqueue_script(...$args) { $GLOBALS['assets'][] = ['script', ...$args]; }
function esc_html_e($text, $domain = '') { echo esc_html($text); }
final class FakeWpdb {
public $users = 'wp_users';
public $prepared = [];
public $executed = [];
public $delete_calls = [];
public $total = 45;
public $application_delete_result = 1;
public $event_delete_result = 1;
public $applications = [];
public function prepare($sql, ...$params) {
$params = count($params) === 1 && is_array($params[0]) ? $params[0] : $params;
$this->prepared[] = ['sql' => (string) $sql, 'params' => $params];
return (string) $sql;
}
public function esc_like($text) { return addcslashes((string) $text, '_%\\'); }
public function get_var($sql) {
$this->executed[] = (string) $sql;
return str_contains((string) $sql, 'SELECT COUNT(*)') ? $this->total : null;
}
public function get_results($sql, $format = null) {
$this->executed[] = (string) $sql;
if (str_contains((string) $sql, 'GROUP BY a.status')) {
return [
['status' => 'manual_pending', 'total' => 2],
['status' => 'verified', 'total' => 1],
['status' => 'rejected', 'total' => 1],
];
}
if (str_contains((string) $sql, 'LIMIT %d OFFSET %d')) return $GLOBALS['render_rows'];
return [];
}
public function get_row($sql, $format = null) {
$this->executed[] = (string) $sql;
$query = $this->prepared[count($this->prepared) - 1] ?? ['params' => []];
$requested_uuid = (string) ($query['params'][0] ?? '');
foreach ($this->applications as $application) {
if (
str_contains((string) $sql, 'application_uuid = %s')
&& str_contains((string) $sql, 'identity_application')
&& (string) ($application['application_uuid'] ?? '') === $requested_uuid
) {
return $application;
}
}
return null;
}
public function delete($table, $data, $format = null) {
$this->delete_calls[] = ['table' => $table, 'data' => $data, 'format' => $format];
if (str_contains((string) $table, 'identity_application')) return $this->application_delete_result;
if (str_contains((string) $table, 'identity_event')) return $this->event_delete_result;
return 1;
}
}
require $root . '/src/Admin/Native/NativeUi.php';
require $root . '/src/Admin/Native/AdminListPagination.php';
require $root . '/src/Identity/ApplicationRepository.php';
require $root . '/src/Identity/EvidenceService.php';
require $root . '/src/Identity/EventRepository.php';
require $root . '/src/Admin/Native/UsersMenusVerifyDialogs.php';
require $root . '/src/Admin/Native/UsersMenusVerify.php';
final class VerifyAdminProbe {
use \AKSRT\Admin\Native\UsersMenusVerify;
public const CAPABILITY = 'manage_options';
public $cap_checks = 0;
private function ensure_cap(): void {
$this->cap_checks++;
if (!current_user_can('manage_options')) wp_die('capability denied');
}
private function flash(): void {}
private function redirect(string $page, string $message, array $extra = []): void {
$url = add_query_arg(array_merge(['page' => $page, 'aksrt_users_msg' => $message], $extra), admin_url('users.php'));
throw new HarnessRedirect($url);
}
public function invoke(string $method, ...$args) { return $this->$method(...$args); }
public function page(): string {
ob_start();
$this->render_verify_page();
return (string) ob_get_clean();
}
public function html(string $method, ...$args): string {
ob_start();
$this->$method(...$args);
return (string) ob_get_clean();
}
}
$GLOBALS['users'] = [
41 => 'Applicant <script>alert(1)</script>',
42 => 'Approved User',
9 => 'Reviewer <img src=x onerror=alert(2)>',
];
$GLOBALS['caps'] = ['manage_options' => true, 'aksrt_review_identity' => true];
$GLOBALS['post_meta'] = [
1001 => [
'_aksrt_verify_evidence_owner' => 41,
'_aksrt_identity_application_uuid' => 'uuid-pending-301',
],
1002 => [
'_aksrt_verify_evidence_owner' => 42,
'_aksrt_identity_application_uuid' => 'uuid-approved-302',
],
];
$GLOBALS['attachment_ids'] = [1001, 1002];
$GLOBALS['deleted_attachments'] = [];
$GLOBALS['cache_calls'] = [];
$GLOBALS['meta_cache_calls'] = [];
$GLOBALS['checked_nonces'] = [];
$GLOBALS['review_calls'] = [];
$GLOBALS['assets'] = [];
$GLOBALS['delete_user_meta_calls'] = [];
$GLOBALS['user_meta'] = [41 => ['auth' => 1, 'auth_info' => ['name' => 'Already verified', 'desc' => 'kept']]];
$GLOBALS['render_rows'] = [
[
'id' => 301,
'application_uuid' => 'uuid-pending-301',
'user_id' => 41,
'status' => 'manual_pending',
'public_name' => 'Applicant name <script>alert(3)</script>',
'description' => 'Bio <img src=x onerror=alert(4)> & text',
'manual_reason' => 'Reason <svg onload=alert(5)>',
'review_note' => '',
'reviewer_id' => 0,
'evidence_ids' => '[1001]',
'created_at' => '2026-10-30 03:30:00',
'completed_at' => null,
'display_name' => 'Applicant <script>alert(1)</script>',
'user_login' => 'applicant"<script>login</script>',
'user_email' => 'applicant"<img src=x onerror=alert(6)>@example.test',
],
[
'id' => 302,
'application_uuid' => 'uuid-approved-302',
'user_id' => 42,
'status' => 'verified',
'public_name' => 'Approved name',
'description' => 'Approved bio',
'manual_reason' => 'Applied earlier',
'review_note' => 'Reviewed safely',
'reviewer_id' => 9,
'evidence_ids' => '[1002,1001]',
'created_at' => '2026-10-29 02:10:00',
'completed_at' => '2026-10-30 04:45:00',
'display_name' => 'Approved User',
'user_login' => 'approved_user',
'user_email' => 'approved@example.test',
],
];
$wpdb = new FakeWpdb();
$probe = new VerifyAdminProbe();
$_GET = [
'page' => 'aksrt_user_auth',
'keyword' => 'search <script>alert(1)</script> &_%',
'paged' => '999',
];
$html = $probe->page();
$page_prepared = $wpdb->prepared;
$page_executed = $wpdb->executed;
$page_cache_calls = $GLOBALS['cache_calls'];
$page_meta_cache_calls = $GLOBALS['meta_cache_calls'];
$pending_empty = $probe->html('render_verify_table', [], ['status' => 'pending', 'keyword' => ''], 1);
$search_empty = $probe->html('render_verify_table', [], ['status' => 'approved', 'keyword' => 'no match'], 1);
$GLOBALS['assets'] = [];
$probe->enqueue_verify_assets('users_page_aksrt_messages');
$wrong_hook_assets = $GLOBALS['assets'];
$probe->enqueue_verify_assets('users_page_aksrt_user_auth');
$auth_page_assets = $GLOBALS['assets'];
$evidence_service = new \AKSRT\Identity\EvidenceService();
$evidence_application = ['user_id' => 41, 'application_uuid' => 'uuid-pending-301', 'evidence_ids' => [1001]];
$evidence_permissions = [];
foreach ([
'manager' => [['manage_options' => true], 7],
'reviewer' => [['aksrt_review_identity' => true], 7],
'outsider' => [[], 7],
'owner' => [[], 41],
] as $role => [$caps, $current_user]) {
$GLOBALS['caps'] = $caps;
$GLOBALS['current_user_id'] = $current_user;
$evidence_permissions[$role] = [
'admin_items' => count($evidence_service->items($evidence_application, true)),
'owner_items' => count($evidence_service->items($evidence_application)),
];
$_GET = ['attachment' => 1001];
$_POST = ['_wpnonce' => wp_create_nonce('aksrt_verify_evidence_1001')];
try {
$evidence_service->serve();
} catch (HarnessWpDie $error) {
$evidence_permissions[$role]['serve_result'] = $error->getMessage();
}
}
$GLOBALS['current_user_id'] = 7;
$GLOBALS['caps'] = ['manage_options' => true, 'aksrt_review_identity' => true];
$GLOBALS['checked_nonces'] = [];
$wpdb->prepared = [];
$wpdb->executed = [];
$wpdb->applications = [[
'id' => 401,
'application_uuid' => 'review-uuid-401',
'user_id' => 41,
'type' => 'general',
'channel' => 'manual',
'status' => 'manual_pending',
'public_name' => 'Before review',
'provider_result_summary' => 'Before bio',
'manual_reason' => 'Manual reason',
'review_note' => '',
'reviewer_id' => 0,
'evidence_ids' => '[1001]',
'created_at' => '2026-10-30 03:30:00',
'completed_at' => null,
'updated_at' => '2026-10-30 03:30:00',
]];
$_POST = [
'application_uuid' => 'review-uuid-401',
'user_id' => '41',
'status' => 'REJECTED!',
'note' => 'Rejected <script>reason</script> & explain',
'name' => 'Ignored <img src=x onerror=alert(1)>',
'description' => 'Ignored <b>description</b>',
'filter_status' => 'approved',
'keyword' => 'review <script>search</script> & phrase',
'paged' => '4',
'_wpnonce' => wp_create_nonce('aksrt_users_review_verify_review-uuid-401'),
];
\AKSRT\User\AuthApply::$next_result = ['status' => 'rejected'];
try {
$probe->handle_review_verify();
} catch (HarnessRedirect $redirect) {
parse_str((string) parse_url($redirect->getMessage(), PHP_URL_QUERY), $query);
$review_redirect = $query;
}
$review_call = $GLOBALS['review_calls'][0] ?? null;
$review_nonce_calls = $GLOBALS['checked_nonces'];
$GLOBALS['review_calls'] = [];
$wpdb->prepared = [];
$_POST = [
'application_uuid' => 'review-uuid-401',
'user_id' => '41',
'status' => 'verified',
'note' => '',
'name' => 'Safe name',
'description' => 'Safe description',
'filter_status' => 'rejected',
'keyword' => 'keep this',
'paged' => '2',
'_wpnonce' => wp_create_nonce('aksrt_users_review_verify_review-uuid-401'),
];
\AKSRT\User\AuthApply::$next_result = new WP_Error('review_failed', '<script>review unavailable</script>');
try {
$probe->handle_review_verify();
} catch (HarnessRedirect $redirect) {
parse_str((string) parse_url($redirect->getMessage(), PHP_URL_QUERY), $query);
$review_error_redirect = $query;
}
$review_error_call = $GLOBALS['review_calls'][0] ?? null;
$GLOBALS['caps'] = ['manage_options' => false, 'aksrt_review_identity' => false];
$permission_denied = false;
try {
$probe->handle_review_verify();
} catch (HarnessWpDie $error) {
$permission_denied = $error->getMessage() === '权限不足';
}
$GLOBALS['caps'] = ['manage_options' => true, 'aksrt_review_identity' => true];
$_POST = [
'application_uuid' => 'review-uuid-401',
'_wpnonce' => wp_create_nonce('aksrt_users_review_verify-wrong-uuid'),
];
$bad_nonce_rejected = false;
try {
$probe->handle_review_verify();
} catch (HarnessWpDie $error) {
$bad_nonce_rejected = $error->getMessage() === 'nonce verification failed';
}
$delete_application = [
'id' => 501,
'application_uuid' => 'delete-uuid-501',
'user_id' => 41,
'type' => 'general',
'channel' => 'manual',
'status' => 'verified',
'public_name' => 'Existing identity',
'provider_result_summary' => 'Existing description',
'manual_reason' => 'Manual reason',
'review_note' => 'Approved',
'reviewer_id' => 9,
'evidence_ids' => '[1001,1002]',
'created_at' => '2026-10-29 02:10:00',
'completed_at' => '2026-10-30 04:45:00',
'updated_at' => '2026-10-30 04:45:00',
];
$wpdb->applications = [$delete_application];
$wpdb->prepared = [];
$wpdb->executed = [];
$wpdb->delete_calls = [];
$wpdb->application_delete_result = 1;
$GLOBALS['deleted_attachments'] = [];
$GLOBALS['user_meta'][41] = ['auth' => 1, 'auth_info' => ['name' => 'Already verified', 'desc' => 'kept']];
$GLOBALS['delete_user_meta_calls'] = [];
$_POST = [
'application_uuid' => 'delete-uuid-501',
'filter_status' => 'rejected',
'keyword' => 'delete search',
'paged' => '6',
'_wpnonce' => wp_create_nonce('aksrt_users_delete_verify_delete-uuid-501'),
];
try {
$probe->handle_delete_verify();
} catch (HarnessRedirect $redirect) {
parse_str((string) parse_url($redirect->getMessage(), PHP_URL_QUERY), $query);
$delete_success_redirect = $query;
}
$delete_success_calls = $wpdb->delete_calls;
$delete_success_attachments = $GLOBALS['deleted_attachments'];
$delete_auth_projection = $GLOBALS['user_meta'][41];
$wpdb->delete_calls = [];
$wpdb->event_delete_result = false;
try {
$probe->handle_delete_verify();
} catch (HarnessRedirect $redirect) {
parse_str((string) parse_url($redirect->getMessage(), PHP_URL_QUERY), $query);
$delete_event_failure_redirect = $query;
}
$delete_event_failure_calls = $wpdb->delete_calls;
$wpdb->event_delete_result = 1;
$wpdb->prepared = [];
$wpdb->executed = [];
$wpdb->delete_calls = [];
$wpdb->application_delete_result = 0;
$GLOBALS['deleted_attachments'] = [];
$_POST = [
'application_uuid' => 'delete-uuid-501',
'filter_status' => 'pending',
'keyword' => 'delete failure',
'paged' => '1',
'_wpnonce' => wp_create_nonce('aksrt_users_delete_verify_delete-uuid-501'),
];
try {
$probe->handle_delete_verify();
} catch (HarnessRedirect $redirect) {
parse_str((string) parse_url($redirect->getMessage(), PHP_URL_QUERY), $query);
$delete_failure_redirect = $query;
}
echo json_encode([
'html' => $html,
'pending_empty' => $pending_empty,
'search_empty' => $search_empty,
'page_prepared' => $page_prepared,
'page_executed' => $page_executed,
'cache_calls' => $page_cache_calls,
'meta_cache_calls' => $page_meta_cache_calls,
'wrong_hook_assets' => $wrong_hook_assets,
'auth_page_assets' => $auth_page_assets,
'evidence_permissions' => $evidence_permissions,
'delete_event_failure_redirect' => $delete_event_failure_redirect ?? [],
'delete_event_failure_calls' => $delete_event_failure_calls,
'review_call' => $review_call,
'review_redirect' => $review_redirect ?? [],
'review_nonce_calls' => $review_nonce_calls,
'review_error_redirect' => $review_error_redirect ?? [],
'review_error_call' => $review_error_call,
'permission_denied' => $permission_denied,
'bad_nonce_rejected' => $bad_nonce_rejected,
'delete_success_redirect' => $delete_success_redirect ?? [],
'delete_success_calls' => $delete_success_calls,
'delete_success_attachments' => $delete_success_attachments,
'delete_auth_projection' => $delete_auth_projection,
'delete_user_meta_calls' => $GLOBALS['delete_user_meta_calls'],
'delete_failure_redirect' => $delete_failure_redirect ?? [],
'delete_failure_calls' => $wpdb->delete_calls,
'delete_failure_attachments' => $GLOBALS['deleted_attachments'],
], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
}