test-wechat-binding-flow.mjs 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. import assert from 'node:assert/strict'
  2. import { readFileSync } from 'node:fs'
  3. import test from 'node:test'
  4. const deferred = () => { let resolve; const promise = new Promise(r => { resolve = r }); return { promise, resolve } }
  5. function setup(overrides = {}) {
  6. const source = readFileSync(new URL('../utils/wechatBindingFlow.js', import.meta.url), 'utf8').replace(/^export /gm, '')
  7. const { createWechatBindingFlow, initialBindingState } = new Function(`${source}; return { createWechatBindingFlow, initialBindingState }`)()
  8. let token = 'user-a', active = true
  9. const state = initialBindingState(), calls = []
  10. const api = {
  11. status: async () => ({ enabled: true, bound: false, subscribed: false }),
  12. start: async () => ({ sessionId: 'session-12345678901234567890', authorizeUrl: 'https://open.weixin.qq.com/connect/oauth2/authorize?state=s' }),
  13. session: async () => ({ status: 'scanned', subscribed: true, verificationCode: '123456' }),
  14. confirm: async () => { calls.push('confirm'); return { enabled: true, bound: true, subscribed: true } },
  15. ...overrides
  16. }
  17. const flow = createWechatBindingFlow({ state, api, getSessionKey: () => token, isActive: () => active,
  18. openAuthorization: async (url) => calls.push(url), askConfirmation: async () => { calls.push('ask'); return true } })
  19. return { flow, state, calls, setToken: v => { token = v }, hide: () => { active = false; flow.cancel() }, show: () => { active = true } }
  20. }
  21. test('explicit mobile start automatically confirms after authorization returns', async () => {
  22. const p = setup()
  23. await p.flow.refresh()
  24. assert.deepEqual(p.calls, [])
  25. await p.flow.start()
  26. assert.equal(p.state.bound, false)
  27. assert.equal(p.state.sessionStatus, 'waiting')
  28. p.hide(); p.show()
  29. await p.flow.refresh()
  30. assert.equal(p.state.bound, true)
  31. assert.equal(p.state.sessionId, '')
  32. assert.equal(p.calls.includes('ask'), false)
  33. assert.equal(p.calls.filter(v => v === 'confirm').length, 1)
  34. })
  35. test('unsubscribed or waiting sessions cannot be confirmed', async () => {
  36. for (const result of [{ status: 'waiting', subscribed: false }, { status: 'scanned', subscribed: false, verificationCode: '123456' }]) {
  37. const p = setup({ session: async () => result })
  38. await p.flow.refresh(); await p.flow.start(); await p.flow.refresh()
  39. assert.ok(!p.calls.includes('confirm'))
  40. assert.equal(p.state.bound, false)
  41. }
  42. })
  43. test('failed confirmation does not claim bound and can retry', async () => {
  44. let fail = true
  45. const p = setup({ confirm: async () => { if (fail) throw new Error('该微信已绑定其他账号'); return { enabled: true, bound: true, subscribed: true } } })
  46. await p.flow.refresh(); await p.flow.start(); await p.flow.refresh()
  47. assert.equal(p.state.bound, false)
  48. assert.match(p.state.error, /其他账号/)
  49. fail = false
  50. await p.flow.refresh()
  51. assert.equal(p.state.bound, true)
  52. })
  53. test('duplicate start clicks create only one binding session', async () => {
  54. const pending = deferred(); let count = 0
  55. const p = setup({ start: () => { count++; return pending.promise } })
  56. await p.flow.refresh()
  57. const first = p.flow.start(); await p.flow.start()
  58. assert.equal(count, 1)
  59. pending.resolve({ sessionId: 'session-12345678901234567890', authorizeUrl: 'url' }); await first
  60. })
  61. test('late authorization start after account switch or hide cannot open webview', async () => {
  62. for (const action of ['account', 'hide']) {
  63. const pending = deferred(), p = setup({ start: () => pending.promise })
  64. await p.flow.refresh(); const start = p.flow.start()
  65. if (action === 'account') p.setToken('user-b'); else p.hide()
  66. pending.resolve({ sessionId: 'session-12345678901234567890', authorizeUrl: 'url' }); await start
  67. assert.deepEqual(p.calls, [])
  68. }
  69. })
  70. test('account switch clears old authorization and late confirmation cannot apply', async () => {
  71. const pending = deferred(), p = setup({ confirm: () => pending.promise })
  72. await p.flow.refresh(); await p.flow.start()
  73. const confirmation = p.flow.refresh(); await new Promise(resolve => setImmediate(resolve))
  74. p.setToken('user-b'); await p.flow.refresh()
  75. pending.resolve({ enabled: true, bound: true, subscribed: true }); await confirmation
  76. assert.equal(p.state.bound, false)
  77. assert.equal(p.state.sessionId, '')
  78. })
  79. test('expired sessions allow restart and disabled feature creates no session', async () => {
  80. const p = setup({ session: async () => ({ status: 'expired', subscribed: false }) })
  81. await p.flow.refresh(); await p.flow.start(); await p.flow.refresh()
  82. assert.equal(p.state.sessionId, '')
  83. assert.match(p.state.error, /过期/)
  84. const disabled = setup({ status: async () => ({ enabled: false, bound: false, subscribed: false }) })
  85. await disabled.flow.refresh(); await disabled.flow.start()
  86. assert.deepEqual(disabled.calls, [])
  87. })
  88. test('ordinary page entry never starts or confirms a binding', async () => {
  89. const p = setup()
  90. await p.flow.refresh(); await p.flow.refresh()
  91. assert.deepEqual(p.calls, [])
  92. })
  93. test('switching account or hiding while reading candidate prevents auto confirmation', async () => {
  94. for (const action of ['account', 'hide']) {
  95. const pending = deferred(), p = setup({ session: () => pending.promise })
  96. await p.flow.refresh(); await p.flow.start()
  97. const reading = p.flow.refresh(); await new Promise(resolve => setImmediate(resolve))
  98. if (action === 'account') p.setToken('user-b'); else p.hide()
  99. pending.resolve({ status: 'scanned', subscribed: true, verificationCode: '123456' })
  100. await reading
  101. assert.equal(p.calls.includes('confirm'), false)
  102. }
  103. })