| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247 |
- import assert from 'node:assert/strict'
- import { existsSync, readFileSync } from 'node:fs'
- import { createRequire } from 'node:module'
- import test from 'node:test'
- const utilUrl = new URL('../utils/wechatFollowReminder.js', import.meta.url)
- const apiUrl = new URL('../api/wechat-notices.js', import.meta.url)
- const load = async () => {
- assert.ok(existsSync(utilUrl), '缺少登录关注提醒控制器')
- return import(`data:text/javascript;base64,${Buffer.from(readFileSync(utilUrl, 'utf8')).toString('base64')}`)
- }
- const deferred = () => {
- let resolve, reject
- const promise = new Promise((yes, no) => { resolve = yes; reject = no })
- return { promise, resolve, reject }
- }
- async function setup(status = { enabled: true, bound: true, subscribed: false }, memory = {}) {
- const { createWechatFollowReminder } = await load()
- const state = { session: 'login-a', active: true }
- const calls = { queries: 0, prompts: [], opens: 0 }
- const deps = {
- getSessionKey: () => state.session,
- isActive: () => state.active,
- loadStatus: async () => { calls.queries++; return status },
- showPrompt: async (options) => { calls.prompts.push(options); return false },
- openGuide: () => { calls.opens++ },
- memory
- }
- return { state, calls, deps, create: () => createWechatFollowReminder(deps) }
- }
- test('confirmed unsubscribed recipient is reminded once per login and may defer', async () => {
- const p = await setup()
- const controller = p.create()
- await controller.check()
- await controller.check()
- assert.equal(p.calls.queries, 1)
- assert.equal(p.calls.prompts.length, 1)
- assert.equal(p.calls.prompts[0].title, '关注服务号')
- assert.equal(p.calls.prompts[0].cancelText, '稍后再说')
- assert.equal(p.calls.opens, 0)
- })
- test('already subscribed and bound users are not prompted', async () => {
- const p = await setup({ enabled: true, bound: true, subscribed: true })
- await p.create().check()
- assert.equal(p.calls.prompts.length, 0)
- })
- test('unbound users receive neutral setup wording, not an unverified subscription claim', async () => {
- const p = await setup({ enabled: true, bound: false, subscribed: false })
- await p.create().check()
- assert.match(p.calls.prompts[0].content, /如已关注/)
- assert.match(p.calls.prompts[0].content, /绑定/)
- assert.doesNotMatch(p.calls.prompts[0].content, /尚未关注|未关注服务号/)
- })
- test('disabled notification feature stays quiet', async () => {
- const p = await setup({ enabled: false, bound: false, subscribed: false })
- await p.create().check()
- assert.equal(p.calls.prompts.length, 0)
- })
- test('confirm opens guide and does not follow or bind automatically', async () => {
- const p = await setup()
- p.deps.showPrompt = async () => true
- await p.create().check()
- assert.equal(p.calls.opens, 1)
- })
- test('reminder dedup survives workbench recreation, next login is independent', async () => {
- const p = await setup()
- await p.create().check()
- await p.create().check()
- assert.equal(p.calls.prompts.length, 1)
- p.state.session = 'login-b'
- await p.create().check()
- assert.equal(p.calls.prompts.length, 2)
- })
- test('network failure neither blocks login nor claims unsubscribed, retry remains possible', async () => {
- const p = await setup()
- p.deps.loadStatus = async () => { throw new Error('network') }
- await p.create().check()
- assert.equal(p.calls.prompts.length, 0)
- p.deps.loadStatus = async () => ({ enabled: true, bound: true, subscribed: false })
- await p.create().check()
- assert.equal(p.calls.prompts.length, 1)
- })
- test('a concurrent show does not duplicate pending status lookup', async () => {
- const p = await setup()
- const response = deferred()
- p.deps.loadStatus = () => { p.calls.queries++; return response.promise }
- const controller = p.create()
- const pending = controller.check()
- await controller.check()
- response.resolve({ enabled: true, bound: true, subscribed: false })
- await pending
- assert.equal(p.calls.queries, 1)
- assert.equal(p.calls.prompts.length, 1)
- })
- test('hide cancels stale response and return can perform a fresh check', async () => {
- const p = await setup()
- const response = deferred()
- p.deps.loadStatus = () => response.promise
- const controller = p.create()
- const pending = controller.check()
- p.state.active = false
- controller.cancel()
- response.resolve({ enabled: true, bound: true, subscribed: false })
- await pending
- assert.equal(p.calls.prompts.length, 0)
- p.state.active = true
- await controller.check()
- assert.equal(p.calls.prompts.length, 1)
- })
- test('account switch during lookup ignores old user response', async () => {
- const p = await setup()
- const response = deferred()
- p.deps.loadStatus = () => response.promise
- const pending = p.create().check()
- p.state.session = 'new-user'
- response.resolve({ enabled: true, bound: true, subscribed: false })
- await pending
- assert.equal(p.calls.prompts.length, 0)
- })
- test('late modal confirm after logout does not navigate', async () => {
- const p = await setup()
- const answer = deferred()
- p.deps.showPrompt = () => answer.promise
- const pending = p.create().check()
- await Promise.resolve()
- p.state.session = ''
- answer.resolve(true)
- await pending
- assert.equal(p.calls.opens, 0)
- })
- test('inactive or logged out pages do not request status', async () => {
- const p = await setup()
- p.state.active = false
- await p.create().check()
- p.state.active = true
- p.state.session = ''
- await p.create().check()
- assert.equal(p.calls.queries, 0)
- })
- function api(response) {
- assert.ok(existsSync(apiUrl), '缺少本人关注状态API封装')
- const code = readFileSync(apiUrl, 'utf8')
- .replace(/^import[^\n]+\n/gm, '').replace(/export /g, '')
- const calls = []
- const fetchStatus = new Function('request', 'parseResponseJson', `${code}; return fetchWechatNoticeStatus`)(
- async (options) => { calls.push(options); return response }, (res) => res.data)
- return { fetchStatus, calls }
- }
- test('status API uses authenticated request and returns only validated status fields', async () => {
- const p = api({ statusCode: 200, data: { code: 200, data: { enabled: true, bound: true, subscribed: true } } })
- assert.deepEqual(await p.fetchStatus(), { enabled: true, bound: true, subscribed: true })
- assert.equal(p.calls[0].url, '/web/wechat-notices/binding')
- assert.equal(p.calls[0].method, 'GET')
- })
- test('HTTP, business and malformed failures cannot be read as unsubscribed', async () => {
- for (const response of [
- { statusCode: 502, data: { detail: '微信查询失败' } },
- { statusCode: 200, data: { code: 500 } },
- { statusCode: 200, data: { code: 200, data: { enabled: true, bound: 'true', subscribed: false } } }
- ]) {
- await assert.rejects(api(response).fetchStatus())
- }
- })
- async function workbench(statusPromise) {
- const require = createRequire(new URL('../../web-admin/package.json', import.meta.url))
- const { ref, computed } = require('vue')
- const { parse } = require('@vue/compiler-sfc')
- const { createWechatFollowReminder } = await load()
- const source = readFileSync(new URL('../pages/workbench/workbench.vue', import.meta.url), 'utf8')
- const body = parse(source).descriptor.scriptSetup.content.replace(/^import[\s\S]*?from ['"][^'"]+['"]\s*\n/gm, '')
- const hooks = {}, calls = { prompts: [], urls: [], profiles: [] }
- const user = { isLoggedIn: true, token: 'page-login', checkLoginStatus() {},
- fetchMenuCodes: async () => {}, logout: async () => { user.token = ''; user.isLoggedIn = false } }
- const deps = {
- openWechatOfficialAccount: (options) => calls.profiles.push(options),
- ref, computed, onUnmounted: () => {}, onHide: (fn) => { hooks.hide = fn }, onShow: (fn) => { hooks.show = fn },
- useUserStore: () => user, WORKBENCH_MODULES: [], WORKBENCH_ROUTE_MAP: {},
- PAGE_PATHS: { login: '/pages/login/login', wechatOfficialAccount: '/pages/common/wechat-official-account' },
- filterWorkbenchModules: () => [], getSafeAreaStyle: () => ({}), useShareToLogin: () => {},
- checkMpUpdate: () => {}, formatMpUpdateLine: () => '', isMpUpdateUrgent: () => false,
- subscribeMpUpdate: () => () => {}, fetchWechatNoticeStatus: () => statusPromise,
- createWechatFollowReminder: (options) => createWechatFollowReminder({ ...options, memory: {} }),
- uni: { showModal: (options) => calls.prompts.push(options), navigateTo: ({ url }) => calls.urls.push(url),
- reLaunch: ({ url }) => calls.urls.push(url), showToast: () => {} }
- }
- const component = new Function(...Object.keys(deps), `${body}; return { handleLogout, openOfficialAccount }`)(...Object.values(deps))
- return { hooks, calls, component, user }
- }
- test('actual workbench show checks status and modal confirm opens existing guide', async () => {
- const p = await workbench(Promise.resolve({ enabled: true, bound: true, subscribed: false }))
- await p.hooks.show()
- assert.equal(p.calls.prompts.length, 1)
- p.calls.prompts[0].success({ confirm: true })
- await Promise.resolve()
- await Promise.resolve()
- assert.deepEqual(p.calls.urls, ['/pages/common/wechat-official-account'])
- p.hooks.hide()
- await p.hooks.show()
- assert.equal(p.calls.prompts.length, 1)
- })
- test('actual workbench hide invalidates pending lookup before response arrives', async () => {
- const response = deferred()
- const p = await workbench(response.promise)
- await p.hooks.show()
- p.hooks.hide()
- response.resolve({ enabled: true, bound: true, subscribed: false })
- await Promise.resolve()
- assert.equal(p.calls.prompts.length, 0)
- })
- test('workbench follow click opens local guide without native authorization prompt', async () => {
- const p = await workbench(Promise.resolve({ enabled: false, bound: false, subscribed: false }))
- await p.hooks.show()
- p.component.openOfficialAccount()
- assert.deepEqual(p.calls.urls, ['/pages/common/wechat-official-account'])
- assert.equal(p.calls.profiles.length, 0)
- })
- test('workbench follow entry does not navigate during logout', async () => {
- const p = await workbench(Promise.resolve({ enabled: false, bound: false, subscribed: false }))
- await p.hooks.show()
- const logout = p.component.handleLogout()
- p.component.openOfficialAccount()
- await logout
- assert.deepEqual(p.calls.urls, [])
- assert.equal(p.calls.profiles.length, 0)
- })
|