/** * lib/sql-discovery — pure-layer tests (row parsing + tree nesting + grant grouping). */ import { describe, it, expect } from 'vitest'; import { parseNavRows, parsePermissionRows, parseRoleCatalogRows, parseRolePermissionRows, buildNavTree, findReplacementCharCorruption, groupGrantsByRole, } from '../sql-discovery.js'; import { parsePipeRows } from '../sqlcmd.js'; const NAV_ROWS = [ ['APP', 'a1', 'administration', 'Administration', '', '/administration', '1'], ['MOD', 'm1', 'users', 'Users', 'a1', '/administration/users', '0'], ['SEC', 's1', 'list', 'List', 'm1', '/administration/users/list', '0'], ['RES', 'r1', 'detail', 'Detail', 's1', '/administration/users/detail', '0'], ['XXX', 'bad', 'noise', '', '', '', '0'], // unknown type ignored ]; describe('parseNavRows', () => { it('maps known TYPE prefixes to kinds and reads the personal flag for apps only', () => { const nodes = parseNavRows(NAV_ROWS); expect(nodes.map((n) => n.kind)).toEqual(['application', 'module', 'section', 'resource']); expect(nodes[0]).toMatchObject({ id: 'a1', code: 'administration', parentId: '', isPersonal: true }); expect(nodes[1]).toMatchObject({ kind: 'module', parentId: 'a1', isPersonal: false }); }); it('parses from raw pipe output end-to-end', () => { const raw = 'APP|a1|administration|Administration||/administration|0'; const nodes = parseNavRows(parsePipeRows(raw)); expect(nodes).toHaveLength(1); expect(nodes[0].isPersonal).toBe(false); }); }); describe('buildNavTree', () => { it('nests modules/sections/resources under their parents', () => { const tree = buildNavTree(parseNavRows(NAV_ROWS)); expect(tree).toHaveLength(1); const app = tree[0]; expect(app.kind).toBe('application'); expect(app.children).toHaveLength(1); expect(app.children[0].kind).toBe('module'); expect(app.children[0].children[0].kind).toBe('section'); expect(app.children[0].children[0].children[0].kind).toBe('resource'); }); it('treats a node with an unknown parent as a root (no orphans dropped)', () => { const orphan = parseNavRows([['MOD', 'm9', 'x', 'X', 'missing', '/x', '0']]); expect(buildNavTree(orphan)).toHaveLength(1); }); }); describe('permissions + role grants', () => { it('parses permission rows with the wildcard flag', () => { const perms = parsePermissionRows([ ['p1', 'administration.users.read', 'read', '1', '0'], ['p2', 'administration.users', '', '0', '1'], ]); expect(perms[0]).toMatchObject({ path: 'administration.users.read', isWildcard: false }); expect(perms[1].isWildcard).toBe(true); }); it('groups role grants by role name into the rbac-matrix grant shape', () => { const grants = parseRolePermissionRows([ ['r1', 'admin', 'core', '1', 'p2', 'administration.users', '1'], ['r1', 'admin', 'core', '1', 'p3', 'administration.roles.read', '0'], ['r2', 'viewer', 'core', '0', 'p4', 'administration.users.read', '0'], ]); const byRole = groupGrantsByRole(grants); expect(Object.keys(byRole).sort()).toEqual(['admin', 'viewer']); expect(byRole.admin).toEqual([ { path: 'administration.users', isWildcard: true }, { path: 'administration.roles.read', isWildcard: false }, ]); expect(byRole.viewer).toEqual([{ path: 'administration.users.read', isWildcard: false }]); }); }); describe('parseRoleCatalogRows', () => { it('parses Id|Name|Code rows — empty Code → no code facet, malformed rows dropped', () => { expect( parseRoleCatalogRows([ ['g-1', 'Gestionnaire de flotte', 'fleet-manager'], ['g-2', 'Platform Administrator', ''], ['g-3', 'API Application Delegate', 'api_account_delegate'], ['', 'orphan-name', ''], ['g-5'], ]), ).toEqual([ { id: 'g-1', name: 'Gestionnaire de flotte', code: 'fleet-manager' }, { id: 'g-2', name: 'Platform Administrator' }, { id: 'g-3', name: 'API Application Delegate', code: 'api_account_delegate' }, ]); }); }); describe('findReplacementCharCorruption', () => { it('flags U+FFFD in nav labels/codes, role names, permission paths and grant role names', () => { const corrupted = findReplacementCharCorruption({ nav: parseNavRows([['APP', 'a1', 'admin', 'Contr�le de gestion', '', '/x', '0']]), roleNames: ['Direction et contr�le de gestion', 'Gestionnaire de flotte'], permissions: parsePermissionRows([['p1', 'administration.d�l�gu�s.read', 'read', '1', '0']]), grants: parseRolePermissionRows([['r1', 'Employ�', 'core', '0', 'p1', 'x.read', '0']]), }); expect(corrupted.map((c) => c.source)).toEqual(['nav', 'role', 'permission', 'grant']); expect(corrupted[1].value).toBe('Direction et contr�le de gestion'); }); it('returns an empty list on healthy (accented, correctly-decoded) data', () => { expect( findReplacementCharCorruption({ nav: parseNavRows([['APP', 'a1', 'admin', 'Contrôle de gestion', '', '/x', '0']]), roleNames: ["Moteur d'alertes et d'échéances"], permissions: [], grants: [], }), ).toEqual([]); }); });