Skip to content

Commit ab96e06

Browse files
committed
🐛 fix(gcal-event-processor): update gcal sync processor
1 parent 667b719 commit ab96e06

6 files changed

Lines changed: 328 additions & 361 deletions

packages/backend/src/sync/services/sync/__tests__/gcal.sync.processor.delete.test.ts

Lines changed: 67 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1-
import { Categories_Recurrence, Schema_Event } from "@core/types/event.types";
1+
import { Categories_Recurrence } from "@core/types/event.types";
22
import { WithGcalId, gSchema$Event } from "@core/types/gcal";
3-
import {
4-
categorizeEvents,
5-
isExistingInstance,
6-
} from "@core/util/event/event.util";
3+
import { categorizeEvents } from "@core/util/event/event.util";
74
import { UtilDriver } from "@backend/__tests__/drivers/util.driver";
85
import {
96
getEventsInDb,
@@ -16,11 +13,8 @@ import {
1613
} from "@backend/__tests__/helpers/mock.db.setup";
1714
import { simulateDbAfterGcalImport } from "@backend/__tests__/helpers/mock.events.init";
1815
import { mockRecurringGcalBaseEvent } from "@backend/__tests__/mocks.gcal/factories/gcal.event.factory";
19-
import { RecurringEventRepository } from "@backend/event/services/recur/repo/recur.event.repo";
20-
import { createSeries } from "@backend/sync/services/sync/__tests__/gcal.sync.processor.delete.util";
2116
import { createCompassSeriesFromGcalBase } from "@backend/sync/services/sync/__tests__/gcal.sync.processor.test.util";
2217
import { GcalSyncProcessor } from "@backend/sync/services/sync/gcal.sync.processor";
23-
import { Change_Gcal } from "@backend/sync/sync.types";
2418

2519
describe("GcalSyncProcessor: DELETE", () => {
2620
beforeAll(setupTestDb);
@@ -32,7 +26,6 @@ describe("GcalSyncProcessor: DELETE", () => {
3226
it("should delete a STANDALONE event", async () => {
3327
/* Assemble */
3428
const { user } = await UtilDriver.setupTestUser();
35-
const repo = new RecurringEventRepository(user._id.toString());
3629

3730
const { gcalEvents } = await simulateDbAfterGcalImport(user._id.toString());
3831

@@ -47,14 +40,14 @@ describe("GcalSyncProcessor: DELETE", () => {
4740
status: "cancelled",
4841
} as WithGcalId<gSchema$Event>;
4942

50-
const processor = new GcalSyncProcessor(repo);
43+
const processor = new GcalSyncProcessor(user._id.toString());
5144
const changes = await processor.processEvents([cancelledGStandalone]);
5245

5346
/* Assert: Should return a DELETED change */
5447
expect(changes).toHaveLength(1);
5548
expect(changes[0]).toMatchObject({
5649
title: cancelledGStandalone.id,
57-
operation: "DELETED",
50+
operation: "STANDALONE_DELETED",
5851
});
5952

6053
// Verify the event is deleted from the DB
@@ -76,48 +69,66 @@ describe("GcalSyncProcessor: DELETE", () => {
7669
it("should delete an INSTANCE after cancelling it", async () => {
7770
/* Assemble */
7871
const { user } = await UtilDriver.setupTestUser();
79-
const repo = new RecurringEventRepository(user._id.toString());
80-
const { compassBaseId, gcalBaseId, meta } = await createSeries(user);
8172

82-
// Query the Compass DB for actual recurring instances
83-
const compassInstances = await getEventsInDb({
84-
gRecurringEventId: compassBaseId,
85-
});
73+
const { gcalEvents, compassEvents } = await simulateDbAfterGcalImport(
74+
user._id.toString(),
75+
);
8676

87-
const compassInstance = compassInstances[0];
77+
const compassInstance = compassEvents[0];
8878

8979
const cancelledGcalInstance = {
90-
kind: "calendar#event",
91-
id: compassInstance?.gEventId,
80+
...gcalEvents.instances[1],
9281
status: "cancelled",
93-
recurringEventId: gcalBaseId,
9482
originalStartTime: {
95-
date: compassInstance?.startDate?.slice(0, 10) || "2025-04-10",
83+
dateTime: compassInstance?.startDate || "2025-04-10",
9684
},
9785
};
9886

9987
/* Act */
100-
const processor = new GcalSyncProcessor(repo);
101-
const changes = await processor.processEvents([cancelledGcalInstance]);
88+
const processor = new GcalSyncProcessor(user._id.toString());
89+
90+
const changes = await processor.processEvents([
91+
gcalEvents.recurring,
92+
cancelledGcalInstance,
93+
]);
10294

10395
/* Assert */
104-
expect(changes).toHaveLength(1);
105-
const expected: Change_Gcal = {
106-
title: cancelledGcalInstance.id as string,
107-
category: Categories_Recurrence.RECURRENCE_INSTANCE,
108-
operation: "DELETED",
109-
};
110-
expect(changes[0]).toEqual(expected);
96+
expect(changes).toHaveLength(3);
97+
98+
expect(changes).toEqual(
99+
expect.arrayContaining([
100+
expect.objectContaining({
101+
title: gcalEvents.recurring.summary,
102+
category: Categories_Recurrence.RECURRENCE_BASE,
103+
operation: "RECURRENCE_BASE_UPDATED",
104+
}),
105+
expect.objectContaining({
106+
title: gcalEvents.recurring.summary,
107+
category: Categories_Recurrence.RECURRENCE_BASE,
108+
operation: "TIMED_INSTANCES_UPDATED",
109+
}),
110+
expect.objectContaining({
111+
title: cancelledGcalInstance.summary as string,
112+
category: Categories_Recurrence.RECURRENCE_INSTANCE,
113+
operation: "RECURRENCE_INSTANCE_DELETED",
114+
}),
115+
]),
116+
);
111117

112118
const remainingEvents = await getEventsInDb({ user: user._id.toString() });
113119

114120
// Verify only the instance was deleted
115-
expect(remainingEvents).toHaveLength(meta.createdCount - 1);
116-
expect(remainingEvents[0]?._id?.toString()).toBe(compassBaseId);
121+
expect(remainingEvents).toHaveLength(compassEvents.length - 1);
122+
expect(remainingEvents[0]?._id?.toString()).toBe(
123+
compassEvents[0]?._id.toString(),
124+
);
117125

118126
expect(
119-
isExistingInstance(remainingEvents[1] as unknown as Schema_Event),
120-
).toBe(true);
127+
remainingEvents.find(
128+
({ gRecurringEventId }) =>
129+
gRecurringEventId === cancelledGcalInstance.id,
130+
),
131+
).toBeUndefined();
121132
});
122133

123134
it("should handle a mixed payload of multiple INSTANCE DELETIONS and one BASE UPSERT", async () => {
@@ -129,36 +140,37 @@ describe("GcalSyncProcessor: DELETE", () => {
129140
});
130141

131142
const { user } = await UtilDriver.setupTestUser();
132-
const repo = new RecurringEventRepository(user._id.toString());
133143

134144
const { state } = await createCompassSeriesFromGcalBase(
135145
user,
136146
gcalBaseEvent,
137147
);
138148

139149
// Create cancellation payloads for each instance
140-
const cancellations = state.instances.map((i) => ({
150+
const cancellations: gSchema$Event[] = state.instances.map((i) => ({
141151
kind: "calendar#event",
142152
id: i.gEventId,
153+
start: { dateTime: i.startDate },
154+
recurringEventId: gcalBaseEvent.id,
143155
status: "cancelled",
144156
}));
145157

146158
/* Act */
147-
const processor = new GcalSyncProcessor(repo);
159+
const processor = new GcalSyncProcessor(user._id.toString());
148160
const changes = await processor.processEvents([
149161
gcalBaseEvent,
150162
...cancellations,
151163
]);
152164

153165
/* Assert */
154166
// Validate all changes detected
155-
expect(changes).toHaveLength(3);
167+
expect(changes).toHaveLength(4);
156168

157169
// Validate change types
158-
const deletions = changes.filter((c) => c.operation === "DELETED");
159-
const upserts = changes.filter((c) => c.operation === "UPSERTED");
160-
expect(deletions).toHaveLength(2);
161-
expect(upserts).toHaveLength(1);
170+
const deletions = changes.filter((c) => c.operation?.includes("DELETED"));
171+
const upserts = changes.filter((c) => c.operation?.includes("UPDATED"));
172+
expect(deletions).toHaveLength(cancellations.length);
173+
expect(upserts).toHaveLength(cancellations.length);
162174

163175
// Validate DB state
164176
const remainingEvents = await getEventsInDb({
@@ -180,7 +192,6 @@ describe("GcalSyncProcessor: DELETE", () => {
180192

181193
it("should delete BASE and all INSTANCES after cancelling a BASE", async () => {
182194
const { user } = await UtilDriver.setupTestUser();
183-
const repo = new RecurringEventRepository(user._id.toString());
184195

185196
const gcalBaseEvent = mockRecurringGcalBaseEvent({
186197
recurrence: ["RRULE:FREQ=DAILY"],
@@ -190,21 +201,24 @@ describe("GcalSyncProcessor: DELETE", () => {
190201

191202
// Cancel the entire series
192203
const cancelledBase = {
193-
kind: "calendar#event",
194-
id: gcalBaseEvent.id,
204+
...gcalBaseEvent,
195205
status: "cancelled",
196206
};
197207

198-
const processor = new GcalSyncProcessor(repo);
208+
const processor = new GcalSyncProcessor(user._id.toString());
199209
const changes = await processor.processEvents([cancelledBase]);
200210

201211
expect(changes).toHaveLength(1);
202-
const expected: Change_Gcal = {
203-
title: cancelledBase.id as string,
204-
category: Categories_Recurrence.RECURRENCE_BASE,
205-
operation: "DELETED",
206-
};
207-
expect(changes[0]).toEqual(expected);
212+
213+
expect(changes).toEqual(
214+
expect.arrayContaining([
215+
expect.objectContaining({
216+
title: cancelledBase.summary as string,
217+
category: Categories_Recurrence.RECURRENCE_BASE,
218+
operation: "SERIES_DELETED",
219+
}),
220+
]),
221+
);
208222

209223
// Verify all Compass events that match the gcal base were deleted
210224
const isEmpty = await isEventCollectionEmpty();

packages/backend/src/sync/services/sync/__tests__/gcal.sync.processor.upsert.base.split.test.ts

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import {
88
setupTestDb,
99
} from "@backend/__tests__/helpers/mock.db.setup";
1010
import { simulateDbAfterGcalImport } from "@backend/__tests__/helpers/mock.events.init";
11-
import { RecurringEventRepository } from "@backend/event/services/recur/repo/recur.event.repo";
1211
import { GcalSyncProcessor } from "@backend/sync/services/sync/gcal.sync.processor";
12+
import { gSchema$Event } from "../../../../../../core/src/types/gcal";
1313
import {
1414
baseHasRecurrenceRule,
1515
noInstancesAfterSplitDate,
@@ -28,7 +28,6 @@ describe("GcalSyncProcessor: UPSERT: BASE SPLIT", () => {
2828
it("should handle new UNTIL in BASE by updating BASE rule and DELETING instances after the UNTIL date", async () => {
2929
/* Assemble */
3030
const { user } = await UtilDriver.setupTestUser();
31-
const repo = new RecurringEventRepository(user._id.toString());
3231

3332
const { gcalEvents } = await simulateDbAfterGcalImport(user._id.toString());
3433

@@ -37,17 +36,34 @@ describe("GcalSyncProcessor: UPSERT: BASE SPLIT", () => {
3736

3837
/* Act */
3938
const origEvents = await getEventsInDb({ user: user._id.toString() });
40-
const processor = new GcalSyncProcessor(repo);
39+
const processor = new GcalSyncProcessor(user._id.toString());
4140
const changes = await processor.processEvents([gBaseWithUntil]);
4241

4342
/* Assert */
4443
// Verify the base was updated
45-
expect(changes).toHaveLength(1);
46-
expect(changes[0]).toEqual({
47-
title: gBaseWithUntil.summary as string,
48-
category: Categories_Recurrence.RECURRENCE_BASE,
49-
operation: "UPSERTED",
50-
});
44+
expect(changes).toHaveLength(3);
45+
expect(changes).toEqual(
46+
expect.arrayContaining([
47+
{
48+
title: gBaseWithUntil.summary as string,
49+
category: Categories_Recurrence.RECURRENCE_BASE,
50+
operation: "SERIES_AFTER_UNTIL_DELETED",
51+
transition: ["RECURRENCE_BASE", "RECURRENCE_BASE_CONFIRMED"],
52+
},
53+
{
54+
title: gBaseWithUntil.summary as string,
55+
category: Categories_Recurrence.RECURRENCE_BASE,
56+
operation: "RECURRENCE_BASE_UPDATED",
57+
transition: ["RECURRENCE_BASE", "RECURRENCE_BASE_CONFIRMED"],
58+
},
59+
{
60+
title: gBaseWithUntil.summary as string,
61+
category: Categories_Recurrence.RECURRENCE_BASE,
62+
operation: "TIMED_INSTANCES_UPDATED",
63+
transition: ["RECURRENCE_BASE", "RECURRENCE_BASE_CONFIRMED"],
64+
},
65+
]),
66+
);
5167

5268
// Verify DB changed
5369
const remainingEvents = await getEventsInDb({
@@ -74,19 +90,19 @@ describe("GcalSyncProcessor: UPSERT: BASE SPLIT", () => {
7490
it("should handle cancelled instance at split point by deleting it", async () => {
7591
/* Assemble */
7692
const { user } = await UtilDriver.setupTestUser();
77-
const repo = new RecurringEventRepository(user._id.toString());
7893

7994
const { gcalEvents } = await simulateDbAfterGcalImport(user._id.toString());
8095

8196
const origEvents = await getEventsInDb({ user: user._id.toString() });
8297

8398
/* Act */
8499
// Simulate a gcal notification payload after an instance was cancelled
85-
const cancelledInstance = Object.assign(gcalEvents.instances[1]!, {
100+
const cancelledInstance: gSchema$Event = {
101+
...gcalEvents.instances[1],
86102
status: "cancelled",
87-
});
103+
};
88104

89-
const processor = new GcalSyncProcessor(repo);
105+
const processor = new GcalSyncProcessor(user._id.toString());
90106
const changes = await processor.processEvents([cancelledInstance]);
91107

92108
/* Assert */
@@ -95,7 +111,8 @@ describe("GcalSyncProcessor: UPSERT: BASE SPLIT", () => {
95111
expect(changes[0]).toEqual({
96112
title: cancelledInstance.summary,
97113
category: Categories_Recurrence.RECURRENCE_INSTANCE,
98-
operation: "DELETED",
114+
operation: "RECURRENCE_INSTANCE_DELETED",
115+
transition: ["RECURRENCE_INSTANCE", "RECURRENCE_INSTANCE_CANCELLED"],
99116
});
100117

101118
// Verify database state

0 commit comments

Comments
 (0)