Skip to main content
Glama

Google Calendar MCP

CreateEventHandler.test.ts21.8 kB
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { CreateEventHandler } from '../../../handlers/core/CreateEventHandler.js'; import { OAuth2Client } from 'google-auth-library'; // Mock the googleapis module vi.mock('googleapis', () => ({ google: { calendar: vi.fn(() => ({ events: { insert: vi.fn() } })) }, calendar_v3: {} })); // Mock the event ID validator vi.mock('../../../utils/event-id-validator.js', () => ({ validateEventId: vi.fn((eventId: string) => { if (eventId && eventId.length < 5 || eventId.length > 1024) { throw new Error(`Invalid event ID: length must be between 5 and 1024 characters`); } if (eventId && !/^[a-zA-Z0-9-]+$/.test(eventId)) { throw new Error(`Invalid event ID: can only contain letters, numbers, and hyphens`); } }) })); // Mock datetime utilities vi.mock('../../../utils/datetime.js', () => ({ createTimeObject: vi.fn((datetime: string, timezone: string) => ({ dateTime: datetime, timeZone: timezone })) })); describe('CreateEventHandler', () => { let handler: CreateEventHandler; let mockOAuth2Client: OAuth2Client; let mockCalendar: any; beforeEach(() => { handler = new CreateEventHandler(); mockOAuth2Client = new OAuth2Client(); // Setup mock calendar mockCalendar = { events: { insert: 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 Creation', () => { it('should create an event without custom ID', async () => { const mockCreatedEvent = { id: 'generated-id-123', summary: 'Test Event', start: { dateTime: '2025-01-15T10:00:00Z' }, end: { dateTime: '2025-01-15T11:00:00Z' }, htmlLink: 'https://calendar.google.com/event?eid=abc123' }; mockCalendar.events.insert.mockResolvedValue({ data: mockCreatedEvent }); const args = { calendarId: 'primary', summary: 'Test Event', start: '2025-01-15T10:00:00', end: '2025-01-15T11:00:00' }; const result = await handler.runTool(args, mockOAuth2Client); expect(mockCalendar.events.insert).toHaveBeenCalledWith({ calendarId: 'primary', requestBody: expect.objectContaining({ summary: 'Test Event', start: { dateTime: '2025-01-15T10:00:00', timeZone: 'America/Los_Angeles' }, end: { dateTime: '2025-01-15T11:00:00', timeZone: 'America/Los_Angeles' } }) }); // Should not include id field when no custom ID provided expect(mockCalendar.events.insert.mock.calls[0][0].requestBody.id).toBeUndefined(); expect(result.content[0].type).toBe('text'); const response = JSON.parse(result.content[0].text); expect(response.event).toBeDefined(); expect(response.event.id).toBe('generated-id-123'); expect(response.event.summary).toBe('Test Event'); }); it('should create event with all basic optional fields', async () => { const mockCreatedEvent = { id: 'full-event', summary: 'Full Event', description: 'Event description', location: 'Conference Room A', start: { dateTime: '2025-01-15T10:00:00Z' }, end: { dateTime: '2025-01-15T11:00:00Z' }, attendees: [{ email: 'test@example.com' }], colorId: '5', reminders: { useDefault: false, overrides: [{ method: 'email', minutes: 30 }] } }; mockCalendar.events.insert.mockResolvedValue({ data: mockCreatedEvent }); const args = { calendarId: 'primary', eventId: 'full-event', summary: 'Full Event', description: 'Event description', location: 'Conference Room A', start: '2025-01-15T10:00:00', end: '2025-01-15T11:00:00', attendees: [{ email: 'test@example.com' }], colorId: '5', reminders: { useDefault: false, overrides: [{ method: 'email' as const, minutes: 30 }] } }; const result = await handler.runTool(args, mockOAuth2Client); expect(mockCalendar.events.insert).toHaveBeenCalledWith({ calendarId: 'primary', requestBody: expect.objectContaining({ id: 'full-event', summary: 'Full Event', description: 'Event description', location: 'Conference Room A', attendees: [{ email: 'test@example.com' }], colorId: '5', reminders: { useDefault: false, overrides: [{ method: 'email', minutes: 30 }] } }) }); const response = JSON.parse(result.content[0].text); expect(response.event).toBeDefined(); expect(response.event.id).toBeDefined(); }); }); describe('Custom Event IDs', () => { it('should create an event with custom ID', async () => { const mockCreatedEvent = { id: 'customevent2025', summary: 'Test Event', start: { dateTime: '2025-01-15T10:00:00Z' }, end: { dateTime: '2025-01-15T11:00:00Z' }, htmlLink: 'https://calendar.google.com/event?eid=abc123' }; mockCalendar.events.insert.mockResolvedValue({ data: mockCreatedEvent }); const args = { calendarId: 'primary', eventId: 'customevent2025', summary: 'Test Event', start: '2025-01-15T10:00:00', end: '2025-01-15T11:00:00' }; const result = await handler.runTool(args, mockOAuth2Client); expect(mockCalendar.events.insert).toHaveBeenCalledWith({ calendarId: 'primary', requestBody: expect.objectContaining({ id: 'customevent2025', summary: 'Test Event', start: { dateTime: '2025-01-15T10:00:00', timeZone: 'America/Los_Angeles' }, end: { dateTime: '2025-01-15T11:00:00', timeZone: 'America/Los_Angeles' } }) }); const response = JSON.parse(result.content[0].text); expect(response.event).toBeDefined(); expect(response.event.id).toBeDefined(); }); it('should validate event ID before making API call', async () => { const args = { calendarId: 'primary', eventId: 'abc', // Too short (< 5 chars) summary: 'Test Event', start: '2025-01-15T10:00:00', end: '2025-01-15T11:00:00' }; await expect(handler.runTool(args, mockOAuth2Client)).rejects.toThrow( 'Invalid event ID: length must be between 5 and 1024 characters' ); // Should not call the API if validation fails expect(mockCalendar.events.insert).not.toHaveBeenCalled(); }); it('should handle invalid custom event ID', async () => { const args = { calendarId: 'primary', eventId: 'bad id', // Contains space summary: 'Test Event', start: '2025-01-15T10:00:00', end: '2025-01-15T11:00:00' }; await expect(handler.runTool(args, mockOAuth2Client)).rejects.toThrow( 'Invalid event ID: can only contain letters, numbers, and hyphens' ); expect(mockCalendar.events.insert).not.toHaveBeenCalled(); }); it('should handle event ID conflict (409 error)', async () => { const conflictError = new Error('Conflict'); (conflictError as any).code = 409; mockCalendar.events.insert.mockRejectedValue(conflictError); const args = { calendarId: 'primary', eventId: 'existing-event', summary: 'Test Event', start: '2025-01-15T10:00:00', end: '2025-01-15T11:00:00' }; await expect(handler.runTool(args, mockOAuth2Client)).rejects.toThrow( "Event ID 'existing-event' already exists. Please use a different ID." ); }); it('should handle event ID conflict with response status', async () => { const conflictError = new Error('Conflict'); (conflictError as any).response = { status: 409 }; mockCalendar.events.insert.mockRejectedValue(conflictError); const args = { calendarId: 'primary', eventId: 'existing-event', summary: 'Test Event', start: '2025-01-15T10:00:00', end: '2025-01-15T11:00:00' }; await expect(handler.runTool(args, mockOAuth2Client)).rejects.toThrow( "Event ID 'existing-event' already exists. Please use a different ID." ); }); }); describe('Guest Management Properties', () => { it('should create event with transparency setting', async () => { const mockCreatedEvent = { id: 'event123', summary: 'Focus Time', transparency: 'transparent' }; mockCalendar.events.insert.mockResolvedValue({ data: mockCreatedEvent }); const args = { calendarId: 'primary', summary: 'Focus Time', start: '2025-01-15T10:00:00', end: '2025-01-15T11:00:00', transparency: 'transparent' as const }; await handler.runTool(args, mockOAuth2Client); expect(mockCalendar.events.insert).toHaveBeenCalledWith( expect.objectContaining({ requestBody: expect.objectContaining({ transparency: 'transparent' }) }) ); }); it('should create event with visibility settings', async () => { const mockCreatedEvent = { id: 'event123', summary: 'Private Meeting', visibility: 'private' }; mockCalendar.events.insert.mockResolvedValue({ data: mockCreatedEvent }); const args = { calendarId: 'primary', summary: 'Private Meeting', start: '2025-01-15T10:00:00', end: '2025-01-15T11:00:00', visibility: 'private' as const }; await handler.runTool(args, mockOAuth2Client); expect(mockCalendar.events.insert).toHaveBeenCalledWith( expect.objectContaining({ requestBody: expect.objectContaining({ visibility: 'private' }) }) ); }); it('should create event with guest permissions', async () => { const mockCreatedEvent = { id: 'event123', summary: 'Team Meeting' }; mockCalendar.events.insert.mockResolvedValue({ data: mockCreatedEvent }); const args = { calendarId: 'primary', summary: 'Team Meeting', start: '2025-01-15T10:00:00', end: '2025-01-15T11:00:00', guestsCanInviteOthers: false, guestsCanModify: true, guestsCanSeeOtherGuests: false, anyoneCanAddSelf: true }; await handler.runTool(args, mockOAuth2Client); expect(mockCalendar.events.insert).toHaveBeenCalledWith( expect.objectContaining({ requestBody: expect.objectContaining({ guestsCanInviteOthers: false, guestsCanModify: true, guestsCanSeeOtherGuests: false, anyoneCanAddSelf: true }) }) ); }); it('should send update notifications when specified', async () => { const mockCreatedEvent = { id: 'event123', summary: 'Meeting' }; mockCalendar.events.insert.mockResolvedValue({ data: mockCreatedEvent }); const args = { calendarId: 'primary', summary: 'Meeting', start: '2025-01-15T10:00:00', end: '2025-01-15T11:00:00', sendUpdates: 'externalOnly' as const }; await handler.runTool(args, mockOAuth2Client); expect(mockCalendar.events.insert).toHaveBeenCalledWith( expect.objectContaining({ sendUpdates: 'externalOnly' }) ); }); }); describe('Conference Data', () => { it('should create event with conference data', async () => { const mockCreatedEvent = { id: 'event123', summary: 'Video Call', conferenceData: { entryPoints: [{ uri: 'https://meet.google.com/abc-defg-hij' }] } }; mockCalendar.events.insert.mockResolvedValue({ data: mockCreatedEvent }); const args = { calendarId: 'primary', summary: 'Video Call', start: '2025-01-15T10:00:00', end: '2025-01-15T11:00:00', conferenceData: { createRequest: { requestId: 'unique-request-123', conferenceSolutionKey: { type: 'hangoutsMeet' as const } } } }; await handler.runTool(args, mockOAuth2Client); expect(mockCalendar.events.insert).toHaveBeenCalledWith( expect.objectContaining({ requestBody: expect.objectContaining({ conferenceData: { createRequest: { requestId: 'unique-request-123', conferenceSolutionKey: { type: 'hangoutsMeet' } } } }), conferenceDataVersion: 1 }) ); }); }); describe('Extended Properties', () => { it('should create event with extended properties', async () => { const mockCreatedEvent = { id: 'event123', summary: 'Custom Event' }; mockCalendar.events.insert.mockResolvedValue({ data: mockCreatedEvent }); const args = { calendarId: 'primary', summary: 'Custom Event', start: '2025-01-15T10:00:00', end: '2025-01-15T11:00:00', extendedProperties: { private: { 'appId': '12345', 'customField': 'value1' }, shared: { 'projectId': 'proj-789', 'category': 'meeting' } } }; await handler.runTool(args, mockOAuth2Client); expect(mockCalendar.events.insert).toHaveBeenCalledWith( expect.objectContaining({ requestBody: expect.objectContaining({ extendedProperties: { private: { 'appId': '12345', 'customField': 'value1' }, shared: { 'projectId': 'proj-789', 'category': 'meeting' } } }) }) ); }); }); describe('Attachments', () => { it('should create event with attachments', async () => { const mockCreatedEvent = { id: 'event123', summary: 'Meeting with Docs' }; mockCalendar.events.insert.mockResolvedValue({ data: mockCreatedEvent }); const args = { calendarId: 'primary', summary: 'Meeting with Docs', start: '2025-01-15T10:00:00', end: '2025-01-15T11:00:00', attachments: [ { fileUrl: 'https://docs.google.com/document/d/123', title: 'Meeting Agenda', mimeType: 'application/vnd.google-apps.document' }, { fileUrl: 'https://drive.google.com/file/d/456', title: 'Presentation', mimeType: 'application/vnd.google-apps.presentation', fileId: '456' } ] }; await handler.runTool(args, mockOAuth2Client); expect(mockCalendar.events.insert).toHaveBeenCalledWith( expect.objectContaining({ requestBody: expect.objectContaining({ attachments: [ { fileUrl: 'https://docs.google.com/document/d/123', title: 'Meeting Agenda', mimeType: 'application/vnd.google-apps.document' }, { fileUrl: 'https://drive.google.com/file/d/456', title: 'Presentation', mimeType: 'application/vnd.google-apps.presentation', fileId: '456' } ] }), supportsAttachments: true }) ); }); }); describe('Enhanced Attendees', () => { it('should create event with detailed attendee information', async () => { const mockCreatedEvent = { id: 'event123', summary: 'Team Sync' }; mockCalendar.events.insert.mockResolvedValue({ data: mockCreatedEvent }); const args = { calendarId: 'primary', summary: 'Team Sync', start: '2025-01-15T10:00:00', end: '2025-01-15T11:00:00', attendees: [ { email: 'alice@example.com', displayName: 'Alice Smith', optional: false, responseStatus: 'accepted' as const }, { email: 'bob@example.com', displayName: 'Bob Jones', optional: true, responseStatus: 'needsAction' as const, comment: 'May join late', additionalGuests: 2 } ] }; await handler.runTool(args, mockOAuth2Client); expect(mockCalendar.events.insert).toHaveBeenCalledWith( expect.objectContaining({ requestBody: expect.objectContaining({ attendees: [ { email: 'alice@example.com', displayName: 'Alice Smith', optional: false, responseStatus: 'accepted' }, { email: 'bob@example.com', displayName: 'Bob Jones', optional: true, responseStatus: 'needsAction', comment: 'May join late', additionalGuests: 2 } ] }) }) ); }); }); describe('Source Property', () => { it('should create event with source information', async () => { const mockCreatedEvent = { id: 'event123', summary: 'Follow-up Meeting' }; mockCalendar.events.insert.mockResolvedValue({ data: mockCreatedEvent }); const args = { calendarId: 'primary', summary: 'Follow-up Meeting', start: '2025-01-15T10:00:00', end: '2025-01-15T11:00:00', source: { url: 'https://example.com/meetings/123', title: 'Original Meeting Request' } }; await handler.runTool(args, mockOAuth2Client); expect(mockCalendar.events.insert).toHaveBeenCalledWith( expect.objectContaining({ requestBody: expect.objectContaining({ source: { url: 'https://example.com/meetings/123', title: 'Original Meeting Request' } }) }) ); }); }); describe('Error Handling', () => { it('should handle API errors other than 409', async () => { const apiError = new Error('API Error'); (apiError as any).code = 500; mockCalendar.events.insert.mockRejectedValue(apiError); const args = { calendarId: 'primary', summary: 'Test Event', start: '2025-01-15T10:00:00', end: '2025-01-15T11:00:00' }; // Mock handleGoogleApiError vi.spyOn(handler as any, 'handleGoogleApiError').mockImplementation(() => { throw new Error('Handled API Error'); }); await expect(handler.runTool(args, mockOAuth2Client)).rejects.toThrow('Handled API Error'); }); it('should handle missing response data', async () => { mockCalendar.events.insert.mockResolvedValue({ data: null }); const args = { calendarId: 'primary', summary: 'Test Event', start: '2025-01-15T10:00:00', end: '2025-01-15T11:00:00' }; await expect(handler.runTool(args, mockOAuth2Client)).rejects.toThrow( 'Failed to create event, no data returned' ); }); }); describe('Combined Properties', () => { it('should create event with multiple enhanced properties', async () => { const mockCreatedEvent = { id: 'event123', summary: 'Complex Event' }; mockCalendar.events.insert.mockResolvedValue({ data: mockCreatedEvent }); const args = { calendarId: 'primary', eventId: 'customcomplexevent', summary: 'Complex Event', description: 'An event with all features', start: '2025-01-15T10:00:00', end: '2025-01-15T11:00:00', location: 'Conference Room A', transparency: 'opaque' as const, visibility: 'public' as const, guestsCanInviteOthers: true, guestsCanModify: false, conferenceData: { createRequest: { requestId: 'conf-123', conferenceSolutionKey: { type: 'hangoutsMeet' as const } } }, attendees: [ { email: 'team@example.com', displayName: 'Team', optional: false } ], extendedProperties: { private: { 'trackingId': '789' } }, source: { url: 'https://example.com/source', title: 'Source System' }, sendUpdates: 'all' as const }; await handler.runTool(args, mockOAuth2Client); const callArgs = mockCalendar.events.insert.mock.calls[0][0]; expect(callArgs.requestBody).toMatchObject({ id: 'customcomplexevent', summary: 'Complex Event', description: 'An event with all features', location: 'Conference Room A', transparency: 'opaque', visibility: 'public', guestsCanInviteOthers: true, guestsCanModify: false }); expect(callArgs.conferenceDataVersion).toBe(1); expect(callArgs.sendUpdates).toBe('all'); }); }); });

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