mobileMenuRegistry.spec.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. import { describe, expect, it } from 'vitest';
  2. import { readFileSync } from 'node:fs';
  3. import { resolve } from 'node:path';
  4. import {
  5. MOBILE_BUSINESS_REGISTRY,
  6. buildMenuPathSet,
  7. canAccessUniPage,
  8. getAuthorizedBusinessEntries,
  9. hasButtonPermission,
  10. } from '@/permissions/mobileMenuRegistry';
  11. describe('mobile menu permissions', () => {
  12. const menuTree = [
  13. {
  14. id: 1,
  15. path: '/aidop',
  16. children: [
  17. { id: 2, path: '/aidop/flowManage/approvalFlowCenter', title: '审批中心' },
  18. { id: 3, path: '/aidop/smart-ops/s3', title: 'S3 看板' },
  19. { id: 4, path: '/aidop/s5/inventory/stock-query', title: '库存查询' },
  20. ],
  21. },
  22. ];
  23. it('registers every implemented mobile business family', () => {
  24. expect(MOBILE_BUSINESS_REGISTRY.map((entry) => entry.key)).toEqual([
  25. 'approval',
  26. 's8',
  27. 'kanban-S1',
  28. 'kanban-S2',
  29. 'kanban-S3',
  30. 'kanban-S4',
  31. 'kanban-S5',
  32. 'kanban-S6',
  33. 'kanban-S7',
  34. 'kanban-S9',
  35. 'stock',
  36. 'workorder',
  37. 'delivery',
  38. 'fqc',
  39. ]);
  40. });
  41. it('flattens nested loginMenuTree paths and exposes only implemented authorized entries', () => {
  42. const paths = buildMenuPathSet(menuTree);
  43. expect(paths.has('/aidop/smart-ops/s3')).toBe(true);
  44. expect(getAuthorizedBusinessEntries(paths).map((entry) => entry.key)).toEqual([
  45. 'approval',
  46. 'kanban-S3',
  47. 'stock',
  48. ]);
  49. });
  50. it('keeps fixed pages available and fails closed for unloaded or unauthorized business pages', () => {
  51. const paths = buildMenuPathSet(menuTree);
  52. expect(canAccessUniPage('/pages/index/index', new Set(), false)).toBe(true);
  53. expect(canAccessUniPage('/pages/message/index', new Set(), false)).toBe(true);
  54. expect(canAccessUniPage('/pages/todo/index', paths, true)).toBe(true);
  55. expect(canAccessUniPage('/pages/approval/detail?taskId=1', paths, true)).toBe(true);
  56. expect(canAccessUniPage('/pages/s8/dashboard', paths, true)).toBe(false);
  57. expect(canAccessUniPage('/pages/s8/detail?id=1', paths, true)).toBe(false);
  58. expect(canAccessUniPage('/pages/query/fqc?id=1', paths, true)).toBe(false);
  59. expect(canAccessUniPage('/pages/query/stock', paths, false)).toBe(false);
  60. });
  61. it('separates S8 dashboard and exception-list permissions', () => {
  62. const dashboardOnly = buildMenuPathSet([{ id: 1, path: '/aidop/s8/dashboard' }]);
  63. const exceptionsOnly = buildMenuPathSet([{ id: 2, path: '/aidop/s8/exceptions' }]);
  64. expect(canAccessUniPage('/pages/s8/dashboard', dashboardOnly, true)).toBe(true);
  65. expect(canAccessUniPage('/pages/s8/list', dashboardOnly, true)).toBe(false);
  66. expect(canAccessUniPage('/pages/s8/detail?id=1', exceptionsOnly, true)).toBe(true);
  67. expect(canAccessUniPage('/pages/s8/dashboard', exceptionsOnly, true)).toBe(false);
  68. expect(getAuthorizedBusinessEntries(exceptionsOnly).find((entry) => entry.key === 's8')?.uniPage)
  69. .toBe('/pages/s8/list');
  70. });
  71. it('guards kanban deep links by the requested module', () => {
  72. const paths = buildMenuPathSet(menuTree);
  73. expect(canAccessUniPage('/pages/kanban/index?module=S3', paths, true)).toBe(true);
  74. expect(canAccessUniPage('/pages/kanban/index?module=S2', paths, true)).toBe(false);
  75. expect(canAccessUniPage('/pages/kanban/index', paths, true)).toBe(false);
  76. });
  77. it('treats an empty or missing buttons list as no permission', () => {
  78. expect(hasButtonPermission(undefined, 'sysTenant:changeTenant')).toBe(false);
  79. expect(hasButtonPermission([], 'sysTenant:changeTenant')).toBe(false);
  80. expect(hasButtonPermission(['sysTenant:changeTenant'], 'sysTenant:changeTenant')).toBe(true);
  81. });
  82. it('keeps a page-level guard on every business page for cold-start deep links', () => {
  83. const pages = [
  84. 'todo/index.vue',
  85. 'approval/detail.vue',
  86. 's8/dashboard.vue',
  87. 's8/list.vue',
  88. 's8/detail.vue',
  89. 'kanban/index.vue',
  90. 'query/stock.vue',
  91. 'query/workorder.vue',
  92. 'query/delivery.vue',
  93. 'query/fqc.vue',
  94. ];
  95. for (const page of pages) {
  96. const source = readFileSync(resolve(process.cwd(), 'src/pages', page), 'utf8');
  97. expect(source, page).toContain('requireMobilePageAccess');
  98. }
  99. });
  100. });