forked from wangziqi/gongxue-base
feat: add CASL authorization and AI configuration
This commit is contained in:
180
apps/server/src/authorization/authorization.service.spec.ts
Normal file
180
apps/server/src/authorization/authorization.service.spec.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user