diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job.ts index 746562daf..f9eafd9aa 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job.ts @@ -157,7 +157,6 @@ export class MessagingMessageListFetchJob { await this.messagingFullMessageListFetchService.processMessageListFetch( messageChannel, - messageChannel.connectedAccount, workspaceId, ); diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-full-message-list-fetch.service.spec.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-full-message-list-fetch.service.spec.ts new file mode 100644 index 000000000..bedb6aab2 --- /dev/null +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-full-message-list-fetch.service.spec.ts @@ -0,0 +1,239 @@ +import { Test, TestingModule } from '@nestjs/testing'; + +import { ConnectedAccountProvider } from 'twenty-shared/types'; + +import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; +import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; +import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; +import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service'; +import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; +import { MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity'; +import { MessagingMessageCleanerService } from 'src/modules/messaging/message-cleaner/services/messaging-message-cleaner.service'; +import { MessagingCursorService } from 'src/modules/messaging/message-import-manager/services/messaging-cursor.service'; +import { MessagingFullMessageListFetchService } from 'src/modules/messaging/message-import-manager/services/messaging-full-message-list-fetch.service'; +import { MessagingGetMessageListService } from 'src/modules/messaging/message-import-manager/services/messaging-get-message-list.service'; +import { MessageImportExceptionHandlerService } from 'src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service'; + +describe('MessagingFullMessageListFetchService', () => { + let messagingFullMessageListFetchService: MessagingFullMessageListFetchService; + let messagingGetMessageListService: MessagingGetMessageListService; + let messageChannelSyncStatusService: MessageChannelSyncStatusService; + let twentyORMManager: TwentyORMManager; + let messagingCursorService: MessagingCursorService; + + let mockMicrosoftMessageChannel: MessageChannelWorkspaceEntity; + let mockGoogleMessageChannel: MessageChannelWorkspaceEntity; + + const workspaceId = 'workspace-id'; + + beforeAll(() => { + mockMicrosoftMessageChannel = { + id: 'microsoft-message-channel-id', + connectedAccount: { + id: 'microsoft-connected-account-id', + provider: ConnectedAccountProvider.MICROSOFT, + handle: 'test@microsoft.com', + refreshToken: 'refresh-token', + handleAliases: '', + }, + messageFolders: [ + { + id: 'inbox-folder-id', + name: 'inbox', + syncCursor: 'inbox-sync-cursor', + messageChannelId: 'microsoft-message-channel-id', + } as MessageFolderWorkspaceEntity, + ], + } as MessageChannelWorkspaceEntity; + + mockGoogleMessageChannel = { + id: 'google-message-channel-id', + connectedAccount: { + id: 'google-connected-account-id', + provider: ConnectedAccountProvider.GOOGLE, + handle: 'test@gmail.com', + refreshToken: 'google-refresh-token', + handleAliases: '', + }, + syncCursor: 'google-sync-cursor', + } as MessageChannelWorkspaceEntity; + }); + + beforeEach(async () => { + const mockMessageChannelMessageAssociationRepository = { + find: jest + .fn() + .mockResolvedValue([ + { messageExternalId: 'external-id-existing-message-1' }, + { messageExternalId: 'external-id-existing-message-2' }, + ]), + delete: jest.fn().mockResolvedValue(undefined), + }; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + MessagingFullMessageListFetchService, + { + provide: CacheStorageNamespace.ModuleMessaging, + useValue: { + setAdd: jest.fn().mockResolvedValue(undefined), + }, + }, + { + provide: MessagingGetMessageListService, + useValue: { + getFullMessageLists: jest + .fn() + .mockImplementation((messageChannel) => { + if ( + messageChannel.connectedAccount.provider === + ConnectedAccountProvider.GOOGLE + ) { + return [ + { + messageExternalIds: [ + 'external-id-existing-message-1', + 'external-id-google-message-1', + 'external-id-google-message-2', + ], + nextSyncCursor: 'new-google-history-id', + folderId: undefined, + }, + ]; + } else { + return [ + { + messageExternalIds: [ + 'external-id-existing-message-1', + 'external-id-new-message-1', + 'external-id-new-message-2', + ], + nextSyncCursor: 'new-sync-cursor', + folderId: 'inbox-folder-id', + }, + ]; + } + }), + }, + }, + { + provide: MessageChannelSyncStatusService, + useValue: { + markAsMessagesListFetchOngoing: jest + .fn() + .mockResolvedValue(undefined), + scheduleMessagesImport: jest.fn().mockResolvedValue(undefined), + }, + }, + { + provide: TwentyORMManager, + useValue: { + getRepository: jest + .fn() + .mockResolvedValue( + mockMessageChannelMessageAssociationRepository, + ), + }, + }, + { + provide: MessagingCursorService, + useValue: { + updateCursor: jest.fn().mockResolvedValue(undefined), + }, + }, + { + provide: CacheStorageService, + useValue: { + setAdd: jest.fn().mockResolvedValue(undefined), + }, + }, + { + provide: MessageImportExceptionHandlerService, + useValue: { + handleDriverException: jest.fn().mockResolvedValue(undefined), + }, + }, + { + provide: MessagingMessageCleanerService, + useValue: { + cleanWorkspaceThreads: jest.fn().mockResolvedValue(undefined), + }, + }, + ], + }).compile(); + + messagingFullMessageListFetchService = + module.get( + MessagingFullMessageListFetchService, + ); + messagingGetMessageListService = module.get( + MessagingGetMessageListService, + ); + messageChannelSyncStatusService = + module.get( + MessageChannelSyncStatusService, + ); + twentyORMManager = module.get(TwentyORMManager); + messagingCursorService = module.get( + MessagingCursorService, + ); + }); + + it('should process Microsoft message list fetch correctly', async () => { + await messagingFullMessageListFetchService.processMessageListFetch( + mockMicrosoftMessageChannel, + workspaceId, + ); + + expect( + messageChannelSyncStatusService.markAsMessagesListFetchOngoing, + ).toHaveBeenCalledWith([mockMicrosoftMessageChannel.id]); + + expect( + messagingGetMessageListService.getFullMessageLists, + ).toHaveBeenCalledWith(mockMicrosoftMessageChannel); + + expect(twentyORMManager.getRepository).toHaveBeenCalledWith( + 'messageChannelMessageAssociation', + ); + + expect(messagingCursorService.updateCursor).toHaveBeenCalledWith( + mockMicrosoftMessageChannel, + 'new-sync-cursor', + 'inbox-folder-id', + ); + + expect( + messageChannelSyncStatusService.scheduleMessagesImport, + ).toHaveBeenCalledWith([mockMicrosoftMessageChannel.id]); + }); + + it('should process Google message list fetch correctly', async () => { + await messagingFullMessageListFetchService.processMessageListFetch( + mockGoogleMessageChannel, + workspaceId, + ); + + expect( + messageChannelSyncStatusService.markAsMessagesListFetchOngoing, + ).toHaveBeenCalledWith([mockGoogleMessageChannel.id]); + + expect( + messagingGetMessageListService.getFullMessageLists, + ).toHaveBeenCalledWith(mockGoogleMessageChannel); + + expect(twentyORMManager.getRepository).toHaveBeenCalledWith( + 'messageChannelMessageAssociation', + ); + + expect(messagingCursorService.updateCursor).toHaveBeenCalledWith( + mockGoogleMessageChannel, + 'new-google-history-id', + undefined, + ); + + expect( + messageChannelSyncStatusService.scheduleMessagesImport, + ).toHaveBeenCalledWith([mockGoogleMessageChannel.id]); + }); +}); diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-full-message-list-fetch.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-full-message-list-fetch.service.ts index b8d78fcfa..834b00243 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-full-message-list-fetch.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-full-message-list-fetch.service.ts @@ -6,7 +6,6 @@ import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decora import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; -import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service'; import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity'; import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; @@ -32,7 +31,6 @@ export class MessagingFullMessageListFetchService { public async processMessageListFetch( messageChannel: MessageChannelWorkspaceEntity, - connectedAccount: ConnectedAccountWorkspaceEntity, workspaceId: string, ) { try { diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-messages-import.service.spec.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-messages-import.service.spec.ts new file mode 100644 index 000000000..b65de9665 --- /dev/null +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-messages-import.service.spec.ts @@ -0,0 +1,284 @@ +import { Logger, Provider } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; + +import { ConnectedAccountProvider } from 'twenty-shared/types'; + +import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; +import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; +import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; +import { BlocklistRepository } from 'src/modules/blocklist/repositories/blocklist.repository'; +import { EmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/services/email-alias-manager.service'; +import { ConnectedAccountRefreshTokensService } from 'src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service'; +import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; +import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service'; +import { + MessageChannelSyncStage, + MessageChannelWorkspaceEntity, +} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; +import { MESSAGING_GMAIL_USERS_MESSAGES_GET_BATCH_SIZE } from 'src/modules/messaging/message-import-manager/drivers/gmail/constants/messaging-gmail-users-messages-get-batch-size.constant'; +import { MessagingGetMessagesService } from 'src/modules/messaging/message-import-manager/services/messaging-get-messages.service'; +import { MessageImportExceptionHandlerService } from 'src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service'; +import { MessagingMessagesImportService } from 'src/modules/messaging/message-import-manager/services/messaging-messages-import.service'; +import { MessagingSaveMessagesAndEnqueueContactCreationService } from 'src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service'; +import { MessagingTelemetryService } from 'src/modules/messaging/monitoring/services/messaging-telemetry.service'; + +describe('MessagingMessagesImportService', () => { + let service: MessagingMessagesImportService; + let messageChannelSyncStatusService: MessageChannelSyncStatusService; + let connectedAccountRefreshTokensService: ConnectedAccountRefreshTokensService; + let emailAliasManagerService: EmailAliasManagerService; + let messagingGetMessagesService: MessagingGetMessagesService; + let saveMessagesService: MessagingSaveMessagesAndEnqueueContactCreationService; + + const workspaceId = 'workspace-id'; + let mockMessageChannel: MessageChannelWorkspaceEntity; + let mockConnectedAccount: ConnectedAccountWorkspaceEntity; + let providersBase: Provider[]; + + beforeEach(async () => { + mockConnectedAccount = { + id: 'connected-account-id', + provider: ConnectedAccountProvider.GOOGLE, + handle: 'test@gmail.com', + refreshToken: 'refresh-token', + accessToken: 'old-access-token', + accountOwnerId: 'account-owner-id', + handleAliases: 'alias1@gmail.com,alias2@gmail.com', + } as ConnectedAccountWorkspaceEntity; + + mockMessageChannel = { + id: 'message-channel-id', + syncStage: MessageChannelSyncStage.MESSAGES_IMPORT_PENDING, + connectedAccountId: mockConnectedAccount.id, + handle: 'test@gmail.com', + } as MessageChannelWorkspaceEntity; + + providersBase = [ + MessagingMessagesImportService, + { + provide: CacheStorageService, + useValue: { + setAdd: jest.fn().mockResolvedValue(undefined), + }, + }, + { + provide: MessageChannelSyncStatusService, + useValue: { + markAsMessagesImportOngoing: jest.fn().mockResolvedValue(undefined), + markAsCompletedAndSchedulePartialMessageListFetch: jest + .fn() + .mockResolvedValue(undefined), + scheduleMessagesImport: jest.fn().mockResolvedValue(undefined), + }, + }, + { + provide: ConnectedAccountRefreshTokensService, + useValue: { + refreshAndSaveTokens: jest.fn().mockResolvedValue('new-access-token'), + }, + }, + { + provide: MessagingTelemetryService, + useValue: { + track: jest.fn().mockResolvedValue(undefined), + }, + }, + { + provide: 'BlocklistRepository', + useValue: { + getByWorkspaceMemberId: jest.fn().mockResolvedValue([]), + }, + }, + { + provide: BlocklistRepository, + useValue: { + getByWorkspaceMemberId: jest.fn().mockResolvedValue([]), + }, + }, + { + provide: EmailAliasManagerService, + useValue: { + refreshHandleAliases: jest.fn().mockResolvedValue(undefined), + }, + }, + { + provide: TwentyORMManager, + useValue: { + getRepository: jest.fn().mockResolvedValue({ + update: jest.fn().mockResolvedValue(undefined), + }), + }, + }, + { + provide: MessagingGetMessagesService, + useValue: { + getMessages: jest.fn().mockResolvedValue([ + { + id: 'message-1', + from: 'sender@example.com', + to: 'test@gmail.com', + }, + { + id: 'message-2', + from: 'test@gmail.com', + to: 'recipient@example.com', + }, + ]), + }, + }, + { + provide: MessagingSaveMessagesAndEnqueueContactCreationService, + useValue: { + saveMessagesAndEnqueueContactCreation: jest + .fn() + .mockResolvedValue(undefined), + }, + }, + { + provide: MessageImportExceptionHandlerService, + useValue: { + handleDriverException: jest.fn().mockResolvedValue(undefined), + }, + }, + ]; + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ...providersBase, + { + provide: CacheStorageNamespace.ModuleMessaging, + useValue: { + setPop: jest + .fn() + .mockResolvedValue(['message-id-1', 'message-id-2']), + setAdd: jest.fn().mockResolvedValue(undefined), + }, + }, + ], + }) + .overrideProvider(Logger) + .useValue({ log: jest.fn() }) + .compile(); + + service = module.get( + MessagingMessagesImportService, + ); + + messageChannelSyncStatusService = + module.get( + MessageChannelSyncStatusService, + ); + connectedAccountRefreshTokensService = + module.get( + ConnectedAccountRefreshTokensService, + ); + emailAliasManagerService = module.get( + EmailAliasManagerService, + ); + messagingGetMessagesService = module.get( + MessagingGetMessagesService, + ); + + saveMessagesService = + module.get( + MessagingSaveMessagesAndEnqueueContactCreationService, + ); + }); + + it('should fails if SyncStage is not MESSAGES_IMPORT_PENDING', async () => { + mockMessageChannel.syncStage = + MessageChannelSyncStage.PARTIAL_MESSAGE_LIST_FETCH_PENDING; + + expect( + service.processMessageBatchImport( + mockMessageChannel, + mockConnectedAccount, + workspaceId, + ), + ).resolves.toBeFalsy(); + }); + + it('should process message batch import successfully', async () => { + await service.processMessageBatchImport( + mockMessageChannel, + mockConnectedAccount, + workspaceId, + ); + expect( + messageChannelSyncStatusService.markAsMessagesImportOngoing, + ).toHaveBeenCalledWith([mockMessageChannel.id]); + expect( + connectedAccountRefreshTokensService.refreshAndSaveTokens, + ).toHaveBeenCalledWith(mockConnectedAccount, workspaceId); + expect(emailAliasManagerService.refreshHandleAliases).toHaveBeenCalledWith( + mockConnectedAccount, + ); + expect(messagingGetMessagesService.getMessages).toHaveBeenCalledWith( + ['message-id-1', 'message-id-2'], + mockConnectedAccount, + workspaceId, + ); + expect( + saveMessagesService.saveMessagesAndEnqueueContactCreation, + ).toHaveBeenCalled(); + expect( + messageChannelSyncStatusService.scheduleMessagesImport, + ).toHaveBeenCalledTimes(0); + }); + + it('should process message batch import of more than MESSAGING_GMAIL_USERS_MESSAGES_GET_BATCH_SIZE successfully', async () => { + const arrayMessagesBig = Array.from( + { length: MESSAGING_GMAIL_USERS_MESSAGES_GET_BATCH_SIZE + 1 }, + (_, index) => `message-id-${index + 1}`, + ); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ...providersBase, + { + provide: CacheStorageNamespace.ModuleMessaging, + useValue: { + setPop: jest.fn().mockResolvedValue(arrayMessagesBig), + setAdd: jest.fn().mockResolvedValue(undefined), + }, + }, + ], + }) + .overrideProvider(Logger) + .useValue({ log: jest.fn() }) + .compile(); + + service = module.get( + MessagingMessagesImportService, + ); + + messageChannelSyncStatusService = + module.get( + MessageChannelSyncStatusService, + ); + connectedAccountRefreshTokensService = + module.get( + ConnectedAccountRefreshTokensService, + ); + emailAliasManagerService = module.get( + EmailAliasManagerService, + ); + messagingGetMessagesService = module.get( + MessagingGetMessagesService, + ); + + saveMessagesService = + module.get( + MessagingSaveMessagesAndEnqueueContactCreationService, + ); + + await service.processMessageBatchImport( + mockMessageChannel, + mockConnectedAccount, + workspaceId, + ); + + expect( + messageChannelSyncStatusService.scheduleMessagesImport, + ).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service.spec.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service.spec.ts new file mode 100644 index 000000000..27127e6ff --- /dev/null +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service.spec.ts @@ -0,0 +1,328 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; + +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 { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; +import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; +import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; +import { CreateCompanyAndContactJob } from 'src/modules/contact-creation-manager/jobs/create-company-and-contact.job'; +import { MessageDirection } from 'src/modules/messaging/common/enums/message-direction.enum'; +import { + MessageChannelContactAutoCreationPolicy, + MessageChannelWorkspaceEntity, +} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; +import { MessagingMessageService } from 'src/modules/messaging/message-import-manager/services/messaging-message.service'; +import { MessagingSaveMessagesAndEnqueueContactCreationService } from 'src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service'; +import { MessageWithParticipants } from 'src/modules/messaging/message-import-manager/types/message'; +import { MessagingMessageParticipantService } from 'src/modules/messaging/message-participant-manager/services/messaging-message-participant.service'; + +describe('MessagingSaveMessagesAndEnqueueContactCreationService', () => { + let service: MessagingSaveMessagesAndEnqueueContactCreationService; + let messageQueueService: MessageQueueService; + let messageService: MessagingMessageService; + let messageParticipantService: MessagingMessageParticipantService; + + let datasourceInstance: { transaction: jest.Mock }; + + const workspaceId = 'workspace-id'; + + const mockConnectedAccount: ConnectedAccountWorkspaceEntity = { + id: 'connected-account-id', + handle: 'test@example.com', + handleAliases: 'alias1@example.com,alias2@example.com', + } as ConnectedAccountWorkspaceEntity; + + const mockMessageChannel: MessageChannelWorkspaceEntity = { + id: 'message-channel-id', + isContactAutoCreationEnabled: true, + contactAutoCreationPolicy: + MessageChannelContactAutoCreationPolicy.SENT_AND_RECEIVED, + excludeNonProfessionalEmails: true, + excludeGroupEmails: true, + } as MessageChannelWorkspaceEntity; + + const mockMessages: MessageWithParticipants[] = [ + { + externalId: 'message-1', + headerMessageId: 'header-message-id-1', + subject: 'Test Subject 1', + text: 'Test content 1', + receivedAt: new Date(), + attachments: [], + messageThreadExternalId: 'thread-1', + direction: MessageDirection.OUTGOING, + participants: [ + { role: 'from', handle: 'test@example.com', displayName: 'Test User' }, + { role: 'to', handle: 'contact@company.com', displayName: 'Contact' }, + ], + }, + { + externalId: 'message-2', + headerMessageId: 'header-message-id-2', + subject: 'Test Subject 2', + text: 'Test content 2', + receivedAt: new Date(), + attachments: [], + messageThreadExternalId: 'thread-1', + direction: MessageDirection.INCOMING, + participants: [ + { role: 'from', handle: 'contact@company.com', displayName: 'Contact' }, + { role: 'to', handle: 'test@example.com', displayName: 'Test User' }, + { role: 'to', handle: 'personal@gmail.com', displayName: 'Personal' }, + { + role: 'to', + handle: 'team@lists.company.com', + displayName: 'Group email', + }, + ], + }, + ]; + + beforeEach(async () => { + datasourceInstance = { + transaction: jest.fn().mockImplementation(async (callback) => { + return callback({}); + }), + }; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + MessagingSaveMessagesAndEnqueueContactCreationService, + { + provide: MessageQueueService, + useValue: { + add: jest.fn().mockResolvedValue(undefined), + }, + }, + { + provide: getQueueToken(MessageQueue.contactCreationQueue), + useValue: { + add: jest.fn().mockResolvedValue(undefined), + }, + }, + { + provide: WorkspaceEventEmitter, + useValue: { + emitDatabaseBatchEvent: jest.fn().mockResolvedValue(undefined), + }, + }, + { + provide: getRepositoryToken(ObjectMetadataEntity, 'metadata'), + useValue: { + findOneOrFail: jest.fn(), + }, + }, + { + provide: MessagingMessageService, + useValue: { + saveMessagesWithinTransaction: jest.fn().mockResolvedValue( + new Map([ + ['message-1', 'db-message-id-1'], + ['message-2', 'db-message-id-2'], + ]), + ), + }, + }, + { + provide: MessagingMessageParticipantService, + useValue: { + saveMessageParticipants: jest.fn().mockResolvedValue(undefined), + }, + }, + { + provide: TwentyORMManager, + useValue: { + getDatasource: jest.fn().mockResolvedValue(datasourceInstance), + }, + }, + ], + }).compile(); + + service = module.get( + MessagingSaveMessagesAndEnqueueContactCreationService, + ); + messageQueueService = module.get( + getQueueToken(MessageQueue.contactCreationQueue), + ); + messageService = module.get( + MessagingMessageService, + ); + messageParticipantService = module.get( + MessagingMessageParticipantService, + ); + }); + + it('should save messages and enqueue contact creation', async () => { + await service.saveMessagesAndEnqueueContactCreation( + mockMessages, + mockMessageChannel, + mockConnectedAccount, + workspaceId, + ); + + expect(messageService.saveMessagesWithinTransaction).toHaveBeenCalledWith( + mockMessages, + mockMessageChannel.id, + expect.any(Object), + ); + + expect( + messageParticipantService.saveMessageParticipants, + ).toHaveBeenCalled(); + expect(messageQueueService.add).toHaveBeenCalled(); + }); + + it('should not enqueue contact creation when it is disabled', async () => { + await service.saveMessagesAndEnqueueContactCreation( + mockMessages, + { + ...mockMessageChannel, + isContactAutoCreationEnabled: false, + }, + mockConnectedAccount, + workspaceId, + ); + + expect(messageService.saveMessagesWithinTransaction).toHaveBeenCalled(); + expect(messageQueueService.add).not.toHaveBeenCalled(); + }); + + it('should create external contacts', async () => { + await service.saveMessagesAndEnqueueContactCreation( + [ + { + ...mockMessages[1], + participants: [ + { + role: 'from', + handle: 'tim@apple.com', + displayName: 'participant email', + }, + ], + }, + ], + mockMessageChannel, + mockConnectedAccount, + workspaceId, + ); + + expect(messageQueueService.add).toHaveBeenCalledWith( + CreateCompanyAndContactJob.name, + { + workspaceId, + connectedAccount: mockConnectedAccount, + source: FieldActorSource.EMAIL, + contactsToCreate: [ + { + handle: 'tim@apple.com', + displayName: 'participant email', + role: 'from', + shouldCreateContact: true, + messageId: 'db-message-id-2', + }, + ], + }, + ); + }); + + it('should not create group emails contacts', async () => { + await service.saveMessagesAndEnqueueContactCreation( + [ + { + ...mockMessages[0], + participants: [ + { + role: 'from', + handle: 'contact@group.com', + displayName: 'participant that is the Connected Account', + }, + ], + }, + ], + mockMessageChannel, + mockConnectedAccount, + workspaceId, + ); + + expect(messageQueueService.add).toHaveBeenCalledWith( + CreateCompanyAndContactJob.name, + { + workspaceId, + connectedAccount: mockConnectedAccount, + source: FieldActorSource.EMAIL, + contactsToCreate: [], + }, + ); + }); + + it('should not create personal emails contacts', async () => { + await service.saveMessagesAndEnqueueContactCreation( + [ + { + ...mockMessages[0], + participants: [ + { + role: 'from', + handle: 'test@gmail.com', + displayName: 'participant personal email', + }, + ], + }, + ], + mockMessageChannel, + mockConnectedAccount, + workspaceId, + ); + + expect(messageQueueService.add).toHaveBeenCalledWith( + CreateCompanyAndContactJob.name, + { + workspaceId, + connectedAccount: mockConnectedAccount, + source: FieldActorSource.EMAIL, + contactsToCreate: [], + }, + ); + }); + it('should not create contact if the participant is the connected account', async () => { + const mockMessagesWithConnectedAccount = [ + { + ...mockMessages[0], + participants: [ + { + role: 'from', + handle: 'connected@account.com', + displayName: 'participant that is the Connected Account', + }, + ], + }, + ]; + + await service.saveMessagesAndEnqueueContactCreation( + mockMessagesWithConnectedAccount, + mockMessageChannel, + { + ...mockConnectedAccount, + handle: 'connected@account.com', + }, + workspaceId, + ); + + expect(messageQueueService.add).toHaveBeenCalledWith( + CreateCompanyAndContactJob.name, + { + workspaceId, + connectedAccount: { + ...mockConnectedAccount, + handle: 'connected@account.com', + }, + source: FieldActorSource.EMAIL, + contactsToCreate: [], + }, + ); + }); +});