mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(core): Add endpoint to transfer folder to another project (no-changelog) (#15005)
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
import { z } from 'zod';
|
||||
import { Z } from 'zod-class';
|
||||
|
||||
import { folderIdSchema } from '../../schemas/folder.schema';
|
||||
|
||||
export class TransferFolderBodyDto extends Z.class({
|
||||
destinationProjectId: z.string(),
|
||||
shareCredentials: z.array(z.string()).optional(),
|
||||
destinationParentFolderId: folderIdSchema,
|
||||
}) {}
|
||||
@@ -63,6 +63,7 @@ export { CreateFolderDto } from './folders/create-folder.dto';
|
||||
export { UpdateFolderDto } from './folders/update-folder.dto';
|
||||
export { DeleteFolderDto } from './folders/delete-folder.dto';
|
||||
export { ListFolderQueryDto } from './folders/list-folder-query.dto';
|
||||
export { TransferFolderBodyDto } from './folders/transfer-folder.dto';
|
||||
|
||||
export { ListInsightsWorkflowQueryDto } from './insights/list-workflow-query.dto';
|
||||
export { InsightsDateFilterDto } from './insights/date-filter.dto';
|
||||
|
||||
@@ -23,7 +23,7 @@ export const RESOURCES = {
|
||||
variable: [...DEFAULT_OPERATIONS] as const,
|
||||
workersView: ['manage'] as const,
|
||||
workflow: ['share', 'execute', 'move', ...DEFAULT_OPERATIONS] as const,
|
||||
folder: [...DEFAULT_OPERATIONS] as const,
|
||||
folder: [...DEFAULT_OPERATIONS, 'move'] as const,
|
||||
insights: ['list'] as const,
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -76,6 +76,7 @@ export const GLOBAL_OWNER_SCOPES: Scope[] = [
|
||||
'project:update',
|
||||
'project:delete',
|
||||
'insights:list',
|
||||
'folder:move',
|
||||
];
|
||||
|
||||
export const GLOBAL_ADMIN_SCOPES = GLOBAL_OWNER_SCOPES.concat();
|
||||
|
||||
@@ -30,6 +30,7 @@ export const REGULAR_PROJECT_ADMIN_SCOPES: Scope[] = [
|
||||
'folder:update',
|
||||
'folder:delete',
|
||||
'folder:list',
|
||||
'folder:move',
|
||||
];
|
||||
|
||||
export const PERSONAL_PROJECT_OWNER_SCOPES: Scope[] = [
|
||||
@@ -55,6 +56,7 @@ export const PERSONAL_PROJECT_OWNER_SCOPES: Scope[] = [
|
||||
'folder:update',
|
||||
'folder:delete',
|
||||
'folder:list',
|
||||
'folder:move',
|
||||
];
|
||||
|
||||
export const PROJECT_EDITOR_SCOPES: Scope[] = [
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
CreateFolderDto,
|
||||
DeleteFolderDto,
|
||||
ListFolderQueryDto,
|
||||
TransferFolderBodyDto,
|
||||
UpdateFolderDto,
|
||||
} from '@n8n/api-types';
|
||||
import {
|
||||
@@ -13,6 +14,8 @@ import {
|
||||
Patch,
|
||||
Delete,
|
||||
Query,
|
||||
Put,
|
||||
Param,
|
||||
} from '@n8n/decorators';
|
||||
import { Response } from 'express';
|
||||
import { UserError } from 'n8n-workflow';
|
||||
@@ -21,13 +24,17 @@ import { FolderNotFoundError } from '@/errors/folder-not-found.error';
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
|
||||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||
import type { ListQuery } from '@/requests';
|
||||
import { AuthenticatedRequest } from '@/requests';
|
||||
import type { ListQuery } from '@/requests';
|
||||
import { FolderService } from '@/services/folder.service';
|
||||
import { EnterpriseWorkflowService } from '@/workflows/workflow.service.ee';
|
||||
|
||||
@RestController('/projects/:projectId/folders')
|
||||
export class ProjectController {
|
||||
constructor(private readonly folderService: FolderService) {}
|
||||
constructor(
|
||||
private readonly folderService: FolderService,
|
||||
private readonly enterpriseWorkflowService: EnterpriseWorkflowService,
|
||||
) {}
|
||||
|
||||
@Post('/')
|
||||
@ProjectScope('folder:create')
|
||||
@@ -145,4 +152,23 @@ export class ProjectController {
|
||||
throw new InternalServerError(undefined, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Put('/:folderId/transfer')
|
||||
@ProjectScope('folder:move')
|
||||
async transferFolderToProject(
|
||||
req: AuthenticatedRequest,
|
||||
_res: unknown,
|
||||
@Param('folderId') sourceFolderId: string,
|
||||
@Param('projectId') sourceProjectId: string,
|
||||
@Body body: TransferFolderBodyDto,
|
||||
) {
|
||||
return await this.enterpriseWorkflowService.transferFolder(
|
||||
req.user,
|
||||
sourceProjectId,
|
||||
sourceFolderId,
|
||||
body.destinationProjectId,
|
||||
body.destinationParentFolderId,
|
||||
body.shareCredentials,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ export = {
|
||||
|
||||
const body = z.object({ destinationProjectId: z.string() }).parse(req.body);
|
||||
|
||||
await Container.get(EnterpriseWorkflowService).transferOne(
|
||||
await Container.get(EnterpriseWorkflowService).transferWorkflow(
|
||||
req.user,
|
||||
workflowId,
|
||||
body.destinationProjectId,
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import type {
|
||||
CredentialsEntity,
|
||||
User,
|
||||
WorkflowEntity,
|
||||
WorkflowWithSharingsAndCredentials,
|
||||
WorkflowWithSharingsMetaDataAndCredentials,
|
||||
} from '@n8n/db';
|
||||
import { Project, SharedWorkflow, CredentialsRepository } from '@n8n/db';
|
||||
import { Folder, Project, SharedWorkflow, CredentialsRepository, FolderRepository } from '@n8n/db';
|
||||
import { Service } from '@n8n/di';
|
||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||
import { In, type EntityManager } from '@n8n/typeorm';
|
||||
import omit from 'lodash/omit';
|
||||
import { Logger } from 'n8n-core';
|
||||
import type { IWorkflowBase, WorkflowId } from 'n8n-workflow';
|
||||
import { NodeOperationError, UserError, WorkflowActivationError } from 'n8n-workflow';
|
||||
import { NodeOperationError, PROJECT_ROOT, UserError, WorkflowActivationError } from 'n8n-workflow';
|
||||
|
||||
import { ActiveWorkflowManager } from '@/active-workflow-manager';
|
||||
import { CredentialsFinderService } from '@/credentials/credentials-finder.service';
|
||||
@@ -43,6 +44,7 @@ export class EnterpriseWorkflowService {
|
||||
private readonly enterpriseCredentialsService: EnterpriseCredentialsService,
|
||||
private readonly workflowFinderService: WorkflowFinderService,
|
||||
private readonly folderService: FolderService,
|
||||
private readonly folderRepository: FolderRepository,
|
||||
) {}
|
||||
|
||||
async shareWithProjects(
|
||||
@@ -260,7 +262,7 @@ export class EnterpriseWorkflowService {
|
||||
});
|
||||
}
|
||||
|
||||
async transferOne(
|
||||
async transferWorkflow(
|
||||
user: User,
|
||||
workflowId: string,
|
||||
destinationProjectId: string,
|
||||
@@ -326,56 +328,114 @@ export class EnterpriseWorkflowService {
|
||||
}
|
||||
|
||||
// 7. transfer the workflow
|
||||
await this.workflowRepository.manager.transaction(async (trx) => {
|
||||
// remove all sharings
|
||||
await trx.remove(workflow.shared);
|
||||
|
||||
// create new owner-sharing
|
||||
await trx.save(
|
||||
trx.create(SharedWorkflow, {
|
||||
workflowId: workflow.id,
|
||||
projectId: destinationProject.id,
|
||||
role: 'workflow:owner',
|
||||
}),
|
||||
);
|
||||
});
|
||||
await this.transferWorkflowOwnership([workflow], destinationProject.id);
|
||||
|
||||
// 8. share credentials into the destination project
|
||||
await this.workflowRepository.manager.transaction(async (trx) => {
|
||||
const allCredentials = await this.credentialsFinderService.findAllCredentialsForUser(
|
||||
user,
|
||||
['credential:share'],
|
||||
trx,
|
||||
);
|
||||
const credentialsAllowedToShare = allCredentials.filter((c) =>
|
||||
shareCredentials.includes(c.id),
|
||||
);
|
||||
|
||||
for (const credential of credentialsAllowedToShare) {
|
||||
await this.enterpriseCredentialsService.shareWithProjects(
|
||||
user,
|
||||
credential.id,
|
||||
[destinationProject.id],
|
||||
trx,
|
||||
);
|
||||
}
|
||||
});
|
||||
await this.shareCredentialsWithProject(user, shareCredentials, destinationProject.id);
|
||||
|
||||
// 9. Move workflow to the right folder if any
|
||||
await this.workflowRepository.update({ id: workflow.id }, { parentFolder });
|
||||
|
||||
// 10. try to activate it again if it was active
|
||||
if (wasActive) {
|
||||
try {
|
||||
await this.activeWorkflowManager.add(workflowId, 'update');
|
||||
return await this.attemptWorkflowReactivation(workflowId);
|
||||
}
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
await this.workflowRepository.updateActiveState(workflowId, false);
|
||||
}
|
||||
|
||||
// Since the transfer worked we return a 200 but also return the
|
||||
// activation error as data.
|
||||
if (error instanceof WorkflowActivationError) {
|
||||
async transferFolder(
|
||||
user: User,
|
||||
sourceProjectId: string,
|
||||
sourceFolderId: string,
|
||||
destinationProjectId: string,
|
||||
destinationParentFolderId: string,
|
||||
shareCredentials: string[] = [],
|
||||
) {
|
||||
// 1. Get all children folders
|
||||
|
||||
const childrenFolderIds = await this.folderRepository.getAllFolderIdsInHierarchy(
|
||||
sourceFolderId,
|
||||
sourceProjectId,
|
||||
);
|
||||
|
||||
// 2. Get all workflows in the nested folders
|
||||
|
||||
const workflows = await this.workflowRepository.find({
|
||||
select: ['id', 'active', 'shared'],
|
||||
relations: ['shared', 'shared.project'],
|
||||
where: {
|
||||
parentFolder: { id: In([...childrenFolderIds, sourceFolderId]) },
|
||||
},
|
||||
});
|
||||
|
||||
const activeWorkflows = workflows.filter((w) => w.active).map((w) => w.id);
|
||||
|
||||
// 3. get destination project
|
||||
const destinationProject = await this.projectService.getProjectWithScope(
|
||||
user,
|
||||
destinationProjectId,
|
||||
['workflow:create'],
|
||||
);
|
||||
NotFoundError.isDefinedAndNotNull(
|
||||
destinationProject,
|
||||
`Could not find project with the id "${destinationProjectId}". Make sure you have the permission to create workflows in it.`,
|
||||
);
|
||||
|
||||
// 4. checks
|
||||
|
||||
if (destinationParentFolderId !== PROJECT_ROOT) {
|
||||
await this.folderRepository.findOneOrFailFolderInProject(
|
||||
destinationParentFolderId,
|
||||
destinationProjectId,
|
||||
);
|
||||
}
|
||||
|
||||
await this.folderRepository.findOneOrFailFolderInProject(sourceFolderId, sourceProjectId);
|
||||
|
||||
for (const workflow of workflows) {
|
||||
const ownerSharing = workflow.shared.find((s) => s.role === 'workflow:owner')!;
|
||||
NotFoundError.isDefinedAndNotNull(
|
||||
ownerSharing,
|
||||
`Could not find owner for workflow "${workflow.id}"`,
|
||||
);
|
||||
const sourceProject = ownerSharing.project;
|
||||
if (sourceProject.id === destinationProject.id) {
|
||||
throw new TransferWorkflowError(
|
||||
"You can't transfer a workflow into the project that's already owning it.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. deactivate all workflows if necessary
|
||||
const deactivateWorkflowsPromises = activeWorkflows.map(
|
||||
async (workflowId) => await this.activeWorkflowManager.remove(workflowId),
|
||||
);
|
||||
|
||||
await Promise.all(deactivateWorkflowsPromises);
|
||||
|
||||
// 6. transfer the workflows
|
||||
await this.transferWorkflowOwnership(workflows, destinationProject.id);
|
||||
|
||||
// 7. share credentials into the destination project
|
||||
await this.shareCredentialsWithProject(user, shareCredentials, destinationProject.id);
|
||||
|
||||
// 8. Move all children folder to the destination project
|
||||
await this.moveFoldersToDestination(
|
||||
sourceFolderId,
|
||||
childrenFolderIds,
|
||||
destinationProjectId,
|
||||
destinationParentFolderId,
|
||||
);
|
||||
|
||||
// 9. try to activate workflows again if they were active
|
||||
|
||||
for (const workflowId of activeWorkflows) {
|
||||
await this.attemptWorkflowReactivation(workflowId);
|
||||
}
|
||||
}
|
||||
|
||||
private formatActivationError(error: WorkflowActivationError) {
|
||||
return {
|
||||
error: error.toJSON
|
||||
? error.toJSON()
|
||||
@@ -386,10 +446,91 @@ export class EnterpriseWorkflowService {
|
||||
};
|
||||
}
|
||||
|
||||
private async attemptWorkflowReactivation(workflowId: string) {
|
||||
try {
|
||||
await this.activeWorkflowManager.add(workflowId, 'update');
|
||||
return;
|
||||
} catch (error) {
|
||||
await this.workflowRepository.updateActiveState(workflowId, false);
|
||||
|
||||
if (error instanceof WorkflowActivationError) {
|
||||
return this.formatActivationError(error);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
private async transferWorkflowOwnership(
|
||||
workflows: WorkflowEntity[],
|
||||
destinationProjectId: string,
|
||||
) {
|
||||
await this.workflowRepository.manager.transaction(async (trx) => {
|
||||
for (const workflow of workflows) {
|
||||
// Remove all sharings
|
||||
await trx.remove(workflow.shared);
|
||||
|
||||
// Create new owner-sharing
|
||||
await trx.save(
|
||||
trx.create(SharedWorkflow, {
|
||||
workflowId: workflow.id,
|
||||
projectId: destinationProjectId,
|
||||
role: 'workflow:owner',
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async shareCredentialsWithProject(
|
||||
user: User,
|
||||
credentialIds: string[],
|
||||
projectId: string,
|
||||
) {
|
||||
await this.workflowRepository.manager.transaction(async (trx) => {
|
||||
const allCredentials = await this.credentialsFinderService.findAllCredentialsForUser(
|
||||
user,
|
||||
['credential:share'],
|
||||
trx,
|
||||
);
|
||||
|
||||
const credentialsToShare = allCredentials.filter((c) => credentialIds.includes(c.id));
|
||||
|
||||
for (const credential of credentialsToShare) {
|
||||
await this.enterpriseCredentialsService.shareWithProjects(
|
||||
user,
|
||||
credential.id,
|
||||
[projectId],
|
||||
trx,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async moveFoldersToDestination(
|
||||
sourceFolderId: string,
|
||||
childrenFolderIds: string[],
|
||||
destinationProjectId: string,
|
||||
destinationParentFolderId: string,
|
||||
) {
|
||||
await this.folderRepository.manager.transaction(async (trx) => {
|
||||
// Move all children folders to the destination project
|
||||
await trx.update(
|
||||
Folder,
|
||||
{ id: In(childrenFolderIds) },
|
||||
{ homeProject: { id: destinationProjectId } },
|
||||
);
|
||||
|
||||
// Move source folder to destination project and under destination folder if specified
|
||||
await trx.update(
|
||||
Folder,
|
||||
{ id: sourceFolderId },
|
||||
{
|
||||
homeProject: { id: destinationProjectId },
|
||||
parentFolder:
|
||||
destinationParentFolderId === PROJECT_ROOT ? null : { id: destinationParentFolderId },
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -548,7 +548,7 @@ export class WorkflowsController {
|
||||
@Param('workflowId') workflowId: string,
|
||||
@Body body: TransferWorkflowBodyDto,
|
||||
) {
|
||||
return await this.enterpriseWorkflowService.transferOne(
|
||||
return await this.enterpriseWorkflowService.transferWorkflow(
|
||||
req.user,
|
||||
workflowId,
|
||||
body.destinationProjectId,
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
import type { Project } from '@n8n/db';
|
||||
import type { Project, ProjectRole } from '@n8n/db';
|
||||
import type { User } from '@n8n/db';
|
||||
import { FolderRepository } from '@n8n/db';
|
||||
import { ProjectRepository } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import { DateTime } from 'luxon';
|
||||
import { PROJECT_ROOT } from 'n8n-workflow';
|
||||
import { ApplicationError, PROJECT_ROOT } from 'n8n-workflow';
|
||||
|
||||
import { ActiveWorkflowManager } from '@/active-workflow-manager';
|
||||
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
|
||||
import { mockInstance } from '@test/mocking';
|
||||
import {
|
||||
getCredentialSharings,
|
||||
saveCredential,
|
||||
shareCredentialWithProjects,
|
||||
shareCredentialWithUsers,
|
||||
} from '@test-integration/db/credentials';
|
||||
import { createFolder } from '@test-integration/db/folders';
|
||||
import { createTag } from '@test-integration/db/tags';
|
||||
import { createWorkflow } from '@test-integration/db/workflows';
|
||||
import { createWorkflow, getWorkflowSharing } from '@test-integration/db/workflows';
|
||||
import { randomCredentialPayload } from '@test-integration/random';
|
||||
|
||||
import { createTeamProject, getPersonalProject, linkUserToProject } from '../shared/db/projects';
|
||||
import { createOwner, createMember } from '../shared/db/users';
|
||||
import { createOwner, createMember, createUser, createAdmin } from '../shared/db/users';
|
||||
import * as testDb from '../shared/test-db';
|
||||
import type { SuperAgentTest } from '../shared/types';
|
||||
import * as utils from '../shared/utils/';
|
||||
@@ -23,6 +32,7 @@ let authOwnerAgent: SuperAgentTest;
|
||||
let authMemberAgent: SuperAgentTest;
|
||||
let ownerProject: Project;
|
||||
let memberProject: Project;
|
||||
let admin: User;
|
||||
|
||||
const testServer = utils.setupTestServer({
|
||||
endpointGroups: ['folder'],
|
||||
@@ -32,6 +42,8 @@ let projectRepository: ProjectRepository;
|
||||
let folderRepository: FolderRepository;
|
||||
let workflowRepository: WorkflowRepository;
|
||||
|
||||
const activeWorkflowManager = mockInstance(ActiveWorkflowManager);
|
||||
|
||||
beforeEach(async () => {
|
||||
await testDb.truncate(['Folder', 'SharedWorkflow', 'TagEntity', 'Project', 'ProjectRelation']);
|
||||
|
||||
@@ -46,6 +58,7 @@ beforeEach(async () => {
|
||||
|
||||
ownerProject = await getPersonalProject(owner);
|
||||
memberProject = await getPersonalProject(member);
|
||||
admin = await createAdmin();
|
||||
});
|
||||
|
||||
describe('POST /projects/:projectId/folders', () => {
|
||||
@@ -1354,3 +1367,805 @@ describe('GET /projects/:projectId/folders/content', () => {
|
||||
expect(response.body.data.totalSubFolders).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /projects/:projectId/folders/:folderId/transfer', () => {
|
||||
test('cannot transfer into the same project', async () => {
|
||||
const sourceProject = await createTeamProject('source project', member);
|
||||
const destinationProject = await createTeamProject('Team Project', member);
|
||||
|
||||
const sourceFolder1 = await createFolder(sourceProject, { name: 'Source Folder 1' });
|
||||
|
||||
await createWorkflow({ active: true, parentFolder: sourceFolder1 }, destinationProject);
|
||||
|
||||
await testServer
|
||||
.authAgentFor(member)
|
||||
.put(`/projects/${sourceProject.id}/folders/${sourceFolder1.id}/transfer`)
|
||||
.send({ destinationProjectId: destinationProject.id, destinationParentFolderId: '0' })
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
test('cannot transfer somebody elses folder', async () => {
|
||||
const sourceProject = await createTeamProject('source project', member);
|
||||
const sourceFolder1 = await createFolder(sourceProject, { name: 'Source Folder 1' });
|
||||
await createWorkflow({ parentFolder: sourceFolder1 }, owner);
|
||||
|
||||
const destinationProject = await createTeamProject('Team Project', admin);
|
||||
const destinationFolder1 = await createFolder(destinationProject, { name: 'Source Folder 1' });
|
||||
|
||||
await testServer
|
||||
.authAgentFor(member)
|
||||
.put(`/projects/${sourceProject.id}/folders/${sourceFolder1.id}/transfer`)
|
||||
.send({
|
||||
destinationProjectId: destinationProject.id,
|
||||
destinationParentFolderId: destinationFolder1,
|
||||
})
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
test("cannot transfer if you're not a member of the destination project", async () => {
|
||||
const sourceProject = await getPersonalProject(member);
|
||||
const destinationProject = await createTeamProject('Team Project', owner);
|
||||
|
||||
const sourceFolder1 = await createFolder(sourceProject, { name: 'Source Folder 1' });
|
||||
|
||||
await createWorkflow({ active: true }, destinationProject);
|
||||
|
||||
await testServer
|
||||
.authAgentFor(member)
|
||||
.put(`/projects/${sourceProject.id}/folders/${sourceFolder1.id}/transfer`)
|
||||
.send({ destinationProjectId: destinationProject.id, destinationParentFolderId: '0' })
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
test.each<ProjectRole>(['project:editor', 'project:viewer'])(
|
||||
'%ss cannot transfer workflows',
|
||||
async (projectRole) => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const sourceProject = await createTeamProject();
|
||||
await linkUserToProject(member, sourceProject, projectRole);
|
||||
|
||||
const sourceFolder1 = await createFolder(sourceProject, { name: 'Source Folder 1' });
|
||||
|
||||
await createWorkflow({}, sourceProject);
|
||||
|
||||
const destinationProject = await createTeamProject();
|
||||
await linkUserToProject(member, destinationProject, 'project:admin');
|
||||
|
||||
//
|
||||
// ACT & ASSERT
|
||||
//
|
||||
await testServer
|
||||
.authAgentFor(member)
|
||||
.put(`/projects/${sourceProject.id}/folders/${sourceFolder1.id}/transfer`)
|
||||
.send({ destinationProjectId: destinationProject.id, destinationParentFolderId: '0' })
|
||||
.expect(403);
|
||||
},
|
||||
);
|
||||
|
||||
test.each<
|
||||
[
|
||||
// user role
|
||||
'owners' | 'admins',
|
||||
// source project type
|
||||
'team' | 'personal',
|
||||
// destination project type
|
||||
'team' | 'personal',
|
||||
// actor
|
||||
() => User,
|
||||
// source project
|
||||
() => Promise<Project> | Project,
|
||||
// destination project
|
||||
() => Promise<Project> | Project,
|
||||
]
|
||||
>([
|
||||
// owner
|
||||
[
|
||||
'owners',
|
||||
'team',
|
||||
'team',
|
||||
() => owner,
|
||||
async () => await createTeamProject('Source Project'),
|
||||
async () => await createTeamProject('Destination Project'),
|
||||
],
|
||||
[
|
||||
'owners',
|
||||
'team',
|
||||
'personal',
|
||||
() => owner,
|
||||
async () => await createTeamProject('Source Project'),
|
||||
() => memberProject,
|
||||
],
|
||||
[
|
||||
'owners',
|
||||
'personal',
|
||||
'team',
|
||||
() => owner,
|
||||
() => memberProject,
|
||||
async () => await createTeamProject('Destination Project'),
|
||||
],
|
||||
|
||||
// admin
|
||||
[
|
||||
'admins',
|
||||
'team',
|
||||
'team',
|
||||
() => admin,
|
||||
async () => await createTeamProject('Source Project'),
|
||||
async () => await createTeamProject('Destination Project'),
|
||||
],
|
||||
[
|
||||
'admins',
|
||||
'team',
|
||||
'personal',
|
||||
() => admin,
|
||||
async () => await createTeamProject('Source Project'),
|
||||
() => memberProject,
|
||||
],
|
||||
[
|
||||
'admins',
|
||||
'personal',
|
||||
'team',
|
||||
() => admin,
|
||||
() => memberProject,
|
||||
async () => await createTeamProject('Destination Project'),
|
||||
],
|
||||
])(
|
||||
'global %s can transfer workflows from a %s project to a %s project',
|
||||
async (
|
||||
_roleName,
|
||||
_sourceProjectName,
|
||||
_destinationProjectName,
|
||||
getActor,
|
||||
getSourceProject,
|
||||
getDestinationProject,
|
||||
) => {
|
||||
// ARRANGE
|
||||
const actor = getActor();
|
||||
const sourceProject = await getSourceProject();
|
||||
const destinationProject = await getDestinationProject();
|
||||
const sourceFolder1 = await createFolder(sourceProject, { name: 'Source Folder 1' });
|
||||
const workflow = await createWorkflow({ parentFolder: sourceFolder1 }, sourceProject);
|
||||
|
||||
// ACT
|
||||
const response = await testServer
|
||||
.authAgentFor(actor)
|
||||
.put(`/projects/${sourceProject.id}/folders/${sourceFolder1.id}/transfer`)
|
||||
.send({ destinationProjectId: destinationProject.id, destinationParentFolderId: '0' })
|
||||
.expect(200);
|
||||
|
||||
// ASSERT
|
||||
expect(response.body).toEqual({});
|
||||
|
||||
const allSharings = await getWorkflowSharing(workflow);
|
||||
expect(allSharings).toHaveLength(1);
|
||||
expect(allSharings[0]).toMatchObject({
|
||||
projectId: destinationProject.id,
|
||||
workflowId: workflow.id,
|
||||
role: 'workflow:owner',
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test('owner transfers folder from project they are not part of, e.g. test global cred sharing scope', async () => {
|
||||
// ARRANGE
|
||||
const admin = await createUser({ role: 'global:admin' });
|
||||
const sourceProject = await createTeamProject('source project', admin);
|
||||
const destinationProject = await createTeamProject('destination project', member);
|
||||
const sourceFolder1 = await createFolder(sourceProject, { name: 'Source Folder 1' });
|
||||
const sourceFolder2 = await createFolder(sourceProject, {
|
||||
name: 'Source Folder 2',
|
||||
parentFolder: sourceFolder1,
|
||||
});
|
||||
|
||||
const workflow1 = await createWorkflow({ parentFolder: sourceFolder1 }, sourceProject);
|
||||
const workflow2 = await createWorkflow({ parentFolder: sourceFolder2 }, sourceProject);
|
||||
|
||||
const credential = await saveCredential(randomCredentialPayload(), {
|
||||
project: sourceProject,
|
||||
role: 'credential:owner',
|
||||
});
|
||||
|
||||
// ACT
|
||||
await testServer
|
||||
.authAgentFor(owner)
|
||||
.put(`/projects/${sourceProject.id}/folders/${sourceFolder1.id}/transfer`)
|
||||
.send({
|
||||
destinationProjectId: destinationProject.id,
|
||||
destinationParentFolderId: '0',
|
||||
shareCredentials: [credential.id],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// ASSERT
|
||||
const workflow1Sharing = await getWorkflowSharing(workflow1);
|
||||
expect(workflow1Sharing).toHaveLength(1);
|
||||
expect(workflow1Sharing[0]).toMatchObject({
|
||||
projectId: destinationProject.id,
|
||||
workflowId: workflow1.id,
|
||||
role: 'workflow:owner',
|
||||
});
|
||||
|
||||
const workflow2Sharing = await getWorkflowSharing(workflow2);
|
||||
expect(workflow2Sharing).toHaveLength(1);
|
||||
expect(workflow2Sharing[0]).toMatchObject({
|
||||
projectId: destinationProject.id,
|
||||
workflowId: workflow2.id,
|
||||
role: 'workflow:owner',
|
||||
});
|
||||
|
||||
const sourceFolderInDb = await folderRepository.findOne({
|
||||
where: { id: sourceFolder1.id },
|
||||
relations: ['parentFolder', 'homeProject'],
|
||||
});
|
||||
expect(sourceFolderInDb).toBeDefined();
|
||||
expect(sourceFolderInDb?.parentFolder).toBeNull();
|
||||
expect(sourceFolderInDb?.homeProject.id).toBe(destinationProject.id);
|
||||
|
||||
const sourceFolder2InDb = await folderRepository.findOne({
|
||||
where: { id: sourceFolder2.id },
|
||||
relations: ['parentFolder', 'homeProject'],
|
||||
});
|
||||
|
||||
expect(sourceFolder2InDb).toBeDefined();
|
||||
expect(sourceFolder2InDb?.parentFolder?.id).toBe(sourceFolder1.id);
|
||||
expect(sourceFolder2InDb?.homeProject.id).toBe(destinationProject.id);
|
||||
|
||||
const allCredentialSharings = await getCredentialSharings(credential);
|
||||
expect(allCredentialSharings).toHaveLength(2);
|
||||
expect(allCredentialSharings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
projectId: sourceProject.id,
|
||||
credentialsId: credential.id,
|
||||
role: 'credential:owner',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
projectId: destinationProject.id,
|
||||
credentialsId: credential.id,
|
||||
role: 'credential:user',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('admin transfers folder from project they are not part of, e.g. test global cred sharing scope', async () => {
|
||||
// ARRANGE
|
||||
const admin = await createUser({ role: 'global:admin' });
|
||||
const sourceProject = await createTeamProject('source project', owner);
|
||||
const destinationProject = await createTeamProject('destination project', owner);
|
||||
const sourceFolder1 = await createFolder(sourceProject, { name: 'Source Folder 1' });
|
||||
const sourceFolder2 = await createFolder(sourceProject, {
|
||||
name: 'Source Folder 2',
|
||||
parentFolder: sourceFolder1,
|
||||
});
|
||||
|
||||
const workflow1 = await createWorkflow({ parentFolder: sourceFolder1 }, sourceProject);
|
||||
const workflow2 = await createWorkflow({ parentFolder: sourceFolder2 }, sourceProject);
|
||||
|
||||
const credential = await saveCredential(randomCredentialPayload(), {
|
||||
project: sourceProject,
|
||||
role: 'credential:owner',
|
||||
});
|
||||
|
||||
// ACT
|
||||
await testServer
|
||||
.authAgentFor(admin)
|
||||
.put(`/projects/${sourceProject.id}/folders/${sourceFolder1.id}/transfer`)
|
||||
.send({
|
||||
destinationProjectId: destinationProject.id,
|
||||
destinationParentFolderId: '0',
|
||||
shareCredentials: [credential.id],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// ASSERT
|
||||
const workflow1Sharing = await getWorkflowSharing(workflow1);
|
||||
expect(workflow1Sharing).toHaveLength(1);
|
||||
expect(workflow1Sharing[0]).toMatchObject({
|
||||
projectId: destinationProject.id,
|
||||
workflowId: workflow1.id,
|
||||
role: 'workflow:owner',
|
||||
});
|
||||
|
||||
const workflow2Sharing = await getWorkflowSharing(workflow2);
|
||||
expect(workflow2Sharing).toHaveLength(1);
|
||||
expect(workflow2Sharing[0]).toMatchObject({
|
||||
projectId: destinationProject.id,
|
||||
workflowId: workflow2.id,
|
||||
role: 'workflow:owner',
|
||||
});
|
||||
|
||||
const sourceFolderInDb = await folderRepository.findOne({
|
||||
where: { id: sourceFolder1.id },
|
||||
relations: ['parentFolder', 'homeProject'],
|
||||
});
|
||||
expect(sourceFolderInDb).toBeDefined();
|
||||
expect(sourceFolderInDb?.parentFolder).toBeNull();
|
||||
expect(sourceFolderInDb?.homeProject.id).toBe(destinationProject.id);
|
||||
|
||||
const sourceFolder2InDb = await folderRepository.findOne({
|
||||
where: { id: sourceFolder2.id },
|
||||
relations: ['parentFolder', 'homeProject'],
|
||||
});
|
||||
|
||||
expect(sourceFolder2InDb).toBeDefined();
|
||||
expect(sourceFolder2InDb?.parentFolder?.id).toBe(sourceFolder1.id);
|
||||
expect(sourceFolder2InDb?.homeProject.id).toBe(destinationProject.id);
|
||||
|
||||
const allCredentialSharings = await getCredentialSharings(credential);
|
||||
expect(allCredentialSharings).toHaveLength(2);
|
||||
expect(allCredentialSharings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
projectId: sourceProject.id,
|
||||
credentialsId: credential.id,
|
||||
role: 'credential:owner',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
projectId: destinationProject.id,
|
||||
credentialsId: credential.id,
|
||||
role: 'credential:user',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('member transfers folder from personal project to team project and one workflow contains a credential that they can use but not share', async () => {
|
||||
// ARRANGE
|
||||
const ownerPersonalProject = await projectRepository.getPersonalProjectForUserOrFail(owner.id);
|
||||
const sourceProject = await projectRepository.getPersonalProjectForUserOrFail(member.id);
|
||||
const destinationProject = await createTeamProject('destination project', member);
|
||||
const sourceFolder1 = await createFolder(sourceProject, { name: 'Source Folder 1' });
|
||||
const sourceFolder2 = await createFolder(sourceProject, {
|
||||
name: 'Source Folder 2',
|
||||
parentFolder: sourceFolder1,
|
||||
});
|
||||
|
||||
const workflow1 = await createWorkflow({ parentFolder: sourceFolder1 }, sourceProject);
|
||||
const workflow2 = await createWorkflow({ parentFolder: sourceFolder2 }, sourceProject);
|
||||
|
||||
const credential = await saveCredential(randomCredentialPayload(), {
|
||||
user: owner,
|
||||
role: 'credential:owner',
|
||||
});
|
||||
|
||||
await shareCredentialWithUsers(credential, [member]);
|
||||
|
||||
// ACT
|
||||
await testServer
|
||||
.authAgentFor(member)
|
||||
.put(`/projects/${sourceProject.id}/folders/${sourceFolder1.id}/transfer`)
|
||||
.send({
|
||||
destinationProjectId: destinationProject.id,
|
||||
destinationParentFolderId: '0',
|
||||
shareCredentials: [credential.id],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// ASSERT
|
||||
const workflow1Sharing = await getWorkflowSharing(workflow1);
|
||||
expect(workflow1Sharing).toHaveLength(1);
|
||||
expect(workflow1Sharing[0]).toMatchObject({
|
||||
projectId: destinationProject.id,
|
||||
workflowId: workflow1.id,
|
||||
role: 'workflow:owner',
|
||||
});
|
||||
|
||||
const workflow2Sharing = await getWorkflowSharing(workflow2);
|
||||
expect(workflow2Sharing).toHaveLength(1);
|
||||
expect(workflow2Sharing[0]).toMatchObject({
|
||||
projectId: destinationProject.id,
|
||||
workflowId: workflow2.id,
|
||||
role: 'workflow:owner',
|
||||
});
|
||||
|
||||
const sourceFolderInDb = await folderRepository.findOne({
|
||||
where: { id: sourceFolder1.id },
|
||||
relations: ['parentFolder', 'homeProject'],
|
||||
});
|
||||
expect(sourceFolderInDb).toBeDefined();
|
||||
expect(sourceFolderInDb?.parentFolder).toBeNull();
|
||||
expect(sourceFolderInDb?.homeProject.id).toBe(destinationProject.id);
|
||||
|
||||
const sourceFolder2InDb = await folderRepository.findOne({
|
||||
where: { id: sourceFolder2.id },
|
||||
relations: ['parentFolder', 'homeProject'],
|
||||
});
|
||||
|
||||
expect(sourceFolder2InDb).toBeDefined();
|
||||
expect(sourceFolder2InDb?.parentFolder?.id).toBe(sourceFolder1.id);
|
||||
expect(sourceFolder2InDb?.homeProject.id).toBe(destinationProject.id);
|
||||
|
||||
const allCredentialSharings = await getCredentialSharings(credential);
|
||||
expect(allCredentialSharings).toHaveLength(2);
|
||||
expect(allCredentialSharings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
projectId: ownerPersonalProject.id,
|
||||
credentialsId: credential.id,
|
||||
role: 'credential:owner',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
projectId: sourceProject.id,
|
||||
credentialsId: credential.id,
|
||||
role: 'credential:user',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('member transfers folder from their personal project to another team project in which they have editor role', async () => {
|
||||
// ARRANGE
|
||||
const sourceProject = await projectRepository.getPersonalProjectForUserOrFail(member.id);
|
||||
const destinationProject = await createTeamProject('destination project');
|
||||
const sourceFolder1 = await createFolder(sourceProject, { name: 'Source Folder 1' });
|
||||
const sourceFolder2 = await createFolder(sourceProject, {
|
||||
name: 'Source Folder 2',
|
||||
parentFolder: sourceFolder1,
|
||||
});
|
||||
|
||||
const workflow1 = await createWorkflow({ parentFolder: sourceFolder1 }, sourceProject);
|
||||
const workflow2 = await createWorkflow({ parentFolder: sourceFolder2 }, sourceProject);
|
||||
|
||||
const credential = await saveCredential(randomCredentialPayload(), {
|
||||
project: sourceProject,
|
||||
role: 'credential:owner',
|
||||
});
|
||||
|
||||
await linkUserToProject(member, destinationProject, 'project:editor');
|
||||
|
||||
// ACT
|
||||
await testServer
|
||||
.authAgentFor(member)
|
||||
.put(`/projects/${sourceProject.id}/folders/${sourceFolder1.id}/transfer`)
|
||||
.send({
|
||||
destinationProjectId: destinationProject.id,
|
||||
destinationParentFolderId: '0',
|
||||
shareCredentials: [credential.id],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// ASSERT
|
||||
const workflow1Sharing = await getWorkflowSharing(workflow1);
|
||||
expect(workflow1Sharing).toHaveLength(1);
|
||||
expect(workflow1Sharing[0]).toMatchObject({
|
||||
projectId: destinationProject.id,
|
||||
workflowId: workflow1.id,
|
||||
role: 'workflow:owner',
|
||||
});
|
||||
|
||||
const workflow2Sharing = await getWorkflowSharing(workflow2);
|
||||
expect(workflow2Sharing).toHaveLength(1);
|
||||
expect(workflow2Sharing[0]).toMatchObject({
|
||||
projectId: destinationProject.id,
|
||||
workflowId: workflow2.id,
|
||||
role: 'workflow:owner',
|
||||
});
|
||||
|
||||
const sourceFolderInDb = await folderRepository.findOne({
|
||||
where: { id: sourceFolder1.id },
|
||||
relations: ['parentFolder', 'homeProject'],
|
||||
});
|
||||
expect(sourceFolderInDb).toBeDefined();
|
||||
expect(sourceFolderInDb?.parentFolder).toBeNull();
|
||||
expect(sourceFolderInDb?.homeProject.id).toBe(destinationProject.id);
|
||||
|
||||
const sourceFolder2InDb = await folderRepository.findOne({
|
||||
where: { id: sourceFolder2.id },
|
||||
relations: ['parentFolder', 'homeProject'],
|
||||
});
|
||||
|
||||
expect(sourceFolder2InDb).toBeDefined();
|
||||
expect(sourceFolder2InDb?.parentFolder?.id).toBe(sourceFolder1.id);
|
||||
expect(sourceFolder2InDb?.homeProject.id).toBe(destinationProject.id);
|
||||
|
||||
const allCredentialSharings = await getCredentialSharings(credential);
|
||||
expect(allCredentialSharings).toHaveLength(2);
|
||||
expect(allCredentialSharings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
projectId: sourceProject.id,
|
||||
credentialsId: credential.id,
|
||||
role: 'credential:owner',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
projectId: destinationProject.id,
|
||||
credentialsId: credential.id,
|
||||
role: 'credential:user',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('member transfers folder from a team project as project admin to another team project in which they have editor role', async () => {
|
||||
// ARRANGE
|
||||
const sourceProject = await createTeamProject('source project', member);
|
||||
const destinationProject = await createTeamProject('destination project');
|
||||
const sourceFolder1 = await createFolder(sourceProject, { name: 'Source Folder 1' });
|
||||
const sourceFolder2 = await createFolder(sourceProject, {
|
||||
name: 'Source Folder 2',
|
||||
parentFolder: sourceFolder1,
|
||||
});
|
||||
|
||||
const workflow1 = await createWorkflow({ parentFolder: sourceFolder1 }, sourceProject);
|
||||
const workflow2 = await createWorkflow({ parentFolder: sourceFolder2 }, sourceProject);
|
||||
|
||||
const credential = await saveCredential(randomCredentialPayload(), {
|
||||
project: sourceProject,
|
||||
role: 'credential:owner',
|
||||
});
|
||||
|
||||
await linkUserToProject(member, destinationProject, 'project:editor');
|
||||
|
||||
// ACT
|
||||
await testServer
|
||||
.authAgentFor(member)
|
||||
.put(`/projects/${sourceProject.id}/folders/${sourceFolder1.id}/transfer`)
|
||||
.send({
|
||||
destinationProjectId: destinationProject.id,
|
||||
destinationParentFolderId: '0',
|
||||
shareCredentials: [credential.id],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// ASSERT
|
||||
const workflow1Sharing = await getWorkflowSharing(workflow1);
|
||||
expect(workflow1Sharing).toHaveLength(1);
|
||||
expect(workflow1Sharing[0]).toMatchObject({
|
||||
projectId: destinationProject.id,
|
||||
workflowId: workflow1.id,
|
||||
role: 'workflow:owner',
|
||||
});
|
||||
|
||||
const workflow2Sharing = await getWorkflowSharing(workflow2);
|
||||
expect(workflow2Sharing).toHaveLength(1);
|
||||
expect(workflow2Sharing[0]).toMatchObject({
|
||||
projectId: destinationProject.id,
|
||||
workflowId: workflow2.id,
|
||||
role: 'workflow:owner',
|
||||
});
|
||||
|
||||
const sourceFolderInDb = await folderRepository.findOne({
|
||||
where: { id: sourceFolder1.id },
|
||||
relations: ['parentFolder', 'homeProject'],
|
||||
});
|
||||
expect(sourceFolderInDb).toBeDefined();
|
||||
expect(sourceFolderInDb?.parentFolder).toBeNull();
|
||||
expect(sourceFolderInDb?.homeProject.id).toBe(destinationProject.id);
|
||||
|
||||
const sourceFolder2InDb = await folderRepository.findOne({
|
||||
where: { id: sourceFolder2.id },
|
||||
relations: ['parentFolder', 'homeProject'],
|
||||
});
|
||||
|
||||
expect(sourceFolder2InDb).toBeDefined();
|
||||
expect(sourceFolder2InDb?.parentFolder?.id).toBe(sourceFolder1.id);
|
||||
expect(sourceFolder2InDb?.homeProject.id).toBe(destinationProject.id);
|
||||
|
||||
const allCredentialSharings = await getCredentialSharings(credential);
|
||||
expect(allCredentialSharings).toHaveLength(2);
|
||||
expect(allCredentialSharings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
projectId: sourceProject.id,
|
||||
credentialsId: credential.id,
|
||||
role: 'credential:owner',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
projectId: destinationProject.id,
|
||||
credentialsId: credential.id,
|
||||
role: 'credential:user',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('member transfers workflow from a team project as project admin to another team project in which they have editor role but cannot share the credential that is only shared into the source project', async () => {
|
||||
// ARRANGE
|
||||
const sourceProject = await createTeamProject('source project', member);
|
||||
const destinationProject = await createTeamProject('destination project');
|
||||
const ownerProject = await getPersonalProject(owner);
|
||||
|
||||
const sourceFolder1 = await createFolder(sourceProject, { name: 'Source Folder 1' });
|
||||
const sourceFolder2 = await createFolder(sourceProject, {
|
||||
name: 'Source Folder 2',
|
||||
parentFolder: sourceFolder1,
|
||||
});
|
||||
|
||||
const workflow1 = await createWorkflow({ parentFolder: sourceFolder1 }, sourceProject);
|
||||
const workflow2 = await createWorkflow({ parentFolder: sourceFolder2 }, sourceProject);
|
||||
|
||||
const credential = await saveCredential(randomCredentialPayload(), {
|
||||
user: owner,
|
||||
role: 'credential:owner',
|
||||
});
|
||||
|
||||
await linkUserToProject(member, destinationProject, 'project:editor');
|
||||
await shareCredentialWithProjects(credential, [sourceProject]);
|
||||
|
||||
// ACT
|
||||
await testServer
|
||||
.authAgentFor(member)
|
||||
.put(`/projects/${sourceProject.id}/folders/${sourceFolder1.id}/transfer`)
|
||||
.send({
|
||||
destinationProjectId: destinationProject.id,
|
||||
destinationParentFolderId: '0',
|
||||
shareCredentials: [credential.id],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// ASSERT
|
||||
const workflow1Sharing = await getWorkflowSharing(workflow1);
|
||||
expect(workflow1Sharing).toHaveLength(1);
|
||||
expect(workflow1Sharing[0]).toMatchObject({
|
||||
projectId: destinationProject.id,
|
||||
workflowId: workflow1.id,
|
||||
role: 'workflow:owner',
|
||||
});
|
||||
|
||||
const workflow2Sharing = await getWorkflowSharing(workflow2);
|
||||
expect(workflow2Sharing).toHaveLength(1);
|
||||
expect(workflow2Sharing[0]).toMatchObject({
|
||||
projectId: destinationProject.id,
|
||||
workflowId: workflow2.id,
|
||||
role: 'workflow:owner',
|
||||
});
|
||||
|
||||
const sourceFolderInDb = await folderRepository.findOne({
|
||||
where: { id: sourceFolder1.id },
|
||||
relations: ['parentFolder', 'homeProject'],
|
||||
});
|
||||
expect(sourceFolderInDb).toBeDefined();
|
||||
expect(sourceFolderInDb?.parentFolder).toBeNull();
|
||||
expect(sourceFolderInDb?.homeProject.id).toBe(destinationProject.id);
|
||||
|
||||
const sourceFolder2InDb = await folderRepository.findOne({
|
||||
where: { id: sourceFolder2.id },
|
||||
relations: ['parentFolder', 'homeProject'],
|
||||
});
|
||||
|
||||
expect(sourceFolder2InDb).toBeDefined();
|
||||
expect(sourceFolder2InDb?.parentFolder?.id).toBe(sourceFolder1.id);
|
||||
expect(sourceFolder2InDb?.homeProject.id).toBe(destinationProject.id);
|
||||
|
||||
const allCredentialSharings = await getCredentialSharings(credential);
|
||||
expect(allCredentialSharings).toHaveLength(2);
|
||||
expect(allCredentialSharings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
projectId: ownerProject.id,
|
||||
credentialsId: credential.id,
|
||||
role: 'credential:owner',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
projectId: sourceProject.id,
|
||||
credentialsId: credential.id,
|
||||
role: 'credential:user',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('member transfers workflow from a team project as project admin to another team project in which they have editor role but cannot share all the credentials', async () => {
|
||||
// ARRANGE
|
||||
const sourceProject = await createTeamProject('source project', member);
|
||||
const destinationProject = await createTeamProject('destination project');
|
||||
|
||||
const sourceFolder1 = await createFolder(sourceProject, { name: 'Source Folder 1' });
|
||||
const sourceFolder2 = await createFolder(sourceProject, {
|
||||
name: 'Source Folder 2',
|
||||
parentFolder: sourceFolder1,
|
||||
});
|
||||
|
||||
const workflow1 = await createWorkflow({ parentFolder: sourceFolder1 }, sourceProject);
|
||||
const workflow2 = await createWorkflow({ parentFolder: sourceFolder2 }, sourceProject);
|
||||
|
||||
const credential = await saveCredential(randomCredentialPayload(), {
|
||||
project: sourceProject,
|
||||
role: 'credential:owner',
|
||||
});
|
||||
|
||||
const ownersCredential = await saveCredential(randomCredentialPayload(), {
|
||||
user: owner,
|
||||
role: 'credential:owner',
|
||||
});
|
||||
|
||||
await linkUserToProject(member, destinationProject, 'project:editor');
|
||||
|
||||
// ACT
|
||||
await testServer
|
||||
.authAgentFor(member)
|
||||
.put(`/projects/${sourceProject.id}/folders/${sourceFolder1.id}/transfer`)
|
||||
.send({
|
||||
destinationProjectId: destinationProject.id,
|
||||
destinationParentFolderId: '0',
|
||||
shareCredentials: [credential.id, ownersCredential.id],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// ASSERT
|
||||
const workflow1Sharing = await getWorkflowSharing(workflow1);
|
||||
expect(workflow1Sharing).toHaveLength(1);
|
||||
expect(workflow1Sharing[0]).toMatchObject({
|
||||
projectId: destinationProject.id,
|
||||
workflowId: workflow1.id,
|
||||
role: 'workflow:owner',
|
||||
});
|
||||
|
||||
const workflow2Sharing = await getWorkflowSharing(workflow2);
|
||||
expect(workflow2Sharing).toHaveLength(1);
|
||||
expect(workflow2Sharing[0]).toMatchObject({
|
||||
projectId: destinationProject.id,
|
||||
workflowId: workflow2.id,
|
||||
role: 'workflow:owner',
|
||||
});
|
||||
|
||||
const sourceFolderInDb = await folderRepository.findOne({
|
||||
where: { id: sourceFolder1.id },
|
||||
relations: ['parentFolder', 'homeProject'],
|
||||
});
|
||||
expect(sourceFolderInDb).toBeDefined();
|
||||
expect(sourceFolderInDb?.parentFolder).toBeNull();
|
||||
expect(sourceFolderInDb?.homeProject.id).toBe(destinationProject.id);
|
||||
|
||||
const sourceFolder2InDb = await folderRepository.findOne({
|
||||
where: { id: sourceFolder2.id },
|
||||
relations: ['parentFolder', 'homeProject'],
|
||||
});
|
||||
|
||||
expect(sourceFolder2InDb).toBeDefined();
|
||||
expect(sourceFolder2InDb?.parentFolder?.id).toBe(sourceFolder1.id);
|
||||
expect(sourceFolder2InDb?.homeProject.id).toBe(destinationProject.id);
|
||||
|
||||
const allCredentialSharings = await getCredentialSharings(credential);
|
||||
expect(allCredentialSharings).toHaveLength(2);
|
||||
expect(allCredentialSharings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
projectId: sourceProject.id,
|
||||
credentialsId: credential.id,
|
||||
role: 'credential:owner',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
projectId: destinationProject.id,
|
||||
credentialsId: credential.id,
|
||||
role: 'credential:user',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('returns a 500 if the workflow cannot be activated due to an unknown error', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
|
||||
const sourceProject = await createTeamProject('source project', member);
|
||||
const destinationProject = await createTeamProject('Team Project', member);
|
||||
|
||||
const sourceFolder1 = await createFolder(sourceProject, { name: 'Source Folder 1' });
|
||||
const sourceFolder2 = await createFolder(sourceProject, {
|
||||
name: 'Source Folder 2',
|
||||
parentFolder: sourceFolder1,
|
||||
});
|
||||
|
||||
await createWorkflow({ active: true, parentFolder: sourceFolder1 }, sourceProject);
|
||||
await createWorkflow({ parentFolder: sourceFolder2 }, sourceProject);
|
||||
|
||||
activeWorkflowManager.add.mockRejectedValue(new ApplicationError('Oh no!'));
|
||||
|
||||
//
|
||||
// ACT & ASSERT
|
||||
//
|
||||
await testServer
|
||||
.authAgentFor(member)
|
||||
.put(`/projects/${sourceProject.id}/folders/${sourceFolder1.id}/transfer`)
|
||||
.send({
|
||||
destinationProjectId: destinationProject.id,
|
||||
destinationParentFolderId: '0',
|
||||
})
|
||||
.expect(500);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,6 +37,7 @@ describe('EnterpriseWorkflowService', () => {
|
||||
mock(),
|
||||
mock(),
|
||||
mock(),
|
||||
mock(),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user