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 { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
import { gmail_v1 as gmailV1 } from 'googleapis';
|
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 { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||||
import {
|
import {
|
||||||
@ -40,7 +39,7 @@ export class GmailGetMessageListService {
|
|||||||
|
|
||||||
let pageToken: string | undefined;
|
let pageToken: string | undefined;
|
||||||
let hasMoreMessages = true;
|
let hasMoreMessages = true;
|
||||||
let firstMessageExternalId: string | undefined;
|
|
||||||
const messageExternalIds: string[] = [];
|
const messageExternalIds: string[] = [];
|
||||||
|
|
||||||
while (hasMoreMessages) {
|
while (hasMoreMessages) {
|
||||||
@ -64,30 +63,28 @@ export class GmailGetMessageListService {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
pageToken = messageList.data.nextPageToken ?? undefined;
|
|
||||||
hasMoreMessages = !!pageToken;
|
|
||||||
|
|
||||||
const { messages } = messageList.data;
|
const { messages } = messageList.data;
|
||||||
|
const hasMessages = messages && messages.length > 0;
|
||||||
|
|
||||||
if (!messages || messages.length === 0) {
|
if (!hasMessages) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!firstMessageExternalId) {
|
pageToken = messageList.data.nextPageToken ?? undefined;
|
||||||
firstMessageExternalId = messageList.data.messages?.[0].id ?? undefined;
|
hasMoreMessages = !!pageToken;
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-expect-error legacy noImplicitAny
|
// @ts-expect-error legacy noImplicitAny
|
||||||
messageExternalIds.push(...messages.map((message) => message.id));
|
messageExternalIds.push(...messages.map((message) => message.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isDefined(firstMessageExternalId)) {
|
if (messageExternalIds.length === 0) {
|
||||||
throw new MessageImportDriverException(
|
return {
|
||||||
`No firstMessageExternalId found for connected account ${connectedAccount.id}`,
|
messageExternalIds,
|
||||||
MessageImportDriverExceptionCode.UNKNOWN,
|
nextSyncCursor: '',
|
||||||
);
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const firstMessageExternalId = messageExternalIds[0];
|
||||||
const firstMessageContent = await gmailClient.users.messages
|
const firstMessageContent = await gmailClient.users.messages
|
||||||
.get({
|
.get({
|
||||||
userId: 'me',
|
userId: 'me',
|
||||||
|
|||||||
@ -43,6 +43,19 @@ export class MessagingFullMessageListFetchService {
|
|||||||
messageChannel,
|
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) {
|
for (const fullMessageList of fullMessageLists) {
|
||||||
const { messageExternalIds, nextSyncCursor, folderId } =
|
const { messageExternalIds, nextSyncCursor, folderId } =
|
||||||
fullMessageList;
|
fullMessageList;
|
||||||
|
|||||||
@ -48,15 +48,19 @@ export class MessagingGetMessageListService {
|
|||||||
messageChannel: MessageChannelWorkspaceEntity,
|
messageChannel: MessageChannelWorkspaceEntity,
|
||||||
): Promise<GetFullMessageListForFoldersResponse[]> {
|
): Promise<GetFullMessageListForFoldersResponse[]> {
|
||||||
switch (messageChannel.connectedAccount.provider) {
|
switch (messageChannel.connectedAccount.provider) {
|
||||||
case ConnectedAccountProvider.GOOGLE:
|
case ConnectedAccountProvider.GOOGLE: {
|
||||||
|
const fullMessageList =
|
||||||
|
await this.gmailGetMessageListService.getFullMessageList(
|
||||||
|
messageChannel.connectedAccount,
|
||||||
|
);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
...(await this.gmailGetMessageListService.getFullMessageList(
|
...fullMessageList,
|
||||||
messageChannel.connectedAccount,
|
|
||||||
)),
|
|
||||||
folderId: undefined,
|
folderId: undefined,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
}
|
||||||
case ConnectedAccountProvider.MICROSOFT: {
|
case ConnectedAccountProvider.MICROSOFT: {
|
||||||
const folderRepository =
|
const folderRepository =
|
||||||
await this.twentyORMManager.getRepository<MessageFolderWorkspaceEntity>(
|
await this.twentyORMManager.getRepository<MessageFolderWorkspaceEntity>(
|
||||||
|
|||||||
Reference in New Issue
Block a user