mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-28 22:19:41 -08:00
5156313074
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Benchmark Docker Image CI / build (push) Waiting to run
1191 lines
38 KiB
TypeScript
1191 lines
38 KiB
TypeScript
import type { Scope } from '@n8n/permissions';
|
|
import { EntityNotFoundError } from '@n8n/typeorm';
|
|
import Container from 'typedi';
|
|
|
|
import { ActiveWorkflowManager } from '@/active-workflow-manager';
|
|
import type { Project } from '@/databases/entities/project';
|
|
import type { ProjectRole } from '@/databases/entities/project-relation';
|
|
import type { GlobalRole } from '@/databases/entities/user';
|
|
import { ProjectRelationRepository } from '@/databases/repositories/project-relation.repository';
|
|
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
|
import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository';
|
|
import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository';
|
|
import { getWorkflowById } from '@/public-api/v1/handlers/workflows/workflows.service';
|
|
import { CacheService } from '@/services/cache/cache.service';
|
|
import { RoleService } from '@/services/role.service';
|
|
|
|
import {
|
|
getCredentialById,
|
|
saveCredential,
|
|
shareCredentialWithProjects,
|
|
} from './shared/db/credentials';
|
|
import {
|
|
createTeamProject,
|
|
linkUserToProject,
|
|
getPersonalProject,
|
|
findProject,
|
|
getProjectRelations,
|
|
} from './shared/db/projects';
|
|
import { createMember, createOwner, createUser } from './shared/db/users';
|
|
import { createWorkflow, shareWorkflowWithProjects } from './shared/db/workflows';
|
|
import { randomCredentialPayload } from './shared/random';
|
|
import * as testDb from './shared/test-db';
|
|
import * as utils from './shared/utils/';
|
|
import { mockInstance } from '../shared/mocking';
|
|
|
|
const testServer = utils.setupTestServer({
|
|
endpointGroups: ['project'],
|
|
enabledFeatures: [
|
|
'feat:advancedPermissions',
|
|
'feat:projectRole:admin',
|
|
'feat:projectRole:editor',
|
|
'feat:projectRole:viewer',
|
|
],
|
|
quotas: {
|
|
'quota:maxTeamProjects': -1,
|
|
},
|
|
});
|
|
|
|
// The `ActiveWorkflowRunner` keeps the event loop alive, which in turn leads to jest not shutting down cleanly.
|
|
// We don't need it for the tests here, so we can mock it and make the tests exit cleanly.
|
|
mockInstance(ActiveWorkflowManager);
|
|
|
|
beforeEach(async () => {
|
|
await testDb.truncate(['User', 'Project']);
|
|
});
|
|
|
|
describe('GET /projects/', () => {
|
|
test('member should get all personal projects and team projects they are apart of', async () => {
|
|
const [testUser1, testUser2, testUser3] = await Promise.all([
|
|
createUser(),
|
|
createUser(),
|
|
createUser(),
|
|
]);
|
|
const [teamProject1, teamProject2] = await Promise.all([
|
|
createTeamProject(undefined, testUser1),
|
|
createTeamProject(),
|
|
]);
|
|
|
|
const [personalProject1, personalProject2, personalProject3] = await Promise.all([
|
|
getPersonalProject(testUser1),
|
|
getPersonalProject(testUser2),
|
|
getPersonalProject(testUser3),
|
|
]);
|
|
|
|
const memberAgent = testServer.authAgentFor(testUser1);
|
|
|
|
const resp = await memberAgent.get('/projects/');
|
|
expect(resp.status).toBe(200);
|
|
const respProjects = resp.body.data as Project[];
|
|
expect(respProjects.length).toBe(4);
|
|
|
|
expect(
|
|
[personalProject1, personalProject2, personalProject3].every((v, i) => {
|
|
const p = respProjects.find((p) => p.id === v.id);
|
|
if (!p) {
|
|
return false;
|
|
}
|
|
const u = [testUser1, testUser2, testUser3][i];
|
|
return p.name === u.createPersonalProjectName();
|
|
}),
|
|
).toBe(true);
|
|
expect(respProjects.find((p) => p.id === teamProject1.id)).not.toBeUndefined();
|
|
expect(respProjects.find((p) => p.id === teamProject2.id)).toBeUndefined();
|
|
});
|
|
|
|
test('owner should get all projects', async () => {
|
|
const [ownerUser, testUser1, testUser2, testUser3] = await Promise.all([
|
|
createOwner(),
|
|
createUser(),
|
|
createUser(),
|
|
createUser(),
|
|
]);
|
|
const [teamProject1, teamProject2] = await Promise.all([
|
|
createTeamProject(undefined, testUser1),
|
|
createTeamProject(),
|
|
]);
|
|
|
|
const [ownerProject, personalProject1, personalProject2, personalProject3] = await Promise.all([
|
|
getPersonalProject(ownerUser),
|
|
getPersonalProject(testUser1),
|
|
getPersonalProject(testUser2),
|
|
getPersonalProject(testUser3),
|
|
]);
|
|
|
|
const memberAgent = testServer.authAgentFor(ownerUser);
|
|
|
|
const resp = await memberAgent.get('/projects/');
|
|
expect(resp.status).toBe(200);
|
|
const respProjects = resp.body.data as Project[];
|
|
expect(respProjects.length).toBe(6);
|
|
|
|
expect(
|
|
[ownerProject, personalProject1, personalProject2, personalProject3].every((v, i) => {
|
|
const p = respProjects.find((p) => p.id === v.id);
|
|
if (!p) {
|
|
return false;
|
|
}
|
|
const u = [ownerUser, testUser1, testUser2, testUser3][i];
|
|
return p.name === u.createPersonalProjectName();
|
|
}),
|
|
).toBe(true);
|
|
expect(respProjects.find((p) => p.id === teamProject1.id)).not.toBeUndefined();
|
|
expect(respProjects.find((p) => p.id === teamProject2.id)).not.toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('GET /projects/count', () => {
|
|
test('should return correct number of projects', async () => {
|
|
const [firstUser] = await Promise.all([
|
|
createUser(),
|
|
createUser(),
|
|
createUser(),
|
|
createUser(),
|
|
createTeamProject(),
|
|
createTeamProject(),
|
|
createTeamProject(),
|
|
]);
|
|
|
|
const resp = await testServer.authAgentFor(firstUser).get('/projects/count');
|
|
|
|
expect(resp.body.data.personal).toBe(4);
|
|
expect(resp.body.data.team).toBe(3);
|
|
});
|
|
});
|
|
|
|
describe('GET /projects/my-projects', () => {
|
|
test('member should get all projects they are apart of', async () => {
|
|
//
|
|
// ARRANGE
|
|
//
|
|
const [testUser1, testUser2, testUser3] = await Promise.all([
|
|
createUser(),
|
|
createUser(),
|
|
createUser(),
|
|
]);
|
|
const [teamProject1, teamProject2] = await Promise.all([
|
|
createTeamProject(undefined, testUser1),
|
|
createTeamProject(undefined, testUser2),
|
|
]);
|
|
|
|
const [personalProject1, personalProject2, personalProject3] = await Promise.all([
|
|
getPersonalProject(testUser1),
|
|
getPersonalProject(testUser2),
|
|
getPersonalProject(testUser3),
|
|
]);
|
|
|
|
//
|
|
// ACT
|
|
//
|
|
const resp = await testServer
|
|
.authAgentFor(testUser1)
|
|
.get('/projects/my-projects')
|
|
.query({ includeScopes: true })
|
|
.expect(200);
|
|
const respProjects: Array<Project & { role: ProjectRole | GlobalRole; scopes?: Scope[] }> =
|
|
resp.body.data;
|
|
|
|
//
|
|
// ASSERT
|
|
//
|
|
expect(respProjects.length).toBe(2);
|
|
|
|
const projectsExpected = [
|
|
[
|
|
personalProject1,
|
|
{
|
|
role: 'project:personalOwner',
|
|
scopes: ['project:list', 'project:read', 'credential:create'],
|
|
},
|
|
],
|
|
[
|
|
teamProject1,
|
|
{
|
|
role: 'project:admin',
|
|
scopes: [
|
|
'project:list',
|
|
'project:read',
|
|
'project:update',
|
|
'project:delete',
|
|
'credential:create',
|
|
],
|
|
},
|
|
],
|
|
] as const;
|
|
|
|
for (const [project, expected] of projectsExpected) {
|
|
const p = respProjects.find((p) => p.id === project.id)!;
|
|
|
|
expect(p.role).toBe(expected.role);
|
|
expect(expected.scopes.every((s) => p.scopes?.includes(s as Scope))).toBe(true);
|
|
}
|
|
|
|
expect(respProjects).not.toContainEqual(expect.objectContaining({ id: personalProject2.id }));
|
|
expect(respProjects).not.toContainEqual(expect.objectContaining({ id: personalProject3.id }));
|
|
expect(respProjects).not.toContainEqual(expect.objectContaining({ id: teamProject2.id }));
|
|
});
|
|
|
|
test('owner should get all projects they are apart of', async () => {
|
|
//
|
|
// ARRANGE
|
|
//
|
|
const [ownerUser, testUser1, testUser2, testUser3] = await Promise.all([
|
|
createOwner(),
|
|
createUser(),
|
|
createUser(),
|
|
createUser(),
|
|
]);
|
|
const [teamProject1, teamProject2, teamProject3, teamProject4] = await Promise.all([
|
|
// owner has no relation ship
|
|
createTeamProject(undefined, testUser1),
|
|
// owner is admin
|
|
createTeamProject(undefined, ownerUser),
|
|
// owner is viewer
|
|
createTeamProject(undefined, testUser2),
|
|
// this project has no relationship at all
|
|
createTeamProject(),
|
|
]);
|
|
|
|
await linkUserToProject(ownerUser, teamProject3, 'project:editor');
|
|
|
|
const [ownerProject, personalProject1, personalProject2, personalProject3] = await Promise.all([
|
|
getPersonalProject(ownerUser),
|
|
getPersonalProject(testUser1),
|
|
getPersonalProject(testUser2),
|
|
getPersonalProject(testUser3),
|
|
]);
|
|
|
|
//
|
|
// ACT
|
|
//
|
|
const resp = await testServer
|
|
.authAgentFor(ownerUser)
|
|
.get('/projects/my-projects')
|
|
.query({ includeScopes: true })
|
|
.expect(200);
|
|
const respProjects: Array<Project & { role: ProjectRole | GlobalRole; scopes?: Scope[] }> =
|
|
resp.body.data;
|
|
|
|
//
|
|
// ASSERT
|
|
//
|
|
expect(respProjects.length).toBe(5);
|
|
|
|
const projectsExpected = [
|
|
[
|
|
ownerProject,
|
|
{
|
|
role: 'project:personalOwner',
|
|
scopes: [
|
|
'project:list',
|
|
'project:create',
|
|
'project:read',
|
|
'project:update',
|
|
'project:delete',
|
|
'credential:create',
|
|
],
|
|
},
|
|
],
|
|
[
|
|
teamProject1,
|
|
{
|
|
role: 'global:owner',
|
|
scopes: [
|
|
'project:list',
|
|
'project:create',
|
|
'project:read',
|
|
'project:update',
|
|
'project:delete',
|
|
'credential:create',
|
|
],
|
|
},
|
|
],
|
|
[
|
|
teamProject2,
|
|
{
|
|
role: 'project:admin',
|
|
scopes: [
|
|
'project:list',
|
|
'project:create',
|
|
'project:read',
|
|
'project:update',
|
|
'project:delete',
|
|
'credential:create',
|
|
],
|
|
},
|
|
],
|
|
[
|
|
teamProject3,
|
|
{
|
|
role: 'project:editor',
|
|
scopes: [
|
|
'project:list',
|
|
'project:create',
|
|
'project:read',
|
|
'project:update',
|
|
'project:delete',
|
|
'credential:create',
|
|
],
|
|
},
|
|
],
|
|
[
|
|
teamProject4,
|
|
{
|
|
role: 'global:owner',
|
|
scopes: [
|
|
'project:list',
|
|
'project:create',
|
|
'project:read',
|
|
'project:update',
|
|
'project:delete',
|
|
'credential:create',
|
|
],
|
|
},
|
|
],
|
|
] as const;
|
|
|
|
for (const [project, expected] of projectsExpected) {
|
|
const p = respProjects.find((p) => p.id === project.id)!;
|
|
|
|
expect(p.role).toBe(expected.role);
|
|
expect(expected.scopes.every((s) => p.scopes?.includes(s as Scope))).toBe(true);
|
|
}
|
|
|
|
expect(respProjects).not.toContainEqual(expect.objectContaining({ id: personalProject1.id }));
|
|
expect(respProjects).not.toContainEqual(expect.objectContaining({ id: personalProject2.id }));
|
|
expect(respProjects).not.toContainEqual(expect.objectContaining({ id: personalProject3.id }));
|
|
});
|
|
});
|
|
|
|
describe('GET /projects/personal', () => {
|
|
test("should return the user's personal project", async () => {
|
|
const user = await createUser();
|
|
const project = await getPersonalProject(user);
|
|
|
|
const memberAgent = testServer.authAgentFor(user);
|
|
|
|
const resp = await memberAgent.get('/projects/personal');
|
|
expect(resp.status).toBe(200);
|
|
const respProject = resp.body.data as Project & { scopes: Scope[] };
|
|
expect(respProject.id).toEqual(project.id);
|
|
expect(respProject.scopes).not.toBeUndefined();
|
|
});
|
|
|
|
test("should return 404 if user doesn't have a personal project", async () => {
|
|
const user = await createUser();
|
|
const project = await getPersonalProject(user);
|
|
await testDb.truncate(['Project']);
|
|
|
|
const memberAgent = testServer.authAgentFor(user);
|
|
|
|
const resp = await memberAgent.get('/projects/personal');
|
|
expect(resp.status).toBe(404);
|
|
const respProject = resp.body?.data as Project;
|
|
expect(respProject?.id).not.toEqual(project.id);
|
|
});
|
|
});
|
|
|
|
describe('POST /projects/', () => {
|
|
test('should create a team project', async () => {
|
|
const ownerUser = await createOwner();
|
|
const ownerAgent = testServer.authAgentFor(ownerUser);
|
|
|
|
const resp = await ownerAgent.post('/projects/').send({ name: 'Test Team Project' });
|
|
expect(resp.status).toBe(200);
|
|
const respProject = resp.body.data as Project;
|
|
expect(respProject.name).toEqual('Test Team Project');
|
|
expect(async () => {
|
|
await findProject(respProject.id);
|
|
}).not.toThrow();
|
|
expect(resp.body.data.role).toBe('project:admin');
|
|
for (const scope of Container.get(RoleService).getRoleScopes('project:admin')) {
|
|
expect(resp.body.data.scopes).toContain(scope);
|
|
}
|
|
});
|
|
|
|
test('should allow to create a team projects if below the quota', async () => {
|
|
testServer.license.setQuota('quota:maxTeamProjects', 1);
|
|
const ownerUser = await createOwner();
|
|
const ownerAgent = testServer.authAgentFor(ownerUser);
|
|
|
|
await ownerAgent.post('/projects/').send({ name: 'Test Team Project' }).expect(200);
|
|
expect(await Container.get(ProjectRepository).count({ where: { type: 'team' } })).toBe(1);
|
|
});
|
|
|
|
test('should fail to create a team project if at quota', async () => {
|
|
testServer.license.setQuota('quota:maxTeamProjects', 1);
|
|
await Promise.all([createTeamProject()]);
|
|
const ownerUser = await createOwner();
|
|
const ownerAgent = testServer.authAgentFor(ownerUser);
|
|
|
|
await ownerAgent.post('/projects/').send({ name: 'Test Team Project' }).expect(400, {
|
|
code: 400,
|
|
message:
|
|
'Attempted to create a new project but quota is already exhausted. You may have a maximum of 1 team projects.',
|
|
});
|
|
|
|
expect(await Container.get(ProjectRepository).count({ where: { type: 'team' } })).toBe(1);
|
|
});
|
|
|
|
test('should fail to create a team project if above the quota', async () => {
|
|
testServer.license.setQuota('quota:maxTeamProjects', 1);
|
|
await Promise.all([createTeamProject(), createTeamProject()]);
|
|
const ownerUser = await createOwner();
|
|
const ownerAgent = testServer.authAgentFor(ownerUser);
|
|
|
|
await ownerAgent.post('/projects/').send({ name: 'Test Team Project' }).expect(400, {
|
|
code: 400,
|
|
message:
|
|
'Attempted to create a new project but quota is already exhausted. You may have a maximum of 1 team projects.',
|
|
});
|
|
|
|
expect(await Container.get(ProjectRepository).count({ where: { type: 'team' } })).toBe(2);
|
|
});
|
|
});
|
|
|
|
describe('PATCH /projects/:projectId', () => {
|
|
test('should update a team project name', async () => {
|
|
const ownerUser = await createOwner();
|
|
const ownerAgent = testServer.authAgentFor(ownerUser);
|
|
|
|
const teamProject = await createTeamProject();
|
|
|
|
const resp = await ownerAgent.patch(`/projects/${teamProject.id}`).send({ name: 'New Name' });
|
|
expect(resp.status).toBe(200);
|
|
|
|
const updatedProject = await findProject(teamProject.id);
|
|
expect(updatedProject.name).toEqual('New Name');
|
|
});
|
|
|
|
test('should not allow viewers to edit team project name', async () => {
|
|
const testUser = await createUser();
|
|
const teamProject = await createTeamProject();
|
|
await linkUserToProject(testUser, teamProject, 'project:viewer');
|
|
|
|
const memberAgent = testServer.authAgentFor(testUser);
|
|
|
|
const resp = await memberAgent.patch(`/projects/${teamProject.id}`).send({ name: 'New Name' });
|
|
expect(resp.status).toBe(403);
|
|
|
|
const updatedProject = await findProject(teamProject.id);
|
|
expect(updatedProject.name).not.toEqual('New Name');
|
|
});
|
|
|
|
test('should not allow owners to edit personal project name', async () => {
|
|
const user = await createUser();
|
|
const personalProject = await getPersonalProject(user);
|
|
|
|
const ownerUser = await createOwner();
|
|
const ownerAgent = testServer.authAgentFor(ownerUser);
|
|
|
|
const resp = await ownerAgent
|
|
.patch(`/projects/${personalProject.id}`)
|
|
.send({ name: 'New Name' });
|
|
expect(resp.status).toBe(403);
|
|
|
|
const updatedProject = await findProject(personalProject.id);
|
|
expect(updatedProject.name).not.toEqual('New Name');
|
|
});
|
|
|
|
describe('member management', () => {
|
|
test('should add or remove users from a project', async () => {
|
|
const [ownerUser, testUser1, testUser2, testUser3] = await Promise.all([
|
|
createOwner(),
|
|
createUser(),
|
|
createUser(),
|
|
createUser(),
|
|
]);
|
|
const [teamProject1, teamProject2] = await Promise.all([
|
|
createTeamProject(undefined, testUser1),
|
|
createTeamProject(undefined, testUser2),
|
|
]);
|
|
const [credential1, credential2] = await Promise.all([
|
|
saveCredential(randomCredentialPayload(), {
|
|
role: 'credential:owner',
|
|
project: teamProject1,
|
|
}),
|
|
saveCredential(randomCredentialPayload(), {
|
|
role: 'credential:owner',
|
|
project: teamProject2,
|
|
}),
|
|
saveCredential(randomCredentialPayload(), {
|
|
role: 'credential:owner',
|
|
project: teamProject2,
|
|
}),
|
|
]);
|
|
await shareCredentialWithProjects(credential2, [teamProject1]);
|
|
|
|
await linkUserToProject(ownerUser, teamProject2, 'project:editor');
|
|
await linkUserToProject(testUser2, teamProject2, 'project:editor');
|
|
|
|
const memberAgent = testServer.authAgentFor(testUser1);
|
|
|
|
const deleteSpy = jest.spyOn(Container.get(CacheService), 'deleteMany');
|
|
const resp = await memberAgent.patch(`/projects/${teamProject1.id}`).send({
|
|
name: teamProject1.name,
|
|
relations: [
|
|
{ userId: testUser1.id, role: 'project:admin' },
|
|
{ userId: testUser3.id, role: 'project:editor' },
|
|
{ userId: ownerUser.id, role: 'project:viewer' },
|
|
] as Array<{
|
|
userId: string;
|
|
role: ProjectRole;
|
|
}>,
|
|
});
|
|
expect(resp.status).toBe(200);
|
|
|
|
expect(deleteSpy).toBeCalledWith([`credential-can-use-secrets:${credential1.id}`]);
|
|
deleteSpy.mockClear();
|
|
|
|
const [tp1Relations, tp2Relations] = await Promise.all([
|
|
getProjectRelations({ projectId: teamProject1.id }),
|
|
getProjectRelations({ projectId: teamProject2.id }),
|
|
]);
|
|
|
|
expect(tp1Relations.length).toBe(3);
|
|
expect(tp2Relations.length).toBe(2);
|
|
|
|
expect(tp1Relations.find((p) => p.userId === testUser1.id)).not.toBeUndefined();
|
|
expect(tp1Relations.find((p) => p.userId === testUser2.id)).toBeUndefined();
|
|
expect(tp1Relations.find((p) => p.userId === testUser1.id)?.role).toBe('project:admin');
|
|
expect(tp1Relations.find((p) => p.userId === testUser3.id)?.role).toBe('project:editor');
|
|
expect(tp1Relations.find((p) => p.userId === ownerUser.id)?.role).toBe('project:viewer');
|
|
|
|
// Check we haven't modified the other team project
|
|
expect(tp2Relations.find((p) => p.userId === testUser2.id)).not.toBeUndefined();
|
|
expect(tp2Relations.find((p) => p.userId === testUser1.id)).toBeUndefined();
|
|
expect(tp2Relations.find((p) => p.userId === testUser2.id)?.role).toBe('project:editor');
|
|
expect(tp2Relations.find((p) => p.userId === ownerUser.id)?.role).toBe('project:editor');
|
|
});
|
|
|
|
test.each([['project:viewer'], ['project:editor']] as const)(
|
|
'`%s`s should not be able to add, update or remove users from a project',
|
|
async (role) => {
|
|
//
|
|
// ARRANGE
|
|
//
|
|
const [actor, projectEditor, userToBeInvited] = await Promise.all([
|
|
createUser(),
|
|
createUser(),
|
|
createUser(),
|
|
]);
|
|
const teamProject1 = await createTeamProject();
|
|
|
|
await linkUserToProject(actor, teamProject1, role);
|
|
await linkUserToProject(projectEditor, teamProject1, 'project:editor');
|
|
|
|
//
|
|
// ACT
|
|
//
|
|
const response = await testServer
|
|
.authAgentFor(actor)
|
|
.patch(`/projects/${teamProject1.id}`)
|
|
.send({
|
|
name: teamProject1.name,
|
|
relations: [
|
|
// update the viewer to be the project admin
|
|
{ userId: actor.id, role: 'project:admin' },
|
|
// add a user to the project
|
|
{ userId: userToBeInvited.id, role: 'project:editor' },
|
|
// implicitly remove the project editor
|
|
] as Array<{
|
|
userId: string;
|
|
role: ProjectRole;
|
|
}>,
|
|
});
|
|
//.expect(403);
|
|
|
|
//
|
|
// ASSERT
|
|
//
|
|
expect(response.status).toBe(403);
|
|
expect(response.body).toMatchObject({
|
|
message: 'User is missing a scope required to perform this action',
|
|
});
|
|
const tp1Relations = await getProjectRelations({ projectId: teamProject1.id });
|
|
|
|
expect(tp1Relations.length).toBe(2);
|
|
expect(tp1Relations).toMatchObject(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ userId: actor.id, role }),
|
|
expect.objectContaining({ userId: projectEditor.id, role: 'project:editor' }),
|
|
]),
|
|
);
|
|
},
|
|
);
|
|
|
|
test.each([
|
|
['project:viewer', 'feat:projectRole:viewer'],
|
|
['project:editor', 'feat:projectRole:editor'],
|
|
] as const)(
|
|
"should not be able to add a user with the role %s if it's not licensed",
|
|
async (role, feature) => {
|
|
testServer.license.disable(feature);
|
|
const [projectAdmin, userToBeInvited] = await Promise.all([createUser(), createUser()]);
|
|
const teamProject = await createTeamProject('Team Project', projectAdmin);
|
|
|
|
await testServer
|
|
.authAgentFor(projectAdmin)
|
|
.patch(`/projects/${teamProject.id}`)
|
|
.send({
|
|
name: teamProject.name,
|
|
relations: [
|
|
{ userId: projectAdmin.id, role: 'project:admin' },
|
|
{ userId: userToBeInvited.id, role },
|
|
] as Array<{
|
|
userId: string;
|
|
role: ProjectRole;
|
|
}>,
|
|
})
|
|
.expect(400);
|
|
|
|
const tpRelations = await getProjectRelations({ projectId: teamProject.id });
|
|
expect(tpRelations.length).toBe(1);
|
|
expect(tpRelations).toMatchObject(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ userId: projectAdmin.id, role: 'project:admin' }),
|
|
]),
|
|
);
|
|
},
|
|
);
|
|
|
|
test("should not edit a relation of a project when changing a user's role to an unlicensed role", async () => {
|
|
testServer.license.disable('feat:projectRole:editor');
|
|
const [testUser1, testUser2, testUser3] = await Promise.all([
|
|
createUser(),
|
|
createUser(),
|
|
createUser(),
|
|
]);
|
|
const teamProject = await createTeamProject(undefined, testUser2);
|
|
|
|
await linkUserToProject(testUser1, teamProject, 'project:admin');
|
|
await linkUserToProject(testUser3, teamProject, 'project:admin');
|
|
|
|
const memberAgent = testServer.authAgentFor(testUser2);
|
|
|
|
const resp = await memberAgent.patch(`/projects/${teamProject.id}`).send({
|
|
name: teamProject.name,
|
|
relations: [
|
|
{ userId: testUser2.id, role: 'project:admin' },
|
|
{ userId: testUser1.id, role: 'project:editor' },
|
|
{ userId: testUser3.id, role: 'project:editor' },
|
|
] as Array<{
|
|
userId: string;
|
|
role: ProjectRole;
|
|
}>,
|
|
});
|
|
expect(resp.status).toBe(400);
|
|
|
|
const tpRelations = await getProjectRelations({ projectId: teamProject.id });
|
|
expect(tpRelations.length).toBe(3);
|
|
|
|
expect(tpRelations.find((p) => p.userId === testUser1.id)).not.toBeUndefined();
|
|
expect(tpRelations.find((p) => p.userId === testUser2.id)).not.toBeUndefined();
|
|
expect(tpRelations.find((p) => p.userId === testUser1.id)?.role).toBe('project:admin');
|
|
expect(tpRelations.find((p) => p.userId === testUser2.id)?.role).toBe('project:admin');
|
|
expect(tpRelations.find((p) => p.userId === testUser3.id)?.role).toBe('project:admin');
|
|
});
|
|
|
|
test("should edit a relation of a project when changing a user's role to an licensed role but unlicensed roles are present", async () => {
|
|
testServer.license.disable('feat:projectRole:viewer');
|
|
const [testUser1, testUser2, testUser3] = await Promise.all([
|
|
createUser(),
|
|
createUser(),
|
|
createUser(),
|
|
]);
|
|
const teamProject = await createTeamProject(undefined, testUser2);
|
|
|
|
await linkUserToProject(testUser1, teamProject, 'project:viewer');
|
|
await linkUserToProject(testUser3, teamProject, 'project:editor');
|
|
|
|
const memberAgent = testServer.authAgentFor(testUser2);
|
|
|
|
const resp = await memberAgent.patch(`/projects/${teamProject.id}`).send({
|
|
name: teamProject.name,
|
|
relations: [
|
|
{ userId: testUser1.id, role: 'project:viewer' },
|
|
{ userId: testUser2.id, role: 'project:admin' },
|
|
{ userId: testUser3.id, role: 'project:admin' },
|
|
] as Array<{
|
|
userId: string;
|
|
role: ProjectRole;
|
|
}>,
|
|
});
|
|
expect(resp.status).toBe(200);
|
|
|
|
const tpRelations = await getProjectRelations({ projectId: teamProject.id });
|
|
expect(tpRelations.length).toBe(3);
|
|
|
|
expect(tpRelations.find((p) => p.userId === testUser1.id)).not.toBeUndefined();
|
|
expect(tpRelations.find((p) => p.userId === testUser2.id)).not.toBeUndefined();
|
|
expect(tpRelations.find((p) => p.userId === testUser3.id)).not.toBeUndefined();
|
|
expect(tpRelations.find((p) => p.userId === testUser1.id)?.role).toBe('project:viewer');
|
|
expect(tpRelations.find((p) => p.userId === testUser2.id)?.role).toBe('project:admin');
|
|
expect(tpRelations.find((p) => p.userId === testUser3.id)?.role).toBe('project:admin');
|
|
});
|
|
|
|
test('should not add or remove users from a personal project', async () => {
|
|
const [testUser1, testUser2] = await Promise.all([createUser(), createUser()]);
|
|
|
|
const personalProject = await getPersonalProject(testUser1);
|
|
|
|
const memberAgent = testServer.authAgentFor(testUser1);
|
|
|
|
const resp = await memberAgent.patch(`/projects/${personalProject.id}`).send({
|
|
relations: [
|
|
{ userId: testUser1.id, role: 'project:personalOwner' },
|
|
{ userId: testUser2.id, role: 'project:admin' },
|
|
] as Array<{
|
|
userId: string;
|
|
role: ProjectRole;
|
|
}>,
|
|
});
|
|
expect(resp.status).toBe(403);
|
|
|
|
const p1Relations = await getProjectRelations({ projectId: personalProject.id });
|
|
expect(p1Relations.length).toBe(1);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('GET /project/:projectId', () => {
|
|
test('should get project details and relations', async () => {
|
|
const [ownerUser, testUser1, testUser2, _testUser3] = await Promise.all([
|
|
createOwner(),
|
|
createUser(),
|
|
createUser(),
|
|
createUser(),
|
|
]);
|
|
const [teamProject1, teamProject2] = await Promise.all([
|
|
createTeamProject(undefined, testUser2),
|
|
createTeamProject(),
|
|
]);
|
|
|
|
await linkUserToProject(testUser1, teamProject1, 'project:editor');
|
|
await linkUserToProject(ownerUser, teamProject2, 'project:editor');
|
|
await linkUserToProject(testUser2, teamProject2, 'project:editor');
|
|
|
|
const memberAgent = testServer.authAgentFor(testUser1);
|
|
|
|
const resp = await memberAgent.get(`/projects/${teamProject1.id}`);
|
|
expect(resp.status).toBe(200);
|
|
|
|
expect(resp.body.data.id).toBe(teamProject1.id);
|
|
expect(resp.body.data.name).toBe(teamProject1.name);
|
|
|
|
expect(resp.body.data.relations.length).toBe(2);
|
|
expect(resp.body.data.relations).toContainEqual({
|
|
id: testUser1.id,
|
|
email: testUser1.email,
|
|
firstName: testUser1.firstName,
|
|
lastName: testUser1.lastName,
|
|
role: 'project:editor',
|
|
});
|
|
expect(resp.body.data.relations).toContainEqual({
|
|
id: testUser2.id,
|
|
email: testUser2.email,
|
|
firstName: testUser2.firstName,
|
|
lastName: testUser2.lastName,
|
|
role: 'project:admin',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('DELETE /project/:projectId', () => {
|
|
test('allows the project:owner to delete a project', async () => {
|
|
const member = await createMember();
|
|
const project = await createTeamProject(undefined, member);
|
|
|
|
await testServer.authAgentFor(member).delete(`/projects/${project.id}`).expect(200);
|
|
|
|
const projectInDB = findProject(project.id);
|
|
|
|
await expect(projectInDB).rejects.toThrowError(EntityNotFoundError);
|
|
});
|
|
|
|
test('allows the instance owner to delete a team project their are not related to', async () => {
|
|
const owner = await createOwner();
|
|
|
|
const member = await createMember();
|
|
const project = await createTeamProject(undefined, member);
|
|
|
|
await testServer.authAgentFor(owner).delete(`/projects/${project.id}`).expect(200);
|
|
|
|
await expect(findProject(project.id)).rejects.toThrowError(EntityNotFoundError);
|
|
});
|
|
|
|
test('does not allow instance members to delete their personal project', async () => {
|
|
const member = await createMember();
|
|
const project = await getPersonalProject(member);
|
|
|
|
await testServer.authAgentFor(member).delete(`/projects/${project.id}`).expect(403);
|
|
|
|
const projectInDB = await findProject(project.id);
|
|
|
|
expect(projectInDB).toHaveProperty('id', project.id);
|
|
});
|
|
|
|
test('does not allow instance owners to delete their personal projects', async () => {
|
|
const owner = await createOwner();
|
|
const project = await getPersonalProject(owner);
|
|
|
|
await testServer.authAgentFor(owner).delete(`/projects/${project.id}`).expect(403);
|
|
|
|
const projectInDB = await findProject(project.id);
|
|
|
|
expect(projectInDB).toHaveProperty('id', project.id);
|
|
});
|
|
|
|
test.each(['project:editor', 'project:viewer'] as ProjectRole[])(
|
|
'does not allow users with the role %s to delete a project',
|
|
async (role) => {
|
|
const member = await createMember();
|
|
const project = await createTeamProject();
|
|
|
|
await linkUserToProject(member, project, role);
|
|
|
|
await testServer.authAgentFor(member).delete(`/projects/${project.id}`).expect(403);
|
|
|
|
const projectInDB = await findProject(project.id);
|
|
|
|
expect(projectInDB).toHaveProperty('id', project.id);
|
|
},
|
|
);
|
|
|
|
test('deletes all workflows and credentials it owns as well as the sharings into other projects', async () => {
|
|
//
|
|
// ARRANGE
|
|
//
|
|
const member = await createMember();
|
|
|
|
const otherProject = await createTeamProject(undefined, member);
|
|
const sharedWorkflow1 = await createWorkflow({}, otherProject);
|
|
const sharedWorkflow2 = await createWorkflow({}, otherProject);
|
|
const sharedCredential = await saveCredential(randomCredentialPayload(), {
|
|
project: otherProject,
|
|
role: 'credential:owner',
|
|
});
|
|
|
|
const projectToBeDeleted = await createTeamProject(undefined, member);
|
|
const ownedWorkflow = await createWorkflow({}, projectToBeDeleted);
|
|
const ownedCredential = await saveCredential(randomCredentialPayload(), {
|
|
project: projectToBeDeleted,
|
|
role: 'credential:owner',
|
|
});
|
|
|
|
await shareCredentialWithProjects(sharedCredential, [otherProject]);
|
|
await shareWorkflowWithProjects(sharedWorkflow1, [
|
|
{ project: otherProject, role: 'workflow:editor' },
|
|
]);
|
|
await shareWorkflowWithProjects(sharedWorkflow2, [
|
|
{ project: otherProject, role: 'workflow:editor' },
|
|
]);
|
|
|
|
//
|
|
// ACT
|
|
//
|
|
await testServer.authAgentFor(member).delete(`/projects/${projectToBeDeleted.id}`).expect(200);
|
|
|
|
//
|
|
// ASSERT
|
|
//
|
|
|
|
// Make sure the project and owned workflow and credential where deleted.
|
|
await expect(getWorkflowById(ownedWorkflow.id)).resolves.toBeNull();
|
|
await expect(getCredentialById(ownedCredential.id)).resolves.toBeNull();
|
|
await expect(findProject(projectToBeDeleted.id)).rejects.toThrowError(EntityNotFoundError);
|
|
|
|
// Make sure the shared workflow and credential were not deleted
|
|
await expect(getWorkflowById(sharedWorkflow1.id)).resolves.not.toBeNull();
|
|
await expect(getCredentialById(sharedCredential.id)).resolves.not.toBeNull();
|
|
|
|
// Make sure the sharings for them have been deleted
|
|
await expect(
|
|
Container.get(SharedWorkflowRepository).findOneByOrFail({
|
|
projectId: projectToBeDeleted.id,
|
|
workflowId: sharedWorkflow1.id,
|
|
}),
|
|
).rejects.toThrowError(EntityNotFoundError);
|
|
await expect(
|
|
Container.get(SharedCredentialsRepository).findOneByOrFail({
|
|
projectId: projectToBeDeleted.id,
|
|
credentialsId: sharedCredential.id,
|
|
}),
|
|
).rejects.toThrowError(EntityNotFoundError);
|
|
});
|
|
|
|
test('unshares all workflows and credentials that were shared with the project', async () => {
|
|
//
|
|
// ARRANGE
|
|
//
|
|
const member = await createMember();
|
|
|
|
const projectToBeDeleted = await createTeamProject(undefined, member);
|
|
const ownedWorkflow1 = await createWorkflow({}, projectToBeDeleted);
|
|
const ownedWorkflow2 = await createWorkflow({}, projectToBeDeleted);
|
|
const ownedCredential = await saveCredential(randomCredentialPayload(), {
|
|
project: projectToBeDeleted,
|
|
role: 'credential:owner',
|
|
});
|
|
|
|
const otherProject = await createTeamProject(undefined, member);
|
|
|
|
await shareCredentialWithProjects(ownedCredential, [otherProject]);
|
|
await shareWorkflowWithProjects(ownedWorkflow1, [
|
|
{ project: otherProject, role: 'workflow:editor' },
|
|
]);
|
|
await shareWorkflowWithProjects(ownedWorkflow2, [
|
|
{ project: otherProject, role: 'workflow:editor' },
|
|
]);
|
|
|
|
//
|
|
// ACT
|
|
//
|
|
await testServer.authAgentFor(member).delete(`/projects/${projectToBeDeleted.id}`).expect(200);
|
|
|
|
//
|
|
// ASSERT
|
|
//
|
|
|
|
// Make sure the project and owned workflow and credential where deleted.
|
|
await expect(getWorkflowById(ownedWorkflow1.id)).resolves.toBeNull();
|
|
await expect(getWorkflowById(ownedWorkflow2.id)).resolves.toBeNull();
|
|
await expect(getCredentialById(ownedCredential.id)).resolves.toBeNull();
|
|
await expect(findProject(projectToBeDeleted.id)).rejects.toThrowError(EntityNotFoundError);
|
|
|
|
// Make sure the sharings for them into the other project have been deleted
|
|
await expect(
|
|
Container.get(SharedWorkflowRepository).findOneByOrFail({
|
|
projectId: projectToBeDeleted.id,
|
|
workflowId: ownedWorkflow1.id,
|
|
}),
|
|
).rejects.toThrowError(EntityNotFoundError);
|
|
await expect(
|
|
Container.get(SharedWorkflowRepository).findOneByOrFail({
|
|
projectId: projectToBeDeleted.id,
|
|
workflowId: ownedWorkflow2.id,
|
|
}),
|
|
).rejects.toThrowError(EntityNotFoundError);
|
|
await expect(
|
|
Container.get(SharedCredentialsRepository).findOneByOrFail({
|
|
projectId: projectToBeDeleted.id,
|
|
credentialsId: ownedCredential.id,
|
|
}),
|
|
).rejects.toThrowError(EntityNotFoundError);
|
|
});
|
|
|
|
test('deletes the project relations', async () => {
|
|
//
|
|
// ARRANGE
|
|
//
|
|
const member = await createMember();
|
|
const editor = await createMember();
|
|
const viewer = await createMember();
|
|
|
|
const project = await createTeamProject(undefined, member);
|
|
await linkUserToProject(editor, project, 'project:editor');
|
|
await linkUserToProject(viewer, project, 'project:viewer');
|
|
|
|
//
|
|
// ACT
|
|
//
|
|
await testServer.authAgentFor(member).delete(`/projects/${project.id}`).expect(200);
|
|
|
|
//
|
|
// ASSERT
|
|
//
|
|
await expect(
|
|
Container.get(ProjectRelationRepository).findOneByOrFail({
|
|
projectId: project.id,
|
|
userId: member.id,
|
|
}),
|
|
).rejects.toThrowError(EntityNotFoundError);
|
|
await expect(
|
|
Container.get(ProjectRelationRepository).findOneByOrFail({
|
|
projectId: project.id,
|
|
userId: editor.id,
|
|
}),
|
|
).rejects.toThrowError(EntityNotFoundError);
|
|
await expect(
|
|
Container.get(ProjectRelationRepository).findOneByOrFail({
|
|
projectId: project.id,
|
|
userId: viewer.id,
|
|
}),
|
|
).rejects.toThrowError(EntityNotFoundError);
|
|
});
|
|
|
|
// Tests related to migrating workflows and credentials to new project:
|
|
|
|
test('should fail if the project to delete does not exist', async () => {
|
|
const member = await createMember();
|
|
|
|
await testServer.authAgentFor(member).delete('/projects/1234').expect(403);
|
|
});
|
|
|
|
test('should fail to delete if project to migrate to and the project to delete are the same', async () => {
|
|
const member = await createMember();
|
|
const project = await createTeamProject(undefined, member);
|
|
|
|
await testServer
|
|
.authAgentFor(member)
|
|
.delete(`/projects/${project.id}`)
|
|
.query({ transferId: project.id })
|
|
.expect(400);
|
|
});
|
|
|
|
test('does not migrate credentials and projects if the user does not have the permissions to create workflows or credentials in the target project', async () => {
|
|
//
|
|
// ARRANGE
|
|
//
|
|
const member = await createMember();
|
|
|
|
const projectToBeDeleted = await createTeamProject(undefined, member);
|
|
const targetProject = await createTeamProject();
|
|
await linkUserToProject(member, targetProject, 'project:viewer');
|
|
|
|
//
|
|
// ACT
|
|
//
|
|
await testServer
|
|
.authAgentFor(member)
|
|
.delete(`/projects/${projectToBeDeleted.id}`)
|
|
.query({ transferId: targetProject.id })
|
|
//
|
|
// ASSERT
|
|
//
|
|
.expect(404);
|
|
});
|
|
|
|
test('migrates workflows and credentials to another project if `migrateToProject` is passed', async () => {
|
|
//
|
|
// ARRANGE
|
|
//
|
|
const member = await createMember();
|
|
|
|
const projectToBeDeleted = await createTeamProject(undefined, member);
|
|
const targetProject = await createTeamProject(undefined, member);
|
|
const otherProject = await createTeamProject(undefined, member);
|
|
|
|
// these should be re-owned to the targetProject
|
|
const ownedCredential = await saveCredential(randomCredentialPayload(), {
|
|
project: projectToBeDeleted,
|
|
role: 'credential:owner',
|
|
});
|
|
const ownedWorkflow = await createWorkflow({}, projectToBeDeleted);
|
|
|
|
// these should stay intact
|
|
await shareCredentialWithProjects(ownedCredential, [otherProject]);
|
|
await shareWorkflowWithProjects(ownedWorkflow, [
|
|
{ project: otherProject, role: 'workflow:editor' },
|
|
]);
|
|
|
|
//
|
|
// ACT
|
|
//
|
|
await testServer
|
|
.authAgentFor(member)
|
|
.delete(`/projects/${projectToBeDeleted.id}`)
|
|
.query({ transferId: targetProject.id })
|
|
.expect(200);
|
|
|
|
//
|
|
// ASSERT
|
|
//
|
|
|
|
// projectToBeDeleted is deleted
|
|
await expect(findProject(projectToBeDeleted.id)).rejects.toThrowError(EntityNotFoundError);
|
|
|
|
// ownedWorkflow has not been deleted
|
|
await expect(getWorkflowById(ownedWorkflow.id)).resolves.toBeDefined();
|
|
|
|
// ownedCredential has not been deleted
|
|
await expect(getCredentialById(ownedCredential.id)).resolves.toBeDefined();
|
|
|
|
// there is a sharing for ownedWorkflow and targetProject
|
|
await expect(
|
|
Container.get(SharedCredentialsRepository).findOneByOrFail({
|
|
credentialsId: ownedCredential.id,
|
|
projectId: targetProject.id,
|
|
role: 'credential:owner',
|
|
}),
|
|
).resolves.toBeDefined();
|
|
|
|
// there is a sharing for ownedCredential and targetProject
|
|
await expect(
|
|
Container.get(SharedWorkflowRepository).findOneByOrFail({
|
|
workflowId: ownedWorkflow.id,
|
|
projectId: targetProject.id,
|
|
role: 'workflow:owner',
|
|
}),
|
|
).resolves.toBeDefined();
|
|
|
|
// there is a sharing for ownedWorkflow and otherProject
|
|
await expect(
|
|
Container.get(SharedWorkflowRepository).findOneByOrFail({
|
|
workflowId: ownedWorkflow.id,
|
|
projectId: otherProject.id,
|
|
role: 'workflow:editor',
|
|
}),
|
|
).resolves.toBeDefined();
|
|
|
|
// there is a sharing for ownedCredential and otherProject
|
|
await expect(
|
|
Container.get(SharedCredentialsRepository).findOneByOrFail({
|
|
credentialsId: ownedCredential.id,
|
|
projectId: otherProject.id,
|
|
role: 'credential:user',
|
|
}),
|
|
).resolves.toBeDefined();
|
|
});
|
|
|
|
// This test is testing behavior that is explicitly not enabled right now,
|
|
// but we want this to work if we in the future allow sharing of credentials
|
|
// and/or workflows between team projects.
|
|
test('should upgrade a projects role if the workflow/credential is already shared with it', async () => {
|
|
//
|
|
// ARRANGE
|
|
//
|
|
const member = await createMember();
|
|
const project = await createTeamProject(undefined, member);
|
|
const credential = await saveCredential(randomCredentialPayload(), {
|
|
project,
|
|
role: 'credential:owner',
|
|
});
|
|
const workflow = await createWorkflow({}, project);
|
|
const projectToMigrateTo = await createTeamProject(undefined, member);
|
|
|
|
await shareWorkflowWithProjects(workflow, [
|
|
{ project: projectToMigrateTo, role: 'workflow:editor' },
|
|
]);
|
|
await shareCredentialWithProjects(credential, [projectToMigrateTo]);
|
|
|
|
//
|
|
// ACT
|
|
//
|
|
await testServer
|
|
.authAgentFor(member)
|
|
.delete(`/projects/${project.id}`)
|
|
.query({ transferId: projectToMigrateTo.id })
|
|
.expect(200);
|
|
|
|
//
|
|
// ASSERT
|
|
//
|
|
|
|
await expect(
|
|
Container.get(SharedCredentialsRepository).findOneByOrFail({
|
|
credentialsId: credential.id,
|
|
projectId: projectToMigrateTo.id,
|
|
role: 'credential:owner',
|
|
}),
|
|
).resolves.toBeDefined();
|
|
await expect(
|
|
Container.get(SharedWorkflowRepository).findOneByOrFail({
|
|
workflowId: workflow.id,
|
|
projectId: projectToMigrateTo.id,
|
|
role: 'workflow:owner',
|
|
}),
|
|
).resolves.toBeDefined();
|
|
});
|
|
});
|