From 5633502c636032240b5b3ef3eca37b30f949ff46 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Tue, 18 Mar 2025 15:25:40 -0400 Subject: [PATCH] feat(core): Allow transferring user's data to team project when deleting them (no-changelog) (#13941) --- .../cli/src/controllers/users.controller.ts | 17 ++-- .../cli/test/integration/users.api.test.ts | 79 ++++++++++++++++++- 2 files changed, 85 insertions(+), 11 deletions(-) diff --git a/packages/cli/src/controllers/users.controller.ts b/packages/cli/src/controllers/users.controller.ts index 000b069ac3..410409541e 100644 --- a/packages/cli/src/controllers/users.controller.ts +++ b/packages/cli/src/controllers/users.controller.ts @@ -189,9 +189,9 @@ export class UsersController { let transfereeId; if (transferId) { - const transfereePersonalProject = await this.projectRepository.findOneBy({ id: transferId }); + const transfereeProject = await this.projectRepository.findOneBy({ id: transferId }); - if (!transfereePersonalProject) { + if (!transfereeProject) { throw new NotFoundError( 'Request to delete a user failed because the transferee project was not found in DB', ); @@ -199,8 +199,7 @@ export class UsersController { const transferee = await this.userRepository.findOneByOrFail({ projectRelations: { - projectId: transfereePersonalProject.id, - role: 'project:personalOwner', + projectId: transfereeProject.id, }, }); @@ -209,25 +208,23 @@ export class UsersController { await this.userService.getManager().transaction(async (trx) => { await this.workflowService.transferAll( personalProjectToDelete.id, - transfereePersonalProject.id, + transfereeProject.id, trx, ); await this.credentialsService.transferAll( personalProjectToDelete.id, - transfereePersonalProject.id, + transfereeProject.id, trx, ); await this.folderService.transferAllFoldersToProject( personalProjectToDelete.id, - transfereePersonalProject.id, + transfereeProject.id, trx, ); }); - await this.projectService.clearCredentialCanUseExternalSecretsCache( - transfereePersonalProject.id, - ); + await this.projectService.clearCredentialCanUseExternalSecretsCache(transfereeProject.id); } const [ownedSharedWorkflows, ownedSharedCredentials] = await Promise.all([ diff --git a/packages/cli/test/integration/users.api.test.ts b/packages/cli/test/integration/users.api.test.ts index 0f886946c6..6bd1a8e600 100644 --- a/packages/cli/test/integration/users.api.test.ts +++ b/packages/cli/test/integration/users.api.test.ts @@ -384,7 +384,7 @@ describe('DELETE /users/:id', () => { expect(credential).toBeNull(); }); - test('should delete user and team relations and transfer their personal resources', async () => { + test('should delete user and team relations and transfer their personal resources to user', async () => { // // ARRANGE // @@ -603,6 +603,83 @@ describe('DELETE /users/:id', () => { expect(deletedUserFolders).toHaveLength(0); }); + test('should delete user and transfer their personal resources to team project', async () => { + // + // ARRANGE + // + const memberToDelete = await createMember(); + + const teamProject = await createTeamProject('test project', owner); + + const memberPersonalProject = await getPersonalProject(memberToDelete); + + const memberToDeleteWorkflow = await createWorkflow({ name: 'workflow1' }, memberToDelete); + const memberToDeleteCredential = await saveCredential(randomCredentialPayload(), { + user: memberToDelete, + role: 'credential:owner', + }); + + await Promise.all([ + createFolder(memberPersonalProject, { name: 'folder1' }), + createFolder(memberPersonalProject, { name: 'folder2' }), + createFolder(teamProject, { name: 'folder3' }), + createFolder(teamProject, { name: 'folder1' }), + ]); + + const deleteSpy = jest.spyOn(Container.get(CacheService), 'deleteMany'); + + // + // ACT + // + await ownerAgent + .delete(`/users/${memberToDelete.id}`) + .query({ transferId: teamProject.id }) + .expect(200); + + // + // ASSERT + // + + deleteSpy.mockClear(); + + const sharedWorkflowRepository = Container.get(SharedWorkflowRepository); + const sharedCredentialRepository = Container.get(SharedCredentialsRepository); + const folderRepository = Container.get(FolderRepository); + const userRepository = Container.get(UserRepository); + + // assert member has been deleted + const user = await userRepository.findOneBy({ id: memberToDelete.id }); + expect(user).toBeNull(); + + // assert the workflow has been transferred + const memberToDeleteWorkflowProjectOwner = + await sharedWorkflowRepository.getWorkflowOwningProject(memberToDeleteWorkflow.id); + + expect(memberToDeleteWorkflowProjectOwner?.id).toBe(teamProject.id); + + // assert the credential has been transferred + const memberToDeleteCredentialProjectOwner = + await sharedCredentialRepository.findCredentialOwningProject(memberToDeleteCredential.id); + + expect(memberToDeleteCredentialProjectOwner?.id).toBe(teamProject.id); + + // assert that the folders have been transferred + const transfereeFolders = await folderRepository.findBy({ + homeProject: { id: teamProject.id }, + }); + + const deletedUserFolders = await folderRepository.findBy({ + homeProject: { id: memberPersonalProject.id }, + }); + + expect(transfereeFolders).toHaveLength(4); + expect(transfereeFolders.map((folder) => folder.name)).toEqual( + expect.arrayContaining(['folder1', 'folder2', 'folder3', 'folder1']), + ); + + expect(deletedUserFolders).toHaveLength(0); + }); + test('should fail to delete self', async () => { await ownerAgent.delete(`/users/${owner.id}`).expect(400);