/** * uat-plan/project-roles — pure projection tests (role ordering, route expansion, * URL/strategy rules, RBAC access + endpoint status maps). */ import { describe, it, expect } from 'vitest'; import { canonicalRoleOrder, routeIdFor, componentKeyFor, urlForView, navStrategyForView, expandNavRoute, projectRoutes, projectEndpoints, expectedStatusByRole, okStatusFor, } from '../project-roles.js'; import type { DiscoveredNavRoute, DiscoveredEndpoint } from '../types.js'; const GRANTS = { 'super-admin': [{ path: 'administration', isWildcard: true }], admin: [{ path: 'administration.users', isWildcard: true }], viewer: [{ path: 'administration.users.read', isWildcard: false }], }; const ROLES = ['super-admin', 'admin', 'viewer', 'anonymous']; describe('canonicalRoleOrder', () => { it('orders known roles by privilege, anonymous last, and dedupes', () => { expect(canonicalRoleOrder(['viewer', 'anonymous', 'admin', 'super-admin', 'admin'])).toEqual([ 'super-admin', 'admin', 'viewer', 'anonymous', ]); }); it('sorts unknown roles by ASCII after the known ones', () => { expect(canonicalRoleOrder(['zulu', 'admin', 'alpha'])).toEqual(['admin', 'alpha', 'zulu']); }); }); describe('routeIdFor / componentKeyFor', () => { it('base views map to the bare UPPER_SNAKE key; suffix views append the view', () => { expect(routeIdFor('administration.users', 'list')).toBe('ADMINISTRATION_USERS'); expect(routeIdFor('administration.users', 'section-home')).toBe('ADMINISTRATION_USERS'); expect(routeIdFor('administration.users', 'detail')).toBe('ADMINISTRATION_USERS_DETAIL'); }); it('component key is the bare key for base views, suffixed otherwise', () => { expect(componentKeyFor('a.b', 'list')).toBe('a.b'); expect(componentKeyFor('a.b', 'create')).toBe('a.b.create'); }); }); describe('urlForView / navStrategyForView', () => { it('maps each view to the DynamicRouter URL convention (incl. id-bearing suffixes)', () => { expect(urlForView('/a/b', 'list')).toBe('/a/b'); expect(urlForView('/a/b', 'detail')).toBe('/a/b/:id'); expect(urlForView('/a/b', 'edit')).toBe('/a/b/:id/edit'); expect(urlForView('/a/b', 'create')).toBe('/a/b/create'); expect(urlForView('/a/b', 'settings')).toBe('/a/b/:id/settings'); expect(urlForView('/a/b', 'duplicate')).toBe('/a/b/:id/duplicate'); expect(urlForView('/a/b', 'import')).toBe('/a/b/import'); expect(urlForView('/a/b', 'dashboard')).toBe('/a/b/dashboard'); // unknown → fallback }); it('id-bearing pages click a row in the parent; lists use the menu; create/collection goto', () => { expect(navStrategyForView('detail')).toBe('click_row_in_parent'); expect(navStrategyForView('edit')).toBe('click_row_in_parent'); expect(navStrategyForView('settings')).toBe('click_row_in_parent'); // /:id/settings expect(navStrategyForView('list')).toBe('menu_click'); expect(navStrategyForView('create')).toBe('goto'); expect(navStrategyForView('import')).toBe('goto'); // collection-level }); }); describe('expandNavRoute', () => { const node: DiscoveredNavRoute = { componentKey: 'administration.users', baseRoute: '/administration/users', label: 'Users', permission: 'administration.users.read', navKind: 'section', views: ['list', 'detail'], }; it('emits one route per view with the shared access map and route-level metadata', () => { const routes = expandNavRoute(node, ROLES, GRANTS); expect(routes.map((r) => r.id)).toEqual(['ADMINISTRATION_USERS', 'ADMINISTRATION_USERS_DETAIL']); const [list, detail] = routes; expect(list.access).toEqual({ 'super-admin': 'allowed', admin: 'allowed', viewer: 'allowed', anonymous: 'redirect_login', }); expect(list.expect.title).toBe('Users'); expect(list.subsets).toEqual([]); expect(list.permission).toBe('administration.users.read'); expect(detail.navigation_strategy).toBe('click_row_in_parent'); expect(detail.parent_route).toBe('/administration/users'); expect(detail.on_empty_list).toBe('INDETERMINATE'); }); it('dedupes repeated views', () => { const dup = expandNavRoute({ ...node, views: ['list', 'list'] }, ROLES, GRANTS); expect(dup).toHaveLength(1); }); it('omits permission + title when the node has none', () => { const bare = expandNavRoute( { componentKey: 'public.home', baseRoute: '/public/home', navKind: 'section', views: ['list'] }, ['admin', 'anonymous'], {}, ); expect(bare[0].permission).toBeUndefined(); expect(bare[0].expect.title).toBeUndefined(); // no required permission => any authenticated role allowed expect(bare[0].access).toEqual({ admin: 'allowed', anonymous: 'redirect_login' }); }); }); describe('projectRoutes', () => { it('flattens nodes in discovery order', () => { const nodes: DiscoveredNavRoute[] = [ { componentKey: 'a.users', baseRoute: '/a/users', navKind: 'section', views: ['list', 'detail'] }, { componentKey: 'a.roles', baseRoute: '/a/roles', navKind: 'section', views: ['list'] }, ]; expect(projectRoutes(nodes, ROLES, GRANTS).map((r) => r.id)).toEqual([ 'A_USERS', 'A_USERS_DETAIL', 'A_ROLES', ]); }); }); describe('expectedStatusByRole / projectEndpoints', () => { it('maps anonymous→401, granted→ok, denied→403', () => { expect(expectedStatusByRole(ROLES, 'administration.users.read', GRANTS, 200)).toEqual({ 'super-admin': 200, admin: 200, viewer: 200, anonymous: 401, }); expect(expectedStatusByRole(['x', 'anonymous'], 'administration.users.read', {}, 200)).toEqual({ x: 403, anonymous: 401, }); }); it('okStatusFor defaults to 200 for every verb', () => { expect(okStatusFor('POST')).toBe(200); expect(okStatusFor('DELETE')).toBe(200); }); it('a wildcard grant covers a deeper create permission; success status honours okStatus (201)', () => { const eps: DiscoveredEndpoint[] = [ { method: 'POST', route: '/api/administration/users', controller: 'UsersController', permission: 'administration.users.create', okStatus: 201, }, ]; const out = projectEndpoints(eps, ROLES, GRANTS); expect(out[0].id).toBe('post:administration/users'); expect(out[0].controller).toBe('UsersController'); expect(out[0].expected_by_role).toEqual({ 'super-admin': 201, // wildcard "administration" admin: 201, // wildcard "administration.users" covers .create viewer: 403, // exact "administration.users.read" only anonymous: 401, }); }); it('allowAnonymous endpoints give every role (incl. anonymous) the success status', () => { const eps: DiscoveredEndpoint[] = [{ method: 'GET', route: '/api/public/health', allowAnonymous: true }]; const out = projectEndpoints(eps, ['admin', 'anonymous'], GRANTS); expect(out[0].expected_by_role).toEqual({ admin: 200, anonymous: 200 }); }); });