diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/__tests__/format-google-calendar-event.util.spec.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/__tests__/format-google-calendar-event.util.spec.ts new file mode 100644 index 000000000..7124f069d --- /dev/null +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/__tests__/format-google-calendar-event.util.spec.ts @@ -0,0 +1,99 @@ +import { calendar_v3 as calendarV3 } from 'googleapis'; + +import { formatGoogleCalendarEvents } from 'src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/format-google-calendar-event.util'; +import { CalendarEventParticipantResponseStatus } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity'; + +describe('formatGoogleCalendarEvents', () => { + const mockGoogleEvent: calendarV3.Schema$Event = { + id: 'event123', + summary: 'Team Meeting', + description: 'Weekly team sync', + location: 'Conference Room A', + status: 'confirmed', + created: '2023-01-01T10:00:00Z', + updated: '2023-01-02T10:00:00Z', + iCalUID: 'event123@google.com', + start: { + dateTime: '2023-01-15T14:00:00Z', + }, + end: { + dateTime: '2023-01-15T15:00:00Z', + }, + attendees: [ + { + email: 'organizer@example.com', + displayName: 'Meeting Organizer', + organizer: true, + responseStatus: 'accepted', + }, + { + email: 'attendee@example.com', + displayName: 'Test Attendee', + responseStatus: 'tentative', + }, + ], + conferenceData: { + conferenceSolution: { + key: { + type: 'hangoutsMeet', + }, + }, + entryPoints: [ + { + uri: 'https://meet.google.com/abc-defg-hij', + }, + ], + }, + }; + + it('should correctly format a normal Google Calendar event', () => { + const result = formatGoogleCalendarEvents([mockGoogleEvent]); + + expect(result).toHaveLength(1); + const formattedEvent = result[0]; + + expect(formattedEvent.title).toBe('Team Meeting'); + expect(formattedEvent.description).toBe('Weekly team sync'); + expect(formattedEvent.location).toBe('Conference Room A'); + expect(formattedEvent.isCanceled).toBe(false); + expect(formattedEvent.isFullDay).toBe(false); + expect(formattedEvent.startsAt).toBe('2023-01-15T14:00:00Z'); + expect(formattedEvent.endsAt).toBe('2023-01-15T15:00:00Z'); + expect(formattedEvent.externalId).toBe('event123'); + expect(formattedEvent.conferenceSolution).toBe('hangoutsMeet'); + expect(formattedEvent.conferenceLinkUrl).toBe( + 'https://meet.google.com/abc-defg-hij', + ); + + expect(formattedEvent.participants).toHaveLength(2); + expect(formattedEvent.participants[0].handle).toBe('organizer@example.com'); + expect(formattedEvent.participants[0].isOrganizer).toBe(true); + expect(formattedEvent.participants[0].responseStatus).toBe( + CalendarEventParticipantResponseStatus.ACCEPTED, + ); + expect(formattedEvent.participants[1].handle).toBe('attendee@example.com'); + expect(formattedEvent.participants[1].responseStatus).toBe( + CalendarEventParticipantResponseStatus.TENTATIVE, + ); + }); + + it('should sanitize a UCALID with improper exit char 0x00', () => { + const mockGoogleEventWithImproperUcalid: calendarV3.Schema$Event = { + ...mockGoogleEvent, + iCalUID: '\u0000eventStrange@google.com', + }; + + const mockGoogleEventWithImproperUcalid2: calendarV3.Schema$Event = { + ...mockGoogleEvent, + iCalUID: '>\u0000\u0015-;_�^�W&�p\u001f�', + }; + + const result = formatGoogleCalendarEvents([ + mockGoogleEventWithImproperUcalid, + mockGoogleEventWithImproperUcalid2, + ]); + + expect(result[0].iCalUID).toBe('eventStrange@google.com'); + expect(result[1].iCalUID).toBe('>\u0015-;_�^�W&�p\u001f�'); + }); +}); diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/format-google-calendar-event.util.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/format-google-calendar-event.util.ts index 06f97e3c5..eef52c0ca 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/format-google-calendar-event.util.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/format-google-calendar-event.util.ts @@ -1,5 +1,6 @@ import { calendar_v3 as calendarV3 } from 'googleapis'; +import { sanitizeCalendarEvent } from 'src/modules/calendar/calendar-event-import-manager/drivers/utils/sanitizeCalendarEvent'; import { CalendarEventParticipantResponseStatus } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity'; import { CalendarEventWithParticipants } from 'src/modules/calendar/common/types/calendar-event'; @@ -25,7 +26,8 @@ const formatGoogleCalendarEvent = ( } }; - return { + // Create the event object + const calendarEvent: CalendarEventWithParticipants = { title: event.summary ?? '', isCanceled: event.status === 'cancelled', isFullDay: event.start?.dateTime == null, @@ -51,4 +53,23 @@ const formatGoogleCalendarEvent = ( })) ?? [], status: event.status ?? '', }; + + const propertiesToSanitize: (keyof CalendarEventWithParticipants)[] = [ + 'title', + 'startsAt', + 'endsAt', + 'externalId', + 'externalCreatedAt', + 'externalUpdatedAt', + 'description', + 'location', + 'iCalUID', + 'conferenceSolution', + 'conferenceLinkLabel', + 'conferenceLinkUrl', + 'recurringEventExternalId', + 'status', + ]; + + return sanitizeCalendarEvent(calendarEvent, propertiesToSanitize); }; diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/__tests__/format-microsoft-calendar-event.util.spec.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/__tests__/format-microsoft-calendar-event.util.spec.ts new file mode 100644 index 000000000..fd48a90e3 --- /dev/null +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/__tests__/format-microsoft-calendar-event.util.spec.ts @@ -0,0 +1,106 @@ +import { Event } from '@microsoft/microsoft-graph-types'; + +import { formatMicrosoftCalendarEvents } from 'src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/format-microsoft-calendar-event.util'; +import { CalendarEventParticipantResponseStatus } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity'; + +describe('formatMicrosoftCalendarEvents', () => { + const mockMicrosoftEvent: Event = { + id: 'event123', + subject: 'Team Meeting', + body: { + content: 'Weekly team sync', + contentType: 'text', + }, + location: { + displayName: 'Conference Room A', + }, + isCancelled: false, + isAllDay: false, + createdDateTime: '2023-01-01T10:00:00Z', + lastModifiedDateTime: '2023-01-02T10:00:00Z', + iCalUId: 'event123@microsoft.com', + start: { + dateTime: '2023-01-15T14:00:00Z', + timeZone: 'UTC', + }, + end: { + dateTime: '2023-01-15T15:00:00Z', + timeZone: 'UTC', + }, + attendees: [ + { + emailAddress: { + address: 'organizer@example.com', + name: 'Meeting Organizer', + }, + status: { + response: 'organizer', + }, + }, + { + emailAddress: { + address: 'attendee@example.com', + name: 'Test Attendee', + }, + status: { + response: 'tentativelyAccepted', + }, + }, + ], + onlineMeetingProvider: 'teamsForBusiness', + onlineMeeting: { + joinUrl: 'https://teams.microsoft.com/l/meetup-join/abc123', + }, + }; + + it('should correctly format a normal Microsoft Calendar event', () => { + const result = formatMicrosoftCalendarEvents([mockMicrosoftEvent]); + + expect(result).toHaveLength(1); + const formattedEvent = result[0]; + + expect(formattedEvent.title).toBe('Team Meeting'); + expect(formattedEvent.description).toBe('Weekly team sync'); + expect(formattedEvent.location).toBe('Conference Room A'); + expect(formattedEvent.isCanceled).toBe(false); + expect(formattedEvent.isFullDay).toBe(false); + expect(formattedEvent.startsAt).toBe('2023-01-15T14:00:00Z'); + expect(formattedEvent.endsAt).toBe('2023-01-15T15:00:00Z'); + expect(formattedEvent.externalId).toBe('event123'); + expect(formattedEvent.conferenceSolution).toBe('teamsForBusiness'); + expect(formattedEvent.conferenceLinkUrl).toBe( + 'https://teams.microsoft.com/l/meetup-join/abc123', + ); + + expect(formattedEvent.participants).toHaveLength(2); + expect(formattedEvent.participants[0].handle).toBe('organizer@example.com'); + expect(formattedEvent.participants[0].isOrganizer).toBe(true); + expect(formattedEvent.participants[0].responseStatus).toBe( + CalendarEventParticipantResponseStatus.ACCEPTED, + ); + expect(formattedEvent.participants[1].handle).toBe('attendee@example.com'); + expect(formattedEvent.participants[1].responseStatus).toBe( + CalendarEventParticipantResponseStatus.TENTATIVE, + ); + }); + + it('should sanitize a Microsoft Calendar event with improper exit char 0x00', () => { + const mockMicrosoftEventWithImproperData: Event = { + ...mockMicrosoftEvent, + iCalUId: '\u0000eventStrange@microsoft.com', + }; + + const mockMicrosoftEventWithImproperData2: Event = { + ...mockMicrosoftEvent, + iCalUId: '>\u0000\u0015-;_�^�W&�p\u001f�', + }; + + const result = formatMicrosoftCalendarEvents([ + mockMicrosoftEventWithImproperData, + mockMicrosoftEventWithImproperData2, + ]); + + expect(result[0].iCalUID).toBe('eventStrange@microsoft.com'); + expect(result[1].iCalUID).toBe('>\u0015-;_�^�W&�p\u001f�'); + }); +}); diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/format-microsoft-calendar-event.util.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/format-microsoft-calendar-event.util.ts index a0085bff2..0beca6c24 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/format-microsoft-calendar-event.util.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/format-microsoft-calendar-event.util.ts @@ -4,6 +4,7 @@ import { ResponseType, } from '@microsoft/microsoft-graph-types'; +import { sanitizeCalendarEvent } from 'src/modules/calendar/calendar-event-import-manager/drivers/utils/sanitizeCalendarEvent'; import { CalendarEventParticipantResponseStatus } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity'; import { CalendarEventWithParticipants } from 'src/modules/calendar/common/types/calendar-event'; @@ -32,7 +33,7 @@ const formatMicrosoftCalendarEvent = ( } }; - return { + const calendarEvent: CalendarEventWithParticipants = { title: event.subject ?? '', isCanceled: !!event.isCancelled, isFullDay: !!event.isAllDay, @@ -57,4 +58,23 @@ const formatMicrosoftCalendarEvent = ( })) ?? [], status: '', }; + + const propertiesToSanitize: (keyof CalendarEventWithParticipants)[] = [ + 'title', + 'startsAt', + 'endsAt', + 'externalId', + 'externalCreatedAt', + 'externalUpdatedAt', + 'description', + 'location', + 'iCalUID', + 'conferenceSolution', + 'conferenceLinkLabel', + 'conferenceLinkUrl', + 'recurringEventExternalId', + 'status', + ]; + + return sanitizeCalendarEvent(calendarEvent, propertiesToSanitize); }; diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/utils/sanitizeCalendarEvent.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/utils/sanitizeCalendarEvent.ts new file mode 100644 index 000000000..29c076ae7 --- /dev/null +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/utils/sanitizeCalendarEvent.ts @@ -0,0 +1,26 @@ +import { isDefined } from 'twenty-shared/utils'; + +export const sanitizeCalendarEvent = >( + event: T, + propertiesToSanitize: (keyof T)[], +): T => { + const sanitizedEvent = { ...event }; + + for (const property of propertiesToSanitize) { + if (!isDefined(sanitizedEvent[property])) { + continue; + } + if (typeof sanitizedEvent[property] !== 'string') { + continue; + } + sanitizedEvent[property] = sanitizeString( + sanitizedEvent[property], + ) as T[typeof property]; + } + + return sanitizedEvent; +}; + +const sanitizeString = (value: string): string => { + return value.replace('\u0000', ''); +};