Files
ftb-project-management/apps/server/src/common/auth/permission.service.spec.ts

145 lines
5.5 KiB
TypeScript

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>): CurrentUser {
return {
id: 'user-1',
name: 'User One',
username: 'user.one',
roleId: 'role-dev',
email: '',
...overrides,
};
}