const test = require('node:test') const assert = require('node:assert/strict') const fs = require('node:fs') const path = require('node:path') const vm = require('node:vm') const ts = require('typescript') const root = path.resolve(__dirname, '..') const flush = () => new Promise(resolve => setImmediate(resolve)) function load(file, requireImpl, env = 'weapp') { const exports = {} const code = ts.transpileModule(fs.readFileSync(path.join(root, file), 'utf8'), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2019, jsx: ts.JsxEmit.ReactJSX } }).outputText vm.runInNewContext(code, { exports, require: requireImpl, process: { env: { TARO_ENV: env } } }) return exports } test('未登录点击天宫不切换Tab,连续点击只打开一次登录页,其他Tab仍可访问', async () => { const routes = [] let complete, loggedIn = false const { switchMainTab } = load('src/services/tabNavigation.ts', id => id === './loginAccess' ? { hasLoginAccess: () => loggedIn, openLogin: () => { routes.push('login'); return new Promise(resolve => { complete = resolve }) } } : { default: { switchTab: async ({ url }) => routes.push(url), showToast: () => {} } }) const first = switchMainTab('fortune', 'my') await switchMainTab('fortune', 'my') assert.deepEqual(routes, ['login']) complete(); await first await switchMainTab('home', 'my') await switchMainTab('my', 'home') assert.deepEqual(routes, ['login', '/pages/home/index', '/pages/my/index']) loggedIn = true await switchMainTab('fortune', 'my') assert.equal(routes.at(-1), '/pages/fortune/index') }) function entryHarness(env = 'weapp') { let token = '', preview = false, demo = false, response = { phoneBound: true, token: 'test', user: { id: 1 } }, failure, resolveLogin const calls = [], routes = [], accepted = [], toasts = [] const service = load('src/services/loginAccess.ts', id => { if (id === './request') return { getToken: () => token, api: { wxLogin: async code => { calls.push(code); if (failure) throw new Error('请求失败'); return response } } } if (id === './account') return { finishAccountLogin: async session => { accepted.push(session) } } if (id === './frontendPreview') return { isFrontendPreview: () => preview } if (id === './avatarDemo') return { isAvatarDemo: () => demo } return { default: { login: () => new Promise(resolve => { resolveLogin = resolve }), navigateTo: async ({ url }) => { routes.push(url) }, showLoading: () => {}, hideLoading: () => {}, showToast: data => toasts.push(data.title) } } }, env) return { service, calls, routes, accepted, toasts, complete: code => resolveLogin({ code }), token: value => { token = value }, preview: value => { preview = value }, demo: value => { demo = value }, response: value => { response = value }, fail: value => { failure = value } } } test('入口一次点击直接微信登录、去重;已注册不跳品牌页,失败可重试', async () => { const h = entryHarness(), { service } = h assert.equal(service.hasLoginAccess(), false) h.token('session'); assert.equal(service.hasLoginAccess(), true) h.token(''); h.preview(true); assert.equal(service.hasLoginAccess(), true) h.preview(false); h.demo(true); assert.equal(service.hasLoginAccess(), true) h.demo(false) const first = service.openLogin(), second = service.openLogin() assert.equal(first, second) await flush(); h.complete('wx-test-code'); await first assert.deepEqual(h.calls, ['wx-test-code']) assert.equal(h.accepted.length, 1) assert.equal(h.routes.length, 0) h.fail(true) const failed = service.openLogin(); await flush(); h.complete('failed'); await failed assert.equal(h.toasts.length, 1) h.fail(false) const retry = service.openLogin(); await flush(); h.complete('retry'); await retry assert.equal(h.accepted.length, 2) }) test('新注册仅进入必要的手机号表单,临时会话留内存、不放入URL', async () => { const h = entryHarness() h.response({ phoneBound: false, sessionKey: 'temporary-test-session' }) const attempt = h.service.openLogin(); await flush(); h.complete('new-user'); await attempt assert.equal(h.service.getRegistrationSession(), 'temporary-test-session') assert.deepEqual(h.routes, ['/pages/login/index']) assert.equal(h.accepted.length, 0) h.service.clearRegistrationSession() assert.equal(h.service.getRegistrationSession(), '') }) test('H5和演示环境不伪造登录;缺少code或token不接受身份', async () => { const web = entryHarness('h5') await web.service.openLogin() assert.equal(web.routes.length, 0); assert.equal(web.calls.length, 0) assert.match(web.toasts[0], /微信开发者工具/) const h = entryHarness() h.preview(true); await h.service.openLogin(); assert.equal(h.calls.length, 0) h.preview(false) const noCode = h.service.openLogin(); await flush(); h.complete(''); await noCode assert.equal(h.calls.length, 0) h.response({ phoneBound: true }) const invalid = h.service.openLogin(); await flush(); h.complete('incomplete'); await invalid assert.equal(h.accepted.length, 0) }) function authHarness(initialToken = '', sandbox = false, fresh = false) { let token = initialToken, stateIndex = 0, refIndex = 0, initialized = false, show, hide, listener, cleanup let freshToken = fresh ? initialToken : '' const states = [], refs = [] let requests = 0, complete, fail const { useRequiredLogin } = load('src/hooks/useRequiredLogin.ts', id => { if (id === 'react') return { useState: initial => { const index = stateIndex++; if (!(index in states)) states[index] = typeof initial === 'function' ? initial() : initial; return [states[index], value => { states[index] = value }] }, useRef: initial => { const index = refIndex++; return refs[index] || (refs[index] = { current: initial }) }, useEffect: effect => { if (!initialized) { cleanup = effect(); initialized = true } } } if (id === '@tarojs/taro') return { useDidShow: callback => { show = callback }, useDidHide: callback => { hide = callback } } if (id === '../services/frontendPreview') return { isFrontendPreview: () => sandbox } if (id === '../services/avatarDemo') return { isAvatarDemo: () => false } return { getToken: () => token, getFreshCachedProfile: () => fresh && token === freshToken ? { id: 1 } : null, subscribeProfile: callback => { listener = callback; return () => { listener = undefined } }, api: { profile: () => { requests++; return new Promise((resolve, reject) => { complete = resolve; fail = reject }) } } } }) const render = () => { stateIndex = 0; refIndex = 0; return useRequiredLogin() } render() return { render, show: () => show(), hide: () => hide(), resolve: () => complete({ id: 1 }), reject: () => fail(new Error('offline')), emit: value => listener?.(value), setToken: value => { token = value }, setFresh: value => { fresh = value; freshToken = value ? token : '' }, count: () => requests, unmount: () => cleanup() } } test('同账号有效登录缓存直接允许进入,前后台切换不再闪检查页或重复请求', async () => { const auth = authHarness('session', false, true) assert.equal(auth.render().status, 'allowed') auth.show(); assert.equal(auth.render().status, 'allowed'); assert.equal(auth.count(), 0) auth.hide(); auth.show(); assert.equal(auth.render().status, 'allowed'); assert.equal(auth.count(), 0) auth.setFresh(false) auth.show(); assert.equal(auth.render().status, 'checking'); assert.equal(auth.count(), 1) auth.reject(); await flush(); assert.equal(auth.render().status, 'error') }) test('缓存页面切换账号或退出登录时,不复用旧账号已允许的状态', () => { const auth = authHarness('first-session', false, true) assert.equal(auth.render().status, 'allowed') auth.hide(); auth.setToken('second-session') assert.equal(auth.render().status, 'checking') auth.setToken('') assert.equal(auth.render().status, 'denied') }) test('未登录不发资料请求;已登录必须校验资料才显示内容,401后立即隐藏', async () => { const guest = authHarness() guest.show(); assert.equal(guest.render().status, 'denied'); assert.equal(guest.count(), 0) const auth = authHarness('session') assert.equal(auth.render().status, 'checking') auth.show(); assert.equal(auth.render().status, 'checking') auth.emit({ id: 1 }); auth.resolve(); await flush(); assert.equal(auth.render().status, 'allowed') auth.setToken(''); auth.emit(null) assert.equal(auth.render().status, 'denied') const demo = authHarness('', true) demo.show(); assert.equal(demo.render().status, 'allowed'); assert.equal(demo.count(), 0) }) test('网络错误保持登录但不显示内容,可重试;失效token不能通过校验', async () => { const auth = authHarness('session') auth.show(); auth.reject(); await flush() assert.equal(auth.render().status, 'error') auth.render().retry(); assert.equal(auth.render().status, 'checking') auth.resolve(); await flush(); assert.equal(auth.render().status, 'allowed') const expired = authHarness('expired') expired.show(); expired.setToken(''); expired.emit(null); expired.reject(); await flush() assert.equal(expired.render().status, 'denied') }) test('页面隐藏或卸载后,旧校验结果不会恢复内容权限', async () => { const hidden = authHarness('session') hidden.show(); hidden.hide(); hidden.emit({ id: 1 }); hidden.resolve(); await flush() assert.equal(hidden.render().status, 'checking') const unmounted = authHarness('session') unmounted.show(); unmounted.unmount(); unmounted.resolve(); await flush() assert.equal(unmounted.render().status, 'checking') }) test('直达天宫和详情页先渲染登录守卫,未验证时不挂载神仙列表或详情', () => { for (const file of ['src/pages/fortune/index.tsx', 'src/pages/tiangong-detail/index.tsx']) { let status = 'denied', session = false, contentReads = 0 const page = load(file, id => { if (id === 'react' || id === 'react/jsx-runtime') return require(id) if (id.endsWith('/useRequiredLogin')) return { useRequiredLogin: () => ({ status, retry: () => {} }) } if (id.endsWith('/loginAccess')) return { hasLoginAccess: () => session } if (id.endsWith('/TiangongLoginGate')) return { TiangongLoginGate: 'login-gate' } if (id.endsWith('/useTiangongSnapshot')) return { useTiangongSnapshot: () => { contentReads++; return {} } } if (id.endsWith('/tiangong')) return { getDeity: () => { contentReads++; return {} } } return {} }) for (const next of ['denied', 'checking', 'error', 'allowed']) { status = next assert.equal(page.default().type, 'login-gate') } assert.equal(contentReads, 0) session = true; status = 'checking'; assert.equal(page.default().type, 'login-gate') status = 'allowed'; assert.equal(typeof page.default().type, 'function') } }) test('必要校验只显示轻量场景占位,不出现整页登录检查提示', () => { const gate = load('src/components/TiangongLoginGate.tsx', id => { if (id === 'react/jsx-runtime') return require(id) if (id === './TiangongPending') return { TiangongPending: 'pending-scene' } if (id === './LoginGate') return { LoginGate: 'login-gate' } return {} }) assert.equal(gate.TiangongLoginGate({ status: 'checking', retry: () => {} }).type, 'pending-scene') assert.equal(gate.TiangongLoginGate({ status: 'denied', retry: () => {} }).type, 'login-gate') assert.equal(gate.TiangongLoginGate({ status: 'error', retry: () => {} }).type, 'login-gate') })