mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
feat(API): Add user management endpoints to the Projects Public API (#12329)
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in> Co-authored-by: Danny Martini <danny@n8n.io> Co-authored-by: Andreas Fitzek <andreas.fitzek@n8n.io> Co-authored-by: Guillaume Jacquart <jacquart.guillaume@gmail.com>
This commit is contained in:
@@ -231,7 +231,7 @@ describe('GET /credentials', () => {
|
||||
// ARRANGE
|
||||
//
|
||||
const project1 = await projectService.createTeamProject(member, { name: 'Team Project' });
|
||||
await projectService.addUser(project1.id, anotherMember.id, 'project:editor');
|
||||
await projectService.addUser(project1.id, { userId: anotherMember.id, role: 'project:editor' });
|
||||
// anotherMember should see this one
|
||||
const credential1 = await saveCredential(randomCredentialPayload(), { project: project1 });
|
||||
|
||||
|
||||
@@ -510,7 +510,7 @@ describe('PATCH /projects/:projectId', () => {
|
||||
const resp = await ownerAgent
|
||||
.patch(`/projects/${personalProject.id}`)
|
||||
.send({ name: 'New Name' });
|
||||
expect(resp.status).toBe(403);
|
||||
expect(resp.status).toBe(404);
|
||||
|
||||
const updatedProject = await findProject(personalProject.id);
|
||||
expect(updatedProject.name).not.toEqual('New Name');
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
import { SharedWorkflowRepository } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
|
||||
import { License } from '@/license';
|
||||
import { ProjectService } from '@/services/project.service.ee';
|
||||
import { LicenseMocker } from '@test-integration/license';
|
||||
|
||||
import { linkUserToProject, createTeamProject } from './shared/db/projects';
|
||||
import { linkUserToProject, createTeamProject, getAllProjectRelations } from './shared/db/projects';
|
||||
import { createUser } from './shared/db/users';
|
||||
import { createWorkflow } from './shared/db/workflows';
|
||||
import * as testDb from './shared/test-db';
|
||||
|
||||
describe('ProjectService', () => {
|
||||
let projectService: ProjectService;
|
||||
|
||||
let sharedWorkflowRepository: SharedWorkflowRepository;
|
||||
|
||||
beforeAll(async () => {
|
||||
await testDb.init();
|
||||
|
||||
projectService = Container.get(ProjectService);
|
||||
|
||||
sharedWorkflowRepository = Container.get(SharedWorkflowRepository);
|
||||
|
||||
const license: LicenseMocker = new LicenseMocker();
|
||||
license.mock(Container.get(License));
|
||||
license.enable('feat:projectRole:editor');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -35,6 +39,71 @@ describe('ProjectService', () => {
|
||||
await testDb.terminate();
|
||||
});
|
||||
|
||||
describe('addUsersToProject', () => {
|
||||
it("don't throw a unique constraint violation error when adding a user that is already part of the project", async () => {
|
||||
// ARRANGE
|
||||
const user = await createUser();
|
||||
const project = await createTeamProject('project', user);
|
||||
|
||||
// ACT
|
||||
// add user again
|
||||
await projectService.addUsersToProject(project.id, [
|
||||
{ userId: user.id, role: 'project:admin' },
|
||||
]);
|
||||
|
||||
// ASSERT
|
||||
const relations = await getAllProjectRelations({ projectId: project.id });
|
||||
expect(relations).toHaveLength(1);
|
||||
expect(relations[0]).toMatchObject({
|
||||
projectId: project.id,
|
||||
userId: user.id,
|
||||
role: 'project:admin',
|
||||
});
|
||||
});
|
||||
|
||||
it('allows changing a users role', async () => {
|
||||
// ARRANGE
|
||||
const user = await createUser();
|
||||
const project = await createTeamProject('project', user);
|
||||
|
||||
// ACT
|
||||
// add user again
|
||||
await projectService.addUsersToProject(project.id, [
|
||||
{ userId: user.id, role: 'project:editor' },
|
||||
]);
|
||||
|
||||
// ASSERT
|
||||
const relations = await getAllProjectRelations({ projectId: project.id });
|
||||
expect(relations).toHaveLength(1);
|
||||
expect(relations[0]).toMatchObject({
|
||||
projectId: project.id,
|
||||
userId: user.id,
|
||||
role: 'project:editor',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('addUser', () => {
|
||||
it("don't throw a unique constraint violation error when adding a user that is already part of the project", async () => {
|
||||
// ARRANGE
|
||||
const user = await createUser();
|
||||
const project = await createTeamProject('project', user);
|
||||
|
||||
// ACT
|
||||
// add user again
|
||||
await projectService.addUser(project.id, { userId: user.id, role: 'project:admin' });
|
||||
|
||||
// ASSERT
|
||||
const relations = await getAllProjectRelations({ projectId: project.id });
|
||||
expect(relations).toHaveLength(1);
|
||||
expect(relations[0]).toMatchObject({
|
||||
projectId: project.id,
|
||||
userId: user.id,
|
||||
role: 'project:admin',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('findRolesInProjects', () => {
|
||||
describe('when user has roles in projects where workflow is accessible', () => {
|
||||
it('should return roles and project IDs', async () => {
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error';
|
||||
import { Telemetry } from '@/telemetry';
|
||||
import { mockInstance } from '@test/mocking';
|
||||
import { createTeamProject, getProjectByNameOrFail } from '@test-integration/db/projects';
|
||||
import { createMemberWithApiKey, createOwnerWithApiKey } from '@test-integration/db/users';
|
||||
import {
|
||||
createTeamProject,
|
||||
getProjectByNameOrFail,
|
||||
linkUserToProject,
|
||||
getAllProjectRelations,
|
||||
getProjectRoleForUser,
|
||||
} from '@test-integration/db/projects';
|
||||
import {
|
||||
createMemberWithApiKey,
|
||||
createOwnerWithApiKey,
|
||||
createMember,
|
||||
} from '@test-integration/db/users';
|
||||
import { setupTestServer } from '@test-integration/utils';
|
||||
|
||||
import * as testDb from '../shared/test-db';
|
||||
@@ -394,4 +404,440 @@ describe('Projects in Public API', () => {
|
||||
expect(response.body).toHaveProperty('message', 'Forbidden');
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /projects/:id/users', () => {
|
||||
it('if not authenticated, should reject with 401', async () => {
|
||||
const project = await createTeamProject();
|
||||
|
||||
const response = await testServer
|
||||
.publicApiAgentWithoutApiKey()
|
||||
.post(`/projects/${project.id}/users`);
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body).toHaveProperty('message', "'X-N8N-API-KEY' header required");
|
||||
});
|
||||
|
||||
it('if not licensed, should reject with a 403', async () => {
|
||||
const owner = await createOwnerWithApiKey();
|
||||
const project = await createTeamProject();
|
||||
const member = await createMember();
|
||||
|
||||
const payload = {
|
||||
relations: [
|
||||
{
|
||||
userId: member.id,
|
||||
role: 'project:viewer',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const response = await testServer
|
||||
.publicApiAgentFor(owner)
|
||||
.post(`/projects/${project.id}/users`)
|
||||
.send(payload);
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body).toHaveProperty(
|
||||
'message',
|
||||
new FeatureNotLicensedError('feat:projectRole:admin').message,
|
||||
);
|
||||
});
|
||||
|
||||
it('if missing scope, should reject with 403', async () => {
|
||||
testServer.license.setQuota('quota:maxTeamProjects', -1);
|
||||
testServer.license.enable('feat:projectRole:admin');
|
||||
const member = await createMemberWithApiKey();
|
||||
const project = await createTeamProject();
|
||||
|
||||
const payload = {
|
||||
relations: [
|
||||
{
|
||||
userId: member.id,
|
||||
role: 'project:viewer',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const response = await testServer
|
||||
.publicApiAgentFor(member)
|
||||
.post(`/projects/${project.id}/users`)
|
||||
.send(payload);
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body).toHaveProperty('message', 'Forbidden');
|
||||
});
|
||||
|
||||
describe('when user has correct license', () => {
|
||||
beforeEach(() => {
|
||||
testServer.license.setQuota('quota:maxTeamProjects', -1);
|
||||
testServer.license.enable('feat:projectRole:admin');
|
||||
});
|
||||
|
||||
it("should reject with 400 if the payload can't be validated", async () => {
|
||||
// ARRANGE
|
||||
const owner = await createOwnerWithApiKey();
|
||||
const member = await createMember();
|
||||
|
||||
const payload = {
|
||||
relations: [
|
||||
{
|
||||
userId: member.id,
|
||||
// role does not exist
|
||||
role: 'project:boss',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// ACT
|
||||
const response = await testServer
|
||||
.publicApiAgentFor(owner)
|
||||
.post('/projects/123456/users')
|
||||
.send(payload)
|
||||
.expect(400);
|
||||
|
||||
// ASSERT
|
||||
expect(response.body).toHaveProperty(
|
||||
'message',
|
||||
"Invalid enum value. Expected 'project:admin' | 'project:editor' | 'project:viewer', received 'project:boss'",
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject with 404 if no project found', async () => {
|
||||
const owner = await createOwnerWithApiKey();
|
||||
const member = await createMember();
|
||||
|
||||
const payload = {
|
||||
relations: [
|
||||
{
|
||||
userId: member.id,
|
||||
role: 'project:viewer',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const response = await testServer
|
||||
.publicApiAgentFor(owner)
|
||||
.post('/projects/123456/users')
|
||||
.send(payload);
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body).toHaveProperty('message', 'Could not find project with ID: 123456');
|
||||
});
|
||||
|
||||
it('should add expected users to project', async () => {
|
||||
testServer.license.enable('feat:projectRole:viewer');
|
||||
testServer.license.enable('feat:projectRole:editor');
|
||||
const owner = await createOwnerWithApiKey();
|
||||
const project = await createTeamProject('shared-project', owner);
|
||||
const member = await createMember();
|
||||
const member2 = await createMember();
|
||||
const projectBefore = await getAllProjectRelations({
|
||||
projectId: project.id,
|
||||
});
|
||||
|
||||
const payload = {
|
||||
relations: [
|
||||
{
|
||||
userId: member.id,
|
||||
role: 'project:viewer',
|
||||
},
|
||||
{
|
||||
userId: member2.id,
|
||||
role: 'project:editor',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const response = await testServer
|
||||
.publicApiAgentFor(owner)
|
||||
.post(`/projects/${project.id}/users`)
|
||||
.send(payload);
|
||||
|
||||
const projectAfter = await getAllProjectRelations({
|
||||
projectId: project.id,
|
||||
});
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(projectBefore.length).toEqual(1);
|
||||
expect(projectBefore[0].userId).toEqual(owner.id);
|
||||
|
||||
expect(projectAfter.length).toEqual(3);
|
||||
const adminRelation = projectAfter.find(
|
||||
(relation) => relation.userId === owner.id && relation.role === 'project:admin',
|
||||
);
|
||||
expect(adminRelation).toEqual(
|
||||
expect.objectContaining({ userId: owner.id, role: 'project:admin' }),
|
||||
);
|
||||
const viewerRelation = projectAfter.find(
|
||||
(relation) => relation.userId === member.id && relation.role === 'project:viewer',
|
||||
);
|
||||
expect(viewerRelation).toEqual(
|
||||
expect.objectContaining({ userId: member.id, role: 'project:viewer' }),
|
||||
);
|
||||
const editorRelation = projectAfter.find(
|
||||
(relation) => relation.userId === member2.id && relation.role === 'project:editor',
|
||||
);
|
||||
expect(editorRelation).toEqual(
|
||||
expect.objectContaining({ userId: member2.id, role: 'project:editor' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject with 400 if license does not include user role', async () => {
|
||||
const owner = await createOwnerWithApiKey();
|
||||
const project = await createTeamProject('shared-project', owner);
|
||||
const member = await createMember();
|
||||
|
||||
const payload = {
|
||||
relations: [
|
||||
{
|
||||
userId: member.id,
|
||||
role: 'project:viewer',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const response = await testServer
|
||||
.publicApiAgentFor(owner)
|
||||
.post(`/projects/${project.id}/users`)
|
||||
.send(payload);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body).toHaveProperty(
|
||||
'message',
|
||||
'Your instance is not licensed to use role "project:viewer".',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /projects/:id/users/:userId', () => {
|
||||
it('if not authenticated, should reject with 401', async () => {
|
||||
const response = await testServer
|
||||
.publicApiAgentWithoutApiKey()
|
||||
.patch('/projects/123/users/456')
|
||||
.send({ role: 'project:viewer' });
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body).toHaveProperty('message', "'X-N8N-API-KEY' header required");
|
||||
});
|
||||
|
||||
it('if not licensed, should reject with a 403', async () => {
|
||||
const owner = await createOwnerWithApiKey();
|
||||
|
||||
const response = await testServer
|
||||
.publicApiAgentFor(owner)
|
||||
.patch('/projects/123/users/456')
|
||||
.send({ role: 'project:viewer' });
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body).toHaveProperty(
|
||||
'message',
|
||||
new FeatureNotLicensedError('feat:projectRole:admin').message,
|
||||
);
|
||||
});
|
||||
|
||||
it('if missing scope, should reject with 403', async () => {
|
||||
testServer.license.setQuota('quota:maxTeamProjects', -1);
|
||||
testServer.license.enable('feat:projectRole:admin');
|
||||
const member = await createMemberWithApiKey();
|
||||
|
||||
const response = await testServer
|
||||
.publicApiAgentFor(member)
|
||||
.patch('/projects/123/users/456')
|
||||
.send({ role: 'project:viewer' });
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body).toHaveProperty('message', 'Forbidden');
|
||||
});
|
||||
|
||||
describe('when user has correct license', () => {
|
||||
beforeEach(() => {
|
||||
testServer.license.setQuota('quota:maxTeamProjects', -1);
|
||||
testServer.license.enable('feat:projectRole:admin');
|
||||
});
|
||||
|
||||
it("should reject with 400 if the payload can't be validated", async () => {
|
||||
// ARRANGE
|
||||
const owner = await createOwnerWithApiKey();
|
||||
|
||||
// ACT
|
||||
const response = await testServer
|
||||
.publicApiAgentFor(owner)
|
||||
.patch('/projects/1234/users/1235')
|
||||
// role does not exist
|
||||
.send({ role: 'project:boss' })
|
||||
.expect(400);
|
||||
|
||||
// ASSERT
|
||||
expect(response.body).toHaveProperty(
|
||||
'message',
|
||||
"Invalid enum value. Expected 'project:admin' | 'project:editor' | 'project:viewer', received 'project:boss'",
|
||||
);
|
||||
});
|
||||
|
||||
it("should change a user's role in a project", async () => {
|
||||
const owner = await createOwnerWithApiKey();
|
||||
const project = await createTeamProject('shared-project', owner);
|
||||
|
||||
const member = await createMember();
|
||||
expect(await getProjectRoleForUser(project.id, member.id)).toBeUndefined();
|
||||
|
||||
await linkUserToProject(member, project, 'project:viewer');
|
||||
expect(await getProjectRoleForUser(project.id, member.id)).toBe('project:viewer');
|
||||
|
||||
await testServer
|
||||
.publicApiAgentFor(owner)
|
||||
.patch(`/projects/${project.id}/users/${member.id}`)
|
||||
.send({ role: 'project:editor' })
|
||||
.expect(204);
|
||||
|
||||
expect(await getProjectRoleForUser(project.id, member.id)).toBe('project:editor');
|
||||
});
|
||||
|
||||
it('should reject with 404 if no project found', async () => {
|
||||
const owner = await createOwnerWithApiKey();
|
||||
const member = await createMember();
|
||||
|
||||
const response = await testServer
|
||||
.publicApiAgentFor(owner)
|
||||
.patch(`/projects/123456/users/${member.id}`)
|
||||
.send({ role: 'project:editor' })
|
||||
.expect(404);
|
||||
|
||||
expect(response.body).toHaveProperty('message', 'Could not find project with ID: 123456');
|
||||
});
|
||||
|
||||
it('should reject with 404 if user is not in the project', async () => {
|
||||
const owner = await createOwnerWithApiKey();
|
||||
const project = await createTeamProject('shared-project', owner);
|
||||
const member = await createMember();
|
||||
|
||||
expect(await getProjectRoleForUser(project.id, member.id)).toBeUndefined();
|
||||
|
||||
const response = await testServer
|
||||
.publicApiAgentFor(owner)
|
||||
.patch(`/projects/${project.id}/users/${member.id}`)
|
||||
.send({ role: 'project:editor' })
|
||||
.expect(404);
|
||||
|
||||
expect(response.body).toHaveProperty(
|
||||
'message',
|
||||
`Could not find project with ID: ${project.id}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /projects/:id/users/:userId', () => {
|
||||
it('if not authenticated, should reject with 401', async () => {
|
||||
const project = await createTeamProject();
|
||||
const member = await createMember();
|
||||
|
||||
const response = await testServer
|
||||
.publicApiAgentWithoutApiKey()
|
||||
.delete(`/projects/${project.id}/users/${member.id}`);
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body).toHaveProperty('message', "'X-N8N-API-KEY' header required");
|
||||
});
|
||||
|
||||
it('if not licensed, should reject with a 403', async () => {
|
||||
const owner = await createOwnerWithApiKey();
|
||||
const project = await createTeamProject();
|
||||
const member = await createMember();
|
||||
|
||||
const response = await testServer
|
||||
.publicApiAgentFor(owner)
|
||||
.delete(`/projects/${project.id}/users/${member.id}`);
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body).toHaveProperty(
|
||||
'message',
|
||||
new FeatureNotLicensedError('feat:projectRole:admin').message,
|
||||
);
|
||||
});
|
||||
|
||||
it('if missing scope, should reject with 403', async () => {
|
||||
testServer.license.setQuota('quota:maxTeamProjects', -1);
|
||||
testServer.license.enable('feat:projectRole:admin');
|
||||
const member = await createMemberWithApiKey();
|
||||
const project = await createTeamProject();
|
||||
|
||||
const response = await testServer
|
||||
.publicApiAgentFor(member)
|
||||
.delete(`/projects/${project.id}/users/${member.id}`);
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body).toHaveProperty('message', 'Forbidden');
|
||||
});
|
||||
|
||||
describe('when user has correct license', () => {
|
||||
beforeEach(() => {
|
||||
testServer.license.setQuota('quota:maxTeamProjects', -1);
|
||||
testServer.license.enable('feat:projectRole:admin');
|
||||
});
|
||||
|
||||
it('should remove given user from project', async () => {
|
||||
const owner = await createOwnerWithApiKey();
|
||||
const project = await createTeamProject('shared-project', owner);
|
||||
const member = await createMember();
|
||||
await linkUserToProject(member, project, 'project:viewer');
|
||||
const projectBefore = await getAllProjectRelations({
|
||||
projectId: project.id,
|
||||
});
|
||||
|
||||
const response = await testServer
|
||||
.publicApiAgentFor(owner)
|
||||
.delete(`/projects/${project.id}/users/${member.id}`);
|
||||
|
||||
const projectAfter = await getAllProjectRelations({
|
||||
projectId: project.id,
|
||||
});
|
||||
|
||||
expect(response.status).toBe(204);
|
||||
expect(projectBefore.length).toEqual(2);
|
||||
expect(projectBefore.find((p) => p.role === 'project:admin')?.userId).toEqual(owner.id);
|
||||
expect(projectBefore.find((p) => p.role === 'project:viewer')?.userId).toEqual(member.id);
|
||||
|
||||
expect(projectAfter.length).toEqual(1);
|
||||
expect(projectAfter[0].userId).toEqual(owner.id);
|
||||
});
|
||||
|
||||
it('should reject with 404 if no project found', async () => {
|
||||
const owner = await createOwnerWithApiKey();
|
||||
const member = await createMember();
|
||||
|
||||
const response = await testServer
|
||||
.publicApiAgentFor(owner)
|
||||
.delete(`/projects/123456/users/${member.id}`);
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body).toHaveProperty('message', 'Could not find project with ID: 123456');
|
||||
});
|
||||
|
||||
it('should remain unchanged if user if not in project', async () => {
|
||||
const owner = await createOwnerWithApiKey();
|
||||
const project = await createTeamProject('shared-project', owner);
|
||||
const member = await createMember();
|
||||
const projectBefore = await getAllProjectRelations({
|
||||
projectId: project.id,
|
||||
});
|
||||
|
||||
const response = await testServer
|
||||
.publicApiAgentFor(owner)
|
||||
.delete(`/projects/${project.id}/users/${member.id}`);
|
||||
|
||||
const projectAfter = await getAllProjectRelations({
|
||||
projectId: project.id,
|
||||
});
|
||||
|
||||
expect(response.status).toBe(204);
|
||||
expect(projectBefore.length).toEqual(1);
|
||||
expect(projectBefore[0].userId).toEqual(owner.id);
|
||||
|
||||
expect(projectAfter.length).toEqual(1);
|
||||
expect(projectAfter[0].userId).toEqual(owner.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -52,7 +52,7 @@ describe('ProjectService', () => {
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await projectService.addUser(project.id, member.id, role);
|
||||
await projectService.addUser(project.id, { userId: member.id, role });
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
@@ -74,7 +74,7 @@ describe('ProjectService', () => {
|
||||
type: 'team',
|
||||
}),
|
||||
);
|
||||
await projectService.addUser(project.id, member.id, 'project:viewer');
|
||||
await projectService.addUser(project.id, { userId: member.id, role: 'project:viewer' });
|
||||
|
||||
await projectRelationRepository.findOneOrFail({
|
||||
where: { userId: member.id, projectId: project.id, role: 'project:viewer' },
|
||||
@@ -83,7 +83,7 @@ describe('ProjectService', () => {
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await projectService.addUser(project.id, member.id, 'project:admin');
|
||||
await projectService.addUser(project.id, { userId: member.id, role: 'project:admin' });
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
@@ -117,7 +117,7 @@ describe('ProjectService', () => {
|
||||
type: 'team',
|
||||
}),
|
||||
);
|
||||
await projectService.addUser(project.id, projectOwner.id, role);
|
||||
await projectService.addUser(project.id, { userId: projectOwner.id, role });
|
||||
|
||||
//
|
||||
// ACT
|
||||
@@ -157,7 +157,7 @@ describe('ProjectService', () => {
|
||||
type: 'team',
|
||||
}),
|
||||
);
|
||||
await projectService.addUser(project.id, projectViewer.id, role);
|
||||
await projectService.addUser(project.id, { userId: projectViewer.id, role });
|
||||
|
||||
//
|
||||
// ACT
|
||||
@@ -200,4 +200,44 @@ describe('ProjectService', () => {
|
||||
expect(projectFromService).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteUserFromProject', () => {
|
||||
it('should not allow project owner to be removed from the project', async () => {
|
||||
const role = 'project:personalOwner';
|
||||
|
||||
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 },
|
||||
});
|
||||
|
||||
expect(relations).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -66,3 +66,23 @@ export const getProjectRelations = async ({
|
||||
where: { projectId, userId, role },
|
||||
});
|
||||
};
|
||||
|
||||
export const getProjectRoleForUser = async (
|
||||
projectId: string,
|
||||
userId: string,
|
||||
): Promise<ProjectRole | undefined> => {
|
||||
return (
|
||||
await Container.get(ProjectRelationRepository).findOne({
|
||||
select: ['role'],
|
||||
where: { projectId, userId },
|
||||
})
|
||||
)?.role;
|
||||
};
|
||||
|
||||
export const getAllProjectRelations = async ({
|
||||
projectId,
|
||||
}: Partial<ProjectRelation>): Promise<ProjectRelation[]> => {
|
||||
return await Container.get(ProjectRelationRepository).find({
|
||||
where: { projectId },
|
||||
});
|
||||
};
|
||||
|
||||
@@ -72,7 +72,7 @@ describe('WorkflowSharingService', () => {
|
||||
// ARRANGE
|
||||
//
|
||||
const project = await projectService.createTeamProject(member, { name: 'Team Project' });
|
||||
await projectService.addUser(project.id, anotherMember.id, 'project:admin');
|
||||
await projectService.addUser(project.id, { userId: anotherMember.id, role: 'project:admin' });
|
||||
const workflow = await createWorkflow(undefined, project);
|
||||
|
||||
//
|
||||
@@ -96,8 +96,14 @@ describe('WorkflowSharingService', () => {
|
||||
const workflow1 = await createWorkflow(undefined, project1);
|
||||
const project2 = await projectService.createTeamProject(member, { name: 'Team Project 2' });
|
||||
const workflow2 = await createWorkflow(undefined, project2);
|
||||
await projectService.addUser(project1.id, anotherMember.id, 'project:admin');
|
||||
await projectService.addUser(project2.id, anotherMember.id, 'project:viewer');
|
||||
await projectService.addUser(project1.id, {
|
||||
userId: anotherMember.id,
|
||||
role: 'project:admin',
|
||||
});
|
||||
await projectService.addUser(project2.id, {
|
||||
userId: anotherMember.id,
|
||||
role: 'project:viewer',
|
||||
});
|
||||
|
||||
//
|
||||
// ACT
|
||||
|
||||
@@ -54,7 +54,6 @@ const { objectContaining, arrayContaining, any } = expect;
|
||||
const activeWorkflowManagerLike = mockInstance(ActiveWorkflowManager);
|
||||
|
||||
let projectRepository: ProjectRepository;
|
||||
let projectService: ProjectService;
|
||||
|
||||
beforeEach(async () => {
|
||||
await testDb.truncate([
|
||||
@@ -68,7 +67,6 @@ beforeEach(async () => {
|
||||
'User',
|
||||
]);
|
||||
projectRepository = Container.get(ProjectRepository);
|
||||
projectService = Container.get(ProjectService);
|
||||
owner = await createOwner();
|
||||
authOwnerAgent = testServer.authAgentFor(owner);
|
||||
member = await createMember();
|
||||
@@ -288,7 +286,10 @@ describe('POST /workflows', () => {
|
||||
type: 'team',
|
||||
}),
|
||||
);
|
||||
await projectService.addUser(project.id, owner.id, 'project:admin');
|
||||
await Container.get(ProjectService).addUser(project.id, {
|
||||
userId: owner.id,
|
||||
role: 'project:admin',
|
||||
});
|
||||
|
||||
//
|
||||
// ACT
|
||||
@@ -362,7 +363,10 @@ describe('POST /workflows', () => {
|
||||
type: 'team',
|
||||
}),
|
||||
);
|
||||
await projectService.addUser(project.id, member.id, 'project:viewer');
|
||||
await Container.get(ProjectService).addUser(project.id, {
|
||||
userId: member.id,
|
||||
role: 'project:viewer',
|
||||
});
|
||||
|
||||
//
|
||||
// ACT
|
||||
|
||||
Reference in New Issue
Block a user