Empty Gmail box bug (#12225)
if gmail is empty, there is an error. This PR handles this use-case
This commit is contained in:
@ -0,0 +1,205 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||
|
||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { GmailClientProvider } from 'src/modules/messaging/message-import-manager/drivers/gmail/providers/gmail-client.provider';
|
||||
import { GmailGetHistoryService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-history.service';
|
||||
import { GmailGetMessageListService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-message-list.service';
|
||||
import { GmailHandleErrorService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-handle-error.service';
|
||||
|
||||
describe('GmailGetMessageListService', () => {
|
||||
let service: GmailGetMessageListService;
|
||||
let gmailClientProvider: GmailClientProvider;
|
||||
|
||||
const mockConnectedAccount: Pick<
|
||||
ConnectedAccountWorkspaceEntity,
|
||||
'provider' | 'refreshToken' | 'id' | 'handle'
|
||||
> = {
|
||||
id: 'connected-account-id',
|
||||
provider: ConnectedAccountProvider.GOOGLE,
|
||||
refreshToken: 'refresh-token',
|
||||
handle: 'test@gmail.com',
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
GmailGetMessageListService,
|
||||
{
|
||||
provide: GmailClientProvider,
|
||||
useValue: {
|
||||
getGmailClient: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: GmailGetHistoryService,
|
||||
useValue: {
|
||||
getHistory: jest.fn(),
|
||||
getMessageIdsFromHistory: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: GmailHandleErrorService,
|
||||
useValue: {
|
||||
handleGmailMessageListFetchError: jest.fn(),
|
||||
handleGmailMessagesImportError: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<GmailGetMessageListService>(
|
||||
GmailGetMessageListService,
|
||||
);
|
||||
gmailClientProvider = module.get<GmailClientProvider>(GmailClientProvider);
|
||||
});
|
||||
|
||||
describe('getFullMessageList', () => {
|
||||
it('should return 0 messageExternalIds when gmail returns 0 messages', async () => {
|
||||
const mockGmailClient = {
|
||||
users: {
|
||||
messages: {
|
||||
list: jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
messages: [],
|
||||
nextPageToken: undefined,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
(gmailClientProvider.getGmailClient as jest.Mock).mockResolvedValue(
|
||||
mockGmailClient,
|
||||
);
|
||||
|
||||
const result = await service.getFullMessageList(mockConnectedAccount);
|
||||
|
||||
expect(result.messageExternalIds).toHaveLength(0);
|
||||
expect(mockGmailClient.users.messages.list).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should return 5 messageExternalIds when gmail returns 5 messages', async () => {
|
||||
const mockMessages = [
|
||||
{
|
||||
id: `message-1`,
|
||||
},
|
||||
{
|
||||
id: `message-2`,
|
||||
},
|
||||
{
|
||||
id: `message-3`,
|
||||
},
|
||||
{
|
||||
id: `message-4`,
|
||||
},
|
||||
{
|
||||
id: `message-5`,
|
||||
},
|
||||
];
|
||||
|
||||
const mockGmailClient = {
|
||||
users: {
|
||||
messages: {
|
||||
list: jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
messages: mockMessages,
|
||||
nextPageToken: undefined,
|
||||
},
|
||||
}),
|
||||
get: jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
historyId: 'history-id-123',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
(gmailClientProvider.getGmailClient as jest.Mock).mockResolvedValue(
|
||||
mockGmailClient,
|
||||
);
|
||||
|
||||
const result = await service.getFullMessageList(mockConnectedAccount);
|
||||
|
||||
expect(result.messageExternalIds).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('should return 3 messageExternalIds when gmail provides a nextpagetoken with 2 messages, then 1', async () => {
|
||||
const mockGmailClient = {
|
||||
users: {
|
||||
messages: {
|
||||
list: jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
data: {
|
||||
messages: [
|
||||
{
|
||||
id: `message-1`,
|
||||
},
|
||||
{
|
||||
id: `message-2`,
|
||||
},
|
||||
],
|
||||
nextPageToken: 'next-page-token',
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
data: {
|
||||
messages: [
|
||||
{
|
||||
id: `message-3`,
|
||||
},
|
||||
],
|
||||
nextPageToken: undefined,
|
||||
},
|
||||
}),
|
||||
get: jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
historyId: 'history-id-123',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
(gmailClientProvider.getGmailClient as jest.Mock).mockResolvedValue(
|
||||
mockGmailClient,
|
||||
);
|
||||
|
||||
const result = await service.getFullMessageList(mockConnectedAccount);
|
||||
|
||||
expect(result.messageExternalIds).toHaveLength(3);
|
||||
expect(mockGmailClient.users.messages.list).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
it('should go through while loop once when gmail provides a nextpagetoken but 0 messages - should never happen IRL', async () => {
|
||||
const mockGmailClient = {
|
||||
users: {
|
||||
messages: {
|
||||
list: jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
messages: [],
|
||||
nextPageToken: 'next-page-token',
|
||||
},
|
||||
}),
|
||||
},
|
||||
get: jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
historyId: 'history-id-123',
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
(gmailClientProvider.getGmailClient as jest.Mock).mockResolvedValue(
|
||||
mockGmailClient,
|
||||
);
|
||||
|
||||
const result = await service.getFullMessageList(mockConnectedAccount);
|
||||
|
||||
expect(result.messageExternalIds).toHaveLength(0);
|
||||
expect(mockGmailClient.users.messages.list).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,7 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { gmail_v1 as gmailV1 } from 'googleapis';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import {
|
||||
@ -40,7 +39,7 @@ export class GmailGetMessageListService {
|
||||
|
||||
let pageToken: string | undefined;
|
||||
let hasMoreMessages = true;
|
||||
let firstMessageExternalId: string | undefined;
|
||||
|
||||
const messageExternalIds: string[] = [];
|
||||
|
||||
while (hasMoreMessages) {
|
||||
@ -64,30 +63,28 @@ export class GmailGetMessageListService {
|
||||
};
|
||||
});
|
||||
|
||||
pageToken = messageList.data.nextPageToken ?? undefined;
|
||||
hasMoreMessages = !!pageToken;
|
||||
|
||||
const { messages } = messageList.data;
|
||||
const hasMessages = messages && messages.length > 0;
|
||||
|
||||
if (!messages || messages.length === 0) {
|
||||
if (!hasMessages) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!firstMessageExternalId) {
|
||||
firstMessageExternalId = messageList.data.messages?.[0].id ?? undefined;
|
||||
}
|
||||
pageToken = messageList.data.nextPageToken ?? undefined;
|
||||
hasMoreMessages = !!pageToken;
|
||||
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
messageExternalIds.push(...messages.map((message) => message.id));
|
||||
}
|
||||
|
||||
if (!isDefined(firstMessageExternalId)) {
|
||||
throw new MessageImportDriverException(
|
||||
`No firstMessageExternalId found for connected account ${connectedAccount.id}`,
|
||||
MessageImportDriverExceptionCode.UNKNOWN,
|
||||
);
|
||||
if (messageExternalIds.length === 0) {
|
||||
return {
|
||||
messageExternalIds,
|
||||
nextSyncCursor: '',
|
||||
};
|
||||
}
|
||||
|
||||
const firstMessageExternalId = messageExternalIds[0];
|
||||
const firstMessageContent = await gmailClient.users.messages
|
||||
.get({
|
||||
userId: 'me',
|
||||
|
||||
@ -43,6 +43,19 @@ export class MessagingFullMessageListFetchService {
|
||||
messageChannel,
|
||||
);
|
||||
|
||||
const isEmptyMailbox = fullMessageLists.some(
|
||||
(fullMessageList) => fullMessageList.messageExternalIds.length === 0,
|
||||
);
|
||||
|
||||
if (isEmptyMailbox) {
|
||||
await this.messageChannelSyncStatusService.resetAndScheduleFullMessageListFetch(
|
||||
[messageChannel.id],
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (const fullMessageList of fullMessageLists) {
|
||||
const { messageExternalIds, nextSyncCursor, folderId } =
|
||||
fullMessageList;
|
||||
|
||||
@ -48,15 +48,19 @@ export class MessagingGetMessageListService {
|
||||
messageChannel: MessageChannelWorkspaceEntity,
|
||||
): Promise<GetFullMessageListForFoldersResponse[]> {
|
||||
switch (messageChannel.connectedAccount.provider) {
|
||||
case ConnectedAccountProvider.GOOGLE:
|
||||
case ConnectedAccountProvider.GOOGLE: {
|
||||
const fullMessageList =
|
||||
await this.gmailGetMessageListService.getFullMessageList(
|
||||
messageChannel.connectedAccount,
|
||||
);
|
||||
|
||||
return [
|
||||
{
|
||||
...(await this.gmailGetMessageListService.getFullMessageList(
|
||||
messageChannel.connectedAccount,
|
||||
)),
|
||||
...fullMessageList,
|
||||
folderId: undefined,
|
||||
},
|
||||
];
|
||||
}
|
||||
case ConnectedAccountProvider.MICROSOFT: {
|
||||
const folderRepository =
|
||||
await this.twentyORMManager.getRepository<MessageFolderWorkspaceEntity>(
|
||||
|
||||
Reference in New Issue
Block a user