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/router/component-loaders/extensions.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 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(avatar, /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, / __\('待审核'/); 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'\]/); });