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) })