'use strict'; const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const test = require('node:test'); const vm = require('node:vm'); const root = path.resolve(__dirname, '..', '..'); const runtime = fs.readFileSync(path.join(root, 'assets/js/captcha-front.js'), 'utf8'); const signinSource = fs.readFileSync(path.join(root, 'assets/js/signin.js'), 'utf8'); function createInput(name, value = '') { return { name, value, remove() {}, nodeType: 1 }; } function createHarness() { const fields = new Map(); const challenge = createInput('aksrt_captcha_challenge', 'challenge-token'); const proof = createInput('aksrt_captcha_proof', ''); const verified = createInput('captcha_verified', '0'); fields.set(challenge.name, challenge); fields.set(proof.name, proof); fields.set(verified.name, verified); const container = { nodeType: 1, contains: () => true, querySelector(selector) { const match = selector.match(/input\[name="([^"]+)"\]/); return match ? fields.get(match[1]) || null : null; }, querySelectorAll(selector) { const value = this.querySelector(selector); return value ? [value] : []; }, appendChild(field) { fields.set(field.name, field); }, }; const box = { nodeType: 1, parentElement: container, closest: () => container, contains: () => false, querySelector: () => null, appendChild() {}, innerHTML: '', }; const mount = { nodeType: 1, dataset: {}, parentElement: container, contains: () => true, setAttribute() {}, }; const scripts = []; const document = { readyState: 'complete', documentElement: { nodeType: 1 }, querySelectorAll(selector) { return selector === '[data-aksrt-captcha-config]' ? [mount] : []; }, querySelector: () => null, getElementById(id) { return id === 'captcha-box' ? box : verified; }, createElement(tag) { if (tag === 'input') return createInput('', ''); return { setAttribute() {}, addEventListener() {}, appendChild() {}, style: {}, dataset: {} }; }, addEventListener() {}, head: { appendChild(script) { scripts.push(script); } }, body: { appendChild() {} }, }; mount.getAttribute = () => JSON.stringify({ provider: 'tencent', scene: 'login', uid: 'captcha-box', vid: 'captcha-verified', tc: { app_id: '1' }, challenge_url: '/challenge', exchange_url: '/exchange', }); let providerCallback; const window = { fetch(url) { if (url === '/challenge') return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, data: { challenge: 'challenge-token' } }) }); if (url === '/exchange') return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, data: { proof: 'proof-token' } }) }); throw new Error('unexpected URL ' + url); }, TencentCaptcha: class { constructor(_id, callback) { providerCallback = callback; } show() { providerCallback({ ret: 0, ticket: 'ticket', randstr: 'rand' }); } }, setTimeout, clearTimeout, Promise, }; vm.runInNewContext(runtime, { window, document, Promise, MutationObserver: undefined, setTimeout, clearTimeout, console }); return { api: window.AKSRTCaptcha, container }; } test('captcha run is single-flight until the owning business request resets', async () => { const { api, container } = createHarness(); await Promise.resolve(); await Promise.resolve(); const first = api.run('login', container); const second = api.run('login', container); const firstPayload = await first; assert.equal(firstPayload.aksrt_captcha_proof, 'proof-token'); let secondSettled = false; second.then(() => { secondSettled = true; }); await Promise.resolve(); assert.equal(secondSettled, false, 'concurrent caller settled before the owner reset'); api.reset('login', container); assert.equal(await second, null); }); test('sign-in waits for main before binding and invokes the signin captcha', async () => { const listeners = {}; const label = { textContent: '' }; const button = { dataset: {}, disabled: false, classList: { toggle() {} }, addEventListener(name, handler) { listeners[name] = handler; }, querySelector(selector) { return selector === '[data-signin-label]' ? label : null; }, getAttribute(name) { if (name === 'data-signin-idle-label') return '签到'; if (name === 'data-signin-done-label') return '已签到'; return ''; }, }; const mount = { nodeType: 1 }; const document = { querySelector(selector) { return selector === '[data-signin-captcha]' ? mount : null; }, querySelectorAll(selector) { return selector === '[data-signin-btn]' ? [button] : []; }, }; const jqueryApi = { off() { return this; }, on() { return this; } }; const jQuery = () => jqueryApi; const window = { aksrtData: { captchaSignin: true } }; let mainReady; let captchaCall = null; const tbquire = (deps, callback) => { assert.deepEqual(Array.from(deps), ['main']); mainReady = callback; }; vm.runInNewContext(signinSource, { window, document, jQuery, tbquire, notyf() {}, Promise, JSON, Number, }); assert.equal(listeners.click, undefined); assert.equal(typeof mainReady, 'function'); window.AksrtRequest = { request(endpoint) { if (endpoint === '/signin/today') return Promise.resolve({ ok: true, status: 200, json: { enabled: true, signed: false, streak: 0, reward_if_signin: 1 } }); if (endpoint === '/signin') return Promise.resolve({ ok: true, status: 200, json: { message: '签到成功', balance: 1 } }); throw new Error('unexpected endpoint ' + endpoint); }, }; window.AKSRTCaptcha = { run(scene, scope) { captchaCall = { scene, scope }; return Promise.resolve({ aksrt_captcha_proof: 'proof' }); }, reset() {}, }; mainReady(); await new Promise((resolve) => setImmediate(resolve)); assert.equal(typeof listeners.click, 'function'); listeners.click({ preventDefault() {} }); await new Promise((resolve) => setImmediate(resolve)); assert.deepEqual(captchaCall, { scene: 'signin', scope: mount }); });