Files
wordpress-theme-aksrt/tests/widget-system/user-features-contract.test.cjs
T
kate522andClaude Fable 5 f9b7997ec2 fix(user): match profile validation hook argument type
Accept the stdClass user data passed by WordPress and derive the user ID only from the hook object. Cover callback registration, nonce and permission guards, and mismatched posted IDs; refresh the identity review dialog contract.

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

231 lines
11 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');
const exists = (file) => fs.existsSync(path.join(root, file));
const expectedFields = [
'avatar_upload_s',
'avatar_external',
'avatar_default_img',
'user_cover_upload_s',
'user_cover_img',
'user_desc_std',
// 打赏组(参考主题 8977 行:post_rewards_* 迁移至用户功能页)。
'post_rewards_s',
'post_rewards_text',
'post_rewards_amounts',
'post_rewards_custom',
'post_rewards_min',
'post_rewards_max',
];
const externalSources = ['', 'gravatar', 'weavatar', 'cravatar', 'geekzu', 'loli', 'sep', 'v2ex'];
test('user features schemas expose the canonical fields plus the migrated rewards group', () => {
const js = read('assets/admin/src/schema/sections/users/features.js');
const phpAll = read('src/Admin/Schema/SchemaUsers.php');
const php = phpAll.slice(phpAll.indexOf("'features' => ["), phpAll.indexOf("'verify' => ["));
const jsFields = [...js.matchAll(/\{\s*key:\s*'([^']+)',\s*label:/g)].map((match) => match[1]);
const phpFields = [...php.matchAll(/\['key'\s*=>\s*'([^']+)',\s*'label'/g)].map((match) => match[1]);
assert.deepEqual(jsFields, expectedFields);
assert.deepEqual(phpFields, expectedFields);
assert.match(js, /avatar_upload_s:\s*true/);
assert.match(js, /avatar_external:\s*''/);
assert.match(js, /assets\/img\/avatar-default\.png/);
assert.match(js, /user_cover_upload_s:\s*true/);
assert.match(js, /assets\/img\/user_t\.jpg/);
assert.match(js, /user_desc_std:\s*'这家伙很懒,什么都没有写\.\.\.'/);
assert.match(php, /avatar_default_img[^\n]+get_theme_file_uri\('assets\/img\/avatar-default\.png'\)/);
assert.match(php, /user_cover_img[^\n]+get_theme_file_uri\('assets\/img\/user_t\.jpg'\)/);
// 打赏组:开关默认开(对齐参考主题),依赖收敛于 post_rewards_s。
assert.match(js, /post_rewards_s:\s*true/);
assert.match(js, /post_rewards_text:\s*'赞赏'/);
assert.match(php, /\['key' => 'rewards', 'title' => '打赏', 'fields' => \[/);
assert.match(php, /'post_rewards_s', 'label' => '用户打赏功能', 'type' => 'switcher', 'default' => true/);
const jsSources = [...js.matchAll(/\{ label: '[^']+', value: '([^']*)' \}/g)].map((match) => match[1]);
const phpSources = [...php.matchAll(/\['label' => '[^']+', 'value' => '([^']*)'\]/g)].map((match) => match[1]);
assert.deepEqual(jsSources, externalSources);
assert.deepEqual(phpSources, externalSources);
});
test('avatar, cover, signature, and upload runtime use the canonical settings', () => {
const helpers = read('inc/helpers/aksrt-avatar.php');
const runtime = read('src/Settings/UserRuntime.php');
const upload = read('src/Admin/UserControllerProfileMediaOperations.php');
const center = read('inc/helpers/aksrt-user-center.php');
const customAt = helpers.indexOf("$custom = aksrt_get_custom_avatar_url");
const externalAt = helpers.indexOf("$external = aksrt_get_external_avatar_url", customAt);
const defaultAt = helpers.indexOf('return aksrt_get_default_avatar_url();', externalAt);
assert.ok(customAt >= 0 && customAt < externalAt && externalAt < defaultAt);
const hosts = {
gravatar: 'https://www.gravatar.com/avatar/',
weavatar: 'https://weavatar.com/avatar/',
cravatar: 'https://cn.cravatar.com/avatar/',
geekzu: 'https://sdn.geekzu.org/avatar/',
loli: 'https://gravatar.loli.net/avatar/',
sep: 'https://cdn.sep.cc/avatar/',
v2ex: 'https://cdn.v2ex.com/gravatar/',
};
for (const [source, host] of Object.entries(hosts)) {
assert.match(helpers, new RegExp(`'${source}'\\s*=>\\s*'${host.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}'`));
}
assert.match(helpers, /hash\('sha256', strtolower\(trim\(\$email\)\)\)/);
assert.match(helpers, /'s'\s*=>\s*\$size/);
assert.match(helpers, /'d'\s*=>\s*aksrt_get_default_avatar_url\(\)/);
assert.match(helpers, /get_user_meta\(\$user_id, 'aksrt_background_image', true\)/);
assert.match(helpers, /return \(string\) \$settings\['user_cover_img'\]/);
assert.match(helpers, /get_user_meta\(\$user_id, 'description', true\)/);
assert.match(helpers, /return \(string\) \$settings\['user_desc_std'\]/);
assert.match(helpers, /!empty\(\$settings\['avatar_upload_s'\]\)/);
assert.match(helpers, /!empty\(\$settings\['user_cover_upload_s'\]\)/);
assert.match(runtime, /Settings::read_domain\('users', 'features'\)/);
assert.doesNotMatch(runtime, /user_center_tabs/);
assert.match(runtime, /'tabs'\s*=>\s*\['profile', 'level', 'comments', 'credits', 'revenue', 'favorites', 'auth'\]/);
assert.match(upload, /aksrt_is_avatar_upload_enabled\(\)/);
assert.match(upload, /aksrt_is_user_cover_upload_enabled\(\)/);
assert.match(center, /'show_avatar_edit'\s*=>\s*\$allow_avatar_upload/);
assert.match(center, /'allow_bg_upload'\s*=>\s*\$allow_bg_upload/);
});
test('public user surfaces consume the canonical cover and signature helpers', () => {
for (const file of [
'templates/parts/user/author-hero.php',
'src/WidgetSystem/Renderers/UserRenderer.php',
'inc/helpers/aksrt-user-dashboard.php',
]) {
assert.match(read(file), /aksrt_get_user_cover_url/);
}
for (const file of [
'templates/parts/user/author-hero.php',
'src/WidgetSystem/Renderers/UserRenderer.php',
'inc/helpers/aksrt-graphic-card.php',
'inc/helpers/aksrt-single-render.php',
'templates/parts/single-author-bar.php',
'templates/shortcodes/profile.php',
'inc/helpers/header/aksrt-header-render.php',
]) {
assert.match(read(file), /aksrt_get_user_description/);
}
});
test('retired admin pages, schemas, APIs, and metadata chains are absent', () => {
const deletedFiles = [
'assets/admin/src/pages/users/UsersFeatures.vue',
'assets/admin/src/pages/users/UsersSignin.vue',
'assets/admin/src/schema/sections/users/signin.js',
'assets/admin/src/pages/tools/ToolsEmailRollback.vue',
'assets/admin/src/schema/sections/tools/email-rollback.js',
'assets/admin/src/schema/sections/tools/frontend-modal-rollout.js',
'src/Admin/Schema/SchemaToolsEmailRollback.php',
'src/Admin/Schema/SchemaToolsFrontendModalRollout.php',
'src/Auth/EmailRollback.php',
'src/Settings/FrontendModalRuntime.php',
'assets/admin/src/pages/extensions/ExtensionTools.vue',
];
for (const file of deletedFiles) {
assert.equal(exists(file), false, `${file} should be deleted`);
}
const sources = [
'assets/admin/src/constants/sections/users.js',
'assets/admin/src/constants/sections/tools.js',
'assets/admin/src/constants/sections/extensions.js',
'assets/admin/src/router/component-loaders/users.js',
'assets/admin/src/router/component-loaders/tools.js',
'assets/admin/src/api/modules/tools.js',
'assets/admin/src/schema/sections/tools/index.js',
'src/Admin/Schema/SchemaTools.php',
'src/Admin/ToolsController.php',
'src/Settings/Settings.php',
'src/Settings/Capabilities.php',
'src/Settings/DomainGroups.php',
'src/Core/ScriptsPart1c.php',
'functions.php',
].map(read).join('\n');
for (const retired of [
'users/signin',
'email-rollback',
'EmailRollback',
'aksrt_email_previous',
'aksrt_email_changed_at',
'frontend_modal_rollout',
'FrontendModalRuntime',
'isRolloutEnabled',
'extensions/tools',
'/tools/maintenance',
'get_system_tools_settings',
'upload_avatar',
'upload_profile_background',
]) {
assert.equal(sources.includes(retired), false, `${retired} should be absent`);
}
assert.match(read('assets/admin/src/pages/tools/ToolsUpdate.vue'), /#\/users\/credits/);
assert.doesNotMatch(read('assets/admin/src/pages/tools/ToolsUpdate.vue'), /#\/users\/signin/);
assert.match(read('assets/admin/src/api/modules/tools.js'), /\/settings\/health/);
assert.match(read('assets/admin/src/constants/sections/tools.js'), /route: '\/tools\/health'/);
});
test('native user profile omits ledger tables and renders canonical images eagerly', () => {
const credits = read('src/User/CreditsAdmin.php');
const balance = read('src/User/BalanceAdmin.php');
const avatar = read('inc/helpers/aksrt-avatar.php');
const profileAdmin = read('src/User/ProfileAdmin.php');
const level = read('src/User/LevelAdmin.php');
assert.doesNotMatch(credits, /LedgerQuery::get_instance\(\)->history/);
assert.doesNotMatch(credits, /近期%s流水|近期积分流水/);
assert.doesNotMatch(balance, /LedgerQuery::get_instance\(\)->history/);
assert.doesNotMatch(balance, /近期流水/);
assert.match(profileAdmin, /get_avatar\(\$user_id, 96, '', \$user->display_name, \['loading' => 'eager'\]\)/);
assert.doesNotMatch(avatar, /aksrt_get_avatar_html\(\$user->ID, 96/);
assert.match(level, /Level::get_level_icon_url\(\$level\)/);
assert.match(level, /<img class="aksrt-user-level-icon aksrt-user-level-icon--admin" src="/);
assert.doesNotMatch(level, /Level::get_badge_html/);
});
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 verifyDialogs = read('src/Admin/Native/UsersMenusVerifyDialogs.php');
const transitions = read('src/Identity/TransitionService.php');
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_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, /use UsersMenusVerifyDialogs;/);
assert.match(verifyDialogs, /\$pending = \$row\['status'\] === 'manual_pending'/);
assert.match(verifyDialogs, /if \(\$pending\) \{\s*echo '<form[^\n]*data-aksrt-verify-review>/);
assert.match(verifyDialogs, /'verified' => __\('通过申请'/);
assert.match(verifyDialogs, /'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 => ''/);
});