diff --git a/packages/app-store/googlecalendar/lib/CalendarService.ts b/packages/app-store/googlecalendar/lib/CalendarService.ts index c4fc1a6a16..92e120f564 100644 --- a/packages/app-store/googlecalendar/lib/CalendarService.ts +++ b/packages/app-store/googlecalendar/lib/CalendarService.ts @@ -29,6 +29,22 @@ type FreeBusyArgs = { timeMin: string; timeMax: string; items: { id: string }[] const log = logger.getSubLogger({ prefix: ["app-store/googlecalendar/lib/CalendarService"] }); +/** + * Google system calendars that don't return proper free/busy information, + * making them useless for availability checking. These include: + * - Holiday calendars (e.g., "en.usa#holiday@group.v.calendar.google.com") + * - Birthdays/contacts calendar ("addressbook#contacts@group.v.calendar.google.com") + */ +const GOOGLE_SYSTEM_CALENDAR_SUFFIXES = [ + "#holiday@group.v.calendar.google.com", + "#contacts@group.v.calendar.google.com", +]; + +function isGoogleSystemCalendar(calendarId: string | null | undefined): boolean { + if (!calendarId) return false; + return GOOGLE_SYSTEM_CALENDAR_SUFFIXES.some((suffix) => calendarId.endsWith(suffix)); +} + /** * Extended interface for Google Calendar service that includes Google-specific methods. * This interface is used by internal Google Calendar modules (callback, tests, etc.) @@ -710,7 +726,11 @@ class GoogleCalendarService implements Calendar { ); if (!cals.items) return []; - return cals.items.map( + + // Filter out Google system calendars (holidays, birthdays) as they don't return proper free/busy information + const filteredCalendars = cals.items.filter((cal) => !isGoogleSystemCalendar(cal.id)); + + return filteredCalendars.map( (cal) => ({ externalId: cal.id ?? "No id", diff --git a/packages/app-store/googlecalendar/lib/__tests__/CalendarService.test.ts b/packages/app-store/googlecalendar/lib/__tests__/CalendarService.test.ts index 149c5e264c..d95d435a01 100644 --- a/packages/app-store/googlecalendar/lib/__tests__/CalendarService.test.ts +++ b/packages/app-store/googlecalendar/lib/__tests__/CalendarService.test.ts @@ -803,3 +803,240 @@ describe("createEvent", () => { log.info("createEvent with just in time reminders test passed"); }); }); + +describe("listCalendars", () => { + test("should filter out Google holiday calendars", async () => { + const calendarService = BuildCalendarService(mockCredential); + setFullMockOAuthManagerRequest(); + + calendarListMock.mockImplementation(() => { + return { + data: { + items: [ + { + id: "user@example.com", + summary: "Primary Calendar", + primary: true, + accessRole: "owner", + }, + { + id: "en.usa#holiday@group.v.calendar.google.com", + summary: "Holidays in United States", + primary: false, + accessRole: "reader", + }, + { + id: "en.christian#holiday@group.v.calendar.google.com", + summary: "Christian Holidays", + primary: false, + accessRole: "reader", + }, + ], + }, + }; + }); + + const calendars = await calendarService.listCalendars(); + + expect(calendars).toHaveLength(1); + expect(calendars[0].externalId).toBe("user@example.com"); + expect(calendars[0].name).toBe("Primary Calendar"); + }); + + test("should filter out Google birthdays/contacts calendar", async () => { + const calendarService = BuildCalendarService(mockCredential); + setFullMockOAuthManagerRequest(); + + calendarListMock.mockImplementation(() => { + return { + data: { + items: [ + { + id: "user@example.com", + summary: "Primary Calendar", + primary: true, + accessRole: "owner", + }, + { + id: "addressbook#contacts@group.v.calendar.google.com", + summary: "Birthdays", + primary: false, + accessRole: "reader", + }, + ], + }, + }; + }); + + const calendars = await calendarService.listCalendars(); + + expect(calendars).toHaveLength(1); + expect(calendars[0].externalId).toBe("user@example.com"); + }); + + test("should filter out all Google system calendars while keeping regular calendars", async () => { + const calendarService = BuildCalendarService(mockCredential); + setFullMockOAuthManagerRequest(); + + calendarListMock.mockImplementation(() => { + return { + data: { + items: [ + { + id: "user@example.com", + summary: "Primary Calendar", + primary: true, + accessRole: "owner", + }, + { + id: "work@example.com", + summary: "Work Calendar", + primary: false, + accessRole: "owner", + }, + { + id: "en.usa#holiday@group.v.calendar.google.com", + summary: "Holidays in United States", + primary: false, + accessRole: "reader", + }, + { + id: "addressbook#contacts@group.v.calendar.google.com", + summary: "Birthdays", + primary: false, + accessRole: "reader", + }, + { + id: "shared@example.com", + summary: "Shared Calendar", + primary: false, + accessRole: "writer", + }, + ], + }, + }; + }); + + const calendars = await calendarService.listCalendars(); + + expect(calendars).toHaveLength(3); + const calendarIds = calendars.map((cal) => cal.externalId); + expect(calendarIds).toContain("user@example.com"); + expect(calendarIds).toContain("work@example.com"); + expect(calendarIds).toContain("shared@example.com"); + expect(calendarIds).not.toContain("en.usa#holiday@group.v.calendar.google.com"); + expect(calendarIds).not.toContain("addressbook#contacts@group.v.calendar.google.com"); + }); + + test("should return empty array when all calendars are system calendars", async () => { + const calendarService = BuildCalendarService(mockCredential); + setFullMockOAuthManagerRequest(); + + calendarListMock.mockImplementation(() => { + return { + data: { + items: [ + { + id: "en.usa#holiday@group.v.calendar.google.com", + summary: "Holidays in United States", + primary: false, + accessRole: "reader", + }, + { + id: "addressbook#contacts@group.v.calendar.google.com", + summary: "Birthdays", + primary: false, + accessRole: "reader", + }, + ], + }, + }; + }); + + const calendars = await calendarService.listCalendars(); + + expect(calendars).toHaveLength(0); + }); + + test("should handle calendars with null or undefined ids", async () => { + const calendarService = BuildCalendarService(mockCredential); + setFullMockOAuthManagerRequest(); + + calendarListMock.mockImplementation(() => { + return { + data: { + items: [ + { + id: "user@example.com", + summary: "Primary Calendar", + primary: true, + accessRole: "owner", + }, + { + id: null, + summary: "Calendar with null id", + primary: false, + accessRole: "reader", + }, + { + id: undefined, + summary: "Calendar with undefined id", + primary: false, + accessRole: "reader", + }, + ], + }, + }; + }); + + const calendars = await calendarService.listCalendars(); + + expect(calendars).toHaveLength(3); + expect(calendars[0].externalId).toBe("user@example.com"); + expect(calendars[1].externalId).toBe("No id"); + expect(calendars[2].externalId).toBe("No id"); + }); + + test("should correctly identify various holiday calendar formats", async () => { + const calendarService = BuildCalendarService(mockCredential); + setFullMockOAuthManagerRequest(); + + calendarListMock.mockImplementation(() => { + return { + data: { + items: [ + { + id: "user@example.com", + summary: "Primary Calendar", + primary: true, + accessRole: "owner", + }, + { + id: "en.uk#holiday@group.v.calendar.google.com", + summary: "Holidays in United Kingdom", + primary: false, + accessRole: "reader", + }, + { + id: "de.german#holiday@group.v.calendar.google.com", + summary: "Holidays in Germany", + primary: false, + accessRole: "reader", + }, + { + id: "ja.japanese#holiday@group.v.calendar.google.com", + summary: "Holidays in Japan", + primary: false, + accessRole: "reader", + }, + ], + }, + }; + }); + + const calendars = await calendarService.listCalendars(); + + expect(calendars).toHaveLength(1); + expect(calendars[0].externalId).toBe("user@example.com"); + }); +});