Files
n8n-enterprise-unlocked/packages/cli/test/integration/services/project.service.test.ts

442 lines
11 KiB
TypeScript

import { LicenseState } from '@n8n/backend-common';
import { testDb } from '@n8n/backend-test-utils';
import { ProjectRelationRepository, ProjectRepository } from '@n8n/db';
import { Container } from '@n8n/di';
import { PROJECT_OWNER_ROLE_SLUG, type ProjectRole, type Scope } from '@n8n/permissions';
import { License } from '@/license';
import { ProjectService } from '@/services/project.service.ee';
import { createRole } from '@test-integration/db/roles';
import { createMember } from '../shared/db/users';
import { LicenseMocker } from '@test-integration/license';
let projectRepository: ProjectRepository;
let projectService: ProjectService;
let projectRelationRepository: ProjectRelationRepository;
beforeAll(async () => {
await testDb.init();
projectRepository = Container.get(ProjectRepository);
projectService = Container.get(ProjectService);
projectRelationRepository = Container.get(ProjectRelationRepository);
const license: LicenseMocker = new LicenseMocker();
license.mock(Container.get(License));
license.mockLicenseState(Container.get(LicenseState));
license.enable('feat:projectRole:editor');
license.enable('feat:projectRole:viewer');
license.enable('feat:customRoles');
});
afterAll(async () => {
await testDb.terminate();
});
afterEach(async () => {
await testDb.truncate(['User']);
});
describe('ProjectService', () => {
describe('addUser', () => {
it.each([
'project:viewer',
'project:admin',
'project:editor',
PROJECT_OWNER_ROLE_SLUG,
] as ProjectRole[])(
'creates a relation between the user and the project using the role %s',
async (role) => {
//
// ARRANGE
//
const member = await createMember();
const project = await projectRepository.save(
projectRepository.create({
name: 'Team Project',
type: 'team',
}),
);
//
// ACT
//
await projectService.addUser(project.id, { userId: member.id, role });
//
// ASSERT
//
await projectRelationRepository.findOneOrFail({
where: { userId: member.id, projectId: project.id, role: { slug: role } },
});
},
);
it('changes the role the user has in the project if the user is already part of the project, instead of creating a new relationship', async () => {
//
// ARRANGE
//
const member = await createMember();
const project = await projectRepository.save(
projectRepository.create({
name: 'Team Project',
type: 'team',
}),
);
await projectService.addUser(project.id, { userId: member.id, role: 'project:viewer' });
await projectRelationRepository.findOneOrFail({
where: { userId: member.id, projectId: project.id, role: { slug: 'project:viewer' } },
});
//
// ACT
//
await projectService.addUser(project.id, { userId: member.id, role: 'project:admin' });
//
// ASSERT
//
const relationships = await projectRelationRepository.find({
where: { userId: member.id, projectId: project.id },
relations: { role: true },
});
expect(relationships).toHaveLength(1);
expect(relationships[0]).toHaveProperty('role.slug', 'project:admin');
});
it('adds a user to a project with a custom role', async () => {
//
// ARRANGE
//
const member = await createMember();
const project = await projectRepository.save(
projectRepository.create({
name: 'Team Project',
type: 'team',
}),
);
const role = await createRole({ slug: 'project:custom', displayName: 'Custom Role' });
//
// ACT
//
await projectService.addUser(project.id, { userId: member.id, role: role.slug });
//
// ASSERT
//
const relationships = await projectRelationRepository.find({
where: { userId: member.id, projectId: project.id },
relations: { role: true },
});
expect(relationships).toHaveLength(1);
expect(relationships[0].role.slug).toBe('project:custom');
});
});
describe('getProjectWithScope', () => {
it.each([
{ role: 'project:admin', scope: 'workflow:list' },
{ role: 'project:admin', scope: 'workflow:create' },
] as Array<{
role: ProjectRole;
scope: Scope;
}>)(
'should return the project if the user has the role $role and wants the scope $scope',
async ({ role, scope }) => {
//
// ARRANGE
//
const projectOwner = await createMember();
const project = await projectRepository.save(
projectRepository.create({
name: 'Team Project',
type: 'team',
}),
);
await projectService.addUser(project.id, { userId: projectOwner.id, role });
//
// ACT
//
const projectFromService = await projectService.getProjectWithScope(
projectOwner,
project.id,
[scope],
);
//
// ASSERT
//
if (projectFromService === null) {
fail('Expected projectFromService not to be null');
}
expect(project.id).toBe(projectFromService.id);
},
);
it.each([
{ role: 'project:viewer', scope: 'workflow:create' },
{ role: 'project:viewer', scope: 'credential:create' },
] as Array<{
role: ProjectRole;
scope: Scope;
}>)(
'should return the project if the user has the role $role and wants the scope $scope',
async ({ role, scope }) => {
//
// ARRANGE
//
const projectViewer = await createMember();
const project = await projectRepository.save(
projectRepository.create({
name: 'Team Project',
type: 'team',
}),
);
await projectService.addUser(project.id, { userId: projectViewer.id, role });
//
// ACT
//
const projectFromService = await projectService.getProjectWithScope(
projectViewer,
project.id,
[scope],
);
//
// ASSERT
//
expect(projectFromService).toBeNull();
},
);
it('should not return the project if the user is not part of it', async () => {
//
// ARRANGE
//
const member = await createMember();
const project = await projectRepository.save(
projectRepository.create({
name: 'Team Project',
type: 'team',
}),
);
//
// ACT
//
const projectFromService = await projectService.getProjectWithScope(member, project.id, [
'workflow:list',
]);
//
// ASSERT
//
expect(projectFromService).toBeNull();
});
});
describe('deleteUserFromProject', () => {
it('should not allow project owner to be removed from the project', async () => {
const role = PROJECT_OWNER_ROLE_SLUG;
const user = await createMember();
const project = await projectRepository.save(
projectRepository.create({
name: 'Team Project',
type: 'team',
}),
);
await projectService.addUser(project.id, { userId: user.id, role });
await expect(projectService.deleteUserFromProject(project.id, user.id)).rejects.toThrowError(
/^Project owner cannot be removed from the project$/,
);
});
it('should remove user from project if not owner', async () => {
const role = 'project:editor';
const user = await createMember();
const project = await projectRepository.save(
projectRepository.create({
name: 'Team Project',
type: 'team',
}),
);
await projectService.addUser(project.id, { userId: user.id, role });
await projectService.deleteUserFromProject(project.id, user.id);
const relations = await projectRelationRepository.findOne({
where: { userId: user.id, projectId: project.id, role: { slug: role } },
});
expect(relations).toBeNull();
});
});
describe('addUsersToProject', () => {
it('should add multiple users to a project', async () => {
//
// ARRANGE
//
const members = await Promise.all([createMember(), createMember()]);
const project = await projectRepository.save(
projectRepository.create({
name: 'Team Project',
type: 'team',
}),
);
//
// ACT
//
await projectService.addUsersToProject(
project.id,
members.map((member) => ({ userId: member.id, role: 'project:editor' })),
);
//
// ASSERT
//
const relations = await projectRelationRepository.find({
where: { projectId: project.id },
});
expect(relations).toHaveLength(members.length);
});
it('fails to add a user to a project with a non-existing role', async () => {
//
// ARRANGE
//
const member = await createMember();
const project = await projectRepository.save(
projectRepository.create({
name: 'Team Project',
type: 'team',
}),
);
//
// ACT
//
await expect(
projectService.addUsersToProject(project.id, [
{ userId: member.id, role: 'custom:non-existing' },
]),
).rejects.toThrowError('Role custom:non-existing does not exist');
});
});
describe('syncProjectRelations', () => {
it('should synchronize project relations for a user', async () => {
//
// ARRANGE
//
const user = await createMember();
const project = await projectRepository.save(
projectRepository.create({
name: 'Team Project',
type: 'team',
}),
);
//
// ACT
//
await projectService.syncProjectRelations(project.id, [
{ userId: user.id, role: 'project:editor' },
]);
//
// ASSERT
//
const relations = await projectRelationRepository.find({
where: { userId: user.id, projectId: project.id },
});
expect(relations).toHaveLength(1);
});
it('should fail to synchronize users with non-existing roles', async () => {
//
// ARRANGE
//
const user = await createMember();
const project = await projectRepository.save(
projectRepository.create({
name: 'Team Project',
type: 'team',
}),
);
//
// ACT
//
await expect(
projectService.syncProjectRelations(project.id, [
{ userId: user.id, role: 'project:non-existing' },
]),
).rejects.toThrowError('Role project:non-existing does not exist');
});
});
describe('changeUserRoleInProject', () => {
it('should change user role in project', async () => {
//
// ARRANGE
//
const user = await createMember();
const project = await projectRepository.save(
projectRepository.create({
name: 'Team Project',
type: 'team',
}),
);
//
// ACT
//
await projectService.addUser(project.id, { userId: user.id, role: 'project:viewer' });
await projectService.changeUserRoleInProject(project.id, user.id, 'project:editor');
//
// ASSERT
//
const relations = await projectRelationRepository.find({
where: { userId: user.id, projectId: project.id },
relations: { role: true },
});
expect(relations).toHaveLength(1);
expect(relations[0].role.slug).toBe('project:editor');
});
it('should fail to change user role in project with non-existing role', async () => {
//
// ARRANGE
//
const user = await createMember();
const project = await projectRepository.save(
projectRepository.create({
name: 'Team Project',
type: 'team',
}),
);
//
// ACT
//
await projectService.addUser(project.id, { userId: user.id, role: 'project:viewer' });
await expect(
projectService.changeUserRoleInProject(project.id, user.id, 'project:non-existing'),
).rejects.toThrowError('Role project:non-existing does not exist');
});
});
});