From de56c01206e5ea41c632b8aabd75fe7d50d77f29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Thu, 5 Dec 2024 17:57:57 +0100 Subject: [PATCH] 8733 refactor gmailhandleerrorservice (#8901) Closes #8733 - Refactor `GmailHandleErrorService` - Add tests and mocks for the errors --- .../create-company-and-contact.service.ts | 5 +- .../drivers/gmail/mocks/gaxios-error-mocks.ts | 117 +++++++++ .../gmail/mocks/gmail-api-error-mocks.ts | 240 ++++++++++++++++++ .../services/gmail-get-history.service.ts | 2 +- .../gmail-get-message-list.service.ts | 15 +- .../services/gmail-get-messages.service.ts | 6 +- .../services/gmail-handle-error.service.ts | 46 ++-- .../__tests__/parse-gaxios-error.spec.ts | 60 +++++ ...rse-gmail-message-list-fetch-error.spec.ts | 122 +++++++++ .../parse-gmail-messages-import-error.spec.ts | 185 ++++++++++++++ .../gmail/utils/parse-gaxios-error.util.ts | 7 +- ...se-gmail-message-list-fetch-error.util.ts} | 28 +- .../parse-gmail-messages-import-error.util.ts | 94 +++++++ 13 files changed, 878 insertions(+), 49 deletions(-) create mode 100644 packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/mocks/gaxios-error-mocks.ts create mode 100644 packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/mocks/gmail-api-error-mocks.ts create mode 100644 packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/__tests__/parse-gaxios-error.spec.ts create mode 100644 packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/__tests__/parse-gmail-message-list-fetch-error.spec.ts create mode 100644 packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/__tests__/parse-gmail-messages-import-error.spec.ts rename packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/{parse-gmail-error.util.ts => parse-gmail-message-list-fetch-error.util.ts} (83%) create mode 100644 packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-messages-import-error.util.ts diff --git a/packages/twenty-server/src/modules/contact-creation-manager/services/create-company-and-contact.service.ts b/packages/twenty-server/src/modules/contact-creation-manager/services/create-company-and-contact.service.ts index 0d3b68024..c1489e888 100644 --- a/packages/twenty-server/src/modules/contact-creation-manager/services/create-company-and-contact.service.ts +++ b/packages/twenty-server/src/modules/contact-creation-manager/services/create-company-and-contact.service.ts @@ -5,8 +5,8 @@ import chunk from 'lodash.chunk'; import compact from 'lodash.compact'; import { Any, EntityManager, Repository } from 'typeorm'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; -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 { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; @@ -24,7 +24,6 @@ import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/perso import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; import { isWorkEmail } from 'src/utils/is-work-email'; -import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; @Injectable() export class CreateCompanyAndContactService { @@ -36,8 +35,6 @@ export class CreateCompanyAndContactService { private readonly workspaceEventEmitter: WorkspaceEventEmitter, @InjectRepository(ObjectMetadataEntity, 'metadata') private readonly objectMetadataRepository: Repository, - @InjectRepository(FieldMetadataEntity, 'metadata') - private readonly fieldMetadataRepository: Repository, private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/mocks/gaxios-error-mocks.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/mocks/gaxios-error-mocks.ts new file mode 100644 index 000000000..e33fa326e --- /dev/null +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/mocks/gaxios-error-mocks.ts @@ -0,0 +1,117 @@ +// Gaxios Network Error Mocks +const gaxiosErrorMocks = { + // Connection Reset Error + connectionReset: { + code: 'ECONNRESET', + name: 'GaxiosError', + message: 'socket hang up', + status: null, + config: { + method: 'GET', + url: 'https://gmail.googleapis.com/gmail/v1/users/me/messages', + headers: { + Authorization: 'Bearer [TOKEN]', + Accept: 'application/json', + }, + timeout: 5000, + responseType: 'json', + }, + response: undefined, + }, + + // Host Not Found Error + hostNotFound: { + code: 'ENOTFOUND', + name: 'GaxiosError', + message: 'getaddrinfo ENOTFOUND gmail.googleapis.com', + status: null, + config: { + method: 'GET', + url: 'https://gmail.googleapis.com/gmail/v1/users/me/messages', + headers: { + Authorization: 'Bearer [TOKEN]', + Accept: 'application/json', + }, + timeout: 5000, + responseType: 'json', + }, + response: undefined, + }, + + // Connection Aborted Error + connectionAborted: { + code: 'ECONNABORTED', + name: 'GaxiosError', + message: 'The request was aborted due to a timeout', + status: null, + config: { + method: 'GET', + url: 'https://gmail.googleapis.com/gmail/v1/users/me/messages', + headers: { + Authorization: 'Bearer [TOKEN]', + Accept: 'application/json', + }, + timeout: 5000, + responseType: 'json', + }, + response: undefined, + }, + + // Timeout Error + timeout: { + code: 'ETIMEDOUT', + name: 'GaxiosError', + message: 'Connection timed out after 5000ms', + status: null, + config: { + method: 'GET', + url: 'https://gmail.googleapis.com/gmail/v1/users/me/messages', + headers: { + Authorization: 'Bearer [TOKEN]', + Accept: 'application/json', + }, + timeout: 5000, + responseType: 'json', + }, + response: undefined, + }, + + // Network Error + networkError: { + code: 'ERR_NETWORK', + name: 'GaxiosError', + message: 'Network Error', + status: null, + config: { + method: 'GET', + url: 'https://gmail.googleapis.com/gmail/v1/users/me/messages', + headers: { + Authorization: 'Bearer [TOKEN]', + Accept: 'application/json', + }, + timeout: 5000, + responseType: 'json', + }, + response: undefined, + }, + + // Helper function to get error by code + getError: function (code: string) { + switch (code) { + case 'ECONNRESET': + return this.connectionReset; + case 'ENOTFOUND': + return this.hostNotFound; + case 'ECONNABORTED': + return this.connectionAborted; + case 'ETIMEDOUT': + return this.timeout; + case 'ERR_NETWORK': + return this.networkError; + default: + throw new Error(`Unknown error code: ${code}`); + } + }, +}; + +export default gaxiosErrorMocks; diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/mocks/gmail-api-error-mocks.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/mocks/gmail-api-error-mocks.ts new file mode 100644 index 000000000..7b5ac9427 --- /dev/null +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/mocks/gmail-api-error-mocks.ts @@ -0,0 +1,240 @@ +// Gmail API Error Response Mocks for users.messages.list +const gmailApiErrorMocks = { + // 400 Bad Request - Invalid query parameters + badRequest: { + error: { + code: 400, + errors: [ + { + domain: 'global', + location: 'orderBy', + locationType: 'parameter', + message: + 'Sorting is not supported for queries with fullText terms. Results are always in descending relevance order.', + reason: 'badRequest', + }, + ], + message: + 'Sorting is not supported for queries with fullText terms. Results are always in descending relevance order.', + }, + }, + + // 400 Invalid Grant + invalidGrant: { + error: { + code: 400, + errors: [ + { + domain: 'global', + reason: 'invalid_grant', + message: 'Invalid Credentials', + }, + ], + message: 'Invalid Credentials', + }, + }, + + // 400 Failed Precondition + failedPrecondition: { + error: { + code: 400, + errors: [ + { + domain: 'global', + reason: 'failedPrecondition', + message: 'Failed Precondition', + }, + ], + message: 'Failed Precondition', + }, + }, + + // 401 Invalid Credentials + invalidCredentials: { + error: { + errors: [ + { + domain: 'global', + reason: 'authError', + message: 'Invalid Credentials', + locationType: 'header', + location: 'Authorization', + }, + ], + code: 401, + message: 'Invalid Credentials', + }, + }, + + // 404 Not Found + notFound: { + error: { + errors: [ + { + domain: 'global', + reason: 'notFound', + message: 'Resource not found: userId', + location: 'userId', + locationType: 'parameter', + }, + ], + code: 404, + message: 'Resource not found: userId', + }, + }, + + // 410 Gone + gone: { + error: { + errors: [ + { + domain: 'global', + reason: 'resourceGone', + message: 'Resource has been deleted', + location: 'messageId', + locationType: 'parameter', + }, + ], + code: 410, + message: 'Resource has been deleted', + }, + }, + + // 403 Daily Limit Exceeded + dailyLimitExceeded: { + error: { + errors: [ + { + domain: 'usageLimits', + reason: 'dailyLimitExceeded', + message: 'Daily Limit Exceeded', + }, + ], + code: 403, + message: 'Daily Limit Exceeded', + }, + }, + + // 403 User Rate Limit Exceeded + userRateLimitExceeded: { + error: { + errors: [ + { + domain: 'usageLimits', + reason: 'userRateLimitExceeded', + message: 'User Rate Limit Exceeded', + }, + ], + code: 403, + message: 'User Rate Limit Exceeded', + }, + }, + + // 403 Rate Limit Exceeded + rateLimitExceeded: { + error: { + errors: [ + { + domain: 'usageLimits', + reason: 'rateLimitExceeded', + message: 'Rate Limit Exceeded', + }, + ], + code: 403, + message: 'Rate Limit Exceeded', + }, + }, + + // 403 Domain Policy Error + domainPolicyError: { + error: { + errors: [ + { + domain: 'global', + reason: 'domainPolicy', + message: 'The domain administrators have disabled Gmail apps.', + }, + ], + code: 403, + message: 'The domain administrators have disabled Gmail apps.', + }, + }, + + // 429 Too Many Requests (Concurrent Requests) + tooManyConcurrentRequests: { + error: { + errors: [ + { + domain: 'global', + reason: 'rateLimitExceeded', + message: 'Too many concurrent requests for user', + }, + ], + code: 429, + message: 'Too many concurrent requests for user', + }, + }, + + // 500 Backend Error + backendError: { + error: { + errors: [ + { + domain: 'global', + reason: 'backendError', + message: 'Backend Error', + }, + ], + code: 500, + message: 'Backend Error', + }, + }, + + getError: function (code: number, type?: string) { + switch (code) { + case 400: + switch (type) { + case 'invalid_grant': + return this.invalidGrant; + case 'failedPrecondition': + return this.failedPrecondition; + default: + return this.badRequest; + } + case 401: + return this.invalidCredentials; + case 403: + switch (type) { + case 'dailyLimit': + return this.dailyLimitExceeded; + case 'userRateLimit': + return this.userRateLimitExceeded; + case 'rateLimit': + return this.rateLimitExceeded; + case 'domainPolicy': + return this.domainPolicyError; + default: + return this.rateLimitExceeded; + } + case 404: + return this.notFound; + case 410: + return this.gone; + case 429: + switch (type) { + case 'concurrent': + return this.tooManyConcurrentRequests; + case 'mailSending': + return this.mailSendingLimitExceeded; + default: + return this.tooManyConcurrentRequests; + } + case 500: + return this.backendError; + default: + throw new Error(`Unknown error code: ${code}`); + } + }, +}; + +export default gmailApiErrorMocks; diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-history.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-history.service.ts index 4e03a104c..194e8ff2a 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-history.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-history.service.ts @@ -36,7 +36,7 @@ export class GmailGetHistoryService { labelId, }) .catch((error) => { - this.gmailHandleErrorService.handleError(error); + this.gmailHandleErrorService.handleGmailMessageListFetchError(error); return { data: { diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-message-list.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-message-list.service.ts index 03509497e..1dfa2bc0a 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-message-list.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-message-list.service.ts @@ -19,6 +19,7 @@ import { GetPartialMessageListResponse, } from 'src/modules/messaging/message-import-manager/services/messaging-get-message-list.service'; import { assertNotNull } from 'src/utils/assert'; +import { isDefined } from 'src/utils/is-defined'; @Injectable() export class GmailGetMessageListService { @@ -53,7 +54,7 @@ export class GmailGetMessageListService { ), }) .catch((error) => { - this.gmailHandleErrorService.handleError(error); + this.gmailHandleErrorService.handleGmailMessageListFetchError(error); return { data: { @@ -79,13 +80,23 @@ export class GmailGetMessageListService { messageExternalIds.push(...messages.map((message) => message.id)); } + if (!isDefined(firstMessageExternalId)) { + throw new MessageImportDriverException( + `No firstMessageExternalId found for connected account ${connectedAccount.id}`, + MessageImportDriverExceptionCode.UNKNOWN, + ); + } + const firstMessageContent = await gmailClient.users.messages .get({ userId: 'me', id: firstMessageExternalId, }) .catch((error) => { - this.gmailHandleErrorService.handleError(error); + this.gmailHandleErrorService.handleGmailMessagesImportError( + error, + firstMessageExternalId as string, + ); }); const nextSyncCursor = firstMessageContent?.data?.historyId; diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-messages.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-messages.service.ts index 98358a889..15590f1f5 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-messages.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-messages.service.ts @@ -77,11 +77,7 @@ export class GmailGetMessagesService { const messages = parsedResponses.map((response, index) => { if ('error' in response) { - if (response.error.code === 404) { - return null; - } - - this.gmailHandleErrorService.handleError( + this.gmailHandleErrorService.handleGmailMessagesImportError( response.error, messageIds[index], ); diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-handle-error.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-handle-error.service.ts index a81a34837..ab4ea7fea 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-handle-error.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-handle-error.service.ts @@ -1,33 +1,37 @@ import { Injectable } from '@nestjs/common'; import { parseGaxiosError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gaxios-error.util'; -import { parseGmailError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-error.util'; +import { parseGmailMessageListFetchError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-message-list-fetch-error.util'; +import { parseGmailMessagesImportError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-messages-import-error.util'; @Injectable() export class GmailHandleErrorService { constructor() {} - public handleError(error: any, messageExternalId?: string): void { - if ( - error.code && - [ - 'ECONNRESET', - 'ENOTFOUND', - 'ECONNABORTED', - 'ETIMEDOUT', - 'ERR_NETWORK', - ].includes(error.code) - ) { - throw parseGaxiosError(error); - } - if (error.code != 410) { - const gmailError = { - code: error.code, - reason: `${error?.errors?.[0].reason || error.response?.data?.error || ''}`, - message: `${error?.errors?.[0].message || error.response?.data?.error_description || ''}${messageExternalId ? ` for message with externalId: ${messageExternalId}` : ''}`, - }; + public handleGmailMessageListFetchError(error: any): void { + const gaxiosError = parseGaxiosError(error); - throw parseGmailError(gmailError); + if (gaxiosError) { + throw gaxiosError; + } + + throw parseGmailMessageListFetchError(error); + } + + public handleGmailMessagesImportError( + error: any, + messageExternalId: string, + ): void { + const gaxiosError = parseGaxiosError(error); + + if (gaxiosError) { + throw gaxiosError; + } + + const gmailError = parseGmailMessagesImportError(error, messageExternalId); + + if (gmailError) { + throw gmailError; } } } diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/__tests__/parse-gaxios-error.spec.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/__tests__/parse-gaxios-error.spec.ts new file mode 100644 index 000000000..81542690b --- /dev/null +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/__tests__/parse-gaxios-error.spec.ts @@ -0,0 +1,60 @@ +import { + MessageImportDriverException, + MessageImportDriverExceptionCode, +} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception'; +import gaxiosErrorMocks from 'src/modules/messaging/message-import-manager/drivers/gmail/mocks/gaxios-error-mocks'; +import { parseGaxiosError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gaxios-error.util'; + +describe('parseGaxiosError', () => { + it('should return a MessageImportDriverException for ECONNRESET', () => { + const error = gaxiosErrorMocks.getError('ECONNRESET'); + const result = parseGaxiosError(error); + + expect(result).toBeInstanceOf(MessageImportDriverException); + expect(result?.message).toBe(error.message); + expect(result?.code).toBe(MessageImportDriverExceptionCode.TEMPORARY_ERROR); + }); + + it('should return a MessageImportDriverException for ENOTFOUND', () => { + const error = gaxiosErrorMocks.getError('ENOTFOUND'); + const result = parseGaxiosError(error); + + expect(result).toBeInstanceOf(MessageImportDriverException); + expect(result?.message).toBe(error.message); + expect(result?.code).toBe(MessageImportDriverExceptionCode.TEMPORARY_ERROR); + }); + + it('should return a MessageImportDriverException for ECONNABORTED', () => { + const error = gaxiosErrorMocks.getError('ECONNABORTED'); + const result = parseGaxiosError(error); + + expect(result).toBeInstanceOf(MessageImportDriverException); + expect(result?.message).toBe(error.message); + expect(result?.code).toBe(MessageImportDriverExceptionCode.TEMPORARY_ERROR); + }); + + it('should return a MessageImportDriverException for ETIMEDOUT', () => { + const error = gaxiosErrorMocks.getError('ETIMEDOUT'); + const result = parseGaxiosError(error); + + expect(result).toBeInstanceOf(MessageImportDriverException); + expect(result?.message).toBe(error.message); + expect(result?.code).toBe(MessageImportDriverExceptionCode.TEMPORARY_ERROR); + }); + + it('should return a MessageImportDriverException for ERR_NETWORK', () => { + const error = gaxiosErrorMocks.getError('ERR_NETWORK'); + const result = parseGaxiosError(error); + + expect(result).toBeInstanceOf(MessageImportDriverException); + expect(result?.message).toBe(error.message); + expect(result?.code).toBe(MessageImportDriverExceptionCode.TEMPORARY_ERROR); + }); + + it('should return undefined for unknown error codes', () => { + const error = { code: 'UNKNOWN_ERROR' } as any; + const result = parseGaxiosError(error); + + expect(result).toBeUndefined(); + }); +}); diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/__tests__/parse-gmail-message-list-fetch-error.spec.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/__tests__/parse-gmail-message-list-fetch-error.spec.ts new file mode 100644 index 000000000..1f41d47d0 --- /dev/null +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/__tests__/parse-gmail-message-list-fetch-error.spec.ts @@ -0,0 +1,122 @@ +import { + MessageImportDriverException, + MessageImportDriverExceptionCode, +} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception'; +import gmailApiErrorMocks from 'src/modules/messaging/message-import-manager/drivers/gmail/mocks/gmail-api-error-mocks'; +import { parseGmailMessageListFetchError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-message-list-fetch-error.util'; + +describe('parseGmailMessageListFetchError', () => { + it('should handle 400 Bad Request', () => { + const error = gmailApiErrorMocks.getError(400); + const exception = parseGmailMessageListFetchError(error.error); + + expect(exception).toBeInstanceOf(MessageImportDriverException); + expect(exception.code).toBe(MessageImportDriverExceptionCode.UNKNOWN); + }); + + it('should handle 400 Invalid Grant', () => { + const error = gmailApiErrorMocks.getError(400, 'invalid_grant'); + const exception = parseGmailMessageListFetchError(error.error); + + expect(exception).toBeInstanceOf(MessageImportDriverException); + expect(exception.code).toBe( + MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, + ); + }); + + it('should handle 400 Failed Precondition', () => { + const error = gmailApiErrorMocks.getError(400, 'failedPrecondition'); + const exception = parseGmailMessageListFetchError(error.error); + + expect(exception).toBeInstanceOf(MessageImportDriverException); + expect(exception.code).toBe( + MessageImportDriverExceptionCode.TEMPORARY_ERROR, + ); + }); + + it('should handle 401 Invalid Credentials', () => { + const error = gmailApiErrorMocks.getError(401); + const exception = parseGmailMessageListFetchError(error.error); + + expect(exception).toBeInstanceOf(MessageImportDriverException); + expect(exception.code).toBe( + MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, + ); + }); + + it('should handle 403 Daily Limit Exceeded', () => { + const error = gmailApiErrorMocks.getError(403, 'dailyLimit'); + const exception = parseGmailMessageListFetchError(error.error); + + expect(exception).toBeInstanceOf(MessageImportDriverException); + expect(exception.code).toBe( + MessageImportDriverExceptionCode.TEMPORARY_ERROR, + ); + }); + + it('should handle 403 User Rate Limit Exceeded', () => { + const error = gmailApiErrorMocks.getError(403, 'userRateLimit'); + const exception = parseGmailMessageListFetchError(error.error); + + expect(exception).toBeInstanceOf(MessageImportDriverException); + expect(exception.code).toBe( + MessageImportDriverExceptionCode.TEMPORARY_ERROR, + ); + }); + + it('should handle 403 Rate Limit Exceeded', () => { + const error = gmailApiErrorMocks.getError(403, 'rateLimit'); + const exception = parseGmailMessageListFetchError(error.error); + + expect(exception).toBeInstanceOf(MessageImportDriverException); + expect(exception.code).toBe( + MessageImportDriverExceptionCode.TEMPORARY_ERROR, + ); + }); + + it('should handle 403 Domain Policy Error', () => { + const error = gmailApiErrorMocks.getError(403, 'domainPolicy'); + const exception = parseGmailMessageListFetchError(error.error); + + expect(exception).toBeInstanceOf(MessageImportDriverException); + expect(exception.code).toBe( + MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, + ); + }); + + it('should handle 404 Not Found', () => { + const error = gmailApiErrorMocks.getError(404); + const exception = parseGmailMessageListFetchError(error.error); + + expect(exception).toBeInstanceOf(MessageImportDriverException); + expect(exception.code).toBe(MessageImportDriverExceptionCode.NOT_FOUND); + }); + + it('should handle 410 Gone', () => { + const error = gmailApiErrorMocks.getError(410); + const exception = parseGmailMessageListFetchError(error.error); + + expect(exception).toBeInstanceOf(MessageImportDriverException); + expect(exception.code).toBe(MessageImportDriverExceptionCode.UNKNOWN); + }); + + it('should handle 429 Too Many Requests', () => { + const error = gmailApiErrorMocks.getError(429, 'concurrent'); + const exception = parseGmailMessageListFetchError(error.error); + + expect(exception).toBeInstanceOf(MessageImportDriverException); + expect(exception.code).toBe( + MessageImportDriverExceptionCode.TEMPORARY_ERROR, + ); + }); + + it('should handle 500 Backend Error', () => { + const error = gmailApiErrorMocks.getError(500); + const exception = parseGmailMessageListFetchError(error.error); + + expect(exception).toBeInstanceOf(MessageImportDriverException); + expect(exception.code).toBe( + MessageImportDriverExceptionCode.TEMPORARY_ERROR, + ); + }); +}); diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/__tests__/parse-gmail-messages-import-error.spec.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/__tests__/parse-gmail-messages-import-error.spec.ts new file mode 100644 index 000000000..4e817897c --- /dev/null +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/__tests__/parse-gmail-messages-import-error.spec.ts @@ -0,0 +1,185 @@ +import { + MessageImportDriverException, + MessageImportDriverExceptionCode, +} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception'; +import gmailApiErrorMocks from 'src/modules/messaging/message-import-manager/drivers/gmail/mocks/gmail-api-error-mocks'; +import { parseGmailMessagesImportError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-messages-import-error.util'; + +const messageExternalId = '123'; + +describe('parseGmailMessagesImportError', () => { + it('should handle 400 Bad Request', () => { + const error = gmailApiErrorMocks.getError(400); + const exception = parseGmailMessagesImportError( + error.error, + messageExternalId, + ); + + expect(exception).toBeInstanceOf(MessageImportDriverException); + expect(exception?.code).toBe(MessageImportDriverExceptionCode.UNKNOWN); + expect(exception?.message).toBe( + `${error.error.errors[0].message} for message with externalId: ${messageExternalId}`, + ); + }); + + it('should handle 400 Invalid Grant', () => { + const error = gmailApiErrorMocks.getError(400, 'invalid_grant'); + const exception = parseGmailMessagesImportError( + error.error, + messageExternalId, + ); + + expect(exception).toBeInstanceOf(MessageImportDriverException); + expect(exception?.code).toBe( + MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, + ); + expect(exception?.message).toBe( + `${error.error.errors[0].message} for message with externalId: ${messageExternalId}`, + ); + }); + + it('should handle 400 Failed Precondition', () => { + const error = gmailApiErrorMocks.getError(400, 'failedPrecondition'); + const exception = parseGmailMessagesImportError( + error.error, + messageExternalId, + ); + + expect(exception).toBeInstanceOf(MessageImportDriverException); + expect(exception?.code).toBe( + MessageImportDriverExceptionCode.TEMPORARY_ERROR, + ); + }); + + it('should handle 401 Invalid Credentials', () => { + const error = gmailApiErrorMocks.getError(401); + const exception = parseGmailMessagesImportError( + error.error, + messageExternalId, + ); + + expect(exception).toBeInstanceOf(MessageImportDriverException); + expect(exception?.code).toBe( + MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, + ); + expect(exception?.message).toBe( + `${error.error.errors[0].message} for message with externalId: ${messageExternalId}`, + ); + }); + + it('should handle 403 Daily Limit Exceeded', () => { + const error = gmailApiErrorMocks.getError(403, 'dailyLimit'); + const exception = parseGmailMessagesImportError( + error.error, + messageExternalId, + ); + + expect(exception).toBeInstanceOf(MessageImportDriverException); + expect(exception?.code).toBe( + MessageImportDriverExceptionCode.TEMPORARY_ERROR, + ); + expect(exception?.message).toBe( + `${error.error.errors[0].message} for message with externalId: ${messageExternalId}`, + ); + }); + + it('should handle 403 User Rate Limit Exceeded', () => { + const error = gmailApiErrorMocks.getError(403, 'userRateLimit'); + const exception = parseGmailMessagesImportError( + error.error, + messageExternalId, + ); + + expect(exception).toBeInstanceOf(MessageImportDriverException); + expect(exception?.code).toBe( + MessageImportDriverExceptionCode.TEMPORARY_ERROR, + ); + expect(exception?.message).toBe( + `${error.error.errors[0].message} for message with externalId: ${messageExternalId}`, + ); + }); + + it('should handle 403 Rate Limit Exceeded', () => { + const error = gmailApiErrorMocks.getError(403, 'rateLimit'); + const exception = parseGmailMessagesImportError( + error.error, + messageExternalId, + ); + + expect(exception).toBeInstanceOf(MessageImportDriverException); + expect(exception?.code).toBe( + MessageImportDriverExceptionCode.TEMPORARY_ERROR, + ); + expect(exception?.message).toBe( + `${error.error.errors[0].message} for message with externalId: ${messageExternalId}`, + ); + }); + + it('should handle 403 Domain Policy Error', () => { + const error = gmailApiErrorMocks.getError(403, 'domainPolicy'); + const exception = parseGmailMessagesImportError( + error.error, + messageExternalId, + ); + + expect(exception).toBeInstanceOf(MessageImportDriverException); + expect(exception?.code).toBe( + MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, + ); + expect(exception?.message).toBe( + `${error.error.errors[0].message} for message with externalId: ${messageExternalId}`, + ); + }); + + it('should handle 404 Not Found', () => { + const error = gmailApiErrorMocks.getError(404); + const exception = parseGmailMessagesImportError( + error.error, + messageExternalId, + ); + + expect(exception).toBe(undefined); + }); + + it('should handle 410 Gone', () => { + const error = gmailApiErrorMocks.getError(410); + const exception = parseGmailMessagesImportError( + error.error, + messageExternalId, + ); + + expect(exception).toBe(undefined); + }); + + it('should handle 429 Too Many Requests', () => { + const error = gmailApiErrorMocks.getError(429, 'concurrent'); + const exception = parseGmailMessagesImportError( + error.error, + messageExternalId, + ); + + expect(exception).toBeInstanceOf(MessageImportDriverException); + expect(exception?.code).toBe( + MessageImportDriverExceptionCode.TEMPORARY_ERROR, + ); + expect(exception?.message).toBe( + `${error.error.errors[0].message} for message with externalId: ${messageExternalId}`, + ); + }); + + it('should handle 500 Backend Error', () => { + const error = gmailApiErrorMocks.getError(500); + const exception = parseGmailMessagesImportError( + error.error, + messageExternalId, + ); + + expect(exception).toBeInstanceOf(MessageImportDriverException); + expect(exception?.code).toBe( + MessageImportDriverExceptionCode.TEMPORARY_ERROR, + ); + expect(exception?.message).toBe( + `${error.error.errors[0].message} for message with externalId: ${messageExternalId}`, + ); + }); +}); diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gaxios-error.util.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gaxios-error.util.ts index 221423ae3..7adbc0b2c 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gaxios-error.util.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gaxios-error.util.ts @@ -7,7 +7,7 @@ import { export const parseGaxiosError = ( error: GaxiosError, -): MessageImportDriverException => { +): MessageImportDriverException | undefined => { const { code } = error; switch (code) { @@ -22,9 +22,6 @@ export const parseGaxiosError = ( ); default: - return new MessageImportDriverException( - error.message, - MessageImportDriverExceptionCode.UNKNOWN, - ); + return undefined; } }; diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-error.util.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-message-list-fetch-error.util.ts similarity index 83% rename from packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-error.util.ts rename to packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-message-list-fetch-error.util.ts index ed52d8b11..655fa83cf 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-error.util.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-message-list-fetch-error.util.ts @@ -3,12 +3,17 @@ import { MessageImportDriverExceptionCode, } from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception'; -export const parseGmailError = (error: { +export const parseGmailMessageListFetchError = (error: { code?: number; - reason: string; - message: string; + errors: { + reason: string; + message: string; + }[]; }): MessageImportDriverException => { - const { code, reason, message } = error; + const { code, errors } = error; + + const reason = errors?.[0]?.reason; + const message = errors?.[0]?.message; switch (code) { case 400: @@ -45,19 +50,23 @@ export const parseGmailError = (error: { case 403: if ( reason === 'rateLimitExceeded' || - reason === 'userRateLimitExceeded' + reason === 'userRateLimitExceeded' || + reason === 'dailyLimitExceeded' ) { return new MessageImportDriverException( message, MessageImportDriverExceptionCode.TEMPORARY_ERROR, ); - } else { + } + if (reason === 'domainPolicy') { return new MessageImportDriverException( message, MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, ); } + break; + case 401: return new MessageImportDriverException( message, @@ -70,13 +79,10 @@ export const parseGmailError = (error: { message, MessageImportDriverExceptionCode.TEMPORARY_ERROR, ); - } else { - return new MessageImportDriverException( - message, - MessageImportDriverExceptionCode.UNKNOWN, - ); } + break; + default: break; } diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-messages-import-error.util.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-messages-import-error.util.ts new file mode 100644 index 000000000..a3118bf1b --- /dev/null +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-messages-import-error.util.ts @@ -0,0 +1,94 @@ +import { + MessageImportDriverException, + MessageImportDriverExceptionCode, +} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception'; + +export const parseGmailMessagesImportError = ( + error: { + code?: number; + errors: { + reason: string; + message: string; + }[]; + }, + messageExternalId: string, +): MessageImportDriverException | undefined => { + const { code, errors } = error; + + const reason = errors?.[0]?.reason; + const message = `${errors?.[0]?.message} for message with externalId: ${messageExternalId}`; + + switch (code) { + case 400: + if (reason === 'invalid_grant') { + return new MessageImportDriverException( + message, + MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, + ); + } + if (reason === 'failedPrecondition') { + return new MessageImportDriverException( + message, + MessageImportDriverExceptionCode.TEMPORARY_ERROR, + ); + } + + return new MessageImportDriverException( + message, + MessageImportDriverExceptionCode.UNKNOWN, + ); + + case 404: + case 410: + return undefined; + + case 429: + return new MessageImportDriverException( + message, + MessageImportDriverExceptionCode.TEMPORARY_ERROR, + ); + + case 403: + if ( + reason === 'rateLimitExceeded' || + reason === 'userRateLimitExceeded' || + reason === 'dailyLimitExceeded' + ) { + return new MessageImportDriverException( + message, + MessageImportDriverExceptionCode.TEMPORARY_ERROR, + ); + } + if (reason === 'domainPolicy') { + return new MessageImportDriverException( + message, + MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, + ); + } + + break; + + case 401: + return new MessageImportDriverException( + message, + MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, + ); + + case 500: + if (reason === 'backendError') { + return new MessageImportDriverException( + message, + MessageImportDriverExceptionCode.TEMPORARY_ERROR, + ); + } + break; + + default: + break; + } + + return new MessageImportDriverException( + message, + MessageImportDriverExceptionCode.UNKNOWN, + ); +};