215 lines
11 KiB
JavaScript
215 lines
11 KiB
JavaScript
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')
|
|
})
|