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