Files
shanxia2.0/miniprogram/scripts/test-wechat-binding-integration.mjs
wuzilongandCursor e7c1762113 fix: 小程序服务号绑定先打开门户域名再跳转微信授权。
web-view 不能直接打开 open.weixin.qq.com,移动端改为打开已配置的业务域名中转页。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 01:44:40 +08:00

106 lines
6.5 KiB
JavaScript

import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import test from 'node:test'
const require = createRequire(new URL('../../web-admin/package.json', import.meta.url))
const { ref, reactive, computed } = require('vue')
const { parse, compileTemplate } = require('@vue/compiler-sfc')
const read = p => readFileSync(new URL(p, import.meta.url), 'utf8')
const strip = s => s.replace(/^import[^\n]+\n/gm, '').replace(/^export /gm, '')
const launchState = 'a'.repeat(32)
const launchUrl = `https://w.shanxiachem.com/api/public/wechat-notices/oauth/launch?state=${launchState}`
function api(data, statusCode = 200, code = 200) {
const calls = []
const source = strip(read('../api/wechat-notices.js'))
const fns = new Function('request', 'parseResponseJson', 'resolveWebOrigin', 'resolveApiBase', `${source}; return { startWechatBinding, fetchWechatBindingSession, confirmWechatBinding, isWechatAuthorizeUrl }`)(
async options => { calls.push(options); return { statusCode, data: { code, data, message: '请求失败' } } },
response => response.data,
() => 'https://w.shanxiachem.com',
() => 'https://w.shanxiachem.com/api')
return { ...fns, calls }
}
test('binding API uses owner-authenticated endpoints and only returns necessary fields', async () => {
const start = api({ sessionId: 'a'.repeat(32), authorizeUrl: launchUrl, openid: 'private' })
assert.equal((await start.startWechatBinding()).openid, undefined)
assert.equal(start.calls[0].method, 'POST')
assert.deepEqual(start.calls[0].data, { mode: 'mobile' })
assert.equal(start.calls[0].url, '/web/wechat-notices/binding/start')
const confirm = api({ enabled: true, bound: true, subscribed: true, openid: 'private' })
assert.deepEqual(await confirm.confirmWechatBinding('session'), { enabled: true, bound: true, subscribed: true })
assert.deepEqual(confirm.calls[0].data, { sessionId: 'session' })
const session = api({ status: 'scanned', subscribed: true, verificationCode: '123456' })
await session.fetchWechatBindingSession('a&b')
assert.ok(session.calls[0].url.endsWith('sessionId=a%26b'))
})
test('binding API rejects foreign URLs, failed and malformed results', async () => {
const fns = api({})
for (const url of [
'javascript:alert(1)',
'http://open.weixin.qq.com/connect/oauth2/authorize?state=x',
'https://open.weixin.qq.com/connect/oauth2/authorize?state=' + launchState,
'https://evil.test/api/public/wechat-notices/oauth/launch?state=' + launchState,
launchUrl + '#wechat_redirect'
]) {
assert.equal(fns.isWechatAuthorizeUrl(url), false)
await assert.rejects(api({ sessionId: 'x'.repeat(32), authorizeUrl: url }).startWechatBinding())
}
await assert.rejects(api({}, 403, 403).startWechatBinding())
await assert.rejects(api({ status: 'scanned', subscribed: true, verificationCode: 'bad' }).fetchWechatBindingSession('session'))
await assert.rejects(api({ enabled: true, bound: 'yes', subscribed: true }).confirmWechatBinding('session'))
})
test('authorization page mounts approved URL after ready and unregisters its channel', () => {
const hooks = {}, handlers = {}, off = []
const channel = { on: (name, fn) => { handlers[name] = fn }, off: (...args) => off.push(args) }
const source = read('../pages/common/wechat-binding-authorize.vue')
const descriptor = parse(source).descriptor
const body = strip(descriptor.scriptSetup.content)
const deps = { ref, getCurrentInstance: () => ({ proxy: { getOpenerEventChannel: () => channel } }),
onLoad: fn => { hooks.load = fn }, onReady: fn => { hooks.ready = fn }, onUnload: fn => { hooks.unload = fn },
isWechatAuthorizeUrl: api({}).isWechatAuthorizeUrl }
const page = new Function(...Object.keys(deps), `${body}; return { src, errorText, onError }`)(...Object.values(deps))
hooks.load({ url: 'https://evil.test' })
assert.equal(page.src.value, '')
const url = launchUrl
handlers.wechatBindingAuthorize({ authorizeUrl: url })
assert.equal(page.src.value, '')
hooks.ready()
assert.equal(page.src.value, url)
page.onError()
assert.match(page.errorText.value, /打开失败/)
hooks.unload()
assert.equal(off[0][0], 'wechatBindingAuthorize')
const compiled = compileTemplate({ source: descriptor.template.content, filename: 'wechat-binding-authorize.vue', id: 'binding' })
assert.deepEqual(compiled.errors, [])
})
test('real page composable returns from OAuth and confirms logged-in account without URL credentials', async () => {
const flowSource = strip(read('../utils/wechatBindingFlow.js'))
const flowFns = new Function(`${flowSource}; return { createWechatBindingFlow, initialBindingState }`)()
const hooks = {}, calls = { routes: [], events: [], prompts: [], confirms: [] }
const user = { isLoggedIn: true, token: 'private-jwt', userInfo: { realName: '张三' }, checkLoginStatus() {} }
let bound = false
const deps = { computed, reactive, onUnmounted: () => {}, onHide: fn => { hooks.hide = fn }, onShow: fn => { hooks.show = fn },
useUserStore: () => user, PAGE_PATHS: { wechatBindingAuthorize: '/pages/common/wechat-binding-authorize', login: '/pages/login/login' },
...flowFns, fetchWechatNoticeStatus: async () => ({ enabled: true, bound, subscribed: bound }),
startWechatBinding: async () => ({ sessionId: 's'.repeat(32), authorizeUrl: 'https://open.weixin.qq.com/connect/oauth2/authorize?state=nonce' }),
fetchWechatBindingSession: async () => ({ status: 'scanned', subscribed: true, verificationCode: '654321' }),
confirmWechatBinding: async id => { calls.confirms.push(id); bound = true; return { enabled: true, bound, subscribed: true } },
uni: { navigateTo: options => { calls.routes.push(options.url); hooks.hide(); options.success({ eventChannel: { emit: (name, value) => calls.events.push({ name, value }) } }) },
showModal: options => { calls.prompts.push(options.content); options.success({ confirm: true }) }, reLaunch: () => {} } }
const use = new Function(...Object.keys(deps), `${strip(read('../utils/useWechatBinding.js'))}; return useWechatBinding`)(...Object.values(deps))
const page = use()
hooks.show(); await new Promise(resolve => setImmediate(resolve))
await page.startBinding()
assert.deepEqual(calls.routes, ['/pages/common/wechat-binding-authorize'])
assert.ok(!JSON.stringify(calls.events).includes(user.token))
hooks.show(); await new Promise(resolve => setImmediate(resolve))
assert.deepEqual(calls.prompts, [])
assert.equal(page.bindingState.bound, true)
assert.deepEqual(calls.confirms, ['s'.repeat(32)])
})