Empty Gmail box bug (#12225)

if gmail is empty, there is an error. This PR handles this use-case
This commit is contained in:
Guillim
2025-05-23 17:21:31 +02:00
committed by GitHub
parent 88b967dfb8
commit 6ef9a3b4c9
4 changed files with 237 additions and 18 deletions

View File

@ -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);
});
});
});

View File

@ -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',

View File

@ -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;

View File

@ -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>(