diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/__tests__/parse-microsoft-calendar-error.util.spec.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/__tests__/parse-microsoft-calendar-error.util.spec.ts new file mode 100644 index 0000000000..8ebecd9f19 --- /dev/null +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/__tests__/parse-microsoft-calendar-error.util.spec.ts @@ -0,0 +1,88 @@ +import { type GraphError } from '@microsoft/microsoft-graph-client'; + +import { CalendarEventImportDriverExceptionCode } from 'src/modules/calendar/calendar-event-import-manager/drivers/exceptions/calendar-event-import-driver.exception'; +import { parseMicrosoftCalendarError } from 'src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/parse-microsoft-calendar-error.util'; + +const buildGraphError = ({ + statusCode, + code = null, + message = 'error message', +}: { + statusCode: number; + code?: string | null; + message?: string; +}) => ({ statusCode, code, message }) as GraphError; + +describe('parseMicrosoftCalendarError', () => { + it('should be temporary when the access token is expired so the next attempt can refresh it', () => { + const exception = parseMicrosoftCalendarError( + buildGraphError({ + statusCode: 401, + code: 'InvalidAuthenticationToken', + message: 'Lifetime validation failed, the token is expired.', + }), + ); + + expect(exception.code).toBe( + CalendarEventImportDriverExceptionCode.TEMPORARY_ERROR, + ); + }); + + it('should be insufficient permissions when access is denied', () => { + const exception = parseMicrosoftCalendarError( + buildGraphError({ statusCode: 403, code: 'ErrorAccessDenied' }), + ); + + expect(exception.code).toBe( + CalendarEventImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, + ); + }); + + it('should be not found for a 404 that is not a mailbox error', () => { + const exception = parseMicrosoftCalendarError( + buildGraphError({ statusCode: 404, code: 'ResourceNotFound' }), + ); + + expect(exception.code).toBe( + CalendarEventImportDriverExceptionCode.NOT_FOUND, + ); + }); + + it('should be a sync cursor error when the delta token is no longer valid', () => { + const exception = parseMicrosoftCalendarError( + buildGraphError({ statusCode: 410, code: 'SyncStateNotFound' }), + ); + + expect(exception.code).toBe( + CalendarEventImportDriverExceptionCode.SYNC_CURSOR_ERROR, + ); + }); + + it('should be temporary when throttled', () => { + const exception = parseMicrosoftCalendarError( + buildGraphError({ statusCode: 429, code: 'TooManyRequests' }), + ); + + expect(exception.code).toBe( + CalendarEventImportDriverExceptionCode.TEMPORARY_ERROR, + ); + }); + + it('should be temporary when the service is unavailable', () => { + const exception = parseMicrosoftCalendarError( + buildGraphError({ statusCode: 503 }), + ); + + expect(exception.code).toBe( + CalendarEventImportDriverExceptionCode.TEMPORARY_ERROR, + ); + }); + + it('should be unknown for an unhandled status code', () => { + const exception = parseMicrosoftCalendarError( + buildGraphError({ statusCode: 418 }), + ); + + expect(exception.code).toBe(CalendarEventImportDriverExceptionCode.UNKNOWN); + }); +}); diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/parse-microsoft-calendar-error.util.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/parse-microsoft-calendar-error.util.ts index 153b7a1676..e373fa6895 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/parse-microsoft-calendar-error.util.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/parse-microsoft-calendar-error.util.ts @@ -26,17 +26,6 @@ export const parseMicrosoftCalendarError = ( ); case 404: - if ( - message?.includes( - 'The mailbox is either inactive, soft-deleted, or is hosted on-premise.', - ) - ) { - return new CalendarEventImportDriverException( - message, - CalendarEventImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, - ); - } - return new CalendarEventImportDriverException( message, CalendarEventImportDriverExceptionCode.NOT_FOUND, @@ -68,7 +57,7 @@ export const parseMicrosoftCalendarError = ( case 401: return new CalendarEventImportDriverException( message, - CalendarEventImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, + CalendarEventImportDriverExceptionCode.TEMPORARY_ERROR, ); default: return new CalendarEventImportDriverException( diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/utils/__tests__/parse-microsoft-messages-import.util.spec.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/utils/__tests__/parse-microsoft-messages-import.util.spec.ts new file mode 100644 index 0000000000..2c334d0243 --- /dev/null +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/utils/__tests__/parse-microsoft-messages-import.util.spec.ts @@ -0,0 +1,118 @@ +import { MessageImportDriverExceptionCode } from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception'; +import { parseMicrosoftMessagesImportError } from 'src/modules/messaging/message-import-manager/drivers/microsoft/utils/parse-microsoft-messages-import.util'; + +describe('parseMicrosoftMessagesImportError', () => { + it('should be temporary when the access token is expired so the next attempt can refresh it', () => { + const exception = parseMicrosoftMessagesImportError({ + statusCode: 401, + code: 'InvalidAuthenticationToken', + message: 'Lifetime validation failed, the token is expired.', + }); + + expect(exception.code).toBe( + MessageImportDriverExceptionCode.TEMPORARY_ERROR, + ); + }); + + it('should keep the Microsoft error code and message so the failure stays diagnosable', () => { + const exception = parseMicrosoftMessagesImportError({ + statusCode: 401, + code: 'InvalidAuthenticationToken', + message: 'Lifetime validation failed, the token is expired.', + }); + + expect(exception.message).toContain('InvalidAuthenticationToken'); + expect(exception.message).toContain( + 'Lifetime validation failed, the token is expired.', + ); + }); + + it('should be insufficient permissions when access is denied', () => { + const exception = parseMicrosoftMessagesImportError({ + statusCode: 403, + code: 'ErrorAccessDenied', + message: 'Access is denied.', + }); + + expect(exception.code).toBe( + MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, + ); + expect(exception.message).toContain('ErrorAccessDenied'); + }); + + it('should be insufficient permissions when the mailbox is not enabled for the REST API', () => { + const exception = parseMicrosoftMessagesImportError({ + statusCode: 404, + code: 'MailboxNotEnabledForRESTAPI', + message: + 'The mailbox is either inactive, soft-deleted, or is hosted on-premise.', + }); + + expect(exception.code).toBe( + MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, + ); + }); + + it('should classify the mailbox error on its code rather than its message wording', () => { + const exception = parseMicrosoftMessagesImportError({ + statusCode: 404, + code: 'MailboxNotEnabledForRESTAPI', + message: 'Some reworded message from Microsoft.', + }); + + expect(exception.code).toBe( + MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, + ); + }); + + it('should be not found for a 404 that is not a mailbox error', () => { + const exception = parseMicrosoftMessagesImportError({ + statusCode: 404, + code: 'ResourceNotFound', + message: 'Resource not found.', + }); + + expect(exception.code).toBe(MessageImportDriverExceptionCode.NOT_FOUND); + }); + + it('should be a sync cursor error when the delta token is no longer valid', () => { + const exception = parseMicrosoftMessagesImportError({ + statusCode: 410, + code: 'SyncStateNotFound', + message: 'The sync state is not found.', + }); + + expect(exception.code).toBe( + MessageImportDriverExceptionCode.SYNC_CURSOR_ERROR, + ); + }); + + it('should be temporary when throttled', () => { + const exception = parseMicrosoftMessagesImportError({ + statusCode: 429, + code: 'TooManyRequests', + message: 'Please retry again later.', + }); + + expect(exception.code).toBe( + MessageImportDriverExceptionCode.TEMPORARY_ERROR, + ); + }); + + it('should be temporary when a 400 has an empty error body', () => { + const exception = parseMicrosoftMessagesImportError({ statusCode: 400 }); + + expect(exception.code).toBe( + MessageImportDriverExceptionCode.TEMPORARY_ERROR, + ); + }); + + it('should be unknown for an unhandled status code', () => { + const exception = parseMicrosoftMessagesImportError({ + statusCode: 418, + message: 'I am a teapot.', + }); + + expect(exception.code).toBe(MessageImportDriverExceptionCode.UNKNOWN); + }); +}); diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/utils/parse-microsoft-messages-import.util.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/utils/parse-microsoft-messages-import.util.ts index b207c0af43..3587c6f792 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/utils/parse-microsoft-messages-import.util.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/utils/parse-microsoft-messages-import.util.ts @@ -4,6 +4,9 @@ import { } from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception'; import { isDefined } from 'twenty-shared/utils'; +const MICROSOFT_MAILBOX_NOT_ENABLED_FOR_REST_API_ERROR_CODE = + 'MailboxNotEnabledForRESTAPI'; + export const parseMicrosoftMessagesImportError = ( error: { statusCode: number; @@ -30,26 +33,22 @@ export const parseMicrosoftMessagesImportError = ( if (error.statusCode === 401) { return new MessageImportDriverException( - 'Unauthorized access to Microsoft Graph API', - MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, + `Unauthorized access to Microsoft Graph API - code:${error.code} ${error.message}`, + MessageImportDriverExceptionCode.TEMPORARY_ERROR, { cause: options?.cause }, ); } if (error.statusCode === 403) { return new MessageImportDriverException( - 'Forbidden access to Microsoft Graph API', + `Forbidden access to Microsoft Graph API - code:${error.code} ${error.message}`, MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, { cause: options?.cause }, ); } if (error.statusCode === 404) { - if ( - error.message?.includes( - 'The mailbox is either inactive, soft-deleted, or is hosted on-premise.', - ) - ) { + if (error.code === MICROSOFT_MAILBOX_NOT_ENABLED_FOR_REST_API_ERROR_CODE) { return new MessageImportDriverException( `Disabled, deleted, inactive or no licence Microsoft account - code:${error.code}`, MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,