Skip to main content
Glama

Google Calendar MCP

UpdateEventHandler.test.ts34.7 kB
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { UpdateEventHandler } from '../../../handlers/core/UpdateEventHandler.js'; import { OAuth2Client } from 'google-auth-library'; import type { UpdateEventInput } from '../../../tools/registry.js'; import type { RecurringEventHelpers } from '../../../handlers/core/RecurringEventHelpers.js'; // Mock the googleapis module vi.mock('googleapis', () => ({ google: { calendar: vi.fn(() => ({ events: { patch: vi.fn(), get: vi.fn() }, calendars: { get: vi.fn() } })) }, calendar_v3: {} })); // Import createTimeObject for proper datetime handling in mocks import { createTimeObject } from '../../../handlers/utils/datetime.js'; // Mock RecurringEventHelpers vi.mock('../../../handlers/core/RecurringEventHelpers.js', () => ({ RecurringEventHelpers: vi.fn().mockImplementation((calendar) => ({ detectEventType: vi.fn().mockResolvedValue('single'), getCalendar: vi.fn(() => calendar), buildUpdateRequestBody: vi.fn((args, defaultTimeZone) => { const body: any = {}; if (args.summary !== undefined && args.summary !== null) body.summary = args.summary; if (args.description !== undefined && args.description !== null) body.description = args.description; if (args.location !== undefined && args.location !== null) body.location = args.location; const tz = args.timeZone || defaultTimeZone; // Use createTimeObject to handle both timed and all-day events if (args.start !== undefined && args.start !== null) { const timeObj = createTimeObject(args.start, tz); // When converting formats, explicitly nullify the opposite field if (timeObj.date !== undefined) { body.start = { date: timeObj.date, dateTime: null }; } else { body.start = { dateTime: timeObj.dateTime, timeZone: timeObj.timeZone, date: null }; } } if (args.end !== undefined && args.end !== null) { const timeObj = createTimeObject(args.end, tz); // When converting formats, explicitly nullify the opposite field if (timeObj.date !== undefined) { body.end = { date: timeObj.date, dateTime: null }; } else { body.end = { dateTime: timeObj.dateTime, timeZone: timeObj.timeZone, date: null }; } } if (args.attendees !== undefined && args.attendees !== null) body.attendees = args.attendees; if (args.colorId !== undefined && args.colorId !== null) body.colorId = args.colorId; if (args.reminders !== undefined && args.reminders !== null) body.reminders = args.reminders; if (args.conferenceData !== undefined && args.conferenceData !== null) body.conferenceData = args.conferenceData; if (args.transparency !== undefined && args.transparency !== null) body.transparency = args.transparency; if (args.visibility !== undefined && args.visibility !== null) body.visibility = args.visibility; if (args.guestsCanInviteOthers !== undefined) body.guestsCanInviteOthers = args.guestsCanInviteOthers; if (args.guestsCanModify !== undefined) body.guestsCanModify = args.guestsCanModify; if (args.guestsCanSeeOtherGuests !== undefined) body.guestsCanSeeOtherGuests = args.guestsCanSeeOtherGuests; if (args.anyoneCanAddSelf !== undefined) body.anyoneCanAddSelf = args.anyoneCanAddSelf; if (args.extendedProperties !== undefined && args.extendedProperties !== null) body.extendedProperties = args.extendedProperties; if (args.attachments !== undefined && args.attachments !== null) body.attachments = args.attachments; return body; }) })), RecurringEventError: class extends Error { code: string; constructor(message: string, code: string) { super(message); this.code = code; } }, RECURRING_EVENT_ERRORS: { NON_RECURRING_SCOPE: 'NON_RECURRING_SCOPE' } })); describe('UpdateEventHandler', () => { let handler: UpdateEventHandler; let mockOAuth2Client: OAuth2Client; let mockCalendar: any; beforeEach(() => { handler = new UpdateEventHandler(); mockOAuth2Client = new OAuth2Client(); // Setup mock calendar mockCalendar = { events: { patch: vi.fn(), get: vi.fn(), insert: vi.fn() }, calendars: { get: vi.fn() } }; // Mock the getCalendar method vi.spyOn(handler as any, 'getCalendar').mockReturnValue(mockCalendar); // Mock getCalendarTimezone vi.spyOn(handler as any, 'getCalendarTimezone').mockResolvedValue('America/Los_Angeles'); }); describe('Basic Event Updates', () => { it('should update event summary', async () => { const mockUpdatedEvent = { id: 'event123', summary: 'Updated Meeting', start: { dateTime: '2025-01-15T10:00:00Z' }, end: { dateTime: '2025-01-15T11:00:00Z' }, htmlLink: 'https://calendar.google.com/event?eid=abc123' }; mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } }); mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedEvent }); const args = { calendarId: 'primary', eventId: 'event123', summary: 'Updated Meeting' }; const result = await handler.runTool(args, mockOAuth2Client); expect(mockCalendar.events.patch).toHaveBeenCalledWith({ calendarId: 'primary', eventId: 'event123', requestBody: expect.objectContaining({ summary: 'Updated Meeting' }) }); expect(result.content[0].type).toBe('text'); const response = JSON.parse((result.content[0] as any).text); expect(response.event).toBeDefined(); expect(response.event.summary).toBe('Updated Meeting'); }); it('should update event description and location', async () => { const mockUpdatedEvent = { id: 'event123', summary: 'Meeting', description: 'New description', location: 'Conference Room B', start: { dateTime: '2025-01-15T10:00:00Z' }, end: { dateTime: '2025-01-15T11:00:00Z' } }; mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } }); mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedEvent }); const args = { calendarId: 'primary', eventId: 'event123', description: 'New description', location: 'Conference Room B' }; const result = await handler.runTool(args, mockOAuth2Client); expect(mockCalendar.events.patch).toHaveBeenCalledWith({ calendarId: 'primary', eventId: 'event123', requestBody: expect.objectContaining({ description: 'New description', location: 'Conference Room B' }) }); const response = JSON.parse((result.content[0] as any).text); expect(response.event).toBeDefined(); expect(response.event.description).toBe('New description'); expect(response.event.location).toBe('Conference Room B'); }); it('should update event times', async () => { const mockUpdatedEvent = { id: 'event123', summary: 'Meeting', start: { dateTime: '2025-01-16T14:00:00Z' }, end: { dateTime: '2025-01-16T15:00:00Z' } }; mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } }); mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedEvent }); const args = { calendarId: 'primary', eventId: 'event123', start: '2025-01-16T14:00:00', end: '2025-01-16T15:00:00', timeZone: 'America/Los_Angeles' }; const result = await handler.runTool(args, mockOAuth2Client); expect(mockCalendar.events.patch).toHaveBeenCalledWith({ calendarId: 'primary', eventId: 'event123', requestBody: expect.objectContaining({ start: { dateTime: '2025-01-16T14:00:00', timeZone: 'America/Los_Angeles', date: null }, end: { dateTime: '2025-01-16T15:00:00', timeZone: 'America/Los_Angeles', date: null } }) }); const response = JSON.parse((result.content[0] as any).text); expect(response.event).toBeDefined(); }); it('should update attendees', async () => { const mockUpdatedEvent = { id: 'event123', summary: 'Meeting', attendees: [ { email: 'alice@example.com' }, { email: 'bob@example.com' } ] }; mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } }); mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedEvent }); const args = { calendarId: 'primary', eventId: 'event123', attendees: [ { email: 'alice@example.com' }, { email: 'bob@example.com' } ], sendUpdates: 'all' as const }; const result = await handler.runTool(args, mockOAuth2Client); expect(mockCalendar.events.patch).toHaveBeenCalledWith({ calendarId: 'primary', eventId: 'event123', requestBody: expect.objectContaining({ attendees: [ { email: 'alice@example.com' }, { email: 'bob@example.com' } ] }) }); const response = JSON.parse((result.content[0] as any).text); expect(response.event).toBeDefined(); }); it('should update reminders', async () => { const mockUpdatedEvent = { id: 'event123', summary: 'Meeting', reminders: { useDefault: false, overrides: [ { method: 'email', minutes: 30 }, { method: 'popup', minutes: 10 } ] } }; mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } }); mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedEvent }); const args = { calendarId: 'primary', eventId: 'event123', reminders: { useDefault: false, overrides: [ { method: 'email' as const, minutes: 30 }, { method: 'popup' as const, minutes: 10 } ] } }; const result = await handler.runTool(args, mockOAuth2Client); expect(mockCalendar.events.patch).toHaveBeenCalledWith({ calendarId: 'primary', eventId: 'event123', requestBody: expect.objectContaining({ reminders: { useDefault: false, overrides: [ { method: 'email', minutes: 30 }, { method: 'popup', minutes: 10 } ] } }) }); const response = JSON.parse((result.content[0] as any).text); expect(response.event).toBeDefined(); }); it('should update guest permissions', async () => { const mockUpdatedEvent = { id: 'event123', summary: 'Team Meeting', guestsCanInviteOthers: false, guestsCanModify: true, guestsCanSeeOtherGuests: false }; mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } }); mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedEvent }); const args = { calendarId: 'primary', eventId: 'event123', guestsCanInviteOthers: false, guestsCanModify: true, guestsCanSeeOtherGuests: false, anyoneCanAddSelf: true }; const result = await handler.runTool(args, mockOAuth2Client); expect(mockCalendar.events.patch).toHaveBeenCalledWith({ calendarId: 'primary', eventId: 'event123', requestBody: expect.objectContaining({ guestsCanInviteOthers: false, guestsCanModify: true, guestsCanSeeOtherGuests: false, anyoneCanAddSelf: true }) }); const response = JSON.parse(result.content[0].text as string); expect(response).toHaveProperty('event'); expect(response.event.id).toBe('event123'); expect(response.event.guestsCanInviteOthers).toBe(false); expect(response.event.guestsCanModify).toBe(true); expect(response.event.guestsCanSeeOtherGuests).toBe(false); }); it('should update event with conference data', async () => { const mockUpdatedEvent = { id: 'event123', summary: 'Video Meeting', conferenceData: { entryPoints: [{ uri: 'https://meet.google.com/abc-defg-hij' }] } }; mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } }); mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedEvent }); const args = { calendarId: 'primary', eventId: 'event123', summary: 'Video Meeting', conferenceData: { createRequest: { requestId: 'unique-request-456', conferenceSolutionKey: { type: 'hangoutsMeet' as const } } } }; const result = await handler.runTool(args, mockOAuth2Client); expect(mockCalendar.events.patch).toHaveBeenCalledWith({ calendarId: 'primary', eventId: 'event123', requestBody: expect.objectContaining({ summary: 'Video Meeting', conferenceData: { createRequest: { requestId: 'unique-request-456', conferenceSolutionKey: { type: 'hangoutsMeet' } } } }), conferenceDataVersion: 1 }); const response = JSON.parse(result.content[0].text as string); expect(response).toHaveProperty('event'); expect(response.event.id).toBe('event123'); expect(response.event.summary).toBe('Video Meeting'); expect(response.event.conferenceData).toBeDefined(); expect(response.event.conferenceData.entryPoints).toBeDefined(); }); it('should update color ID', async () => { const mockUpdatedEvent = { id: 'event123', summary: 'Meeting', colorId: '7' }; mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } }); mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedEvent }); const args = { calendarId: 'primary', eventId: 'event123', colorId: '7' }; const result = await handler.runTool(args, mockOAuth2Client); expect(mockCalendar.events.patch).toHaveBeenCalledWith({ calendarId: 'primary', eventId: 'event123', requestBody: expect.objectContaining({ colorId: '7' }) }); const response = JSON.parse((result.content[0] as any).text); expect(response.event).toBeDefined(); }); it('should update multiple fields at once', async () => { const mockUpdatedEvent = { id: 'event123', summary: 'Updated Meeting', description: 'Updated description', location: 'New Location', start: { dateTime: '2025-01-16T14:00:00Z' }, end: { dateTime: '2025-01-16T15:00:00Z' }, attendees: [{ email: 'alice@example.com' }], colorId: '5', reminders: { useDefault: true } }; mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } }); mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedEvent }); const args = { calendarId: 'primary', eventId: 'event123', summary: 'Updated Meeting', description: 'Updated description', location: 'New Location', start: '2025-01-16T14:00:00', end: '2025-01-16T15:00:00', attendees: [{ email: 'alice@example.com' }], colorId: '5', reminders: { useDefault: true }, sendUpdates: 'externalOnly' as const }; const result = await handler.runTool(args, mockOAuth2Client); expect(mockCalendar.events.patch).toHaveBeenCalledWith({ calendarId: 'primary', eventId: 'event123', requestBody: expect.objectContaining({ summary: 'Updated Meeting', description: 'Updated description', location: 'New Location', colorId: '5' }) }); const response = JSON.parse((result.content[0] as any).text); expect(response.event).toBeDefined(); }); }); describe('Attachments and conference data handling', () => { it('should set supportsAttachments when clearing attachments', async () => { const mockUpdatedEvent = { id: 'event123', summary: 'Meeting' }; mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } }); mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedEvent }); const args = { calendarId: 'primary', eventId: 'event123', attachments: [] }; await handler.runTool(args, mockOAuth2Client); const patchCall = mockCalendar.events.patch.mock.calls[0][0]; expect(patchCall.requestBody.attachments).toEqual([]); expect(patchCall.supportsAttachments).toBe(true); }); it('should set supportsAttachments when duplicating attachments for future instances', async () => { const originalEvent = { id: 'recurring123', recurrence: ['RRULE:FREQ=WEEKLY'], start: { dateTime: '2025-01-01T10:00:00Z' }, end: { dateTime: '2025-01-01T11:00:00Z' } }; mockCalendar.events.get.mockResolvedValue({ data: originalEvent }); mockCalendar.events.patch.mockResolvedValue({ data: {} }); mockCalendar.events.insert.mockResolvedValue({ data: { id: 'newEvent' } }); const helpersStub = { getCalendar: () => mockCalendar, buildUpdateRequestBody: vi.fn().mockReturnValue({}), cleanEventForDuplication: vi.fn().mockReturnValue({ attachments: [{ fileId: 'file1', fileUrl: 'https://drive.google.com/file1' }], recurrence: originalEvent.recurrence }), calculateEndTime: vi.fn().mockReturnValue('2025-02-01T11:00:00Z'), calculateUntilDate: vi.fn().mockReturnValue('20250131T100000Z'), updateRecurrenceWithUntil: vi.fn().mockReturnValue(['RRULE:FREQ=WEEKLY;UNTIL=20250131T100000Z']) } as unknown as RecurringEventHelpers; const args = { calendarId: 'primary', eventId: 'recurring123', futureStartDate: '2025-02-01T10:00:00-08:00', timeZone: 'America/Los_Angeles' } as UpdateEventInput; await (handler as any).updateFutureInstances(helpersStub, args, 'America/Los_Angeles'); const insertCall = mockCalendar.events.insert.mock.calls[0][0]; expect(insertCall.supportsAttachments).toBe(true); expect(insertCall.requestBody.attachments).toEqual([ { fileId: 'file1', fileUrl: 'https://drive.google.com/file1' } ]); }); it('should set conferenceDataVersion when duplicating conference data for future instances', async () => { const originalEvent = { id: 'recurring123', recurrence: ['RRULE:FREQ=WEEKLY'], start: { dateTime: '2025-01-01T10:00:00Z' }, end: { dateTime: '2025-01-01T11:00:00Z' }, conferenceData: { entryPoints: [{ entryPointType: 'video', uri: 'https://meet.google.com/abc-defg-hij' }], conferenceId: 'abc-defg-hij' } }; mockCalendar.events.get.mockResolvedValue({ data: originalEvent }); mockCalendar.events.patch.mockResolvedValue({ data: {} }); mockCalendar.events.insert.mockResolvedValue({ data: { id: 'newEvent' } }); const helpersStub = { getCalendar: () => mockCalendar, buildUpdateRequestBody: vi.fn().mockReturnValue({}), cleanEventForDuplication: vi.fn().mockReturnValue({ conferenceData: originalEvent.conferenceData, recurrence: originalEvent.recurrence }), calculateEndTime: vi.fn().mockReturnValue('2025-02-01T11:00:00Z'), calculateUntilDate: vi.fn().mockReturnValue('20250131T100000Z'), updateRecurrenceWithUntil: vi.fn().mockReturnValue(['RRULE:FREQ=WEEKLY;UNTIL=20250131T100000Z']) } as unknown as RecurringEventHelpers; const args = { calendarId: 'primary', eventId: 'recurring123', futureStartDate: '2025-02-01T10:00:00-08:00', timeZone: 'America/Los_Angeles' } as UpdateEventInput; await (handler as any).updateFutureInstances(helpersStub, args, 'America/Los_Angeles'); const insertCall = mockCalendar.events.insert.mock.calls[0][0]; expect(insertCall.conferenceDataVersion).toBe(1); expect(insertCall.requestBody.conferenceData).toEqual(originalEvent.conferenceData); }); }); describe('Send Updates Options', () => { it('should send updates to all when specified', async () => { const mockUpdatedEvent = { id: 'event123', summary: 'Updated Meeting' }; mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } }); mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedEvent }); const args = { calendarId: 'primary', eventId: 'event123', summary: 'Updated Meeting', sendUpdates: 'all' as const }; await handler.runTool(args, mockOAuth2Client); expect(mockCalendar.events.patch).toHaveBeenCalledWith({ calendarId: 'primary', eventId: 'event123', requestBody: expect.objectContaining({ summary: 'Updated Meeting' }) }); }); it('should send updates to external users only when specified', async () => { const mockUpdatedEvent = { id: 'event123', summary: 'Updated Meeting' }; mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } }); mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedEvent }); const args = { calendarId: 'primary', eventId: 'event123', summary: 'Updated Meeting', sendUpdates: 'externalOnly' as const }; await handler.runTool(args, mockOAuth2Client); expect(mockCalendar.events.patch).toHaveBeenCalledWith({ calendarId: 'primary', eventId: 'event123', requestBody: expect.objectContaining({ summary: 'Updated Meeting' }) }); }); it('should not send updates when none specified', async () => { const mockUpdatedEvent = { id: 'event123', summary: 'Updated Meeting' }; mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } }); mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedEvent }); const args = { calendarId: 'primary', eventId: 'event123', summary: 'Updated Meeting', sendUpdates: 'none' as const }; await handler.runTool(args, mockOAuth2Client); expect(mockCalendar.events.patch).toHaveBeenCalledWith({ calendarId: 'primary', eventId: 'event123', requestBody: expect.objectContaining({ summary: 'Updated Meeting' }) }); }); }); describe('Error Handling', () => { it('should handle event not found error', async () => { const notFoundError = new Error('Not Found'); (notFoundError as any).code = 404; mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } }); mockCalendar.events.patch.mockRejectedValue(notFoundError); const args = { calendarId: 'primary', eventId: 'nonexistent', summary: 'Updated Meeting' }; // The actual error will be "Not Found" since handleGoogleApiError is not being called await expect(handler.runTool(args, mockOAuth2Client)).rejects.toThrow('Not Found'); }); it('should handle permission denied error', async () => { const permissionError = new Error('Forbidden'); (permissionError as any).code = 403; mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } }); mockCalendar.events.patch.mockRejectedValue(permissionError); const args = { calendarId: 'primary', eventId: 'event123', summary: 'Updated Meeting' }; // Don't mock handleGoogleApiError - let the actual error pass through await expect(handler.runTool(args, mockOAuth2Client)).rejects.toThrow('Forbidden'); }); it('should reject modification scope on non-recurring events', async () => { // Mock detectEventType to return 'single' for non-recurring event mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } }); const args = { calendarId: 'primary', eventId: 'event123', summary: 'Updated Meeting', modificationScope: 'thisEventOnly' as const }; await expect(handler.runTool(args, mockOAuth2Client)).rejects.toThrow( 'Scope other than "all" only applies to recurring events' ); }); it('should handle API errors with response status', async () => { const apiError = new Error('Bad Request'); (apiError as any).response = { status: 400 }; mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } }); mockCalendar.events.patch.mockRejectedValue(apiError); const args = { calendarId: 'primary', eventId: 'event123', summary: 'Updated Meeting' }; // Mock handleGoogleApiError vi.spyOn(handler as any, 'handleGoogleApiError').mockImplementation(() => { throw new Error('Bad Request'); }); await expect(handler.runTool(args, mockOAuth2Client)).rejects.toThrow('Bad Request'); }); it('should handle missing response data', async () => { mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } }); mockCalendar.events.patch.mockResolvedValue({ data: null }); const args = { calendarId: 'primary', eventId: 'event123', summary: 'Updated Meeting' }; await expect(handler.runTool(args, mockOAuth2Client)).rejects.toThrow( 'Failed to update event' ); }); }); describe('Timezone Handling', () => { it('should use calendar default timezone when not specified', async () => { const mockUpdatedEvent = { id: 'event123', summary: 'Meeting', start: { dateTime: '2025-01-16T14:00:00-08:00' }, end: { dateTime: '2025-01-16T15:00:00-08:00' } }; mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } }); mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedEvent }); const args = { calendarId: 'primary', eventId: 'event123', start: '2025-01-16T14:00:00', end: '2025-01-16T15:00:00' // No timeZone specified }; await handler.runTool(args, mockOAuth2Client); // Should use the mocked default timezone 'America/Los_Angeles' expect(mockCalendar.events.patch).toHaveBeenCalledWith({ calendarId: 'primary', eventId: 'event123', requestBody: expect.objectContaining({ start: { dateTime: '2025-01-16T14:00:00', timeZone: 'America/Los_Angeles', date: null }, end: { dateTime: '2025-01-16T15:00:00', timeZone: 'America/Los_Angeles', date: null } }) }); }); it('should override calendar timezone when specified', async () => { const mockUpdatedEvent = { id: 'event123', summary: 'Meeting', start: { dateTime: '2025-01-16T14:00:00+00:00' }, end: { dateTime: '2025-01-16T15:00:00+00:00' } }; mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } }); mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedEvent }); const args = { calendarId: 'primary', eventId: 'event123', start: '2025-01-16T14:00:00', end: '2025-01-16T15:00:00', timeZone: 'UTC' }; await handler.runTool(args, mockOAuth2Client); expect(mockCalendar.events.patch).toHaveBeenCalledWith({ calendarId: 'primary', eventId: 'event123', requestBody: expect.objectContaining({ start: { dateTime: '2025-01-16T14:00:00', timeZone: 'UTC', date: null }, end: { dateTime: '2025-01-16T15:00:00', timeZone: 'UTC', date: null } }) }); }); }); describe('All-day Event Conversion (Issue #118)', () => { it('should convert timed event to all-day event', async () => { const existingTimedEvent = { id: 'event123', summary: 'Timed Meeting', start: { dateTime: '2025-10-18T10:00:00-07:00' }, end: { dateTime: '2025-10-18T11:00:00-07:00' } }; const mockUpdatedAllDayEvent = { id: 'event123', summary: 'Timed Meeting', start: { date: '2025-10-18' }, end: { date: '2025-10-19' } }; mockCalendar.events.get.mockResolvedValue({ data: existingTimedEvent }); mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedAllDayEvent }); const args = { calendarId: 'primary', eventId: 'event123', start: '2025-10-18', end: '2025-10-19' }; const result = await handler.runTool(args, mockOAuth2Client); // Verify patch was called with correct all-day format expect(mockCalendar.events.patch).toHaveBeenCalledWith({ calendarId: 'primary', eventId: 'event123', requestBody: expect.objectContaining({ start: { date: '2025-10-18', dateTime: null }, end: { date: '2025-10-19', dateTime: null } }) }); const response = JSON.parse((result.content[0] as any).text); expect(response.event).toBeDefined(); expect(response.event.start.date).toBe('2025-10-18'); expect(response.event.end.date).toBe('2025-10-19'); }); it('should convert all-day event to timed event', async () => { const existingAllDayEvent = { id: 'event456', summary: 'All Day Event', start: { date: '2025-10-18' }, end: { date: '2025-10-19' } }; const mockUpdatedTimedEvent = { id: 'event456', summary: 'All Day Event', start: { dateTime: '2025-10-18T10:00:00-07:00', timeZone: 'America/Los_Angeles' }, end: { dateTime: '2025-10-18T11:00:00-07:00', timeZone: 'America/Los_Angeles' } }; mockCalendar.events.get.mockResolvedValue({ data: existingAllDayEvent }); mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedTimedEvent }); const args = { calendarId: 'primary', eventId: 'event456', start: '2025-10-18T10:00:00', end: '2025-10-18T11:00:00', timeZone: 'America/Los_Angeles' }; const result = await handler.runTool(args, mockOAuth2Client); // Verify patch was called with correct timed format expect(mockCalendar.events.patch).toHaveBeenCalledWith({ calendarId: 'primary', eventId: 'event456', requestBody: expect.objectContaining({ start: { dateTime: '2025-10-18T10:00:00', timeZone: 'America/Los_Angeles', date: null }, end: { dateTime: '2025-10-18T11:00:00', timeZone: 'America/Los_Angeles', date: null } }) }); const response = JSON.parse((result.content[0] as any).text); expect(response.event).toBeDefined(); expect(response.event.start.dateTime).toBeDefined(); expect(response.event.end.dateTime).toBeDefined(); }); it('should keep all-day event as all-day when updating', async () => { const existingAllDayEvent = { id: 'event789', summary: 'All Day Event', start: { date: '2025-10-18' }, end: { date: '2025-10-19' } }; const mockUpdatedAllDayEvent = { id: 'event789', summary: 'All Day Event', start: { date: '2025-10-20' }, end: { date: '2025-10-21' } }; mockCalendar.events.get.mockResolvedValue({ data: existingAllDayEvent }); mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedAllDayEvent }); const args = { calendarId: 'primary', eventId: 'event789', start: '2025-10-20', end: '2025-10-21' }; const result = await handler.runTool(args, mockOAuth2Client); // Verify patch was called with all-day format expect(mockCalendar.events.patch).toHaveBeenCalledWith({ calendarId: 'primary', eventId: 'event789', requestBody: expect.objectContaining({ start: { date: '2025-10-20', dateTime: null }, end: { date: '2025-10-21', dateTime: null } }) }); const response = JSON.parse((result.content[0] as any).text); expect(response.event).toBeDefined(); expect(response.event.start.date).toBe('2025-10-20'); expect(response.event.end.date).toBe('2025-10-21'); }); it('should keep timed event as timed when updating', async () => { const existingTimedEvent = { id: 'event999', summary: 'Timed Meeting', start: { dateTime: '2025-10-18T10:00:00-07:00' }, end: { dateTime: '2025-10-18T11:00:00-07:00' } }; const mockUpdatedTimedEvent = { id: 'event999', summary: 'Timed Meeting', start: { dateTime: '2025-10-18T14:00:00-07:00', timeZone: 'America/Los_Angeles' }, end: { dateTime: '2025-10-18T15:00:00-07:00', timeZone: 'America/Los_Angeles' } }; mockCalendar.events.get.mockResolvedValue({ data: existingTimedEvent }); mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedTimedEvent }); const args = { calendarId: 'primary', eventId: 'event999', start: '2025-10-18T14:00:00', end: '2025-10-18T15:00:00' }; const result = await handler.runTool(args, mockOAuth2Client); // Verify patch was called with timed format expect(mockCalendar.events.patch).toHaveBeenCalledWith({ calendarId: 'primary', eventId: 'event999', requestBody: expect.objectContaining({ start: { dateTime: '2025-10-18T14:00:00', timeZone: 'America/Los_Angeles', date: null }, end: { dateTime: '2025-10-18T15:00:00', timeZone: 'America/Los_Angeles', date: null } }) }); const response = JSON.parse((result.content[0] as any).text); expect(response.event).toBeDefined(); expect(response.event.start.dateTime).toBeDefined(); expect(response.event.end.dateTime).toBeDefined(); }); }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/nspady/google-calendar-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server