import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import test from 'node:test' const deferred = () => { let resolve; const promise = new Promise(r => { resolve = r }); return { promise, resolve } } function setup(overrides = {}) { const source = readFileSync(new URL('../utils/wechatBindingFlow.js', import.meta.url), 'utf8').replace(/^export /gm, '') const { createWechatBindingFlow, initialBindingState } = new Function(`${source}; return { createWechatBindingFlow, initialBindingState }`)() let token = 'user-a', active = true const state = initialBindingState(), calls = [] const api = { status: async () => ({ enabled: true, bound: false, subscribed: false }), start: async () => ({ sessionId: 'session-12345678901234567890', authorizeUrl: 'https://open.weixin.qq.com/connect/oauth2/authorize?state=s' }), session: async () => ({ status: 'scanned', subscribed: true, verificationCode: '123456' }), confirm: async () => { calls.push('confirm'); return { enabled: true, bound: true, subscribed: true } }, ...overrides } const flow = createWechatBindingFlow({ state, api, getSessionKey: () => token, isActive: () => active, openAuthorization: async (url) => calls.push(url), askConfirmation: async () => { calls.push('ask'); return true } }) return { flow, state, calls, setToken: v => { token = v }, hide: () => { active = false; flow.cancel() }, show: () => { active = true } } } test('explicit mobile start automatically confirms after authorization returns', async () => { const p = setup() await p.flow.refresh() assert.deepEqual(p.calls, []) await p.flow.start() assert.equal(p.state.bound, false) assert.equal(p.state.sessionStatus, 'waiting') p.hide(); p.show() await p.flow.refresh() assert.equal(p.state.bound, true) assert.equal(p.state.sessionId, '') assert.equal(p.calls.includes('ask'), false) assert.equal(p.calls.filter(v => v === 'confirm').length, 1) }) test('unsubscribed or waiting sessions cannot be confirmed', async () => { for (const result of [{ status: 'waiting', subscribed: false }, { status: 'scanned', subscribed: false, verificationCode: '123456' }]) { const p = setup({ session: async () => result }) await p.flow.refresh(); await p.flow.start(); await p.flow.refresh() assert.ok(!p.calls.includes('confirm')) assert.equal(p.state.bound, false) } }) test('failed confirmation does not claim bound and can retry', async () => { let fail = true const p = setup({ confirm: async () => { if (fail) throw new Error('该微信已绑定其他账号'); return { enabled: true, bound: true, subscribed: true } } }) await p.flow.refresh(); await p.flow.start(); await p.flow.refresh() assert.equal(p.state.bound, false) assert.match(p.state.error, /其他账号/) fail = false await p.flow.refresh() assert.equal(p.state.bound, true) }) test('duplicate start clicks create only one binding session', async () => { const pending = deferred(); let count = 0 const p = setup({ start: () => { count++; return pending.promise } }) await p.flow.refresh() const first = p.flow.start(); await p.flow.start() assert.equal(count, 1) pending.resolve({ sessionId: 'session-12345678901234567890', authorizeUrl: 'url' }); await first }) test('late authorization start after account switch or hide cannot open webview', async () => { for (const action of ['account', 'hide']) { const pending = deferred(), p = setup({ start: () => pending.promise }) await p.flow.refresh(); const start = p.flow.start() if (action === 'account') p.setToken('user-b'); else p.hide() pending.resolve({ sessionId: 'session-12345678901234567890', authorizeUrl: 'url' }); await start assert.deepEqual(p.calls, []) } }) test('account switch clears old authorization and late confirmation cannot apply', async () => { const pending = deferred(), p = setup({ confirm: () => pending.promise }) await p.flow.refresh(); await p.flow.start() const confirmation = p.flow.refresh(); await new Promise(resolve => setImmediate(resolve)) p.setToken('user-b'); await p.flow.refresh() pending.resolve({ enabled: true, bound: true, subscribed: true }); await confirmation assert.equal(p.state.bound, false) assert.equal(p.state.sessionId, '') }) test('expired sessions allow restart and disabled feature creates no session', async () => { const p = setup({ session: async () => ({ status: 'expired', subscribed: false }) }) await p.flow.refresh(); await p.flow.start(); await p.flow.refresh() assert.equal(p.state.sessionId, '') assert.match(p.state.error, /过期/) const disabled = setup({ status: async () => ({ enabled: false, bound: false, subscribed: false }) }) await disabled.flow.refresh(); await disabled.flow.start() assert.deepEqual(disabled.calls, []) }) test('ordinary page entry never starts or confirms a binding', async () => { const p = setup() await p.flow.refresh(); await p.flow.refresh() assert.deepEqual(p.calls, []) }) test('switching account or hiding while reading candidate prevents auto confirmation', async () => { for (const action of ['account', 'hide']) { const pending = deferred(), p = setup({ session: () => pending.promise }) await p.flow.refresh(); await p.flow.start() const reading = p.flow.refresh(); await new Promise(resolve => setImmediate(resolve)) if (action === 'account') p.setToken('user-b'); else p.hide() pending.resolve({ status: 'scanned', subscribed: true, verificationCode: '123456' }) await reading assert.equal(p.calls.includes('confirm'), false) } })