test-wechat-binding-integration.mjs 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. import assert from 'node:assert/strict'
  2. import { readFileSync } from 'node:fs'
  3. import { createRequire } from 'node:module'
  4. import test from 'node:test'
  5. const require = createRequire(new URL('../../web-admin/package.json', import.meta.url))
  6. const { ref, reactive, computed } = require('vue')
  7. const { parse, compileTemplate } = require('@vue/compiler-sfc')
  8. const read = p => readFileSync(new URL(p, import.meta.url), 'utf8')
  9. const strip = s => s.replace(/^import[^\n]+\n/gm, '').replace(/^export /gm, '')
  10. const launchState = 'a'.repeat(32)
  11. const launchUrl = `https://w.shanxiachem.com/api/public/wechat-notices/oauth/launch?state=${launchState}`
  12. function api(data, statusCode = 200, code = 200) {
  13. const calls = []
  14. const source = strip(read('../api/wechat-notices.js'))
  15. const fns = new Function('request', 'parseResponseJson', 'resolveWebOrigin', 'resolveApiBase', `${source}; return { startWechatBinding, fetchWechatBindingSession, confirmWechatBinding, isWechatAuthorizeUrl }`)(
  16. async options => { calls.push(options); return { statusCode, data: { code, data, message: '请求失败' } } },
  17. response => response.data,
  18. () => 'https://w.shanxiachem.com',
  19. () => 'https://w.shanxiachem.com/api')
  20. return { ...fns, calls }
  21. }
  22. test('binding API uses owner-authenticated endpoints and only returns necessary fields', async () => {
  23. const start = api({ sessionId: 'a'.repeat(32), authorizeUrl: launchUrl, openid: 'private' })
  24. assert.equal((await start.startWechatBinding()).openid, undefined)
  25. assert.equal(start.calls[0].method, 'POST')
  26. assert.deepEqual(start.calls[0].data, { mode: 'mobile' })
  27. assert.equal(start.calls[0].url, '/web/wechat-notices/binding/start')
  28. const confirm = api({ enabled: true, bound: true, subscribed: true, openid: 'private' })
  29. assert.deepEqual(await confirm.confirmWechatBinding('session'), { enabled: true, bound: true, subscribed: true })
  30. assert.deepEqual(confirm.calls[0].data, { sessionId: 'session' })
  31. const session = api({ status: 'scanned', subscribed: true, verificationCode: '123456' })
  32. await session.fetchWechatBindingSession('a&b')
  33. assert.ok(session.calls[0].url.endsWith('sessionId=a%26b'))
  34. })
  35. test('binding API rejects foreign URLs, failed and malformed results', async () => {
  36. const fns = api({})
  37. for (const url of [
  38. 'javascript:alert(1)',
  39. 'http://open.weixin.qq.com/connect/oauth2/authorize?state=x',
  40. 'https://open.weixin.qq.com/connect/oauth2/authorize?state=' + launchState,
  41. 'https://evil.test/api/public/wechat-notices/oauth/launch?state=' + launchState,
  42. launchUrl + '#wechat_redirect'
  43. ]) {
  44. assert.equal(fns.isWechatAuthorizeUrl(url), false)
  45. await assert.rejects(api({ sessionId: 'x'.repeat(32), authorizeUrl: url }).startWechatBinding())
  46. }
  47. await assert.rejects(api({}, 403, 403).startWechatBinding())
  48. await assert.rejects(api({ status: 'scanned', subscribed: true, verificationCode: 'bad' }).fetchWechatBindingSession('session'))
  49. await assert.rejects(api({ enabled: true, bound: 'yes', subscribed: true }).confirmWechatBinding('session'))
  50. })
  51. test('authorization page mounts approved URL after ready and unregisters its channel', () => {
  52. const hooks = {}, handlers = {}, off = []
  53. const channel = { on: (name, fn) => { handlers[name] = fn }, off: (...args) => off.push(args) }
  54. const source = read('../pages/common/wechat-binding-authorize.vue')
  55. const descriptor = parse(source).descriptor
  56. const body = strip(descriptor.scriptSetup.content)
  57. const deps = { ref, getCurrentInstance: () => ({ proxy: { getOpenerEventChannel: () => channel } }),
  58. onLoad: fn => { hooks.load = fn }, onReady: fn => { hooks.ready = fn }, onUnload: fn => { hooks.unload = fn },
  59. isWechatAuthorizeUrl: api({}).isWechatAuthorizeUrl }
  60. const page = new Function(...Object.keys(deps), `${body}; return { src, errorText, onError }`)(...Object.values(deps))
  61. hooks.load({ url: 'https://evil.test' })
  62. assert.equal(page.src.value, '')
  63. const url = launchUrl
  64. handlers.wechatBindingAuthorize({ authorizeUrl: url })
  65. assert.equal(page.src.value, '')
  66. hooks.ready()
  67. assert.equal(page.src.value, url)
  68. page.onError()
  69. assert.match(page.errorText.value, /打开失败/)
  70. hooks.unload()
  71. assert.equal(off[0][0], 'wechatBindingAuthorize')
  72. const compiled = compileTemplate({ source: descriptor.template.content, filename: 'wechat-binding-authorize.vue', id: 'binding' })
  73. assert.deepEqual(compiled.errors, [])
  74. })
  75. test('real page composable returns from OAuth and confirms logged-in account without URL credentials', async () => {
  76. const flowSource = strip(read('../utils/wechatBindingFlow.js'))
  77. const flowFns = new Function(`${flowSource}; return { createWechatBindingFlow, initialBindingState }`)()
  78. const hooks = {}, calls = { routes: [], events: [], prompts: [], confirms: [] }
  79. const user = { isLoggedIn: true, token: 'private-jwt', userInfo: { realName: '张三' }, checkLoginStatus() {} }
  80. let bound = false
  81. const deps = { computed, reactive, onUnmounted: () => {}, onHide: fn => { hooks.hide = fn }, onShow: fn => { hooks.show = fn },
  82. useUserStore: () => user, PAGE_PATHS: { wechatBindingAuthorize: '/pages/common/wechat-binding-authorize', login: '/pages/login/login' },
  83. ...flowFns, fetchWechatNoticeStatus: async () => ({ enabled: true, bound, subscribed: bound }),
  84. startWechatBinding: async () => ({ sessionId: 's'.repeat(32), authorizeUrl: 'https://open.weixin.qq.com/connect/oauth2/authorize?state=nonce' }),
  85. fetchWechatBindingSession: async () => ({ status: 'scanned', subscribed: true, verificationCode: '654321' }),
  86. confirmWechatBinding: async id => { calls.confirms.push(id); bound = true; return { enabled: true, bound, subscribed: true } },
  87. uni: { navigateTo: options => { calls.routes.push(options.url); hooks.hide(); options.success({ eventChannel: { emit: (name, value) => calls.events.push({ name, value }) } }) },
  88. showModal: options => { calls.prompts.push(options.content); options.success({ confirm: true }) }, reLaunch: () => {} } }
  89. const use = new Function(...Object.keys(deps), `${strip(read('../utils/useWechatBinding.js'))}; return useWechatBinding`)(...Object.values(deps))
  90. const page = use()
  91. hooks.show(); await new Promise(resolve => setImmediate(resolve))
  92. await page.startBinding()
  93. assert.deepEqual(calls.routes, ['/pages/common/wechat-binding-authorize'])
  94. assert.ok(!JSON.stringify(calls.events).includes(user.token))
  95. hooks.show(); await new Promise(resolve => setImmediate(resolve))
  96. assert.deepEqual(calls.prompts, [])
  97. assert.equal(page.bindingState.bound, true)
  98. assert.deepEqual(calls.confirms, ['s'.repeat(32)])
  99. })