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, '') function api(data, statusCode = 200, code = 200) { const calls = [] const source = strip(read('../api/wechat-notices.js')) const fns = new Function('request', 'parseResponseJson', `${source}; return { startWechatBinding, fetchWechatBindingSession, confirmWechatBinding, isWechatAuthorizeUrl }`)( async options => { calls.push(options); return { statusCode, data: { code, data, message: '请求失败' } } }, response => response.data) return { ...fns, calls } } test('binding API uses owner-authenticated endpoints and only returns necessary fields', async () => { const start = api({ sessionId: 'a'.repeat(32), authorizeUrl: 'https://open.weixin.qq.com/connect/oauth2/authorize?state=state#wechat_redirect', 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.evil.test/connect/oauth2/authorize?state=x']) { 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 = 'https://open.weixin.qq.com/connect/oauth2/authorize?state=xyz' 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)]) })