Repository: SwitchbackTech/compass (ts)

Issue:
Not importing all events during resync / signup for user with lots of events
### Where did this happen?

Production app

### Expected Behavior

When importing events, all timed/allday events from a user's primary calendar should show up in Compass.

### Current Behavior

When importing events for user with lots of events (10k), no events are showing up in Compass.

Logs for user with medium amount of events, where events are rendering in Compass after import:

```
25-05-04 09:44:54 [debug] app:sync.service: Stopping all gcal event watches for user: another-user-id
25-05-04 09:44:57 [debug] app:sync.helpers: REMINDER: Channel will expire in 7200 minutes (5 days)
25-05-04 09:44:57 [info] app:sync.import: Starting importAllEvents for user another-user-id, calendar anotheruser@gmail.com@gmail.com.
25-05-04 09:44:57 [info] sync.import.all.gcal: Pass 1: Fetching events for anotheruser@gmail.com@gmail.com. Params: {"singleEvents":false}
25-05-04 09:44:58 [info] app:sync.service: Sync initialized for channelId: 4511d8e6-2fed-4cad-b448-481a0cf21dcb
200 POST /api/sync/gcal/notifications 1.409ms Sun, 04 May 2025 09:44:58 GMT
25-05-04 09:44:58 [info] sync.import.all.gcal: Pass 1 completed for anotheruser@gmail.com@gmail.com. Processed 542 API events. Saved 11 total events.
25-05-04 09:44:58 [info] sync.import.all.gcal: Pass 2: Fetching events for anotheruser@gmail.com@gmail.com. Params: {"singleEvents":true}
25-05-04 09:45:06 [info] sync.import.all.gcal: Pass 2: Reached last page. Got nextSyncToken: ...
25-05-04 09:45:06 [info] sync.import.all.gcal: Pass 2 completed for anotheruser@gmail.com@gmail.com. Processed 7125 API events. Saved 7125 total events.
25-05-04 09:45:06 [info] app:sync.import: importAllEvents completed for anotheruser@gmail.com@gmail.com. Total API events processed: 7667. Total base/single saved in Pass 1: 11, Total new instances/single saved in Pass 2: 7125. Total Saved/Changed: 7136. Final nextSyncToken acquired.
```



Logs for user with lots of events, where events aren't rendering in Compass after import.

```
25-05-04 09:44:04 [info] app:sync.import: Starting importAllEvents for user some-user-id, calendar lorem@ipsum.com.
25-05-04 09:44:04 [info] sync.import.all.gcal: Pass 1: Fetching events for lorem@ipsum.com. Params: {"singleEvents":false}
25-05-04 09:44:18 [info] sync.import.all.gcal: Pass 1 completed for lorem@ipsum.com. Processed 10333 API events. Saved 543 total events.
25-05-04 09:44:18 [info] sync.import.all.gcal: Pass 2: Fetching events for lorem@ipsum.com. Params: {"singleEvents":true}
200 POST /api/oauth/google 18610.943ms Sun, 04 May 2025 09:44:20 GMT
304 GET /api/auth/google 59.633ms Sun, 04 May 2025 09:44:20 GMT
304 GET /api/event?someday=true&start=2025-05-01&end=2025-05-31 13.633ms Sun, 04 May 2025 09:44:20 GMT
200 GET /api/event?start=2025-05-04T00:00:00-05:00&end=2025-05-10T23:59:59-05:00 20.028ms Sun, 04 May 2025 09:44:20 GMT
25-05-04 09:44:20 [debug] app:websocket.server: Connection made to: some-user-id
Map(1) { 'some-user-id' => '8-ULC1aqfK5fJTkrAAMM' }
304 GET /api/auth/google 55.744ms Sun, 04 May 2025 09:44:20 GMT
304 GET /api/event?someday=true&start=2025-05-01&end=2025-05-31 4.994ms Sun, 04 May 2025 09:44:20 GMT
304 GET /api/event?someday=true&start=2025-05-01&end=2025-05-31 9.647ms Sun, 04 May 2025 09:44:20 GMT
304 GET /api/auth/google 42.766ms Sun, 04 May 2025 09:44:20 GMT
```

> [!NOTE]
> Notice how the ` Total API events processed` line is missing in this one  


### Steps to Reproduce

Import events using a Compass account that is over a year old

### Possible Solution

Correct how legacy google events with their `extendedProperties` are mapped

### Context

_No response_

Pull request description:
🐛 Fix: map unsupported priority to UNASSIGNED
Closes #390 

In an earlier version of the app we used 'relations' instead of 'relationships'. When an older user tried using the app now, it'd fail validation because of the unknown value.

Now, if we find a priority that isn't part of the `Priorities` enum, we default to the `UNASSIGNED` value.

----

This pull request introduces error handling improvements, test restructuring, and refactoring of priority handling for Google Calendar event mapping. The most significant changes include adding error handling to the `fetchAndProcessEventsPageByPage` function, splitting tests into separate files for better organization, and centralizing priority handling logic into a new `getPriority` function.

### Error Handling Improvements:
* Added a `try-catch` block to the `fetchAndProcessEventsPageByPage` function to log and rethrow unhandled errors, improving the robustness of the event processing pipeline. (`packages/backend/src/sync/services/import/all/import.all.gcal.ts`) [[1]](diffhunk://#diff-76bfdcf9436cdee202ac3d09ce039f0c5926cc68c9046a92f4f825478c50e937R38) [[2]](diffhunk://#diff-76bfdcf9436cdee202ac3d09ce039f0c5926cc68c9046a92f4f825478c50e937R130-R135)

### Test Restructuring:
* Renamed `map.event.test.ts` to `map.event.to-compass.test.ts` and moved tests related to mapping events to Google Calendar (`toGcal`) into a new file, `map.event.to-gcal.test.ts`, improving test clarity and separation of concerns. (`packages/core/src/mappers/map.event.to-compass.test.ts`, `packages/core/src/mappers/map.event.to-gcal.test.ts`) [[1]](diffhunk://#diff-61b1898b5b22d884824a8de672ae1821aa475b409e1f3e9f79ad439e50c710e3R1-L100) [[2]](diffhunk://#diff-42a53eba5002934ff3e8744b324d73022d5d285b4f7aa765503695b2d2362cb8R1-R95)

### Refactoring Priority Handling:
* Introduced a new `getPriority` function to encapsulate the logic for determining event priority, including handling invalid or missing priorities by defaulting to `UNASSIGNED`. This simplifies the `_toCompass` function and improves maintainability. (`packages/core/src/mappers/map.event.ts`) [[1]](diffhunk://#diff-aebc1778ab2a59b70d0ad54476a6f05a9e2c6dd5829d832cfb29d1dcf3f61e5cL82-R82) [[2]](diffhunk://#diff-aebc1778ab2a59b70d0ad54476a6f05a9e2c6dd5829d832cfb29d1dcf3f61e5cR115-R136)

### Code Cleanup:
* Removed unused imports and adjusted import paths for consistency after test file restructuring. (`packages/core/src/mappers/map.event.to-compass.test.ts`, `packages/core/src/mappers/map.event.ts`) [[1]](diffhunk://#diff-aebc1778ab2a59b70d0ad54476a6f05a9e2c6dd5829d832cfb29d1dcf3f61e5cL2-R2) [[2]](diffhunk://#diff-61b1898b5b22d884824a8de672ae1821aa475b409e1f3e9f79ad439e50c710e3R1-L100)

These changes collectively enhance code quality, maintainability, and error resilience while improving the test structure for better readability and organization.


Patch:
diff --git a/packages/backend/src/sync/services/import/all/import.all.gcal.ts b/packages/backend/src/sync/services/import/all/import.all.gcal.ts
index 7ad4035c..2694925b 100644
--- a/packages/backend/src/sync/services/import/all/import.all.gcal.ts
+++ b/packages/backend/src/sync/services/import/all/import.all.gcal.ts
@@ -35,95 +35,102 @@ export const fetchAndProcessEventsPageByPage = async (
   nextSyncToken: string | undefined;
   baseEventMap: Map<string, ObjectId>;
 }> => {
-  let nextPageToken: string | undefined = undefined;
-  let finalNextSyncToken: string | undefined = undefined;
-  let totalProcessedApi = 0;
-  let totalSaved = 0;
-  const passIdentifier = gcalApiParams.singleEvents ? "Pass 2" : "Pass 1"; // For logging
-  let baseEventMap = sharedState.baseEventMap;
+  try {
+    let nextPageToken: string | undefined = undefined;
+    let finalNextSyncToken: string | undefined = undefined;
+    let totalProcessedApi = 0;
+    let totalSaved = 0;
+    const passIdentifier = gcalApiParams.singleEvents ? "Pass 2" : "Pass 1"; // For logging
+    let baseEventMap = sharedState.baseEventMap;
 
-  logger.info(
-    `${passIdentifier}: Fetching events for ${calendarId}. Params: ${JSON.stringify(gcalApiParams)}`,
-  );
+    logger.info(
+      `${passIdentifier}: Fetching events for ${calendarId}. Params: ${JSON.stringify(gcalApiParams)}`,
+    );
 
-  do {
-    const params: gParamsImportAllEvents = {
-      calendarId,
-      pageToken: nextPageToken,
-      maxResults: 250, // Consider making configurable
-      ...gcalApiParams, // Spread specific parameters like singleEvents
-    };
+    do {
+      const params: gParamsImportAllEvents = {
+        calendarId,
+        pageToken: nextPageToken,
+        maxResults: 250, // Consider making configurable
+        ...gcalApiParams, // Spread specific parameters like singleEvents
+      };
 
-    const gEventsResponse = await gcalService.getEvents(gcal, params);
+      const gEventsResponse = await gcalService.getEvents(gcal, params);
 
-    // Handle cases where the API call might fail or return unexpected structure
-    if (!gEventsResponse?.data) {
-      // Check for data property
-      logger.warn(
-        `${passIdentifier}: Invalid response structure or error fetching page for ${calendarId}. PageToken: ${nextPageToken}`,
-        gEventsResponse,
-      );
-      if (!nextPageToken && totalProcessedApi === 0) {
-        // Error on first fetch is critical
-        throw error(
-          EventError.NoGevents, // Or a more specific Gcal API error
-          `${passIdentifier}: Initial fetch failed or returned invalid data for ${calendarId}`,
+      // Handle cases where the API call might fail or return unexpected structure
+      if (!gEventsResponse?.data) {
+        // Check for data property
+        logger.warn(
+          `${passIdentifier}: Invalid response structure or error fetching page for ${calendarId}. PageToken: ${nextPageToken}`,
+          gEventsResponse,
         );
+        if (!nextPageToken && totalProcessedApi === 0) {
+          // Error on first fetch is critical
+          throw error(
+            EventError.NoGevents, // Or a more specific Gcal API error
+            `${passIdentifier}: Initial fetch failed or returned invalid data for ${calendarId}`,
+          );
+        }
+        break;
       }
-      break;
-    }
 
-    const gEvents = gEventsResponse.data.items || [];
-    totalProcessedApi += gEvents.length;
+      const gEvents = gEventsResponse.data.items || [];
+      totalProcessedApi += gEvents.length;
 
-    if (gEvents.length > 0) {
-      const eventsToSave: gSchema$Event[] = [];
-      gEvents.forEach((event) => {
-        // Call the provided processor function
-        const _shouldSave = shouldSave(event, sharedState);
-        if (_shouldSave) {
-          eventsToSave.push(event);
-        }
-      });
+      if (gEvents.length > 0) {
+        const eventsToSave: gSchema$Event[] = [];
+        gEvents.forEach((event) => {
+          // Call the provided processor function
+          const _shouldSave = shouldSave(event, sharedState);
+          if (_shouldSave) {
+            eventsToSave.push(event);
+          }
+        });
 
-      if (eventsToSave.length > 0) {
-        const cEvents = MapEvent.toCompass(
-          userId,
-          eventsToSave,
-          Origin.GOOGLE_IMPORT,
-        );
-        baseEventMap = assignIds(cEvents, baseEventMap);
-        if (cEvents.length > 0) {
-          const result = await eventService.createMany(cEvents, {
-            stripIds: false, // Preserve our generated ids in order to match the gcal ids
-          });
-          const savedCount =
-            result && typeof result.insertedCount === "number"
-              ? result.insertedCount
-              : 0;
-          totalSaved += savedCount;
+        if (eventsToSave.length > 0) {
+          const cEvents = MapEvent.toCompass(
+            userId,
+            eventsToSave,
+            Origin.GOOGLE_IMPORT,
+          );
+          baseEventMap = assignIds(cEvents, baseEventMap);
+          if (cEvents.length > 0) {
+            const result = await eventService.createMany(cEvents, {
+              stripIds: false, // Preserve our generated ids in order to match the gcal ids
+            });
+            const savedCount =
+              result && typeof result.insertedCount === "number"
+                ? result.insertedCount
+                : 0;
+            totalSaved += savedCount;
+          }
         }
       }
-    }
 
-    nextPageToken = gEventsResponse.data.nextPageToken ?? undefined;
-    // Capture nextSyncToken ONLY on the last page AND if requested
-    if (captureSyncToken && !nextPageToken) {
-      finalNextSyncToken = gEventsResponse.data.nextSyncToken ?? undefined;
-      logger.info(
-        `${passIdentifier}: Reached last page. Got nextSyncToken: ${finalNextSyncToken ? "..." : "null"}`,
-      );
-    }
-  } while (nextPageToken !== undefined);
+      nextPageToken = gEventsResponse.data.nextPageToken ?? undefined;
+      // Capture nextSyncToken ONLY on the last page AND if requested
+      if (captureSyncToken && !nextPageToken) {
+        finalNextSyncToken = gEventsResponse.data.nextSyncToken ?? undefined;
+        logger.info(
+          `${passIdentifier}: Reached last page. Got nextSyncToken: ${finalNextSyncToken ? "..." : "null"}`,
+        );
+      }
+    } while (nextPageToken !== undefined);
 
-  logger.info(
-    `${passIdentifier} completed for ${calendarId}. Processed ${totalProcessedApi} API events. Saved ${totalSaved} total events.`,
-  );
+    logger.info(
+      `${passIdentifier} completed for ${calendarId}. Processed ${totalProcessedApi} API events. Saved ${totalSaved} total events.`,
+    );
 
-  return {
-    savedCount: totalSaved,
-    processedCount: totalProcessedApi,
-    nextSyncToken: finalNextSyncToken, // Will be undefined if captureSyncToken was false
-    baseEventMap,
-  };
+    return {
+      savedCount: totalSaved,
+      processedCount: totalProcessedApi,
+      nextSyncToken: finalNextSyncToken, // Will be undefined if captureSyncToken was false
+      baseEventMap,
+    };
+  } catch (err) {
+    logger.error(
+      `Unhandled error in fetchAndProcessEventsPageByPage for ${calendarId}: ${err}`,
+    );
+    throw err;
+  }
 };
diff --git a/packages/core/src/mappers/map.event.ts b/packages/core/src/mappers/map.event.ts
index 1265164f..741ff826 100644
--- a/packages/core/src/mappers/map.event.ts
+++ b/packages/core/src/mappers/map.event.ts
@@ -1,5 +1,5 @@
 /* eslint-disable @typescript-eslint/no-namespace */
-import { Origin, Priorities, Priority } from "@core/constants/core.constants";
+import { Origin, Priorities } from "@core/constants/core.constants";
 import { BaseError } from "@core/errors/errors.base";
 import { Event_Core, Schema_Event } from "@core/types/event.types";
 import { gSchema$Event } from "@core/types/gcal";
@@ -79,11 +79,7 @@ const _toCompass = (
   const _start = gEvent.start == undefined ? placeHolder.start : gEvent.start;
   const _end = gEvent.end === undefined ? placeHolder.end : gEvent.end;
   const _isAllDay = gEvent.start !== undefined && "date" in gEvent.start;
-  const _origPriority = gEvent.extendedProperties?.private?.["priority"];
-  const _priority =
-    _origPriority === undefined
-      ? Priorities.UNASSIGNED
-      : (_origPriority as Priority);
+  const priority = getPriority(gEvent);
 
   const compassEvent: Schema_Event = {
     gEventId: gEventId,
@@ -98,7 +94,7 @@ const _toCompass = (
     startDate: _isAllDay ? _start.date : _start.dateTime,
     // @ts-ignore
     endDate: _isAllDay ? _end.date : _end.dateTime,
-    priority: _priority,
+    priority,
     updatedAt: new Date(),
   };
 
@@ -116,6 +112,28 @@ const _toCompass = (
   return validatedCompassEvent;
 };
 
+const getPriority = (gEvent: gSchema$Event): Priorities => {
+  const priorityExists =
+    gEvent.extendedProperties?.private?.["priority"] !== undefined &&
+    gEvent.extendedProperties?.private?.["priority"] !== null;
+  if (priorityExists) {
+    const priority = gEvent.extendedProperties?.private?.["priority"];
+    if (
+      priority &&
+      Object.values(Priorities).includes(priority as Priorities)
+    ) {
+      return priority as Priorities;
+    }
+    // Found a priority that doesn't match enum, set to unassigned
+    console.warn(
+      `Found a priority that doesn't match enum: ${priority}`,
+      gEvent,
+    );
+    return Priorities.UNASSIGNED;
+  }
+  return Priorities.UNASSIGNED;
+};
+
 const getRecurrence = (gEvent: gSchema$Event) => {
   const recurrenceExists =
     gEvent.recurrence !== undefined && gEvent.recurrence !== null;


Test patch:
diff --git a/packages/core/src/mappers/map.event.test.ts b/packages/core/src/mappers/map.event.to-compass.test.ts
similarity index 65%
rename from packages/core/src/mappers/map.event.test.ts
rename to packages/core/src/mappers/map.event.to-compass.test.ts
index 1c06423f..04160637 100644
--- a/packages/core/src/mappers/map.event.test.ts
+++ b/packages/core/src/mappers/map.event.to-compass.test.ts
@@ -1,103 +1,11 @@
+import { gcalEvents } from "@core/__mocks__/v1/events/gcal/gcal.event";
 import { recurring } from "@core/__mocks__/v1/events/gcal/gcal.recurring";
 import { timed } from "@core/__mocks__/v1/events/gcal/gcal.timed";
+import { Origin, Priorities } from "@core/constants/core.constants";
 import { Schema_Event } from "@core/types/event.types";
 import { gSchema$Event } from "@core/types/gcal";
-import { gcalEvents } from "../__mocks__/v1/events/gcal/gcal.event";
-import { Origin, Priorities } from "../constants/core.constants";
 import { MapEvent } from "./map.event";
 
-describe("toGcal", () => {
-  const validGcalDates = [
-    { start: "2022-01-01", end: "2022-01-02" },
-    { start: "2022-01-01T07:07:00-05:00", end: "2022-01-01T12:27:23+10:00" },
-  ];
-
-  const validateGcalDateFormat = (gEvent: gSchema$Event) => {
-    if (!gEvent.start || !gEvent.end) {
-      throw new Error("Event must have start and end times");
-    }
-
-    // ensures YYYY-MM-DD format
-    const _usesDashesCorrectly = (dateStr: string) => {
-      expect(dateStr[4]).toBe("-");
-      expect(dateStr[7]).toBe("-");
-    };
-
-    const _isAllDay = "date" in gEvent.start && "date" in gEvent.end;
-
-    if (_isAllDay && gEvent.start.date && gEvent.end.date) {
-      _usesDashesCorrectly(gEvent.start.date);
-      _usesDashesCorrectly(gEvent.end.date);
-    } else if (gEvent.start.dateTime && gEvent.end.dateTime) {
-      const yyyymmddStart = gEvent.start.dateTime.slice(0, 10);
-      const yyyymmddEnd = gEvent.end.dateTime.slice(0, 10);
-      _usesDashesCorrectly(yyyymmddStart);
-      _usesDashesCorrectly(yyyymmddEnd);
-
-      // confirms the expected offset/timezone indicator char is in the string
-      // for example, these both match: 2022-01-01T03:00:00-5:00, 2022-01-01T03:00:00+10:00
-      // the + or - is the 19th char in the str
-      const _hasTzOffset = (dateStr: string) =>
-        ["-", "+"].includes(dateStr[19]);
-      _hasTzOffset(gEvent.start.dateTime);
-      _hasTzOffset(gEvent.end.dateTime);
-    } else {
-      throw new Error("Event must have either date or dateTime");
-    }
-  };
-
-  it("returns dates in expected format", () => {
-    validGcalDates.forEach((dates) => {
-      const gcalEvt = MapEvent.toGcal({
-        startDate: dates.start,
-        endDate: dates.end,
-      });
-      validateGcalDateFormat(gcalEvt);
-    });
-  });
-
-  it("saves priority as private extended property", () => {
-    const gcalEvent = MapEvent.toGcal({
-      _id: "yupm",
-      user: "user1",
-      title: "Jan 1 2021",
-      isAllDay: true,
-      startDate: "2021-01-01",
-      endDate: "2021-01-02",
-      priority: Priorities.WORK,
-    });
-    expect(gcalEvent.extendedProperties?.private?.["priority"]).toBe(
-      Priorities.WORK,
-    );
-  });
-  it("sets priority to unassigned as private extended properties when none provided", () => {
-    const gcalEvent = MapEvent.toGcal({
-      _id: "yupm",
-      user: "user1",
-      title: "Jan 1 2021",
-      isAllDay: true,
-      startDate: "2021-01-01",
-      endDate: "2021-01-02",
-    });
-    expect(gcalEvent.extendedProperties?.private?.["priority"]).toBe(
-      Priorities.UNASSIGNED,
-    );
-  });
-  it("set origin to unsure as private extended properties when none provided", () => {
-    const gcalEvent = MapEvent.toGcal({
-      _id: "yupm",
-      user: "user1",
-      title: "Jan 1 2021",
-      isAllDay: true,
-      startDate: "2021-01-01",
-      endDate: "2021-01-02",
-    });
-    expect(gcalEvent.extendedProperties?.private?.["origin"]).toBe(
-      Origin.UNSURE,
-    );
-  });
-});
-
 describe("toCompass", () => {
   const eventsFromCompass = MapEvent.toCompass(
     "user1",
@@ -181,6 +89,27 @@ describe("toCompass", () => {
 
       expect(cEvent.priority).toBe("work");
     });
+
+    it("sets priority to unassigned if a priority exists but doesn't match enum", () => {
+      const gEvent = (gcalEvents.items as gSchema$Event[]).find(
+        (ge) => ge.summary === "Meeting with Stan",
+      );
+      if (!gEvent) {
+        throw new Error("Test event not found");
+      }
+      gEvent.extendedProperties = {
+        private: {
+          priority: "not-a-priority",
+        },
+      };
+
+      const cEvent = MapEvent.toCompass("user1", [gEvent], Origin.COMPASS)[0];
+      if (!cEvent) {
+        throw new Error("Failed to map event");
+      }
+
+      expect(cEvent.priority).toBe(Priorities.UNASSIGNED);
+    });
   });
 
   describe("recurrence", () => {
diff --git a/packages/core/src/mappers/map.event.to-gcal.test.ts b/packages/core/src/mappers/map.event.to-gcal.test.ts
new file mode 100644
index 00000000..057e922e
--- /dev/null
+++ b/packages/core/src/mappers/map.event.to-gcal.test.ts
@@ -0,0 +1,95 @@
+import { gSchema$Event } from "@core/types/gcal";
+import { Origin, Priorities } from "../constants/core.constants";
+import { MapEvent } from "./map.event";
+
+describe("toGcal", () => {
+  const validGcalDates = [
+    { start: "2022-01-01", end: "2022-01-02" },
+    { start: "2022-01-01T07:07:00-05:00", end: "2022-01-01T12:27:23+10:00" },
+  ];
+
+  const validateGcalDateFormat = (gEvent: gSchema$Event) => {
+    if (!gEvent.start || !gEvent.end) {
+      throw new Error("Event must have start and end times");
+    }
+
+    // ensures YYYY-MM-DD format
+    const _usesDashesCorrectly = (dateStr: string) => {
+      expect(dateStr[4]).toBe("-");
+      expect(dateStr[7]).toBe("-");
+    };
+
+    const _isAllDay = "date" in gEvent.start && "date" in gEvent.end;
+
+    if (_isAllDay && gEvent.start.date && gEvent.end.date) {
+      _usesDashesCorrectly(gEvent.start.date);
+      _usesDashesCorrectly(gEvent.end.date);
+    } else if (gEvent.start.dateTime && gEvent.end.dateTime) {
+      const yyyymmddStart = gEvent.start.dateTime.slice(0, 10);
+      const yyyymmddEnd = gEvent.end.dateTime.slice(0, 10);
+      _usesDashesCorrectly(yyyymmddStart);
+      _usesDashesCorrectly(yyyymmddEnd);
+
+      // confirms the expected offset/timezone indicator char is in the string
+      // for example, these both match: 2022-01-01T03:00:00-5:00, 2022-01-01T03:00:00+10:00
+      // the + or - is the 19th char in the str
+      const _hasTzOffset = (dateStr: string) =>
+        ["-", "+"].includes(dateStr[19]);
+      _hasTzOffset(gEvent.start.dateTime);
+      _hasTzOffset(gEvent.end.dateTime);
+    } else {
+      throw new Error("Event must have either date or dateTime");
+    }
+  };
+
+  it("returns dates in expected format", () => {
+    validGcalDates.forEach((dates) => {
+      const gcalEvt = MapEvent.toGcal({
+        startDate: dates.start,
+        endDate: dates.end,
+      });
+      validateGcalDateFormat(gcalEvt);
+    });
+  });
+
+  it("saves priority as private extended property", () => {
+    const gcalEvent = MapEvent.toGcal({
+      _id: "yupm",
+      user: "user1",
+      title: "Jan 1 2021",
+      isAllDay: true,
+      startDate: "2021-01-01",
+      endDate: "2021-01-02",
+      priority: Priorities.WORK,
+    });
+    expect(gcalEvent.extendedProperties?.private?.["priority"]).toBe(
+      Priorities.WORK,
+    );
+  });
+  it("sets priority to unassigned as private extended properties when none provided", () => {
+    const gcalEvent = MapEvent.toGcal({
+      _id: "yupm",
+      user: "user1",
+      title: "Jan 1 2021",
+      isAllDay: true,
+      startDate: "2021-01-01",
+      endDate: "2021-01-02",
+    });
+    expect(gcalEvent.extendedProperties?.private?.["priority"]).toBe(
+      Priorities.UNASSIGNED,
+    );
+  });
+  it("set origin to unsure as private extended properties when none provided", () => {
+    const gcalEvent = MapEvent.toGcal({
+      _id: "yupm",
+      user: "user1",
+      title: "Jan 1 2021",
+      isAllDay: true,
+      startDate: "2021-01-01",
+      endDate: "2021-01-02",
+    });
+    expect(gcalEvent.extendedProperties?.private?.["origin"]).toBe(
+      Origin.UNSURE,
+    );
+  });
+});