forked from wangziqi/gongxue-base
181 lines
6.8 KiB
TypeScript
181 lines
6.8 KiB
TypeScript
import { ForbiddenException } from '@nestjs/common';
|
|
import { AuthorizationService } from './authorization.service';
|
|
import { CaslAbilityFactory } from './casl-ability.factory';
|
|
import { CaslAction, SubjectName } from './casl.constants';
|
|
import { AuthenticatedUser } from './interfaces';
|
|
|
|
describe('AuthorizationService', () => {
|
|
const factory = new CaslAbilityFactory();
|
|
const service = new AuthorizationService(factory);
|
|
|
|
const superAdmin: AuthenticatedUser = {
|
|
id: 1,
|
|
username: 'admin',
|
|
permissions: [],
|
|
isSuperAdmin: true,
|
|
roles: ['超管'],
|
|
};
|
|
|
|
const teacher: AuthenticatedUser = {
|
|
id: 2,
|
|
username: 'teacher',
|
|
permissions: ['student:view', 'class:view'],
|
|
isSuperAdmin: false,
|
|
roles: ['老师'],
|
|
};
|
|
|
|
const emptyUserReq = (user: AuthenticatedUser) => ({
|
|
user,
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// HTTP convenience methods
|
|
// -----------------------------------------------------------------------
|
|
|
|
describe('can() — HTTP request convenience', () => {
|
|
it('returns true for super admin on any action/subject', () => {
|
|
expect(service.can(emptyUserReq(superAdmin), CaslAction.Manage, SubjectName.Student)).toBe(
|
|
true,
|
|
);
|
|
expect(service.can(emptyUserReq(superAdmin), CaslAction.Delete, 'all')).toBe(true);
|
|
});
|
|
|
|
it('returns true for user with matching permission', () => {
|
|
expect(service.can(emptyUserReq(teacher), CaslAction.Read, SubjectName.Student)).toBe(true);
|
|
});
|
|
|
|
it('returns false for user without matching permission', () => {
|
|
expect(service.can(emptyUserReq(teacher), CaslAction.Create, SubjectName.Student)).toBe(
|
|
false,
|
|
);
|
|
expect(service.can(emptyUserReq(teacher), CaslAction.Read, SubjectName.Bill)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('assert() — HTTP request convenience', () => {
|
|
it('does not throw for super admin', () => {
|
|
expect(() =>
|
|
service.assert(emptyUserReq(superAdmin), CaslAction.Delete, SubjectName.Room),
|
|
).not.toThrow();
|
|
});
|
|
|
|
it('does not throw for user with permission', () => {
|
|
expect(() =>
|
|
service.assert(emptyUserReq(teacher), CaslAction.Read, SubjectName.Student),
|
|
).not.toThrow();
|
|
});
|
|
|
|
it('throws ForbiddenException for user without permission', () => {
|
|
expect(() =>
|
|
service.assert(emptyUserReq(teacher), CaslAction.Create, SubjectName.Student),
|
|
).toThrow(ForbiddenException);
|
|
});
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Non-HTTP reuse (Agent Tool / background job pattern)
|
|
// -----------------------------------------------------------------------
|
|
|
|
describe('abilityForRequest()', () => {
|
|
it('builds ability from request.user', () => {
|
|
const ability = service.abilityForRequest(emptyUserReq(teacher));
|
|
expect(ability.can(CaslAction.Read, SubjectName.Class)).toBe(true);
|
|
expect(ability.can(CaslAction.Delete, SubjectName.Student)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('canAbility() / assertAbility() — non-HTTP usage', () => {
|
|
const ability = factory.createForUser(teacher);
|
|
|
|
it('canAbility returns boolean', () => {
|
|
expect(service.canAbility(ability, CaslAction.Read, SubjectName.Student)).toBe(true);
|
|
expect(service.canAbility(ability, CaslAction.Delete, SubjectName.Student)).toBe(false);
|
|
});
|
|
|
|
it('assertAbility throws on denial', () => {
|
|
expect(() =>
|
|
service.assertAbility(ability, CaslAction.Read, SubjectName.Student),
|
|
).not.toThrow();
|
|
|
|
expect(() => service.assertAbility(ability, CaslAction.Delete, SubjectName.Student)).toThrow(
|
|
ForbiddenException,
|
|
);
|
|
});
|
|
|
|
it('canAbility and assertAbility work independently of HTTP context', () => {
|
|
// This is the key Agent Tool pattern:
|
|
// 1. Build ability from a user object (no req needed)
|
|
const toolAbility = factory.createForUser({
|
|
permissions: ['attendance:view', 'attendance:create'],
|
|
isSuperAdmin: false,
|
|
});
|
|
|
|
// 2. Check / assert using the service
|
|
expect(service.canAbility(toolAbility, CaslAction.Read, SubjectName.Attendance)).toBe(true);
|
|
expect(service.canAbility(toolAbility, CaslAction.Create, SubjectName.Attendance)).toBe(true);
|
|
expect(service.canAbility(toolAbility, CaslAction.Delete, SubjectName.Attendance)).toBe(
|
|
false,
|
|
);
|
|
|
|
// 3. assertAbility for write operations
|
|
expect(() =>
|
|
service.assertAbility(toolAbility, CaslAction.Create, SubjectName.Attendance),
|
|
).not.toThrow();
|
|
expect(() =>
|
|
service.assertAbility(toolAbility, CaslAction.Delete, SubjectName.Attendance),
|
|
).toThrow(ForbiddenException);
|
|
});
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// canPermission() / assertPermission() — exact-code permission checks
|
|
// -----------------------------------------------------------------------
|
|
|
|
describe('canPermission() / assertPermission() — exact-code checks', () => {
|
|
const ability = factory.createForUser(teacher);
|
|
|
|
it('canPermission returns true for owned exact permission code', () => {
|
|
expect(service.canPermission(ability, 'student:view')).toBe(true);
|
|
expect(service.canPermission(ability, 'class:view')).toBe(true);
|
|
});
|
|
|
|
it('canPermission returns false for unowned exact permission code', () => {
|
|
expect(service.canPermission(ability, 'student:delete')).toBe(false);
|
|
expect(service.canPermission(ability, 'bill:view')).toBe(false);
|
|
});
|
|
|
|
it('canPermission uses Access + permissionCodeSubject, not domain action', () => {
|
|
// teacher has student:view and class:view. Custom code check is exact.
|
|
expect(service.canPermission(ability, 'student:export')).toBe(false);
|
|
});
|
|
|
|
it('assertPermission does not throw for owned code', () => {
|
|
expect(() => service.assertPermission(ability, 'student:view')).not.toThrow();
|
|
});
|
|
|
|
it('assertPermission throws ForbiddenException for unowned code', () => {
|
|
expect(() => service.assertPermission(ability, 'student:delete')).toThrow(
|
|
ForbiddenException,
|
|
);
|
|
});
|
|
|
|
it('assertPermission error message includes permission code', () => {
|
|
expect(() => service.assertPermission(ability, 'bill:view')).toThrow(
|
|
/bill:view/,
|
|
);
|
|
});
|
|
|
|
it('super admin canPermission returns true for any code', () => {
|
|
const saAbility = factory.createForUser(superAdmin);
|
|
expect(service.canPermission(saAbility, 'student:view')).toBe(true);
|
|
expect(service.canPermission(saAbility, 'custom:action')).toBe(true);
|
|
expect(service.canPermission(saAbility, 'bill:export-excel')).toBe(true);
|
|
});
|
|
|
|
it('super admin assertPermission never throws', () => {
|
|
const saAbility = factory.createForUser(superAdmin);
|
|
expect(() => service.assertPermission(saAbility, 'ghost:action')).not.toThrow();
|
|
});
|
|
});
|
|
});
|