Files
gongxue-base/apps/server/src/authorization/authorization.service.spec.ts

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();
});
});
});