import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import { createRequire } from 'node:module'; import { resolve } from 'node:path'; import test from 'node:test'; import { fileURLToPath, pathToFileURL } from 'node:url'; import vm from 'node:vm'; const require = createRequire(process.env.KEFU_SSO_TYPESCRIPT ? pathToFileURL(resolve(process.env.KEFU_SSO_TYPESCRIPT, 'package.json')) : new URL('../package.json', import.meta.url)); const ts = require('typescript'); const vue = require('vue'); const pinia = require('pinia'); const routing = require('vue-router'); const workspace = process.env.KEFU_SSO_WORKSPACE || fileURLToPath(new URL('../../../', import.meta.url)); const source = relative => readFile(resolve(workspace, relative), 'utf8'); const staged = relative => readFile(new URL(`../src/${relative}`, import.meta.url), 'utf8'); const normalized = value => JSON.parse(JSON.stringify(value)); const clone = value => Array.isArray(value) ? value.map(clone) : value && typeof value === 'object' ? Object.fromEntries(Object.entries(value).map(([key, item]) => [key, clone(item)])) : value; function load(code, dependencies = {}, globals = {}) { const compiled = ts.transpileModule(code.replaceAll('import.meta.hot', 'undefined'), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, }, reportDiagnostics: true }); assert.deepEqual(compiled.diagnostics, []); const module = { exports: {} }; vm.runInNewContext(compiled.outputText, { module, exports: module.exports, require(name) { assert.ok(name in dependencies, `Unexpected import ${name}`); return dependencies[name]; }, console, ...globals }); return module.exports; } const preferences = { app: { accessMode: 'frontend', loginExpiredMode: 'modal', enableRefreshToken: false }, tabbar: { visitHistory: true, maxCount: 0 } }; const tree = load(await source('packages/@core/base/shared/src/utils/tree.ts')); const stack = load(await source('packages/@core/base/shared/src/utils/stack.ts')); const { useAccessStore } = load(await source('packages/stores/src/modules/access.ts'), { pinia }); const { useUserStore } = load(await source('packages/stores/src/modules/user.ts'), { pinia }); const { useTabbarStore, getTabKey } = load(await source('packages/stores/src/modules/tabbar.ts'), { vue, pinia, '@vben-core/preferences': { preferences }, '@vben-core/shared/utils': { ...stack, startProgress() {}, stopProgress() {}, openWindow() {} }, }, { sessionStorage: {} }); const stores = { useAccessStore, useUserStore, useTabbarStore, getTabKey }; const { useAccess } = load(await source('packages/effects/access/src/use-access.ts'), { vue, '@vben/preferences': { preferences }, '@vben/stores': stores, }); const { generateRoutesByFrontend } = load(await source('packages/utils/src/helpers/generate-routes-frontend.ts'), { '@vben-core/shared/utils': tree, }); const { generateMenus } = load(await source('packages/utils/src/helpers/generate-menus.ts'), { '@vben-core/shared/utils': tree, }); const { generateAccessible } = load(await source('packages/effects/access/src/accessible.ts'), { vue, '@vben/utils': { ...tree, cloneDeep: clone, generateMenus, generateRoutesByFrontend, generateRoutesByBackend() { throw new Error('Unexpected backend access mode'); }, isFunction: value => typeof value === 'function', isString: value => typeof value === 'string' }, }); const syncModule = load(await staged('utils/authorization-sync.ts')); const menuHelpers = load(await source('apps/web-antd/src/router/menu-access.ts')); const originalRoutes = load(await staged('router/routes/modules/console.ts')).default; function pageRoutes(input) { return input.map(route => ({ ...route, meta: { ...route.meta }, ...(route.component ? { component: { name: `Test${route.name}`, render: () => null } } : {}), children: route.children ? pageRoutes(route.children) : undefined })); } const accessRoutes = pageRoutes(originalRoutes); const routes = [ { name: 'Root', path: '/', component: { render: () => vue.h(routing.RouterView) }, children: [] }, { name: 'Login', path: '/auth/login', component: { render: () => null } }, { name: 'FallbackNotFound', path: '/:path(.*)*', component: { render: () => null } }, ]; function environment() { const window = new EventTarget(); const document = new EventTarget(); document.visibilityState = 'visible'; const timers = new Map(); let sequence = 0; window.setInterval = (callback, delay) => { const id = ++sequence; timers.set(id, { callback, delay }); return id; }; window.clearInterval = id => timers.delete(id); return { window, document, timers, tick() { for (const timer of [...timers.values()]) timer.callback(); }, focus() { window.dispatchEvent(new Event('focus')); }, visible(value) { document.visibilityState = value ? 'visible' : 'hidden'; document.dispatchEvent(new Event('visibilitychange')); } }; } const settle = () => new Promise(resolve => setImmediate(resolve)); const info = (roles = [], userId = '7') => ({ userId, username: `user${userId}`, realName: 'Test', roles }); const deferred = () => { let resolve, reject; const promise = new Promise((yes, no) => { resolve = yes; reject = no; }); return { promise, resolve, reject }; }; async function harness(initial = []) { pinia.setActivePinia(pinia.createPinia()); const access = useAccessStore(); const user = useUserStore(); const tabbar = useTabbarStore(); const env = environment(); const router = routing.createRouter({ history: routing.createMemoryHistory(), routes: clone(routes) }); const requests = []; let answer = info(initial); let preparations = 0; const generateAccess = async options => { preparations += 1; const generated = await generateAccessible('frontend', options); return { ...generated, accessibleMenus: menuHelpers.pruneEmptyMenus(generated.accessibleMenus, menuHelpers.parentPaths(options.routes)) }; }; const adapter = load(await staged('router/authorization-sync.ts'), { vue, 'vue-router': routing, '@vben/stores': stores, '#/api/core/user': { getUserInfoApi() { requests.push('GET /me'); return Promise.resolve(typeof answer === 'function' ? answer() : clone(answer)); } }, '#/utils/authorization-sync': syncModule, './access': { generateAccess }, './routes': { accessRoutes, routes: clone(routes) }, }, env); access.setAccessToken('same-session'); access.setAccessCodes(initial); user.setUserInfo(info(initial)); const first = await generateAccess({ roles: initial, routes: accessRoutes, router }); access.setAccessMenus(first.accessibleMenus); access.setAccessRoutes(first.accessibleRoutes); access.setIsAccessChecked(true); await router.push('/group/welcome'); const dispose = adapter.installAuthorizationSync(router); await settle(); return { access, user, tabbar, env, router, requests, adapter, dispose, preparations: () => preparations, answer(value) { answer = value; }, async poll(value) { answer = value; env.tick(); await settle(); }, }; } test('same logged-in token gains/revokes real menus, matcher routes and reactive action rights', async () => { const h = await harness(); try { const { hasAccessByCodes } = useAccess(); const createButton = vue.computed(() => hasAccessByCodes(['role:create'])); assert.equal(h.router.hasRoute('RoleManage'), false); assert.equal(createButton.value, false); await h.poll(info(['role:read'])); assert.equal(h.router.hasRoute('RoleManage'), true); assert.ok(h.access.accessMenus.some(menu => menu.children?.some(item => item.path === '/system/roles'))); await h.router.push('/system/roles'); const current = h.router.currentRoute.value; const record = h.router.getRoutes().find(route => route.name === 'RoleManage'); const component = record.components.default; const cachedDraft = { component: vue.markRaw({ draft: 'unsaved role form' }), key: '/system/roles', route: vue.markRaw(current) }; h.tabbar.cachedRoutes.set('/system/roles', cachedDraft); await h.poll(info(['role:read', 'role:create'])); assert.equal(createButton.value, true); assert.equal(h.router.currentRoute.value, current, 'authorized current route was navigated/replaced'); assert.equal(h.router.getRoutes().find(route => route.name === 'RoleManage').components.default, component); assert.equal(h.tabbar.cachedRoutes.get('/system/roles').component.draft, 'unsaved role form'); await h.poll(info(['role:read'])); assert.equal(createButton.value, false); assert.equal(h.router.currentRoute.value, current); assert.equal(h.access.accessToken, 'same-session'); await h.poll(info([])); assert.equal(h.router.hasRoute('RoleManage'), false); assert.equal(h.router.hasRoute('SystemCenter'), false, 'fully revoked branch survived'); assert.equal(h.router.resolve('/system/roles').name, 'FallbackNotFound'); assert.ok(!h.access.accessMenus.some(menu => menu.path === '/system')); assert.equal(h.router.currentRoute.value.path, '/group/welcome'); assert.equal(h.access.accessToken, 'same-session'); assert.equal(h.tabbar.cachedRoutes.has('/system/roles'), false); } finally { h.dispose(); } }); test('grant/revoke siblings preserves an authorized current matcher, component and pinned tab', async () => { const h = await harness(['user:read']); try { await h.router.push('/system/users'); const current = h.router.currentRoute.value; const components = h.router.getRoutes().find(route => route.name === 'UserManage').components; h.tabbar.addTab({ ...current, key: '/system/users', meta: { ...current.meta, affixTab: true, keepAlive: true } }); await h.poll(info(['user:read', 'audit:read'])); assert.equal(h.router.hasRoute('AuditLog'), true); assert.equal(h.router.currentRoute.value, current); assert.equal(h.router.getRoutes().find(route => route.name === 'UserManage').components, components); const audit = h.router.resolve('/system/audit'); h.tabbar.addTab({ ...audit, key: '/system/audit', meta: { ...audit.meta, affixTab: true, keepAlive: true } }); h.tabbar.cachedRoutes.set('/system/audit', { key: '/system/audit', route: vue.markRaw(audit), component: vue.markRaw({}) }); await h.poll(info(['user:read'])); assert.equal(h.router.hasRoute('AuditLog'), false); assert.equal(h.router.currentRoute.value, current); assert.deepEqual(normalized(h.tabbar.tabs.map(tab => tab.path)), ['/system/users']); assert.equal(h.tabbar.cachedTabs.has('AuditLog'), false); assert.equal(h.tabbar.cachedRoutes.has('/system/audit'), false); assert.equal(h.tabbar.visitHistory.size, 1); } finally { h.dispose(); } }); test('equal/reordered permissions refresh profile without rebuilding menus; hidden polls pause and focus resumes', async () => { const h = await harness(['role:read', 'role:create']); try { const before = h.preparations(); await h.poll({ ...info(['role:create', 'role:read', 'role:read']), realName: 'Changed display name' }); assert.equal(h.preparations(), before); assert.equal(h.user.userInfo.realName, 'Changed display name'); assert.deepEqual([...h.env.timers.values()].map(timer => timer.delay), [3000]); const requests = h.requests.length; h.env.visible(false); h.env.tick(); h.env.focus(); await settle(); assert.equal(h.requests.length, requests); h.answer(info([])); h.env.visible(true); await settle(); assert.equal(h.router.hasRoute('RoleManage'), false); assert.equal(h.requests.length, requests + 1); h.answer(info(['role:read'])); h.env.focus(); await settle(); assert.equal(h.router.hasRoute('RoleManage'), true); h.dispose(); const disposed = h.requests.length; h.env.tick(); h.env.focus(); h.env.visible(true); await settle(); assert.equal(h.env.timers.size, 0); assert.equal(h.requests.length, disposed); } finally { h.dispose(); } }); test('concurrent events coalesce; logout disposes; old account response cannot overwrite a new session', async () => { const h = await harness(); try { const old = deferred(); h.answer(() => old.promise); const before = h.requests.length; h.env.tick(); h.env.focus(); h.env.visible(true); h.env.tick(); assert.equal(h.requests.length, before + 1); h.access.setAccessToken(null); assert.equal(h.env.timers.size, 0); h.user.setUserInfo(info([], '8')); h.access.setAccessCodes([]); h.answer(info(['role:read'], '8')); h.access.setAccessToken('new-session'); await settle(); assert.equal(h.router.hasRoute('RoleManage'), true); old.resolve(info(['user:read', 'role:create'], '7')); await settle(); assert.equal(h.access.accessToken, 'new-session'); assert.equal(h.user.userInfo.userId, '8'); assert.deepEqual(normalized(h.access.accessCodes), ['role:read']); assert.equal(h.router.hasRoute('UserManage'), false); assert.equal(h.env.timers.size, 1); } finally { h.dispose(); } }); test('wrong identity and late preparation are ignored; failed preparation can recover', async () => { const env = environment(); let session = { token: 'A', userId: '7' }; let answer = info(['role:read'], '8'); const stagedSnapshot = deferred(); let prepares = 0, applied = 0; let prepare = async () => { prepares += 1; return stagedSnapshot.promise; }; const sync = syncModule.createAuthorizationSync({ ...env, getSession: () => session, getCodes: () => [], fetch: async () => answer, prepare: value => prepare(value), apply() { applied += 1; } }); sync.start(); await sync.refresh(); assert.equal(prepares, 0); answer = info(['role:read']); const pending = sync.refresh(); await Promise.resolve(); session = { token: 'B', userId: '9' }; stagedSnapshot.resolve({}); await pending; assert.equal(applied, 0); sync.stop(); session = { token: 'A', userId: '7' }; prepare = async () => { throw new Error('Temporary menu read failure'); }; sync.start(); await sync.refresh(); assert.equal(applied, 0); prepare = async () => ({}); await sync.refresh(); assert.equal(applied, 1); sync.stop(); }); test('reinstall is singleton and an old disposer cannot stop its replacement', async () => { const h = await harness(); const replacement = h.adapter.installAuthorizationSync(h.router); try { h.dispose(); await settle(); assert.equal(h.env.timers.size, 1); h.answer(info(['role:read'])); h.env.focus(); await settle(); assert.equal(h.router.hasRoute('RoleManage'), true); } finally { replacement(); } assert.equal(h.env.timers.size, 0); }); test('actual request auth interceptor ignores an old 401; current 401 retains expiry handling; 403 never retries', async () => { pinia.setActivePinia(pinia.createPinia()); const access = useAccessStore(); access.setAccessToken('new-session'); access.setIsAccessChecked(true); const clients = []; class RequestClient { constructor() { this.responses = []; clients.push(this); } addRequestInterceptor() {} addResponseInterceptor(value) { this.responses.push(value); } } const preset = load(await source('packages/effects/request/src/request-client/preset-interceptors.ts'), { '@vben/locales': { $t: key => key }, '@vben/utils': { isFunction: value => typeof value === 'function' }, axios: require('axios'), }); let logouts = 0; load((await staged('api/request.ts')).replaceAll('import.meta.env.PROD', 'false').replaceAll('import.meta.env', '{}'), { '@vben/hooks': { useAppConfig: () => ({ apiURL: '/api/v2' }) }, '@vben/preferences': { preferences }, '@vben/request': { ...preset, RequestClient }, '@vben/stores': stores, 'ant-design-vue': { message: { error() {} } }, '#/locales/t': { t: key => key }, '#/store': { useAuthStore: () => ({ logout: async () => { logouts += 1; } }) }, '#/utils/authorization-sync': syncModule, './core': { refreshTokenApi: async () => { throw new Error('No refresh token'); } }, './error-message': { responseErrorMessage: () => 'Error' }, }); const authentication = clients[0].responses[1]; const error = (status, token) => ({ config: { headers: { Authorization: `Bearer ${token}` } }, response: { status } }); await assert.rejects(authentication.rejected(error(401, 'old-session'))); assert.equal(access.accessToken, 'new-session'); assert.equal(access.loginExpired, false); await assert.rejects(authentication.rejected(error(403, 'new-session'))); assert.equal(access.accessToken, 'new-session'); assert.equal(logouts, 0); await assert.rejects(authentication.rejected(error(401, 'new-session'))); assert.equal(access.accessToken, null); assert.equal(access.loginExpired, true); });