'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'); 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); });