import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { describe, expect, it } from 'vitest'; import type { RouteRecordRaw } from 'vue-router'; import routesContract from '../contracts/routes.json'; import { allRoutes } from '../src/router'; import { canAccess, filterMenu, hasRoleAccess } from '../src/store/modules/menu-access'; describe('router and TDesign menu contracts', () => { it('keeps every PAGE route declared and the static deep-link fallback in the router', () => { expect(routesContract.routes.map((route) => route.pageId).sort()).toEqual( Array.from({ length: 23 }, (_, index) => `PAGE-${String(index + 1).padStart(2, '0')}`), ); expect(routesContract.routes.find((route) => route.pageId === 'PAGE-15')?.path).toBe('/finance/payments'); expect(readFileSync(resolve(process.cwd(), 'src/router/index.ts'), 'utf8')).toContain('createWebHistory'); expect(readFileSync(resolve(process.cwd(), 'src/router/modules/system.ts'), 'utf8')).toContain( "{ path: '/:pathMatch(.*)*', redirect: '/result/404' }", ); expect(readFileSync(resolve(process.cwd(), 'src/router/modules/result.ts'), 'utf8')).toContain("path: '403'"); expect(readFileSync(resolve(process.cwd(), 'src/router/modules/system.ts'), 'utf8')).toContain("path: '/setup'"); }); it('keeps only contracted business routes in the production route tree', () => { const flattenedPaths: string[] = []; const collectPaths = (routes: RouteRecordRaw[], parent = '') => { for (const route of routes) { const path = route.path.startsWith('/') ? route.path : `${parent}/${route.path}`.replace(/\/+/g, '/'); flattenedPaths.push(path); if (route.children) collectPaths(route.children, path); } }; collectPaths(allRoutes); expect(flattenedPaths).toContain('/projects/'); expect(flattenedPaths).not.toContain('/dashboard/base'); expect(flattenedPaths).not.toContain('/detail/base'); expect(flattenedPaths).not.toContain('/list/base'); }); it('filters menu leaves with the same role and permission rules used by route access', () => { const projectIdentity = { currentRole: 'PROJECT_MANAGER', permissions: ['project:project:view'] }; const financeIdentity = { currentRole: 'FINANCE_MANAGER', permissions: ['masterdata:company:view', 'receivable:invoice:view'], }; const projectRoute = { path: 'project', meta: { roleCodes: ['PROJECT_MANAGER'], permission: 'project:project:view' }, } as RouteRecordRaw; const financeRoute = { path: 'finance', meta: { roleCodes: ['FINANCE_MANAGER'], permission: 'workflow:task:view' }, } as RouteRecordRaw; const receiptRoute = { path: 'receipts', meta: { roleCodes: ['FINANCE_MANAGER'], permission: ['receivable:receipt:view', 'receivable:invoice:view'] }, } as RouteRecordRaw; const accountingRoute = { path: 'accounting', meta: { roleCodes: ['FINANCE_MANAGER'], permission: ['accounting:event:view', 'accounting:voucher:view'] }, } as RouteRecordRaw; expect(canAccess(projectRoute, projectIdentity)).toBe(true); expect(canAccess(financeRoute, projectIdentity)).toBe(false); expect(canAccess(receiptRoute, financeIdentity)).toBe(true); expect(canAccess(accountingRoute, financeIdentity)).toBe(false); expect(hasRoleAccess(['PROJECT_MANAGER'], 'SYSTEM_ADMIN')).toBe(true); expect( canAccess(accountingRoute, { currentRole: 'SYSTEM_ADMIN', permissions: [], }), ).toBe(true); }); it('allows the isolated system administrator to access every contracted business route', () => { const contractedRoutes: RouteRecordRaw[] = []; const collect = (routes: RouteRecordRaw[]) => { for (const route of routes) { if (typeof route.meta?.pageId === 'string') contractedRoutes.push(route); if (route.children) collect(route.children); } }; collect(allRoutes); const businessPageIds = new Set( routesContract.routes.filter((route) => route.pageType === 'business').map((route) => route.pageId), ); const businessRoutes = contractedRoutes.filter((route) => businessPageIds.has(String(route.meta?.pageId))); expect(new Set(businessRoutes.map((route) => route.meta?.pageId))).toEqual(businessPageIds); expect( businessRoutes.filter((route) => !canAccess(route, { currentRole: 'SYSTEM_ADMIN', permissions: [] })), ).toEqual([]); }); it('sends unauthenticated refreshes to login and denied deep links to the 403 result route', () => { const guardSource = readFileSync(resolve(process.cwd(), 'src/permission.ts'), 'utf8'); const userStoreSource = readFileSync(resolve(process.cwd(), 'src/store/modules/user.ts'), 'utf8'); expect(userStoreSource).toContain("if (!this.authenticated) throw new Error('Session is not authenticated');"); expect(guardSource).toContain("return { path: '/login', query: { redirect: to.fullPath } };"); expect(guardSource).toContain("return '/result/403';"); expect(guardSource).toContain("to.path === '/setup'"); }); it('redirects each visible menu group to its first permitted child', () => { const routes: RouteRecordRaw[] = [ { path: '/finance', children: [ { path: 'master-data', meta: { roleCodes: ['FINANCE_MANAGER'], permission: 'masterdata:company:view' } }, { path: 'contracts-costs', meta: { roleCodes: ['PROJECT_MANAGER'], permission: 'contractcost:ledger:view' } }, ], } as RouteRecordRaw, { path: '/workbench', children: [ { path: 'project', meta: { roleCodes: ['PROJECT_MANAGER'], permission: 'project:project:view' } }, { path: 'finance', meta: { roleCodes: ['FINANCE_MANAGER'], permission: 'workflow:task:view' } }, ], } as RouteRecordRaw, ]; const projectMenu = filterMenu(routes, { currentRole: 'PROJECT_MANAGER', permissions: ['contractcost:ledger:view', 'project:project:view'], }); const financeGroup = projectMenu.find((route) => route.path === '/finance'); expect(financeGroup?.redirect).toBe('/finance/contracts-costs'); const financeMenu = filterMenu(routes, { currentRole: 'FINANCE_MANAGER', permissions: ['workflow:task:view', 'masterdata:company:view'], }); const workbenchGroup = financeMenu.find((route) => route.path === '/workbench'); expect(workbenchGroup?.redirect).toBe('/workbench/finance'); expect(financeMenu.find((route) => route.path === '/finance')?.redirect).toBe('/finance/master-data'); const superAdminMenu = filterMenu(routes, { currentRole: 'SYSTEM_ADMIN', permissions: [], }); expect(superAdminMenu.find((route) => route.path === '/workbench')?.children).toHaveLength(2); expect(superAdminMenu.find((route) => route.path === '/finance')?.redirect).toBe('/finance/master-data'); }); });