202 lines
6.3 KiB
TypeScript
202 lines
6.3 KiB
TypeScript
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<string, unknown>();
|
|
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);
|
|
});
|
|
});
|