import { PermissionGuard } from './permission.guard'; import { CaslAbilityFactory } from '../../authorization/casl-ability.factory'; import { CaslAction, permissionCodeSubject } from '../../authorization/casl.constants'; describe('PermissionGuard', () => { const abilityFactory = new CaslAbilityFactory(); /** Mock NestJS ExecutionContext with reflector overrides */ function createContext( user: unknown, overrides: { isPublic?: boolean; authenticatedOnly?: boolean; permissions?: string[]; checkPolicies?: unknown[]; } = {}, ) { const meta = new Map(); if (overrides.isPublic !== undefined) meta.set('isPublic', overrides.isPublic); if (overrides.authenticatedOnly !== undefined) meta.set('authenticatedOnly', overrides.authenticatedOnly); if (overrides.permissions) meta.set('permissions', overrides.permissions); if (overrides.checkPolicies) meta.set('check_policies', overrides.checkPolicies); const reflector = { getAllAndOverride: jest.fn((key: string) => meta.get(key) ?? undefined), getAllAndMerge: jest.fn((key: string) => meta.get(key) ?? []), }; const guard = new PermissionGuard(reflector as never, abilityFactory); return guard.canActivate({ getHandler: () => function handler() {}, getClass: () => class Controller {}, switchToHttp: () => ({ getRequest: () => ({ user }) }), } as never); } // ----------------------------------------------------------------------- // @Public / @Authenticated / undeclared // ----------------------------------------------------------------------- it('denies routes that forgot to declare permissions', () => { expect(createContext(undefined)).toBe(false); expect(createContext({ permissions: [], isSuperAdmin: false })).toBe(false); }); it('allows explicitly public routes without a user', () => { expect(createContext(undefined, { isPublic: true })).toBe(true); }); it('allows authenticated-only routes for logged-in users', () => { expect( createContext({ permissions: [], isSuperAdmin: false }, { authenticatedOnly: true }), ).toBe(true); }); it('denies authenticated-only routes when no user is present', () => { expect(createContext(undefined, { authenticatedOnly: true })).toBe(false); }); it('does not let controller-level @Authenticated bypass handler permissions', () => { expect( createContext( { permissions: [], isSuperAdmin: false }, { authenticatedOnly: true, permissions: ['user:edit'] }, ), ).toBe(false); }); it('still enforces policies when @Authenticated and @CheckPolicies coexist', () => { expect( createContext( { permissions: [], isSuperAdmin: false }, { authenticatedOnly: true, checkPolicies: [(ability: any) => ability.can('read', 'Student')], }, ), ).toBe(true); }); // ----------------------------------------------------------------------- // @CheckPolicies passthrough // ----------------------------------------------------------------------- it('allows pass-through for @CheckPolicies routes with user present', () => { expect( createContext( { permissions: [], isSuperAdmin: false }, { checkPolicies: [(ab: any) => ab.can('read', 'Student')] }, ), ).toBe(true); }); it('denies pass-through for @CheckPolicies routes without user', () => { expect( createContext(undefined, { checkPolicies: [(ab: any) => ab.can('read', 'Student')], }), ).toBe(false); }); // ----------------------------------------------------------------------- // CASL exact-code authorization (collision-free) // ----------------------------------------------------------------------- it('grants access to super admin for any permission', () => { expect( createContext({ permissions: [], isSuperAdmin: true }, { permissions: ['student:view'] }), ).toBe(true); }); it('grants access when user has the exact required permission', () => { expect( createContext( { permissions: ['student:view'], isSuperAdmin: false }, { permissions: ['student:view'] }, ), ).toBe(true); }); it('denies access when user lacks the required permission', () => { expect( createContext( { permissions: ['student:view'], isSuperAdmin: false }, { permissions: ['class:delete'] }, ), ).toBe(false); }); it('denies access when user has no permissions', () => { expect( createContext({ permissions: [], isSuperAdmin: false }, { permissions: ['student:view'] }), ).toBe(false); }); it('denies unknown permission codes (no user holds them)', () => { expect( createContext( { permissions: ['unknown:action'], isSuperAdmin: false }, { permissions: ['other:thing'] }, ), ).toBe(false); }); it('grants exact-code access for custom permissions', () => { expect( createContext( { permissions: ['custom:special'], isSuperAdmin: false }, { permissions: ['custom:special'] }, ), ).toBe(true); }); it('grants with OR matching: one of multiple required permissions', () => { expect( createContext( { permissions: ['class:view'], isSuperAdmin: false }, { permissions: ['student:delete', 'class:view'] }, ), ).toBe(true); }); // ── Collision regression tests ── it('denies bill:export-excel when user only has bill:view', () => { const ability = abilityFactory.createForUser({ permissions: ['bill:view'], isSuperAdmin: false, }); // Domain layer: both would give read Bill — but exact-code check must discriminate expect(ability.can(CaslAction.Read, 'Bill')).toBe(true); // Exact-code check: bill:view user must NOT have bill:export-excel expect(ability.can(CaslAction.Access, permissionCodeSubject('bill:export-excel'))).toBe(false); }); it('denies bill:confirm when user only has bill:view', () => { const ability = abilityFactory.createForUser({ permissions: ['bill:view'], isSuperAdmin: false, }); // Domain layer: confirm → update, view → read — already distinct at domain level expect(ability.can(CaslAction.Update, 'Bill')).toBe(false); // Exact-code: must also fail expect(ability.can(CaslAction.Access, permissionCodeSubject('bill:confirm'))).toBe(false); }); it('denies deposit:approve when user only has deposit:edit', () => { const ability = abilityFactory.createForUser({ permissions: ['deposit:edit'], isSuperAdmin: false, }); // Domain layer: both map to update — would pass domain check expect(ability.can(CaslAction.Update, 'Deposit')).toBe(true); // Exact-code: must fail — edit is not approve expect(ability.can(CaslAction.Access, permissionCodeSubject('deposit:approve'))).toBe(false); }); it('denies attendance:export when user only has attendance:view', () => { const ability = abilityFactory.createForUser({ permissions: ['attendance:view'], isSuperAdmin: false, }); // Domain layer: both map to read expect(ability.can(CaslAction.Read, 'Attendance')).toBe(true); // Exact-code: must fail expect(ability.can(CaslAction.Access, permissionCodeSubject('attendance:export'))).toBe(false); }); it('unknown code student:nuke does not create domain ability', () => { const ability = abilityFactory.createForUser({ permissions: ['student:nuke'], isSuperAdmin: false, }); expect(ability.can(CaslAction.Manage, 'Student')).toBe(false); expect(ability.can(CaslAction.Read, 'Student')).toBe(false); expect(ability.can(CaslAction.Access, permissionCodeSubject('student:nuke'))).toBe(true); }); });