From 8762c06ff2b164738e45b88d9ca2a45fd53751aa Mon Sep 17 00:00:00 2001 From: Etienne <45695613+etiennejouan@users.noreply.github.com> Date: Fri, 28 Feb 2025 10:38:51 +0100 Subject: [PATCH] add tests on workspace deletion logic (#10530) closes [#424](https://github.com/twentyhq/core-team-issues/issues/424) --- .../__tests__/workspace.service.spec.ts | 228 ++++++++++++++++++ .../services/workspace.service.spec.ts | 119 --------- .../workspace-manager.service.spec.ts | 163 +++++++++++++ 3 files changed, 391 insertions(+), 119 deletions(-) create mode 100644 packages/twenty-server/src/engine/core-modules/workspace/__tests__/workspace.service.spec.ts delete mode 100644 packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.spec.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/__tests__/workspace-manager.service.spec.ts diff --git a/packages/twenty-server/src/engine/core-modules/workspace/__tests__/workspace.service.spec.ts b/packages/twenty-server/src/engine/core-modules/workspace/__tests__/workspace.service.spec.ts new file mode 100644 index 000000000..54dfa4285 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/workspace/__tests__/workspace.service.spec.ts @@ -0,0 +1,228 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; + +import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; +import { BillingService } from 'src/engine/core-modules/billing/services/billing.service'; +import { CustomDomainService } from 'src/engine/core-modules/domain-manager/services/custom-domain.service'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { EmailService } from 'src/engine/core-modules/email/email.service'; +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; +import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; +import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; +import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; +import { getQueueToken } from 'src/engine/core-modules/message-queue/utils/get-queue-token.util'; +import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; +import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; +import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; +import { UserService } from 'src/engine/core-modules/user/services/user.service'; +import { User } from 'src/engine/core-modules/user/user.entity'; +import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service'; +import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; +import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; +import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service'; + +describe('WorkspaceService', () => { + let service: WorkspaceService; + let userWorkspaceRepository: Repository; + let userRepository: Repository; + let workspaceRepository: Repository; + let workspaceCacheStorageService: WorkspaceCacheStorageService; + let messageQueueService: MessageQueueService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + WorkspaceService, + { + provide: getRepositoryToken(Workspace, 'core'), + useValue: { + findOne: jest.fn(), + softDelete: jest.fn(), + delete: jest.fn(), + }, + }, + { + provide: getRepositoryToken(UserWorkspace, 'core'), + useValue: { + find: jest.fn(), + softDelete: jest.fn(), + delete: jest.fn(), + }, + }, + { + provide: getRepositoryToken(User, 'core'), + useValue: { + softDelete: jest.fn(), + }, + }, + ...[ + WorkspaceManagerService, + WorkspaceManagerService, + UserWorkspaceService, + UserService, + DomainManagerService, + CustomDomainService, + BillingSubscriptionService, + BillingService, + EnvironmentService, + EmailService, + OnboardingService, + WorkspaceInvitationService, + PermissionsService, + FeatureFlagService, + ExceptionHandlerService, + PermissionsService, + ].map((service) => ({ + provide: service, + useValue: {}, + })), + { + provide: WorkspaceCacheStorageService, + useValue: { + flush: jest.fn(), + }, + }, + { + provide: getQueueToken(MessageQueue.deleteCascadeQueue), + useValue: { + add: jest.fn(), + }, + }, + ], + }).compile(); + + service = module.get(WorkspaceService); + userWorkspaceRepository = module.get>( + getRepositoryToken(UserWorkspace, 'core'), + ); + userRepository = module.get>( + getRepositoryToken(User, 'core'), + ); + workspaceRepository = module.get>( + getRepositoryToken(Workspace, 'core'), + ); + workspaceCacheStorageService = module.get( + WorkspaceCacheStorageService, + ); + messageQueueService = module.get( + getQueueToken(MessageQueue.deleteCascadeQueue), + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('handleRemoveWorkspaceMember', () => { + it('should soft delete the user workspace record', async () => { + jest.spyOn(userWorkspaceRepository, 'find').mockResolvedValue([]); + + await service.handleRemoveWorkspaceMember( + 'workspace-id', + 'user-id', + true, + ); + + expect(userWorkspaceRepository.softDelete).toHaveBeenCalledWith({ + userId: 'user-id', + workspaceId: 'workspace-id', + }); + expect(userWorkspaceRepository.delete).not.toHaveBeenCalled(); + expect(userRepository.softDelete).toHaveBeenCalledWith('user-id'); + }); + it('should destroy the user workspace record', async () => { + jest.spyOn(userWorkspaceRepository, 'find').mockResolvedValue([]); + + await service.handleRemoveWorkspaceMember( + 'workspace-id', + 'user-id', + false, + ); + + expect(userWorkspaceRepository.delete).toHaveBeenCalledWith({ + userId: 'user-id', + workspaceId: 'workspace-id', + }); + expect(userWorkspaceRepository.softDelete).not.toHaveBeenCalled(); + expect(userRepository.softDelete).toHaveBeenCalledWith('user-id'); + }); + + it('should not soft delete the user record if there are other user workspace records', async () => { + jest + .spyOn(userWorkspaceRepository, 'find') + .mockResolvedValue([ + { id: 'remaining-user-workspace-id' } as UserWorkspace, + ]); + + await service.handleRemoveWorkspaceMember( + 'workspace-id', + 'user-id', + false, + ); + + expect(userWorkspaceRepository.delete).toHaveBeenCalledWith({ + userId: 'user-id', + workspaceId: 'workspace-id', + }); + expect(userWorkspaceRepository.softDelete).not.toHaveBeenCalled(); + expect(userRepository.softDelete).not.toHaveBeenCalled(); + }); + }); + + describe('deleteWorkspace', () => { + it('should delete the workspace', async () => { + const mockWorkspace = { + id: 'workspace-id', + metadataVersion: 0, + } as Workspace; + + jest + .spyOn(workspaceRepository, 'findOne') + .mockResolvedValue(mockWorkspace); + jest.spyOn(userWorkspaceRepository, 'find').mockResolvedValue([]); + jest + .spyOn(service, 'deleteMetadataSchemaCacheAndUserWorkspace') + .mockResolvedValue({} as Workspace); + + await service.deleteWorkspace(mockWorkspace.id, false); + + expect(workspaceRepository.delete).toHaveBeenCalledWith(mockWorkspace.id); + expect( + service.deleteMetadataSchemaCacheAndUserWorkspace, + ).toHaveBeenCalled(); + expect(workspaceRepository.softDelete).not.toHaveBeenCalled(); + expect(workspaceCacheStorageService.flush).toHaveBeenCalledWith( + mockWorkspace.id, + mockWorkspace.metadataVersion, + ); + expect(messageQueueService.add).toHaveBeenCalled(); + }); + + it('should soft delete the workspace', async () => { + const mockWorkspace = { + id: 'workspace-id', + metadataVersion: 0, + } as Workspace; + + jest + .spyOn(workspaceRepository, 'findOne') + .mockResolvedValue(mockWorkspace); + jest.spyOn(userWorkspaceRepository, 'find').mockResolvedValue([]); + await service.deleteWorkspace(mockWorkspace.id, true); + + expect(workspaceRepository.softDelete).toHaveBeenCalledWith({ + id: mockWorkspace.id, + }); + expect(workspaceRepository.delete).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.spec.ts b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.spec.ts deleted file mode 100644 index a9c4b4dc8..000000000 --- a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.spec.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { getRepositoryToken } from '@nestjs/typeorm'; - -import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; -import { BillingService } from 'src/engine/core-modules/billing/services/billing.service'; -import { CustomDomainService } from 'src/engine/core-modules/domain-manager/services/custom-domain.service'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; -import { EmailService } from 'src/engine/core-modules/email/email.service'; -import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; -import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; -import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; -import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; -import { getQueueToken } from 'src/engine/core-modules/message-queue/utils/get-queue-token.util'; -import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; -import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; -import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; -import { UserService } from 'src/engine/core-modules/user/services/user.service'; -import { User } from 'src/engine/core-modules/user/user.entity'; -import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service'; -import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; -import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; -import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service'; - -import { WorkspaceService } from './workspace.service'; - -describe('WorkspaceService', () => { - let service: WorkspaceService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - WorkspaceService, - { - provide: getRepositoryToken(Workspace, 'core'), - useValue: {}, - }, - { - provide: getRepositoryToken(UserWorkspace, 'core'), - useValue: {}, - }, - { - provide: getRepositoryToken(User, 'core'), - useValue: {}, - }, - { - provide: WorkspaceManagerService, - useValue: {}, - }, - { - provide: UserWorkspaceService, - useValue: {}, - }, - { - provide: UserService, - useValue: {}, - }, - { - provide: DomainManagerService, - useValue: {}, - }, - { - provide: CustomDomainService, - useValue: {}, - }, - { - provide: BillingSubscriptionService, - useValue: {}, - }, - { - provide: BillingService, - useValue: {}, - }, - { - provide: EnvironmentService, - useValue: {}, - }, - { - provide: EmailService, - useValue: {}, - }, - { - provide: OnboardingService, - useValue: {}, - }, - { - provide: WorkspaceInvitationService, - useValue: {}, - }, - { - provide: FeatureFlagService, - useValue: {}, - }, - { - provide: ExceptionHandlerService, - useValue: {}, - }, - { - provide: PermissionsService, - useValue: {}, - }, - { - provide: WorkspaceCacheStorageService, - useValue: {}, - }, - { - provide: getQueueToken(MessageQueue.deleteCascadeQueue), - useValue: {}, - }, - ], - }).compile(); - - service = module.get(WorkspaceService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/packages/twenty-server/src/engine/workspace-manager/__tests__/workspace-manager.service.spec.ts b/packages/twenty-server/src/engine/workspace-manager/__tests__/workspace-manager.service.spec.ts new file mode 100644 index 000000000..083d73e51 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/__tests__/workspace-manager.service.spec.ts @@ -0,0 +1,163 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; + +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; +import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; +import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; +import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; +import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; +import { RoleService } from 'src/engine/metadata-modules/role/role.service'; +import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; +import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; +import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service'; +import { SeederService } from 'src/engine/seeder/seeder.service'; +import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; +import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service'; +import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service'; + +describe('WorkspaceManagerService', () => { + let service: WorkspaceManagerService; + let objectMetadataService: ObjectMetadataService; + let workspaceMigrationRepository: Repository; + let dataSourceRepository: Repository; + let workspaceRelationMetadataRepository: Repository; + let workspaceFieldMetadataRepository: Repository; + let workspaceDataSourceService: WorkspaceDataSourceService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + WorkspaceManagerService, + WorkspaceMigrationService, + DataSourceService, + { + provide: getRepositoryToken(Workspace, 'core'), + useValue: {}, + }, + { + provide: getRepositoryToken(UserWorkspace, 'core'), + useValue: {}, + }, + { + provide: getRepositoryToken(FieldMetadataEntity, 'metadata'), + useValue: { + delete: jest.fn(), + }, + }, + { + provide: getRepositoryToken(RelationMetadataEntity, 'metadata'), + useValue: { + delete: jest.fn(), + }, + }, + { + provide: getRepositoryToken(ObjectMetadataEntity, 'metadata'), + useValue: { + delete: jest.fn(), + }, + }, + { + provide: getRepositoryToken(WorkspaceMigrationEntity, 'metadata'), + useValue: { + delete: jest.fn(), + }, + }, + { + provide: getRepositoryToken(DataSourceEntity, 'metadata'), + useValue: { + delete: jest.fn(), + }, + }, + { + provide: PermissionsService, + useValue: {}, + }, + { + provide: FeatureFlagService, + useValue: {}, + }, + { + provide: RoleService, + useValue: {}, + }, + { + provide: UserRoleService, + useValue: {}, + }, + { + provide: WorkspaceDataSourceService, + useValue: { + deleteWorkspaceDBSchema: jest.fn(), + }, + }, + { + provide: WorkspaceSyncMetadataService, + useValue: {}, + }, + { + provide: SeederService, + useValue: {}, + }, + { + provide: ObjectMetadataService, + useValue: { + deleteObjectsMetadata: jest.fn(), + }, + }, + ], + }).compile(); + + service = module.get(WorkspaceManagerService); + objectMetadataService = module.get( + ObjectMetadataService, + ); + workspaceMigrationRepository = module.get< + Repository + >(getRepositoryToken(WorkspaceMigrationEntity, 'metadata')); + dataSourceRepository = module.get>( + getRepositoryToken(DataSourceEntity, 'metadata'), + ); + workspaceRelationMetadataRepository = module.get< + Repository + >(getRepositoryToken(RelationMetadataEntity, 'metadata')); + workspaceFieldMetadataRepository = module.get< + Repository + >(getRepositoryToken(FieldMetadataEntity, 'metadata')); + workspaceDataSourceService = module.get( + WorkspaceDataSourceService, + ); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('delete', () => { + it('should delete all the workspace metadata tables and workspace schema', async () => { + await service.delete('workspace-id'); + expect(objectMetadataService.deleteObjectsMetadata).toHaveBeenCalled(); + expect(workspaceRelationMetadataRepository.delete).toHaveBeenCalledWith({ + workspaceId: 'workspace-id', + }); + expect(workspaceFieldMetadataRepository.delete).toHaveBeenCalledWith({ + workspaceId: 'workspace-id', + }); + expect(workspaceMigrationRepository.delete).toHaveBeenCalledWith({ + workspaceId: 'workspace-id', + }); + expect(dataSourceRepository.delete).toHaveBeenCalledWith({ + workspaceId: 'workspace-id', + }); + expect( + workspaceDataSourceService.deleteWorkspaceDBSchema, + ).toHaveBeenCalled(); + }); + }); +});