'use strict'; const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const test = require('node:test'); const { execFileSync } = require('node:child_process'); const root = path.resolve(__dirname, '..', '..'); const read = (file) => fs.readFileSync(path.join(root, file), 'utf8'); test('captcha PHP security harness passes', () => { const output = execFileSync('php', [path.join(root, 'tests/widget-system/captcha-security-harness.php')], { encoding: 'utf8' }); assert.match(output, /Captcha security harness passed/); }); test('captcha exchange binds provider tokens to scene browser and one-time business proofs', () => { const module = read('inc/modules/captcha.php'); const front = read('assets/js/captcha-front.js'); assert.match(module, /\/captcha\/exchange/); assert.match(module, /aksrt_captcha_issue_challenge/); assert.match(module, /aksrt_captcha_validate_challenge/); assert.match(module, /aksrt_captcha_issue_proof/); assert.match(module, /aksrt_captcha_consume_proof/); assert.match(module, /browser_hash/); assert.match(front, /exchange_url/); assert.match(front, /aksrt_captcha_proof/); assert.match(front, /runLocked/); }); test('captcha defaults are opt-in and provider metadata is centralized', () => { const module = read('inc/modules/captcha.php'); const phpSchema = read('src/Admin/Schema/SchemaPermissions.php'); const uiSchema = read('assets/admin/src/schema/sections/permissions/index.js'); assert.match(module, /function aksrt_captcha_provider_metadata/); assert.match(module, /'enabled_scenarios'\s*=>\s*\[\]/); assert.match(phpSchema, /'default'\s*=>\s*\[\]/); assert.match(uiSchema, /enabled_scenarios:\s*\[\]/); assert.match(phpSchema, /aksrt_captcha_provider_metadata/); }); test('captcha verification is fail-closed with a true-or-WP_Error contract', () => { const module = read('inc/modules/captcha.php'); assert.match(module, /return aksrt_captcha_error\('aksrt_captcha_config'/); assert.match(module, /@return true\|\\WP_Error/); assert.doesNotMatch(module, /is_wp_error\(\$result\) \? \$result->get_error_message\(\) : true/); }); test('cloudflare siteverify performs one request and strictly binds action, cdata, and hostname', () => { const module = read('inc/modules/captcha.php'); const start = module.indexOf('function aksrt_verify_cloudflare'); const end = module.indexOf('function aksrt_verify_geetest', start); const cloudflare = module.slice(start, end); assert.equal((cloudflare.match(/wp_remote_post\(/g) || []).length, 1); assert.match(cloudflare, /array_key_exists\('action'/); assert.match(cloudflare, /array_key_exists\('cdata'/); assert.match(cloudflare, /hostname mismatch/); assert.match(cloudflare, /hash_equals/); }); test('TC3 signing derives tc3_request from the service key', () => { const module = read('inc/modules/captcha.php'); assert.match(module, /hash_hmac\('sha256', 'tc3_request', \$secret_service, true\)/); assert.doesNotMatch(module, /hash_hmac\('sha256', 'tc3_request', \$secret_signing, true\)/); }); test('nested captcha secrets are masked, preserved, explicitly clearable, and excluded from standalone exports', () => { const provider = read('src/Admin/SchemaProvider.php'); const settings = read('src/Settings/Settings.php'); const page = read('assets/admin/src/pages/SchemaPage.vue'); assert.match(provider, /mask_captcha_provider_secrets/); assert.match(provider, /merge_captcha_provider_config/); assert.match(provider, /\$value === '' \|\| \$value === '\*\*\*'/); assert.match(settings, /SCHEMA_MANAGED_STANDALONE_OPTION_KEYS/); assert.match(settings, /'aksrt_captcha_config'/); assert.match(page, /captchaSecretsToClear/); assert.match(page, /clear-secret/); }); test('admin captcha test obtains a frontend token then calls the protected server verifier', () => { const controller = read('src/Admin/ImportExportController.php'); const api = read('assets/admin/src/api/modules/schema.js'); const page = read('assets/admin/src/pages/SchemaPage.vue'); assert.match(controller, /'\/captcha\/test'/); assert.match(controller, /aksrt_verify_captcha_provider/); assert.match(api, /testCaptcha\(provider, token\)/); assert.match(page, /submitCaptchaTest/); assert.match(page, /action:\s*'aksrt-admin-test'/); assert.match(page, /cdata:\s*'admin-captcha-test'/); assert.doesNotMatch(page, /配置正确/); }); test('Cloudflare custom domain setting is removed', () => { const component = read('assets/admin/src/components/captcha/CloudflareConfig.vue'); assert.doesNotMatch(component, /自定义域名|model\.domain/); }); test('cloud captcha server verifies over the WP HTTP API without disabling TLS', () => { const module = read('inc/modules/captcha.php'); assert.doesNotMatch(module, /curl_init|curl_setopt|CURLOPT_SSL_VERIFYPEER/); const posts = (module.match(/wp_remote_post\(/g) || []).length; assert.ok(posts >= 4, `expected all four providers on wp_remote_post, found ${posts}`); assert.match(module, /wp_generate_uuid4\(\)/, 'aliyun SignatureNonce must not be time-derived'); assert.doesNotMatch(module, /uniqid\(/); }); test('cloud captcha verify requests use a 5s timeout and log raw failures', () => { const module = read('inc/modules/captcha.php'); const timeouts = [...module.matchAll(/'timeout'\s*=>\s*(\d+)/g)].map((m) => Number(m[1])); assert.ok(timeouts.length >= 4, 'every provider request must declare a timeout'); for (const timeout of timeouts) { assert.strictEqual(timeout, 5); } assert.match(module, /error_log\('\[AKSRT Captcha\]/); }); test('business requests consume one-time proofs without a shared download bypass', () => { const module = read('inc/modules/captcha.php'); assert.match(module, /aksrt_captcha_consume_proof/); assert.doesNotMatch(module, /aksrt_captcha_pass_ttl|aksrt_captcha_pass_key|aksrt_captcha_pass_/); }); test('cloudflare siteverify stays bound to the aksrt action and scene', () => { const module = read('inc/modules/captcha.php'); assert.match(module, /aksrt_verify_cloudflare\(array \$pcfg, array \$post_data, string \$client_ip, string \$scene = ''(?:, string \$action = 'aksrt-verify')?\)/); assert.match(module, /action mismatch/); assert.match(module, /cdata mismatch/); }); test('captcha render is a data-driven mount without inline scripts or stylesheets', () => { const module = read('inc/modules/captcha.php'); assert.match(module, /data-aksrt-captcha-config/); assert.doesNotMatch(module, / { const module = read('inc/modules/captcha.php'); assert.doesNotMatch(module, /machine-verification|imagecaptcha|slidercaptcha|canvas_yz/); }); test('captcha front runtime ships inside the public bundle', () => { const build = read('tools/build-public-assets.mjs'); assert.match(build, /'assets\/js\/captcha-front\.js'/); assert.match(build, /'assets\/css\/shared\/captcha\.css'/); const front = read('assets/js/captcha-front.js'); assert.match(front, /AKSRTCaptcha/); assert.match(front, /data-aksrt-captcha-config/); assert.match(front, /cdata: C\.scene/, 'turnstile widget must bind cdata to the scene'); }); test('captcha public runtime exposes only the shared API and resolves scoped uid instances', () => { const front = read('assets/js/captcha-front.js'); assert.match(front, /window\.AKSRTCaptcha\s*=\s*Object\.freeze\(\{[\s\S]*verify:\s*verify,[\s\S]*collect:\s*collect,[\s\S]*reset:\s*reset,[\s\S]*run:\s*run,[\s\S]*TOKEN_FIELDS:\s*TOKEN_FIELDS/); assert.match(front, /instances\[instance\.uid\]\s*=\s*instance/); assert.match(front, /function resolve\(scene, scope\)/); assert.doesNotMatch(front, /AKSRTCaptchaState/); assert.doesNotMatch(front, /tencent_sid/); }); test('captcha consumers use the shared API and reset every token-bearing request', () => { const consumers = [ 'assets/js/auth.js', 'assets/js/core/comments.js', 'assets/js/routes/user-center.js', 'assets/js/cms-download.js', 'assets/js/signin.js', 'assets/js/front-submit.js', ].map(read).join('\n'); assert.match(consumers, /AKSRTCaptcha\.run|captcha\.run/); assert.match(consumers, /AKSRTCaptcha\.reset|captcha\.reset|resetCaptcha/); assert.doesNotMatch(consumers, /AKSRTCaptchaState/); assert.doesNotMatch(consumers, /tencent_sid/); assert.doesNotMatch(read('assets/js/core/comments.js'), /wp-comments-post|performLegacySubmit/); }); test('sign-in waits for main and fails closed when its enabled captcha mount is unavailable', () => { const signin = read('assets/js/signin.js'); const scripts = read('src/Core/ScriptsPart1c.php'); assert.match(signin, /tbquire\(\['main'\], boot\)/); assert.match(signin, /var captchaRequired = !!window\.aksrtData\.captchaSignin/); assert.match(signin, /captchaRequired && \(!captcha \|\| !mount\)[\s\S]*?return;/); assert.doesNotMatch(signin, /captcha \? captcha\.run\('signin', mount\) : Promise\.resolve\(\{\}\)/); assert.match(scripts, /'captchaSignin'\s*=>\s*function_exists\('aksrt_is_captcha_enabled_for'\)\s*&&\s*aksrt_is_captcha_enabled_for\('signin'\)/); }); test('auth and account security flows consume fresh captcha tickets', () => { const auth = read('assets/js/auth.js'); assert.match(auth, /request\('code',[\s\S]*\.finally\(function \(\) \{[\s\S]*resetCaptcha/); assert.match(auth, /function submit\(form\)[\s\S]*captcha\.run\(scene,[\s\S]*request\(action,[\s\S]*resetCaptcha/); assert.match(auth, /qrPayload\.bind_pass = root\.dataset\.bindPass/); assert.match(auth, /data\.bind_pass = root\.dataset\.bindPass/); 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, /\/oauth\/bind-pass/); assert.match(center, /bind_verification_required:\s*'请先完成人机验证后再绑定'/); }); test('captcha admin schema drops the dead single-option trigger mode', () => { const schema = read('src/Admin/Schema/SchemaPermissions.php'); assert.doesNotMatch(schema, /'mode'/); const ui = read('assets/admin/src/schema/sections/permissions/index.js'); assert.doesNotMatch(ui, /\bmode\s*:/); assert.doesNotMatch(ui, /'mode'/); const provider = read('src/Admin/SchemaProvider.php'); assert.doesNotMatch(provider, /'mode'/); });