diff --git a/frontend/src/__tests__/shell/routePermissions.test.ts b/frontend/src/__tests__/shell/routePermissions.test.ts new file mode 100644 index 0000000..373528b --- /dev/null +++ b/frontend/src/__tests__/shell/routePermissions.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +/** + * Welle 3 (Kritikpunkt 7/8): Statische Route-Permissions müssen die + * Backend-/Manifest-Wahrheit spiegeln. Beweist per Source-Inspektion: + * - /communication → comm:read (Backend verlangt comm:read; communication:read + * war eine Phantom-Permission, die nirgends registriert war) + * - /mail/settings → mail:config (Backend mail-settings verlangt mail:config) + * - /import-export → import_export:read (Backend verlangt import_export:read; + * vorher fälschlich contacts:read) + * - /activity → audit:read (Seite nutzt Audit-API; activity:read war Phantom) + * - /wiki → wiki:read (Backend verlangt wiki:read; vorher ungeschützt) + */ +const routerSource = readFileSync( + join(__dirname, '..', '..', 'routes', 'index.tsx'), + 'utf-8', +); + +function routePermission(path: string): string | null { + const marker = `path: '${path}'`; + const idx = routerSource.indexOf(marker); + if (idx === -1) return null; + const lineEnd = routerSource.indexOf('\n', idx); + const line = routerSource.slice(idx, lineEnd); + const match = line.match(/permission="([^"]+)"/); + return match ? match[1] : null; +} + +describe('static route permissions match backend/manifest truth', () => { + it('communication uses comm:read (registered permission)', () => { + expect(routePermission('/communication')).toBe('comm:read'); + }); + + it('mail/settings uses mail:config (backend requires mail:config)', () => { + expect(routePermission('/mail/settings')).toBe('mail:config'); + }); + + it('import-export uses import_export:read (backend requires it)', () => { + expect(routePermission('/import-export')).toBe('import_export:read'); + }); + + it('activity uses audit:read (page calls audit API)', () => { + expect(routePermission('/activity')).toBe('audit:read'); + }); + + it('wiki is protected with wiki:read (backend requires wiki:read)', () => { + expect(routePermission('/wiki')).toBe('wiki:read'); + }); + + it('phantom permissions are gone from the router', () => { + expect(routerSource).not.toContain('communication:read'); + expect(routerSource).not.toContain('activity:read'); + }); +}); diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx index ae7b13d..30593bf 100644 --- a/frontend/src/routes/index.tsx +++ b/frontend/src/routes/index.tsx @@ -252,17 +252,17 @@ const router = createBrowserRouter([ { path: '/dms/trash', element: {withSuspense()} }, { path: '/trash', element: {withSuspense()} }, { path: '/mail', element: {withSuspense()} }, - { path: '/mail/settings', element: {withSuspense()} }, + { path: '/mail/settings', element: {withSuspense()} }, { path: '/reports', element: {withSuspense()} }, { path: '/tasks', element: {withSuspense()} }, - { path: '/communication', element: {withSuspense()} }, + { path: '/communication', element: {withSuspense()} }, { path: '/workflows', element: {withSuspense()} }, { path: '/contacts/dedup', element: {withSuspense()} }, - { path: '/import-export', element: {withSuspense()} }, + { path: '/import-export', element: {withSuspense()} }, { path: 'tags', element: {withSuspense()} }, { path: '/api-docs', element: {withSuspense()} }, - { path: '/activity', element: {withSuspense()} }, - { path: '/wiki', element: withSuspense() }, + { path: '/activity', element: {withSuspense()} }, + { path: '/wiki', element: {withSuspense()} }, { path: '/system-dashboard', element: withSuspense() }, { path: '/profile', element: withSuspense() }, { path: '*', element: {} },