import { subject } from '@casl/ability'; import { CaslAbilityFactory } from './casl-ability.factory'; import { CaslAction, SubjectName, permissionCodeSubject, mapPermissionCode, isKnownPermissionCode, } from './casl.constants'; describe('CaslAbilityFactory', () => { const factory = new CaslAbilityFactory(); // ----------------------------------------------------------------------- it('grants manage all for super admin regardless of permissions list', () => { const ability = factory.createForUser({ permissions: [], isSuperAdmin: true, }); expect(ability.can(CaslAction.Manage, 'all')).toBe(true); expect(ability.can(CaslAction.Read, SubjectName.Student)).toBe(true); expect(ability.can(CaslAction.Delete, SubjectName.Class)).toBe(true); }); it('super admin ability can manage arbitrary subject strings', () => { const ability = factory.createForUser({ permissions: [], isSuperAdmin: true }); expect(ability.can(CaslAction.Manage, 'FictionalEntity')).toBe(true); }); // ----------------------------------------------------------------------- // Exact-code permissions (layer 1 — collision-free) // ----------------------------------------------------------------------- it('grants exact-code access for specific permission codes', () => { const ability = factory.createForUser({ permissions: ['bill:view'], isSuperAdmin: false, }); expect(ability.can(CaslAction.Access, permissionCodeSubject('bill:view'))).toBe(true); }); it('does NOT grant exact-code access for a different code in same domain', () => { const ability = factory.createForUser({ permissions: ['bill:view'], isSuperAdmin: false, }); // bill:view user should NOT have bill:export-excel exact code expect(ability.can(CaslAction.Access, permissionCodeSubject('bill:export-excel'))).toBe(false); }); it('does NOT allow export to satisfy view or vice versa', () => { const viewUser = factory.createForUser({ permissions: ['attendance:view'], isSuperAdmin: false, }); const exportUser = factory.createForUser({ permissions: ['attendance:export'], isSuperAdmin: false, }); expect(viewUser.can(CaslAction.Access, permissionCodeSubject('attendance:export'))).toBe(false); expect(exportUser.can(CaslAction.Access, permissionCodeSubject('attendance:view'))).toBe(false); }); it('does NOT allow edit to satisfy approve on same resource', () => { const editor = factory.createForUser({ permissions: ['deposit:edit'], isSuperAdmin: false, }); expect(editor.can(CaslAction.Access, permissionCodeSubject('deposit:approve'))).toBe(false); }); it('custom/unknown codes get exact-code ability but NO domain ability', () => { const ability = factory.createForUser({ permissions: ['student:nuke'], isSuperAdmin: false, }); // Exact code should be granted expect(ability.can(CaslAction.Access, permissionCodeSubject('student:nuke'))).toBe(true); // But no domain ability should exist expect(ability.can(CaslAction.Manage, SubjectName.Student)).toBe(false); expect(ability.can(CaslAction.Delete, SubjectName.Student)).toBe(false); expect(ability.can(CaslAction.Read, SubjectName.Student)).toBe(false); expect(ability.can(CaslAction.Update, SubjectName.Student)).toBe(false); expect(ability.can(CaslAction.Create, SubjectName.Student)).toBe(false); }); it('unknown resource custom code gets exact-code but no domain', () => { const ability = factory.createForUser({ permissions: ['custom:action'], isSuperAdmin: false, }); expect(ability.can(CaslAction.Access, permissionCodeSubject('custom:action'))).toBe(true); // No domain ability for unknown resource const hasAnyDomain = [SubjectName.Student, SubjectName.Bill, SubjectName.Class].some((s) => ability.can(CaslAction.Read, s), ); expect(hasAnyDomain).toBe(false); }); // ----------------------------------------------------------------------- // Domain-level permissions (layer 2 — for service scoping) // ----------------------------------------------------------------------- it('maps student:view to domain read Student', () => { const ability = factory.createForUser({ permissions: ['student:view'], isSuperAdmin: false, }); expect(ability.can(CaslAction.Read, SubjectName.Student)).toBe(true); expect(ability.can(CaslAction.Update, SubjectName.Student)).toBe(false); expect(ability.can(CaslAction.Delete, SubjectName.Student)).toBe(false); }); it('maps student:edit to domain update Student', () => { const ability = factory.createForUser({ permissions: ['student:edit'], isSuperAdmin: false, }); expect(ability.can(CaslAction.Update, SubjectName.Student)).toBe(true); expect(ability.can(CaslAction.Read, SubjectName.Student)).toBe(false); }); it('maps student:create to domain create Student', () => { const ability = factory.createForUser({ permissions: ['student:create'], isSuperAdmin: false, }); expect(ability.can(CaslAction.Create, SubjectName.Student)).toBe(true); }); it('maps student:delete to domain delete Student', () => { const ability = factory.createForUser({ permissions: ['student:delete'], isSuperAdmin: false, }); expect(ability.can(CaslAction.Delete, SubjectName.Student)).toBe(true); }); it('does not broaden occupancy:checkin into generic create ability', () => { const ability = factory.createForUser({ permissions: ['occupancy:checkin'], isSuperAdmin: false, }); expect(ability.can(CaslAction.Create, SubjectName.Occupancy)).toBe(false); }); it('does not broaden bill:export-excel into generic read ability', () => { const ability = factory.createForUser({ permissions: ['bill:export-excel'], isSuperAdmin: false, }); expect(ability.can(CaslAction.Read, SubjectName.Bill)).toBe(false); // But exact code is separate expect(ability.can(CaslAction.Access, permissionCodeSubject('bill:export-excel'))).toBe(true); expect(ability.can(CaslAction.Access, permissionCodeSubject('bill:view'))).toBe(false); }); it('cumulative: multiple permissions all apply at both layers', () => { const ability = factory.createForUser({ permissions: ['student:view', 'room:create', 'bill:delete'], isSuperAdmin: false, }); // Domain layer expect(ability.can(CaslAction.Read, SubjectName.Student)).toBe(true); expect(ability.can(CaslAction.Create, SubjectName.Room)).toBe(true); expect(ability.can(CaslAction.Delete, SubjectName.Bill)).toBe(true); // Exact-code layer expect(ability.can(CaslAction.Access, permissionCodeSubject('student:view'))).toBe(true); expect(ability.can(CaslAction.Access, permissionCodeSubject('room:create'))).toBe(true); expect(ability.can(CaslAction.Access, permissionCodeSubject('bill:delete'))).toBe(true); }); it('recognizes CASL subject instances instead of treating them as all', () => { const ability = factory.createForUser({ permissions: ['student:view'], isSuperAdmin: false, }); const student = subject(SubjectName.Student, { id: 1, classId: 7 }); expect(ability.can(CaslAction.Read, student)).toBe(true); expect(ability.can(CaslAction.Update, student)).toBe(false); }); it('does not let special exact-code permissions grant generic CRUD policies', () => { const ability = factory.createForUser({ permissions: ['student:import', 'deposit:approve', 'bill:confirm'], isSuperAdmin: false, }); expect(ability.can(CaslAction.Create, SubjectName.Student)).toBe(false); expect(ability.can(CaslAction.Update, SubjectName.Deposit)).toBe(false); expect(ability.can(CaslAction.Update, SubjectName.Bill)).toBe(false); }); it('does not broaden archive into generic delete ability', () => { const ability = factory.createForUser({ permissions: ['student:archive'], isSuperAdmin: false, }); expect(ability.can(CaslAction.Delete, SubjectName.Student)).toBe(false); expect(ability.can(CaslAction.Access, permissionCodeSubject('student:archive'))).toBe(true); }); // ----------------------------------------------------------------------- // Unknown permission codes // ----------------------------------------------------------------------- it('silently ignores unknown codes for domain but grants exact-code access', () => { const ability = factory.createForUser({ permissions: ['unknown:stuff', 'student:view'], isSuperAdmin: false, }); // Domain: only known code applies expect(ability.can(CaslAction.Read, SubjectName.Student)).toBe(true); // Exact: both codes get access expect(ability.can(CaslAction.Access, permissionCodeSubject('student:view'))).toBe(true); expect(ability.can(CaslAction.Access, permissionCodeSubject('unknown:stuff'))).toBe(true); }); // ----------------------------------------------------------------------- // Profile auto-grant // ----------------------------------------------------------------------- it('does not grant broad Profile read without an explicit permission', () => { const ability = factory.createForUser({ permissions: [], isSuperAdmin: false, }); expect(ability.can(CaslAction.Read, SubjectName.Profile)).toBe(false); // No exact-code access either expect(ability.can(CaslAction.Access, permissionCodeSubject('profile:view'))).toBe(false); }); // ----------------------------------------------------------------------- // createFromPermissions helper // ----------------------------------------------------------------------- it('createFromPermissions helper works for tests', () => { const ability = factory.createFromPermissions(['student:view']); expect(ability.can(CaslAction.Read, SubjectName.Student)).toBe(true); expect(ability.can(CaslAction.Access, permissionCodeSubject('student:view'))).toBe(true); }); it('createFromPermissions helper supports isSuperAdmin flag', () => { const ability = factory.createFromPermissions([], true); expect(ability.can(CaslAction.Manage, 'all')).toBe(true); }); }); // --------------------------------------------------------------------------- // Standalone mapping function tests // --------------------------------------------------------------------------- describe('mapPermissionCode', () => { it('maps known codes', () => { expect(mapPermissionCode('student:view')).toEqual({ action: CaslAction.Read, subject: SubjectName.Student, }); }); it('returns null for unknown resource', () => { expect(mapPermissionCode('ghost:action')).toBeNull(); }); it('returns null for empty string', () => { expect(mapPermissionCode('')).toBeNull(); }); it('does not map occupancy:checkin to a generic domain action', () => { expect(mapPermissionCode('occupancy:checkin')).toBeNull(); }); it('does not map bill:export-excel to generic read', () => { expect(mapPermissionCode('bill:export-excel')).toBeNull(); }); it('does not map bill:generate to generic update', () => { expect(mapPermissionCode('bill:generate')).toBeNull(); }); it('does not map deposit:approve to generic update', () => { expect(mapPermissionCode('deposit:approve')).toBeNull(); }); it('does not map attendance:export to generic read', () => { expect(mapPermissionCode('attendance:export')).toBeNull(); }); it('does not map archive to generic delete', () => { expect(mapPermissionCode('student:archive')).toBeNull(); }); it('returns null for unknown action on known resource (student:nuke)', () => { expect(mapPermissionCode('student:nuke')).toBeNull(); }); }); describe('isKnownPermissionCode', () => { it('recognizes known codes', () => { expect(isKnownPermissionCode('student:view')).toBe(true); }); it('rejects unknown resources', () => { expect(isKnownPermissionCode('ghost:action')).toBe(false); }); });