// Provider contract tests use a fake widget and fake proof only; no real captcha or network request is performed. import { test } from 'node:test'; import assert from 'node:assert/strict'; import { setImmediate } from 'node:timers/promises'; import { solveCaptcha } from '../../public/js/skland/captcha.js'; function fakeProvider(proof) { const events = {}; const calls = { shown: 0, destroyed: 0, options: null }; const widget = { onReady(fn) { events.ready = fn; }, onSuccess(fn) { events.success = fn; }, onClose(fn) { events.close = fn; }, onError(fn) { events.error = fn; }, getValidate() { return proof; }, showCaptcha() { calls.shown++; }, verify() { calls.shown++; }, destroy() { calls.destroyed++; events.close?.(); }, }; const init = (options, ready) => { calls.options = options; ready(widget); }; return { events, calls, scope: { initGeetest4: init, initGeetest: init }, load: async () => {} }; } test('captcha v4 opens the provider widget and preserves its proof plus the upstream challenge', async () => { const proof = { captcha_id: 'TEST', lot_number: 'TEST-LOT', captcha_output: 'TEST-PROOF', pass_token: 'TEST-PASS', gen_time: '1' }; const provider = fakeProvider(proof); const pending = solveCaptcha({ geetestId: 'TEST', challenge: 'TEST-CHALLENGE', riskType: 'slide' }, provider); await setImmediate(); assert.equal(provider.calls.shown, 0, 'initialization does not submit a proof'); provider.events.ready(); assert.equal(provider.calls.shown, 1); assert.deepEqual(provider.calls.options, { captchaId: 'TEST', riskType: 'slide', product: 'bind', language: 'zho' }); provider.events.success(); assert.deepEqual(await pending, { ...proof, challenge: 'TEST-CHALLENGE' }); assert.equal(provider.calls.destroyed, 1); }); test('captcha v3 preserves the three provider fields for the SMS retry', async () => { const proof = { geetest_challenge: 'TEST-CH', geetest_validate: 'TEST-VALIDATE', geetest_seccode: 'TEST-SEC' }; const provider = fakeProvider(proof); const pending = solveCaptcha({ gt: 'TEST', challenge: 'TEST-CH', new_captcha: true }, provider); await setImmediate(); provider.events.ready(); provider.events.success(); assert.deepEqual(await pending, proof); assert.equal(provider.calls.options.https, true); }); test('captcha cancellation never yields a proof, including after a provider retry event', async () => { const provider = fakeProvider({ captcha_output: 'UNUSED-TEST-PROOF' }); const pending = solveCaptcha({ geetestId: 'TEST' }, provider); await setImmediate(); provider.events.error({ error_code: 'error_01' }); provider.events.close(); provider.events.success(); assert.equal(await pending, null); assert.equal(provider.calls.destroyed, 1); }); test('unsupported or failed captchas are rejected so the UI can offer retry', async () => { await assert.rejects(() => solveCaptcha({}), /captcha_type/); await assert.rejects(() => solveCaptcha({ geetestId: 'TEST' }, { load: async () => { throw new Error('captcha_load'); } }), /captcha_load/); const provider = fakeProvider(null); const pending = solveCaptcha({ geetestId: 'TEST' }, provider); const rejected = assert.rejects(pending, /captcha_validate/); await setImmediate(); provider.events.success(); await rejected; });