import { describe, expect, it } from 'vitest'; import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { MOBILE_BUSINESS_REGISTRY, buildMenuPathSet, canAccessUniPage, getAuthorizedBusinessEntries, hasButtonPermission, } from '@/permissions/mobileMenuRegistry'; describe('mobile menu permissions', () => { const menuTree = [ { id: 1, path: '/aidop', children: [ { id: 2, path: '/aidop/flowManage/approvalFlowCenter', title: '审批中心' }, { id: 3, path: '/aidop/smart-ops/s3', title: 'S3 看板' }, { id: 4, path: '/aidop/s5/inventory/stock-query', title: '库存查询' }, ], }, ]; it('registers every implemented mobile business family', () => { expect(MOBILE_BUSINESS_REGISTRY.map((entry) => entry.key)).toEqual([ 'approval', 's8', 'kanban-S1', 'kanban-S2', 'kanban-S3', 'kanban-S4', 'kanban-S5', 'kanban-S6', 'kanban-S7', 'kanban-S9', 'stock', 'workorder', 'delivery', 'fqc', ]); }); it('flattens nested loginMenuTree paths and exposes only implemented authorized entries', () => { const paths = buildMenuPathSet(menuTree); expect(paths.has('/aidop/smart-ops/s3')).toBe(true); expect(getAuthorizedBusinessEntries(paths).map((entry) => entry.key)).toEqual([ 'approval', 'kanban-S3', 'stock', ]); }); it('keeps fixed pages available and fails closed for unloaded or unauthorized business pages', () => { const paths = buildMenuPathSet(menuTree); expect(canAccessUniPage('/pages/index/index', new Set(), false)).toBe(true); expect(canAccessUniPage('/pages/message/index', new Set(), false)).toBe(true); expect(canAccessUniPage('/pages/todo/index', paths, true)).toBe(true); expect(canAccessUniPage('/pages/approval/detail?taskId=1', paths, true)).toBe(true); expect(canAccessUniPage('/pages/s8/dashboard', paths, true)).toBe(false); expect(canAccessUniPage('/pages/s8/detail?id=1', paths, true)).toBe(false); expect(canAccessUniPage('/pages/query/fqc?id=1', paths, true)).toBe(false); expect(canAccessUniPage('/pages/query/stock', paths, false)).toBe(false); }); it('separates S8 dashboard and exception-list permissions', () => { const dashboardOnly = buildMenuPathSet([{ id: 1, path: '/aidop/s8/dashboard' }]); const exceptionsOnly = buildMenuPathSet([{ id: 2, path: '/aidop/s8/exceptions' }]); expect(canAccessUniPage('/pages/s8/dashboard', dashboardOnly, true)).toBe(true); expect(canAccessUniPage('/pages/s8/list', dashboardOnly, true)).toBe(false); expect(canAccessUniPage('/pages/s8/detail?id=1', exceptionsOnly, true)).toBe(true); expect(canAccessUniPage('/pages/s8/dashboard', exceptionsOnly, true)).toBe(false); expect(getAuthorizedBusinessEntries(exceptionsOnly).find((entry) => entry.key === 's8')?.uniPage) .toBe('/pages/s8/list'); }); it('guards kanban deep links by the requested module', () => { const paths = buildMenuPathSet(menuTree); expect(canAccessUniPage('/pages/kanban/index?module=S3', paths, true)).toBe(true); expect(canAccessUniPage('/pages/kanban/index?module=S2', paths, true)).toBe(false); expect(canAccessUniPage('/pages/kanban/index', paths, true)).toBe(false); }); it('treats an empty or missing buttons list as no permission', () => { expect(hasButtonPermission(undefined, 'sysTenant:changeTenant')).toBe(false); expect(hasButtonPermission([], 'sysTenant:changeTenant')).toBe(false); expect(hasButtonPermission(['sysTenant:changeTenant'], 'sysTenant:changeTenant')).toBe(true); }); it('keeps a page-level guard on every business page for cold-start deep links', () => { const pages = [ 'todo/index.vue', 'approval/detail.vue', 's8/dashboard.vue', 's8/list.vue', 's8/detail.vue', 'kanban/index.vue', 'query/stock.vue', 'query/workorder.vue', 'query/delivery.vue', 'query/fqc.vue', ]; for (const page of pages) { const source = readFileSync(resolve(process.cwd(), 'src/pages', page), 'utf8'); expect(source, page).toContain('requireMobilePageAccess'); } }); });