import { ForbiddenException, UnauthorizedException } from '@nestjs/common'; import { PermissionService } from './permission.service'; import type { CurrentUser } from './auth-context.service'; describe('PermissionService', () => { const versionFindUnique = jest.fn(); const projectMemberFindUnique = jest.fn(); const appDataFindUnique = jest.fn(); const prisma = { version: { findUnique: versionFindUnique }, projectMember: { findUnique: projectMemberFindUnique }, appData: { findUnique: appDataFindUnique }, } as any; const service = new PermissionService(prisma); beforeEach(() => { jest.clearAllMocks(); appDataFindUnique.mockResolvedValue(null); }); it('allows the built-in super admin wildcard for any permission without a scope lookup', async () => { const user = currentUser({ id: 'm-8', roleId: 'role-admin' }); await expect(service.can(user, 'audit:view', { versionId: 'version-1' })).resolves.toBe(true); expect(versionFindUnique).not.toHaveBeenCalled(); expect(projectMemberFindUnique).not.toHaveBeenCalled(); }); it('denies version work mutations for project members outside the version members list', async () => { versionFindUnique.mockResolvedValue({ id: 'version-1', projectId: 'project-1', members: [] }); projectMemberFindUnique.mockResolvedValue({ projectId: 'project-1', userId: 'dev-1', role: 'member' }); await expect(service.can(currentUser({ id: 'dev-1', roleId: 'role-dev' }), 'version.devtask:manage', { versionId: 'version-1', })).resolves.toBe(false); expect(versionFindUnique).toHaveBeenCalledWith({ where: { id: 'version-1' }, select: { id: true, projectId: true, members: true }, }); expect(projectMemberFindUnique).toHaveBeenCalledWith({ where: { projectId_userId: { projectId: 'project-1', userId: 'dev-1' } }, }); }); it('allows version work mutations when the user has the role permission and is a version member', async () => { versionFindUnique.mockResolvedValue({ id: 'version-1', projectId: 'project-1', members: [{ id: 'dev-1', name: 'Dev One' }], }); projectMemberFindUnique.mockResolvedValue({ projectId: 'project-1', userId: 'dev-1', role: 'member' }); await expect(service.can(currentUser({ id: 'dev-1', name: 'Dev One', roleId: 'role-dev' }), 'version.devtask:manage', { versionId: 'version-1', })).resolves.toBe(true); }); it('uses configured role permissions before built-in role defaults', async () => { appDataFindUnique.mockResolvedValue({ value: { roles: [ { id: 'role-dev', permissions: ['version.devtask:view'] }, ], }, }); versionFindUnique.mockResolvedValue({ id: 'version-1', projectId: 'project-1', members: [{ id: 'dev-1', name: 'Dev One' }], }); projectMemberFindUnique.mockResolvedValue({ projectId: 'project-1', userId: 'dev-1', role: 'member' }); await expect(service.can(currentUser({ id: 'dev-1', name: 'Dev One', roleId: 'role-dev' }), 'version.devtask:manage', { versionId: 'version-1', })).resolves.toBe(false); }); it('allows a version member when no project membership exists', async () => { versionFindUnique.mockResolvedValue({ id: 'version-1', projectId: 'project-1', members: [{ id: 'tester-1', name: 'Tester One' }], }); projectMemberFindUnique.mockResolvedValue(null); await expect(service.can(currentUser({ id: 'tester-1', name: 'Tester One', roleId: 'role-test' }), 'version.testcase:manage', { versionId: 'version-1', })).resolves.toBe(true); }); it('allows a project member to write version-scoped work activity evidence', async () => { versionFindUnique.mockResolvedValue({ id: 'version-1', projectId: 'project-1', members: [] }); projectMemberFindUnique.mockResolvedValue({ projectId: 'project-1', userId: 'dev-1', role: 'member' }); await expect(service.can(currentUser({ id: 'dev-1', roleId: 'role-dev' }), 'work-activity:manage', { versionId: 'version-1', })).resolves.toBe(true); }); it('denies users who have a global role permission but are outside the resource scope', async () => { versionFindUnique.mockResolvedValue({ id: 'version-1', projectId: 'project-1', members: [] }); projectMemberFindUnique.mockResolvedValue(null); await expect(service.can(currentUser({ id: 'dev-1', roleId: 'role-dev' }), 'version.devtask:manage', { versionId: 'version-1', })).resolves.toBe(false); }); it('throws explicit auth exceptions for assertion callers', async () => { await expect(service.assertCan(null, 'product:create')).rejects.toBeInstanceOf(UnauthorizedException); await expect(service.assertCan(currentUser({ roleId: 'role-dev' }), 'product:delete')).rejects.toBeInstanceOf(ForbiddenException); }); it('resolves configured role permissions for the current user', async () => { appDataFindUnique.mockResolvedValue({ value: { roles: [ { id: 'role-dev', permissions: ['project:view'] }, ], }, }); await expect(service.resolveUserPermissions(currentUser({ roleId: 'role-dev' }))).resolves.toEqual(['project:view']); }); it('returns no permissions for anonymous users', async () => { await expect(service.resolveUserPermissions(null)).resolves.toEqual([]); }); }); function currentUser(overrides: Partial): CurrentUser { return { id: 'user-1', name: 'User One', username: 'user.one', roleId: 'role-dev', email: '', ...overrides, }; }