| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114 |
- 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)
- }
- })
|