import { PoliciesGuard } from './policies.guard'; import { CaslAbilityFactory } from '../casl-ability.factory'; import { AppAbility, IPolicyHandler } from '../interfaces'; describe('PoliciesGuard', () => { const factory = new CaslAbilityFactory(); /** Build a mock NestJS ExecutionContext for PoliciesGuard */ function createContext( user: unknown, opts: { policyHandlers?: Array<((ability: AppAbility) => boolean) | IPolicyHandler> | null; controllerPolicyHandlers?: Array<((ability: AppAbility) => boolean) | IPolicyHandler>; isPublic?: boolean; } = {}, ) { const meta = new Map(); if (opts.policyHandlers !== undefined) meta.set('check_policies', opts.policyHandlers); if (opts.isPublic !== undefined) meta.set('isPublic', opts.isPublic); const reflector = { getAllAndOverride: jest.fn((key: string) => meta.get(key) ?? undefined), getAllAndMerge: jest.fn((key: string) => { if (key !== 'check_policies') return []; return [...(opts.policyHandlers ?? []), ...(opts.controllerPolicyHandlers ?? [])]; }), }; const guard = new PoliciesGuard(reflector as never, factory); return guard.canActivate({ getHandler: () => function handler() {}, getClass: () => class Controller {}, switchToHttp: () => ({ getRequest: () => ({ user }), }), } as never); } // ----------------------------------------------------------------------- // No @CheckPolicies → pass-through // ----------------------------------------------------------------------- it('passes through when no @CheckPolicies is declared', () => { expect(createContext(undefined)).toBe(true); expect(createContext(null)).toBe(true); }); // ----------------------------------------------------------------------- // @Public interaction // ----------------------------------------------------------------------- it('skips when @Public is declared, even with @CheckPolicies', () => { expect( createContext(undefined, { policyHandlers: [(ability) => ability.can('read', 'Student')], isPublic: true, }), ).toBe(true); }); // ----------------------------------------------------------------------- // User absent // ----------------------------------------------------------------------- it('denies when @CheckPolicies is declared but no user present', () => { expect( createContext(undefined, { policyHandlers: [(ability) => ability.can('read', 'Student')], }), ).toBe(false); }); // ----------------------------------------------------------------------- // Callback handlers // ----------------------------------------------------------------------- it('grants when all policies pass for super admin', () => { expect( createContext( { permissions: [], isSuperAdmin: true }, { policyHandlers: [ (ability) => ability.can('read', 'Student'), (ability) => ability.can('delete', 'Class'), ], }, ), ).toBe(true); }); it('grants when all policies pass for user with correct permissions', () => { expect( createContext( { permissions: ['student:view', 'class:view'], isSuperAdmin: false }, { policyHandlers: [ (ability) => ability.can('read', 'Student'), (ability) => ability.can('read', 'Class'), ], }, ), ).toBe(true); }); it('denies when any policy fails (AND semantics)', () => { expect( createContext( { permissions: ['student:view'], isSuperAdmin: false }, { policyHandlers: [ (ability) => ability.can('read', 'Student'), // passes (ability) => ability.can('delete', 'Student'), // fails ], }, ), ).toBe(false); }); it('empty handlers array passes (no policies to check)', () => { expect( createContext({ permissions: ['student:view'], isSuperAdmin: false }, { policyHandlers: [] }), ).toBe(true); }); it('merges controller and handler policies with AND semantics', () => { expect( createContext( { permissions: ['student:view'], isSuperAdmin: false }, { policyHandlers: [(ability) => ability.can('read', 'Student')], controllerPolicyHandlers: [(ability) => ability.can('read', 'Class')], }, ), ).toBe(false); }); // ----------------------------------------------------------------------- // Class-based handlers // ----------------------------------------------------------------------- it('supports class-based policy handlers', () => { class ReadStudentPolicy implements IPolicyHandler { handle(ability: AppAbility): boolean { return ability.can('read', 'Student'); } } expect( createContext( { permissions: ['student:view'], isSuperAdmin: false }, { policyHandlers: [new ReadStudentPolicy()] }, ), ).toBe(true); }); it('denies when class-based handler fails', () => { class DeleteStudentPolicy implements IPolicyHandler { handle(ability: AppAbility): boolean { return ability.can('delete', 'Student'); } } expect( createContext( { permissions: ['student:view'], isSuperAdmin: false }, { policyHandlers: [new DeleteStudentPolicy()] }, ), ).toBe(false); }); it('mixes callback and class-based handlers', () => { class ReadStudentPolicy implements IPolicyHandler { handle(ability: AppAbility): boolean { return ability.can('read', 'Student'); } } expect( createContext( { permissions: ['student:view', 'class:view'], isSuperAdmin: false }, { policyHandlers: [new ReadStudentPolicy(), (ability) => ability.can('read', 'Class')], }, ), ).toBe(true); }); // ----------------------------------------------------------------------- // Instance-level policy // ----------------------------------------------------------------------- it('custom instance-level policy: checks specific resource conditions', () => { const ability = factory.createForUser({ permissions: ['student:edit'], isSuperAdmin: false, }); const ownsResource = (ab: typeof ability) => ab.can('update', 'Student'); expect(ownsResource(ability)).toBe(true); }); });