115 lines
5.5 KiB
JavaScript
115 lines
5.5 KiB
JavaScript
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)
|
|
}
|
|
})
|