Skip to main content
Glama

Google Calendar MCP

BatchRequestHandler.test.ts11.2 kB
/** * @jest-environment node */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { OAuth2Client } from 'google-auth-library'; import { BatchRequestHandler, BatchRequest, BatchResponse } from '../../../handlers/core/BatchRequestHandler.js'; describe('BatchRequestHandler', () => { let mockOAuth2Client: OAuth2Client; let batchHandler: BatchRequestHandler; beforeEach(() => { vi.clearAllMocks(); mockOAuth2Client = { getAccessToken: vi.fn().mockResolvedValue({ token: 'mock_access_token' }) } as any; batchHandler = new BatchRequestHandler(mockOAuth2Client); }); describe('Batch Request Creation', () => { it('should create proper multipart request body with single request', () => { const requests: BatchRequest[] = [ { method: 'GET', path: '/calendar/v3/calendars/primary/events?singleEvents=true&orderBy=startTime' } ]; const result = (batchHandler as any).createBatchBody(requests); const boundary = (batchHandler as any).boundary; expect(result).toContain(`--${boundary}`); expect(result).toContain('Content-Type: application/http'); expect(result).toContain('Content-ID: <item1>'); expect(result).toContain('GET /calendar/v3/calendars/primary/events'); expect(result).toContain('singleEvents=true'); expect(result).toContain('orderBy=startTime'); expect(result).toContain(`--${boundary}--`); }); it('should create proper multipart request body with multiple requests', () => { const requests: BatchRequest[] = [ { method: 'GET', path: '/calendar/v3/calendars/primary/events' }, { method: 'GET', path: '/calendar/v3/calendars/work%40example.com/events' }, { method: 'GET', path: '/calendar/v3/calendars/personal%40example.com/events' } ]; const result = (batchHandler as any).createBatchBody(requests); const boundary = (batchHandler as any).boundary; expect(result).toContain('Content-ID: <item1>'); expect(result).toContain('Content-ID: <item2>'); expect(result).toContain('Content-ID: <item3>'); expect(result).toContain('calendars/primary/events'); expect(result).toContain('calendars/work%40example.com/events'); expect(result).toContain('calendars/personal%40example.com/events'); // Should have proper boundary structure const boundaryCount = (result.match(new RegExp(`--${boundary}`, 'g')) || []).length; expect(boundaryCount).toBe(4); // 3 request boundaries + 1 end boundary }); it('should handle requests with custom headers', () => { const requests: BatchRequest[] = [ { method: 'POST', path: '/calendar/v3/calendars/primary/events', headers: { 'If-Match': '"etag123"', 'X-Custom-Header': 'custom-value' } } ]; const result = (batchHandler as any).createBatchBody(requests); expect(result).toContain('If-Match: "etag123"'); expect(result).toContain('X-Custom-Header: custom-value'); }); it('should handle requests with JSON body', () => { const requestBody = { summary: 'Test Event', start: { dateTime: '2024-01-15T10:00:00Z' }, end: { dateTime: '2024-01-15T11:00:00Z' } }; const requests: BatchRequest[] = [ { method: 'POST', path: '/calendar/v3/calendars/primary/events', body: requestBody } ]; const result = (batchHandler as any).createBatchBody(requests); expect(result).toContain('Content-Type: application/json'); expect(result).toContain(JSON.stringify(requestBody)); expect(result).toContain('"summary":"Test Event"'); }); it('should encode URLs properly in batch requests', () => { const requests: BatchRequest[] = [ { method: 'GET', path: '/calendar/v3/calendars/test%40example.com/events?timeMin=2024-01-01T00%3A00%3A00Z' } ]; const result = (batchHandler as any).createBatchBody(requests); expect(result).toContain('calendars/test%40example.com/events'); expect(result).toContain('timeMin=2024-01-01T00%3A00%3A00Z'); }); }); describe('Batch Response Parsing', () => { it('should parse successful response correctly', () => { const mockResponseText = `HTTP/1.1 200 OK Content-Length: response_total_content_length Content-Type: multipart/mixed; boundary=batch_abc123 --batch_abc123 Content-Type: application/http Content-ID: <response-item1> HTTP/1.1 200 OK Content-Type: application/json Content-Length: 123 { "items": [ { "id": "event1", "summary": "Test Event", "start": {"dateTime": "2024-01-15T10:00:00Z"}, "end": {"dateTime": "2024-01-15T11:00:00Z"} } ] } --batch_abc123--`; const responses = (batchHandler as any).parseBatchResponse(mockResponseText); expect(responses).toHaveLength(1); expect(responses[0].statusCode).toBe(200); expect(responses[0].body.items).toHaveLength(1); expect(responses[0].body.items[0].summary).toBe('Test Event'); }); it('should parse multiple responses correctly', () => { const mockResponseText = `HTTP/1.1 200 OK Content-Type: multipart/mixed; boundary=batch_abc123 --batch_abc123 Content-Type: application/http Content-ID: <response-item1> HTTP/1.1 200 OK Content-Type: application/json {"items": [{"id": "event1", "summary": "Event 1"}]} --batch_abc123 Content-Type: application/http Content-ID: <response-item2> HTTP/1.1 200 OK Content-Type: application/json {"items": [{"id": "event2", "summary": "Event 2"}]} --batch_abc123--`; const responses = (batchHandler as any).parseBatchResponse(mockResponseText); expect(responses).toHaveLength(2); expect(responses[0].body.items[0].summary).toBe('Event 1'); expect(responses[1].body.items[0].summary).toBe('Event 2'); }); it('should handle error responses in batch', () => { const mockResponseText = `HTTP/1.1 200 OK Content-Type: multipart/mixed; boundary=batch_abc123 --batch_abc123 Content-Type: application/http Content-ID: <response-item1> HTTP/1.1 404 Not Found Content-Type: application/json { "error": { "code": 404, "message": "Calendar not found" } } --batch_abc123--`; const responses = (batchHandler as any).parseBatchResponse(mockResponseText); expect(responses).toHaveLength(1); expect(responses[0].statusCode).toBe(404); expect(responses[0].body.error.code).toBe(404); expect(responses[0].body.error.message).toBe('Calendar not found'); }); it('should handle mixed success and error responses', () => { const mockResponseText = `HTTP/1.1 200 OK Content-Type: multipart/mixed; boundary=batch_abc123 --batch_abc123 Content-Type: application/http Content-ID: <response-item1> HTTP/1.1 200 OK Content-Type: application/json {"items": [{"id": "event1", "summary": "Success"}]} --batch_abc123 Content-Type: application/http Content-ID: <response-item2> HTTP/1.1 403 Forbidden Content-Type: application/json { "error": { "code": 403, "message": "Access denied" } } --batch_abc123--`; const responses = (batchHandler as any).parseBatchResponse(mockResponseText); expect(responses).toHaveLength(2); expect(responses[0].statusCode).toBe(200); expect(responses[0].body.items[0].summary).toBe('Success'); expect(responses[1].statusCode).toBe(403); expect(responses[1].body.error.message).toBe('Access denied'); }); it('should handle empty response parts gracefully', () => { const mockResponseText = `HTTP/1.1 200 OK Content-Type: multipart/mixed; boundary=batch_abc123 --batch_abc123 --batch_abc123 Content-Type: application/http Content-ID: <response-item1> HTTP/1.1 200 OK Content-Type: application/json {"items": []} --batch_abc123--`; const responses = (batchHandler as any).parseBatchResponse(mockResponseText); expect(responses).toHaveLength(1); expect(responses[0].statusCode).toBe(200); expect(responses[0].body.items).toEqual([]); }); it('should handle malformed JSON gracefully', () => { const mockResponseText = `HTTP/1.1 200 OK Content-Type: multipart/mixed; boundary=batch_abc123 --batch_abc123 Content-Type: application/http Content-ID: <response-item1> HTTP/1.1 200 OK Content-Type: application/json {invalid json here} --batch_abc123--`; const responses = (batchHandler as any).parseBatchResponse(mockResponseText); expect(responses).toHaveLength(1); expect(responses[0].statusCode).toBe(200); expect(responses[0].body).toBe('{invalid json here}'); }); }); describe('Integration Tests', () => { it('should execute batch request with mocked fetch', async () => { const mockResponseText = `HTTP/1.1 200 OK Content-Type: multipart/mixed; boundary=batch_abc123 --batch_abc123 Content-Type: application/http HTTP/1.1 200 OK Content-Type: application/json {"items": [{"id": "event1", "summary": "Test"}]} --batch_abc123--`; global.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200, statusText: 'OK', text: () => Promise.resolve(mockResponseText) }); const requests: BatchRequest[] = [ { method: 'GET', path: '/calendar/v3/calendars/primary/events' } ]; const responses = await batchHandler.executeBatch(requests); expect(global.fetch).toHaveBeenCalledWith( 'https://www.googleapis.com/batch/calendar/v3', expect.objectContaining({ method: 'POST', headers: expect.objectContaining({ 'Authorization': 'Bearer mock_access_token', 'Content-Type': expect.stringContaining('multipart/mixed; boundary=') }) }) ); expect(responses).toHaveLength(1); expect(responses[0].statusCode).toBe(200); }); it('should handle network errors during batch execution', async () => { // Create a handler with no retries for this test const noRetryHandler = new BatchRequestHandler(mockOAuth2Client); (noRetryHandler as any).maxRetries = 0; // Override max retries global.fetch = vi.fn().mockRejectedValue(new Error('Network error')); const requests: BatchRequest[] = [ { method: 'GET', path: '/calendar/v3/calendars/primary/events' } ]; await expect(noRetryHandler.executeBatch(requests)) .rejects.toThrow('Failed to execute batch request: Network error'); }); it('should handle authentication errors', async () => { mockOAuth2Client.getAccessToken = vi.fn().mockRejectedValue( new Error('Authentication failed') ); const requests: BatchRequest[] = [ { method: 'GET', path: '/calendar/v3/calendars/primary/events' } ]; await expect(batchHandler.executeBatch(requests)) .rejects.toThrow('Authentication failed'); }); }); });

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