n8n/packages/cli/test/integration/project.api.test.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

1191 lines
38 KiB
TypeScript
Raw Normal View History

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();
});
});