Files
calendar/packages/app-store/googlecalendar/lib/__tests__/CalendarService.test.ts
T

1314 lines
42 KiB
TypeScript

import oAuthManagerMock, {
defaultMockOAuthManager,
setFullMockOAuthManagerRequest,
} from "../../../tests/__mocks__/OAuthManager";
import "../__mocks__/features.repository";
import "../__mocks__/getGoogleAppKeys";
import {
adminMock,
calendarListMock,
calendarMock,
freebusyQueryMock,
setCredentialsMock,
setLastCreatedJWT,
setLastCreatedOAuth2Client,
} from "../__mocks__/googleapis";
import { beforeEach, describe, expect, test, vi } from "vitest";
import "vitest-fetch-mock";
import process from "node:process";
import { MeetLocationType } from "@calcom/app-store/constants";
import logger from "@calcom/lib/logger";
import type { CredentialForCalendarServiceWithEmail } from "@calcom/types/Credential";
import BuildCalendarService, { createGoogleCalendarServiceWithGoogleType } from "../CalendarService";
import { createCredentialForCalendarService, createMockJWTInstance } from "./utils";
const log = logger.getSubLogger({ prefix: ["CalendarService.test"] });
beforeEach(() => {
vi.clearAllMocks();
setCredentialsMock.mockClear();
oAuthManagerMock.OAuthManager = defaultMockOAuthManager;
calendarMock.calendar_v3.Calendar.mockClear();
adminMock.admin_directory_v1.Admin.mockClear();
setLastCreatedJWT(null);
setLastCreatedOAuth2Client(null);
createMockJWTInstance({});
});
const mockCredential: CredentialForCalendarServiceWithEmail = {
id: 1,
userId: 1,
appId: "google-calendar",
type: "google_calendar",
key: {
access_token: "<INVALID_TOKEN>",
},
user: {
email: "user@example.com",
},
delegationCredentialId: null,
delegatedTo: null,
invalid: false,
teamId: null,
encryptedKey: null,
};
describe("getAvailability", () => {
test("returns availability for selected calendars", async () => {
const calendarService = BuildCalendarService(mockCredential);
setFullMockOAuthManagerRequest();
const mockedBusyTimes1 = [
{
start: "2024-01-01",
end: "2024-01-02",
},
];
const mockedBusyTimes2 = [
{
start: "2024-01-03",
end: "2024-01-04",
},
];
const mockedBusyTimes = [mockedBusyTimes1, mockedBusyTimes2];
calendarListMock.mockImplementation(() => {
return {
data: {
items: [
{
id: "calendar1@test.com",
},
{
id: "calendar2@test.com",
},
],
},
};
});
// Mock Once so that the getAvailability call doesn't accidentally reuse this mock result
freebusyQueryMock.mockImplementation(({ requestBody }: { requestBody: any }) => {
const calendarsObject: any = {};
requestBody.items.forEach((item: any, index: number) => {
calendarsObject[item.id] = {
busy: mockedBusyTimes[index],
};
});
return {
data: {
calendars: calendarsObject,
},
};
});
const availabilityWithPrimaryAsFallback = await calendarService.getAvailability({
dateFrom: "2024-01-01",
dateTo: "2024-01-02",
selectedCalendars: [],
mode: "slots",
fallbackToPrimary: true,
});
expect(availabilityWithPrimaryAsFallback).toEqual(mockedBusyTimes1);
const availabilityWithAllCalendarsAsFallback = await calendarService.getAvailability({
dateFrom: "2024-01-01",
dateTo: "2024-01-02",
selectedCalendars: [],
mode: "slots",
fallbackToPrimary: false,
});
expect(availabilityWithAllCalendarsAsFallback).toEqual([...mockedBusyTimes1, ...mockedBusyTimes2]);
});
});
describe("getPrimaryCalendar", () => {
test("should fetch primary calendar using 'primary' keyword", async () => {
const calendarService = createGoogleCalendarServiceWithGoogleType(mockCredential);
setFullMockOAuthManagerRequest();
const mockPrimaryCalendar = {
id: "user@example.com",
summary: "Primary Calendar",
description: "Primary calendar for user@example.com",
timeZone: "America/New_York",
};
const calendarsGetMock = vi.fn().mockResolvedValue({
data: mockPrimaryCalendar,
});
calendarMock.calendar_v3.Calendar().calendars.get = calendarsGetMock;
const result = await calendarService.getPrimaryCalendar();
expect(calendarsGetMock).toHaveBeenCalledTimes(1);
expect(calendarsGetMock).toHaveBeenCalledWith({
calendarId: "primary",
});
expect(result).toEqual(mockPrimaryCalendar);
});
});
describe("Date Optimization Benchmarks", () => {
test("native Date calculations should be significantly faster than dayjs while producing identical results", async () => {
const dayjs = (await import("@calcom/dayjs")).default;
const testCases = [
{
dateFrom: "2024-01-01T00:00:00Z",
dateTo: "2024-01-31T00:00:00Z",
name: "30 days",
expectedDiff: 30,
},
{
dateFrom: "2024-01-01T00:00:00Z",
dateTo: "2024-03-31T00:00:00Z",
name: "90 days (API limit)",
expectedDiff: 90,
},
{
dateFrom: "2024-01-01T00:00:00Z",
dateTo: "2024-07-01T00:00:00Z",
name: "182 days (chunking required)",
expectedDiff: 182,
},
];
const iterations = 1000; // Reduced for test performance
for (const testCase of testCases) {
log.info(`Testing ${testCase.name}...`);
// Test correctness first
const dayjsDiff = dayjs(testCase.dateTo).diff(dayjs(testCase.dateFrom), "days");
const nativeDiff = Math.floor(
(new Date(testCase.dateTo).getTime() - new Date(testCase.dateFrom).getTime()) / (1000 * 60 * 60 * 24)
);
// Verify identical results
expect(nativeDiff).toBe(dayjsDiff);
expect(nativeDiff).toBe(testCase.expectedDiff);
// Performance test - dayjs approach
const dayjsStart = performance.now();
for (let i = 0; i < iterations; i++) {
const start = dayjs(testCase.dateFrom);
const end = dayjs(testCase.dateTo);
const diff = end.diff(start, "days");
}
const dayjsTime = performance.now() - dayjsStart;
// Performance test - native Date approach
const nativeStart = performance.now();
for (let i = 0; i < iterations; i++) {
const start = new Date(testCase.dateFrom);
const end = new Date(testCase.dateTo);
const diff = Math.floor((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24));
}
const nativeTime = performance.now() - nativeStart;
const speedupRatio = dayjsTime / nativeTime;
log.info(
`${testCase.name} - Dayjs: ${dayjsTime.toFixed(2)}ms, Native: ${nativeTime.toFixed(
2
)}ms, Speedup: ${speedupRatio.toFixed(1)}x`
);
if (!process.env.CI) {
const minSpeedup = 5; // Assert significant performance improvement (at least 5x faster)
expect(speedupRatio).toBeGreaterThan(minSpeedup);
}
}
});
test("chunking logic should produce identical results between dayjs and native Date implementations", async () => {
const dayjs = (await import("@calcom/dayjs")).default;
const testCases = [
{
dateFrom: "2024-01-01T00:00:00Z",
dateTo: "2024-04-01T00:00:00Z", // 91 days - requires chunking
name: "91 days (minimal chunking)",
},
{
dateFrom: "2024-01-01T00:00:00Z",
dateTo: "2024-07-01T00:00:00Z", // 182 days - multiple chunks
name: "182 days (multiple chunks)",
},
];
for (const testCase of testCases) {
const fromDate = new Date(testCase.dateFrom);
const toDate = new Date(testCase.dateTo);
const diff = Math.floor((toDate.getTime() - fromDate.getTime()) / (1000 * 60 * 60 * 24));
// Only test cases that require chunking (> 90 days)
if (diff <= 90) continue;
// OLD WAY (dayjs-based chunking)
const originalStartDate = dayjs(testCase.dateFrom);
const originalEndDate = dayjs(testCase.dateTo);
const loopsNumber = Math.ceil(diff / 90);
let startDate = originalStartDate;
let endDate = originalStartDate.add(90, "days");
const oldChunks = [];
for (let i = 0; i < loopsNumber; i++) {
if (endDate.isAfter(originalEndDate)) endDate = originalEndDate;
oldChunks.push({
start: startDate.toISOString(),
end: endDate.toISOString(),
});
startDate = endDate.add(1, "minutes");
endDate = startDate.add(90, "days");
}
// NEW WAY (native Date-based chunking)
let currentStartTime = fromDate.getTime();
const originalEndTime = toDate.getTime();
const ninetyDaysMs = 90 * 24 * 60 * 60 * 1000;
const oneMinuteMs = 60 * 1000;
const newChunks = [];
for (let i = 0; i < loopsNumber; i++) {
let currentEndTime = currentStartTime + ninetyDaysMs;
if (currentEndTime > originalEndTime) {
currentEndTime = originalEndTime;
}
newChunks.push({
start: new Date(currentStartTime).toISOString(),
end: new Date(currentEndTime).toISOString(),
});
currentStartTime = currentEndTime + oneMinuteMs;
}
// Verify identical chunking results
expect(newChunks).toHaveLength(oldChunks.length);
for (let i = 0; i < oldChunks.length; i++) {
expect(newChunks[i].start).toBe(oldChunks[i].start);
expect(newChunks[i].end).toBe(oldChunks[i].end);
}
log.info(`${testCase.name} - Generated ${newChunks.length} identical chunks`);
}
});
test("date parsing should be consistent between dayjs and native Date for all expected input formats", async () => {
const dayjs = (await import("@calcom/dayjs")).default;
// Test various date formats that Google Calendar API might return
const testDates = [
"2024-01-01T00:00:00Z", // UTC
"2024-01-01T12:30:45.123Z", // UTC with milliseconds
"2024-01-01T00:00:00-08:00", // Timezone offset
"2024-01-01T00:00:00+05:30", // Positive timezone offset
"2024-12-31T23:59:59Z", // End of year
"2024-02-29T12:00:00Z", // Leap year date
];
for (const dateString of testDates) {
const dayjsTime = dayjs(dateString).valueOf();
const nativeTime = new Date(dateString).getTime();
expect(nativeTime).toBe(dayjsTime);
// Also verify ISO string output consistency
const dayjsISO = dayjs(dateString).toISOString();
const nativeISO = new Date(dateString).toISOString();
expect(nativeISO).toBe(dayjsISO);
log.debug(`Date parsing verified: ${dateString} -> ${nativeTime}`);
}
});
test("fetchAvailabilityData should handle both single API call and chunked scenarios correctly", async () => {
const calendarService = BuildCalendarService(mockCredential);
setFullMockOAuthManagerRequest();
const mockBusyData = [
{ start: "2024-01-01T10:00:00Z", end: "2024-01-01T11:00:00Z" },
{ start: "2024-01-01T14:00:00Z", end: "2024-01-01T15:00:00Z" },
];
// Mock the getCacheOrFetchAvailability method to return consistent data
const getFreeBusyDataSpy = vi
.spyOn(calendarService as any, "getFreeBusyData")
.mockResolvedValue(mockBusyData.map((item) => ({ ...item, id: "test@calendar.com" })));
// Test single API call scenario (≤ 90 days)
const shortRangeResult = await (calendarService as any).fetchAvailabilityData(
["test@calendar.com"],
"2024-01-01T00:00:00Z",
"2024-01-31T00:00:00Z", // 30 days
false
);
expect(shortRangeResult).toEqual(mockBusyData);
expect(getFreeBusyDataSpy).toHaveBeenCalledTimes(1);
getFreeBusyDataSpy.mockClear();
// Test chunked scenario (> 90 days)
const longRangeResult = await (calendarService as any).fetchAvailabilityData(
["test@calendar.com"],
"2024-01-01T00:00:00Z",
"2024-07-01T00:00:00Z", // 182 days - should require chunking
false
);
// Should return concatenated results from multiple chunks
expect(longRangeResult.length).toBeGreaterThan(0);
expect(getFreeBusyDataSpy).toHaveBeenCalledTimes(3); // 182 days / 90 = ~2.02 -> 3 chunks
getFreeBusyDataSpy.mockRestore();
});
});
describe("createEvent", () => {
test("should create event with correct input/output format and handle all expected properties", async () => {
const calendarService = BuildCalendarService(mockCredential);
setFullMockOAuthManagerRequest();
// Mock Google Calendar API response
const mockGoogleEvent = {
id: "mock-event-id-123",
summary: "Test Meeting",
description: "Test meeting description",
start: {
dateTime: "2024-06-15T10:00:00Z",
timeZone: "UTC",
},
end: {
dateTime: "2024-06-15T11:00:00Z",
timeZone: "UTC",
},
attendees: [
{
email: "organizer@example.com",
displayName: "Test Organizer",
responseStatus: "accepted",
organizer: true,
},
{
email: "attendee@example.com",
displayName: "Test Attendee",
responseStatus: "accepted",
},
],
location: "Test Location",
iCalUID: "test-ical-uid@google.com",
recurrence: null,
};
// Mock calendar.events.insert
const eventsInsertMock = vi.fn().mockResolvedValue({
data: mockGoogleEvent,
});
calendarMock.calendar_v3.Calendar().events.insert = eventsInsertMock;
// Test input - simplified CalendarServiceEvent
const testCalEvent = {
type: "test-event-type",
uid: "cal-event-uid-123",
title: "Test Meeting",
startTime: "2024-06-15T10:00:00Z",
endTime: "2024-06-15T11:00:00Z",
organizer: {
id: 1,
name: "Test Organizer",
email: "organizer@example.com",
timeZone: "UTC",
language: {
translate: (...args: any[]) => args[0], // Mock translate function
locale: "en",
},
},
attendees: [
{
id: 2,
name: "Test Attendee",
email: "attendee@example.com",
timeZone: "UTC",
language: {
translate: (...args: any[]) => args[0], // Mock translate function
locale: "en",
},
},
],
location: "Test Location",
calendarDescription: "Test meeting description",
destinationCalendar: [
{
id: 1,
integration: "google_calendar",
externalId: "primary",
primaryEmail: null,
userId: mockCredential.userId,
eventTypeId: null,
credentialId: mockCredential.id,
delegationCredentialId: null,
domainWideDelegationCredentialId: null,
createdAt: new Date("2024-06-15T11:00:00Z"),
updatedAt: new Date("2024-06-15T11:00:00Z"),
customCalendarReminder: 10,
},
],
iCalUID: "test-ical-uid@google.com",
conferenceData: undefined,
hideCalendarEventDetails: false,
seatsPerTimeSlot: null,
seatsShowAttendees: true,
};
// Call createEvent and verify result using inline snapshot
const result = await calendarService.createEvent(testCalEvent, mockCredential.id);
// Verify input processing - check that Google API was called with correct payload
expect(eventsInsertMock).toHaveBeenCalledTimes(1);
const insertCall = eventsInsertMock.mock.calls[0][0];
// Use inline snapshot for input validation
expect(insertCall).toMatchInlineSnapshot(`
{
"calendarId": "primary",
"conferenceDataVersion": 1,
"requestBody": {
"attendees": [
{
"displayName": "Test Organizer",
"email": "primary",
"id": "1",
"language": {
"locale": "en",
"translate": [Function],
},
"name": "Test Organizer",
"organizer": true,
"responseStatus": "accepted",
"timeZone": "UTC",
},
{
"email": "attendee@example.com",
"language": {
"locale": "en",
"translate": [Function],
},
"name": "Test Attendee",
"responseStatus": "accepted",
"timeZone": "UTC",
},
],
"description": "Test meeting description",
"end": {
"dateTime": "2024-06-15T11:00:00Z",
"timeZone": "UTC",
},
"guestsCanSeeOtherGuests": true,
"iCalUID": "test-ical-uid@google.com",
"location": "Test Location",
"reminders": {
"useDefault": true,
},
"start": {
"dateTime": "2024-06-15T10:00:00Z",
"timeZone": "UTC",
},
"summary": "Test Meeting",
},
"sendUpdates": "none",
}
`);
// Use inline snapshot for output validation
expect(result).toMatchInlineSnapshot(`
{
"additionalInfo": {
"hangoutLink": "",
},
"attendees": [
{
"displayName": "Test Organizer",
"email": "organizer@example.com",
"organizer": true,
"responseStatus": "accepted",
},
{
"displayName": "Test Attendee",
"email": "attendee@example.com",
"responseStatus": "accepted",
},
],
"description": "Test meeting description",
"end": {
"dateTime": "2024-06-15T11:00:00Z",
"timeZone": "UTC",
},
"iCalUID": "test-ical-uid@google.com",
"id": "mock-event-id-123",
"location": "Test Location",
"password": "",
"recurrence": null,
"start": {
"dateTime": "2024-06-15T10:00:00Z",
"timeZone": "UTC",
},
"summary": "Test Meeting",
"thirdPartyRecurringEventId": null,
"type": "google_calendar",
"uid": "",
"url": "",
}
`);
log.info("createEvent test passed - input/output formats verified");
});
test("should handle recurring events correctly", async () => {
const calendarService = BuildCalendarService(mockCredential);
setFullMockOAuthManagerRequest();
// Mock recurring event response
const mockRecurringEvent = {
id: "recurring-event-id",
summary: "Weekly Meeting",
recurrence: ["RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=10"],
start: { dateTime: "2024-06-15T10:00:00Z", timeZone: "UTC" },
end: { dateTime: "2024-06-15T11:00:00Z", timeZone: "UTC" },
};
const mockFirstInstance = {
id: "recurring-event-id_20240615T100000Z",
summary: "Weekly Meeting",
start: { dateTime: "2024-06-15T10:00:00Z", timeZone: "UTC" },
end: { dateTime: "2024-06-15T11:00:00Z", timeZone: "UTC" },
};
calendarMock.calendar_v3.Calendar().events.insert = vi.fn().mockResolvedValue({
data: mockRecurringEvent,
});
calendarMock.calendar_v3.Calendar().events.instances = vi.fn().mockResolvedValue({
data: { items: [mockFirstInstance] },
});
const recurringCalEvent = {
type: "recurring-meeting",
title: "Weekly Meeting",
startTime: "2024-06-15T10:00:00Z",
endTime: "2024-06-15T11:00:00Z",
organizer: {
id: 1,
name: "Organizer",
email: "organizer@example.com",
timeZone: "UTC",
language: {
translate: (...args: any[]) => args[0], // Mock translate function
locale: "en",
},
},
attendees: [],
recurringEvent: {
freq: 2, // Weekly
interval: 1,
count: 10,
},
destinationCalendar: [
{
id: 1,
integration: "google_calendar",
externalId: "primary",
primaryEmail: null,
userId: mockCredential.userId,
eventTypeId: null,
credentialId: mockCredential.id,
delegationCredentialId: null,
domainWideDelegationCredentialId: null,
createdAt: new Date("2024-06-15T11:00:00Z"),
updatedAt: new Date("2024-06-15T11:00:00Z"),
customCalendarReminder: 10,
},
],
calendarDescription: "Weekly team meeting",
};
const result = await calendarService.createEvent(recurringCalEvent, mockCredential.id);
// Use inline snapshot for recurring event result
expect(result).toMatchInlineSnapshot(`
{
"additionalInfo": {
"hangoutLink": "",
},
"end": {
"dateTime": "2024-06-15T11:00:00Z",
"timeZone": "UTC",
},
"iCalUID": undefined,
"id": "recurring-event-id_20240615T100000Z",
"password": "",
"start": {
"dateTime": "2024-06-15T10:00:00Z",
"timeZone": "UTC",
},
"summary": "Weekly Meeting",
"thirdPartyRecurringEventId": "recurring-event-id",
"type": "google_calendar",
"uid": "",
"url": "",
}
`);
// Verify recurrence rule was included in the request
const insertCall = calendarMock.calendar_v3.Calendar().events.insert.mock.calls[0][0];
expect(insertCall.requestBody.recurrence).toEqual(["RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=10"]);
log.info("createEvent recurring event test passed");
});
test("should use default reminders when no custom reminder is configured", async () => {
const calendarService = BuildCalendarService(mockCredential);
setFullMockOAuthManagerRequest();
const mockGoogleEvent = {
id: "mock-event-default-reminder",
summary: "Test Meeting Default Reminder",
start: { dateTime: "2024-06-15T10:00:00Z", timeZone: "UTC" },
end: { dateTime: "2024-06-15T11:00:00Z", timeZone: "UTC" },
};
const eventsInsertMock = vi.fn().mockResolvedValue({
data: mockGoogleEvent,
});
calendarMock.calendar_v3.Calendar().events.insert = eventsInsertMock;
const testCalEvent = {
type: "test-event-type",
uid: "cal-event-uid-456",
title: "Test Meeting Default Reminder",
startTime: "2024-06-15T10:00:00Z",
endTime: "2024-06-15T11:00:00Z",
organizer: {
id: 1,
name: "Test Organizer",
email: "organizer@example.com",
timeZone: "UTC",
language: { translate: (...args: any[]) => args[0], locale: "en" },
},
attendees: [],
calendarDescription: "Test meeting description",
destinationCalendar: [
{
id: 1,
integration: "google_calendar",
externalId: "primary",
primaryEmail: null,
userId: mockCredential.userId,
eventTypeId: null,
credentialId: mockCredential.id,
delegationCredentialId: null,
domainWideDelegationCredentialId: null,
createdAt: new Date("2024-06-15T11:00:00Z"),
updatedAt: new Date("2024-06-15T11:00:00Z"),
customCalendarReminder: null,
},
],
};
// Mock the getReminderDuration method to return null (no custom reminder configured)
vi.spyOn(calendarService as any, "getReminderDuration").mockResolvedValue(null);
await calendarService.createEvent(testCalEvent, mockCredential.id);
const insertCall = eventsInsertMock.mock.calls[0][0];
// When no custom reminder is configured, should use Google Calendar's default
expect(insertCall.requestBody.reminders).toEqual({
useDefault: true,
});
log.info("createEvent with default reminders test passed");
});
test("should handle 'just in time' reminder (0 minutes) correctly", async () => {
const calendarService = BuildCalendarService(mockCredential);
setFullMockOAuthManagerRequest();
// Mock the getReminderDuration method to return 0 (just in time)
vi.spyOn(calendarService as any, "getReminderDuration").mockResolvedValue(0);
const mockGoogleEvent = {
id: "mock-event-just-in-time",
summary: "Test Meeting Just In Time",
start: { dateTime: "2024-06-15T10:00:00Z", timeZone: "UTC" },
end: { dateTime: "2024-06-15T11:00:00Z", timeZone: "UTC" },
};
const eventsInsertMock = vi.fn().mockResolvedValue({
data: mockGoogleEvent,
});
calendarMock.calendar_v3.Calendar().events.insert = eventsInsertMock;
const testCalEvent = {
type: "test-event-type",
uid: "cal-event-uid-789",
title: "Test Meeting Just In Time",
startTime: "2024-06-15T10:00:00Z",
endTime: "2024-06-15T11:00:00Z",
organizer: {
id: 1,
name: "Test Organizer",
email: "organizer@example.com",
timeZone: "UTC",
language: { translate: (...args: any[]) => args[0], locale: "en" },
},
attendees: [],
calendarDescription: "Test meeting description",
destinationCalendar: [
{
id: 1,
integration: "google_calendar",
externalId: "primary",
primaryEmail: null,
userId: mockCredential.userId,
eventTypeId: null,
credentialId: mockCredential.id,
delegationCredentialId: null,
domainWideDelegationCredentialId: null,
createdAt: new Date("2024-06-15T11:00:00Z"),
updatedAt: new Date("2024-06-15T11:00:00Z"),
customCalendarReminder: 0,
},
],
};
await calendarService.createEvent(testCalEvent, mockCredential.id);
const insertCall = eventsInsertMock.mock.calls[0][0];
// When "just in time" (0 minutes) is configured, should override with 0-minute reminders
expect(insertCall.requestBody.reminders).toEqual({
useDefault: false,
overrides: [
{ method: "popup", minutes: 0 },
{ method: "email", minutes: 0 },
],
});
log.info("createEvent with just in time reminders test passed");
});
test("should patch event location with hangoutLink when event is created with hangoutLink", async () => {
const calendarService = BuildCalendarService(mockCredential);
setFullMockOAuthManagerRequest();
const mockHangoutLink = "https://meet.google.com/abc-defg-hij";
const mockGoogleEvent = {
id: "mock-event-with-hangout",
summary: "Test Meeting with Hangout",
start: { dateTime: "2024-06-15T10:00:00Z", timeZone: "UTC" },
end: { dateTime: "2024-06-15T11:00:00Z", timeZone: "UTC" },
hangoutLink: mockHangoutLink,
};
const eventsInsertMock = vi.fn().mockResolvedValue({
data: mockGoogleEvent,
});
const eventsPatchMock = vi.fn().mockResolvedValue({
data: { ...mockGoogleEvent, location: mockHangoutLink },
});
calendarMock.calendar_v3.Calendar().events.insert = eventsInsertMock;
calendarMock.calendar_v3.Calendar().events.patch = eventsPatchMock;
const testCalEvent = {
type: "test-event-type",
uid: "cal-event-uid-hangout",
title: "Test Meeting with Hangout",
startTime: "2024-06-15T10:00:00Z",
endTime: "2024-06-15T11:00:00Z",
organizer: {
id: 1,
name: "Test Organizer",
email: "organizer@example.com",
timeZone: "UTC",
language: { translate: (...args: any[]) => args[0], locale: "en" },
},
attendees: [],
location: "Test Location",
calendarDescription: "Test meeting description",
destinationCalendar: [
{
id: 1,
integration: "google_calendar",
externalId: "primary",
primaryEmail: null,
userId: mockCredential.userId,
eventTypeId: null,
credentialId: mockCredential.id,
delegationCredentialId: null,
domainWideDelegationCredentialId: null,
createdAt: new Date("2024-06-15T11:00:00Z"),
updatedAt: new Date("2024-06-15T11:00:00Z"),
customCalendarReminder: null,
},
],
};
await calendarService.createEvent(testCalEvent, mockCredential.id);
// Verify that patch was called with hangoutLink in additionalInformation
expect(eventsPatchMock).toHaveBeenCalledTimes(1);
const patchCall = eventsPatchMock.mock.calls[0][0];
expect(patchCall.calendarId).toBe("primary");
expect(patchCall.eventId).toBe("mock-event-with-hangout");
expect(patchCall.requestBody.location).toBe(mockHangoutLink);
expect(patchCall.requestBody.description).toBeDefined();
log.info("createEvent with hangoutLink patch test passed");
});
});
describe("updateEvent", () => {
test("should patch event location with hangoutLink when event is updated with hangoutLink and MeetLocationType", async () => {
const calendarService = BuildCalendarService(mockCredential);
setFullMockOAuthManagerRequest();
const mockHangoutLink = "https://meet.google.com/xyz-uvwx-rst";
const mockUpdatedEvent = {
id: "existing-event-id",
summary: "Updated Meeting with Hangout",
start: { dateTime: "2024-06-15T10:00:00Z", timeZone: "UTC" },
end: { dateTime: "2024-06-15T11:00:00Z", timeZone: "UTC" },
hangoutLink: mockHangoutLink,
};
const eventsUpdateMock = vi.fn().mockResolvedValue({
data: mockUpdatedEvent,
});
const eventsPatchMock = vi.fn().mockResolvedValue({
data: { ...mockUpdatedEvent, location: mockHangoutLink },
});
calendarMock.calendar_v3.Calendar().events.update = eventsUpdateMock;
calendarMock.calendar_v3.Calendar().events.patch = eventsPatchMock;
const testCalEvent = {
type: "test-event-type",
uid: "existing-event-id",
title: "Updated Meeting with Hangout",
startTime: "2024-06-15T10:00:00Z",
endTime: "2024-06-15T11:00:00Z",
organizer: {
id: 1,
name: "Test Organizer",
email: "organizer@example.com",
timeZone: "UTC",
language: { translate: (...args: any[]) => args[0], locale: "en" },
},
attendees: [],
location: MeetLocationType,
calendarDescription: "Updated meeting description",
destinationCalendar: [
{
id: 1,
integration: "google_calendar",
externalId: "primary",
primaryEmail: null,
userId: mockCredential.userId,
eventTypeId: null,
credentialId: mockCredential.id,
delegationCredentialId: null,
domainWideDelegationCredentialId: null,
createdAt: new Date("2024-06-15T11:00:00Z"),
updatedAt: new Date("2024-06-15T11:00:00Z"),
customCalendarReminder: null,
},
],
};
const result = await calendarService.updateEvent("existing-event-id", testCalEvent, "primary");
// Verify that patch was called with hangoutLink in additionalInformation
expect(eventsPatchMock).toHaveBeenCalledTimes(1);
const patchCall = eventsPatchMock.mock.calls[0][0];
expect(patchCall.calendarId).toBe("primary");
expect(patchCall.eventId).toBe("existing-event-id");
expect(patchCall.requestBody.location).toBe(mockHangoutLink);
expect(patchCall.requestBody.description).toBeDefined();
// Verify result includes hangoutLink in additionalInfo
expect(result.additionalInfo?.hangoutLink).toBe(mockHangoutLink);
log.info("updateEvent with hangoutLink patch test passed");
});
test("should not patch event when hangoutLink is present but location is not MeetLocationType", async () => {
const calendarService = BuildCalendarService(mockCredential);
setFullMockOAuthManagerRequest();
const mockHangoutLink = "https://meet.google.com/xyz-uvwx-rst";
const mockUpdatedEvent = {
id: "existing-event-id",
summary: "Updated Meeting with Hangout",
start: { dateTime: "2024-06-15T10:00:00Z", timeZone: "UTC" },
end: { dateTime: "2024-06-15T11:00:00Z", timeZone: "UTC" },
hangoutLink: mockHangoutLink,
};
const eventsUpdateMock = vi.fn().mockResolvedValue({
data: mockUpdatedEvent,
});
const eventsPatchMock = vi.fn();
calendarMock.calendar_v3.Calendar().events.update = eventsUpdateMock;
calendarMock.calendar_v3.Calendar().events.patch = eventsPatchMock;
const testCalEvent = {
type: "test-event-type",
uid: "existing-event-id",
title: "Updated Meeting with Hangout",
startTime: "2024-06-15T10:00:00Z",
endTime: "2024-06-15T11:00:00Z",
organizer: {
id: 1,
name: "Test Organizer",
email: "organizer@example.com",
timeZone: "UTC",
language: { translate: (...args: any[]) => args[0], locale: "en" },
},
attendees: [],
location: "Some other location",
calendarDescription: "Updated meeting description",
destinationCalendar: [
{
id: 1,
integration: "google_calendar",
externalId: "primary",
primaryEmail: null,
userId: mockCredential.userId,
eventTypeId: null,
credentialId: mockCredential.id,
delegationCredentialId: null,
domainWideDelegationCredentialId: null,
createdAt: new Date("2024-06-15T11:00:00Z"),
updatedAt: new Date("2024-06-15T11:00:00Z"),
customCalendarReminder: null,
},
],
};
await calendarService.updateEvent("existing-event-id", testCalEvent, "primary");
// Verify that patch was NOT called when location is not MeetLocationType
expect(eventsPatchMock).not.toHaveBeenCalled();
log.info("updateEvent without MeetLocationType should not patch test passed");
});
test("should not patch event when hangoutLink is not present", async () => {
const calendarService = BuildCalendarService(mockCredential);
setFullMockOAuthManagerRequest();
const mockUpdatedEvent = {
id: "existing-event-id",
summary: "Updated Meeting without Hangout",
start: { dateTime: "2024-06-15T10:00:00Z", timeZone: "UTC" },
end: { dateTime: "2024-06-15T11:00:00Z", timeZone: "UTC" },
// No hangoutLink
};
const eventsUpdateMock = vi.fn().mockResolvedValue({
data: mockUpdatedEvent,
});
const eventsPatchMock = vi.fn();
calendarMock.calendar_v3.Calendar().events.update = eventsUpdateMock;
calendarMock.calendar_v3.Calendar().events.patch = eventsPatchMock;
const testCalEvent = {
type: "test-event-type",
uid: "existing-event-id",
title: "Updated Meeting without Hangout",
startTime: "2024-06-15T10:00:00Z",
endTime: "2024-06-15T11:00:00Z",
organizer: {
id: 1,
name: "Test Organizer",
email: "organizer@example.com",
timeZone: "UTC",
language: { translate: (...args: any[]) => args[0], locale: "en" },
},
attendees: [],
location: MeetLocationType,
calendarDescription: "Updated meeting description",
destinationCalendar: [
{
id: 1,
integration: "google_calendar",
externalId: "primary",
primaryEmail: null,
userId: mockCredential.userId,
eventTypeId: null,
credentialId: mockCredential.id,
delegationCredentialId: null,
domainWideDelegationCredentialId: null,
createdAt: new Date("2024-06-15T11:00:00Z"),
updatedAt: new Date("2024-06-15T11:00:00Z"),
customCalendarReminder: null,
},
],
};
await calendarService.updateEvent("existing-event-id", testCalEvent, "primary");
// Verify that patch was NOT called when hangoutLink is not present
expect(eventsPatchMock).not.toHaveBeenCalled();
log.info("updateEvent without hangoutLink should not patch 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");
});
});