This is page 5 of 5. Use http://codebase.md/nspady/google-calendar-mcp?page={x} to view the full context.
# Directory Structure
```
├── .cursorignore
├── .dockerignore
├── .env.example
├── .github
│ └── workflows
│ ├── ci.yml
│ ├── publish.yml
│ └── README.md
├── .gitignore
├── .release-please-manifest.json
├── AGENTS.md
├── CHANGELOG.md
├── CLAUDE.md
├── docker-compose.yml
├── Dockerfile
├── docs
│ ├── advanced-usage.md
│ ├── architecture.md
│ ├── authentication.md
│ ├── deployment.md
│ ├── development.md
│ ├── docker.md
│ ├── README.md
│ └── testing.md
├── examples
│ ├── http-client.js
│ └── http-with-curl.sh
├── future_features
│ └── ARCHITECTURE_REDESIGN.md
├── gcp-oauth.keys.example.json
├── instructions
│ └── file_structure.md
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── scripts
│ ├── account-manager.js
│ ├── build.js
│ ├── dev.js
│ └── test-docker.sh
├── src
│ ├── auth
│ │ ├── client.ts
│ │ ├── paths.d.ts
│ │ ├── paths.js
│ │ ├── server.ts
│ │ ├── tokenManager.ts
│ │ └── utils.ts
│ ├── auth-server.ts
│ ├── config
│ │ └── TransportConfig.ts
│ ├── handlers
│ │ ├── core
│ │ │ ├── BaseToolHandler.ts
│ │ │ ├── BatchRequestHandler.ts
│ │ │ ├── CreateEventHandler.ts
│ │ │ ├── DeleteEventHandler.ts
│ │ │ ├── FreeBusyEventHandler.ts
│ │ │ ├── GetCurrentTimeHandler.ts
│ │ │ ├── GetEventHandler.ts
│ │ │ ├── ListCalendarsHandler.ts
│ │ │ ├── ListColorsHandler.ts
│ │ │ ├── ListEventsHandler.ts
│ │ │ ├── RecurringEventHelpers.ts
│ │ │ ├── SearchEventsHandler.ts
│ │ │ └── UpdateEventHandler.ts
│ │ ├── utils
│ │ │ └── datetime.ts
│ │ └── utils.ts
│ ├── index.ts
│ ├── schemas
│ │ └── types.ts
│ ├── server.ts
│ ├── services
│ │ └── conflict-detection
│ │ ├── config.ts
│ │ ├── ConflictAnalyzer.ts
│ │ ├── ConflictDetectionService.ts
│ │ ├── EventSimilarityChecker.ts
│ │ ├── index.ts
│ │ └── types.ts
│ ├── tests
│ │ ├── integration
│ │ │ ├── claude-mcp-integration.test.ts
│ │ │ ├── direct-integration.test.ts
│ │ │ ├── docker-integration.test.ts
│ │ │ ├── openai-mcp-integration.test.ts
│ │ │ └── test-data-factory.ts
│ │ └── unit
│ │ ├── console-statements.test.ts
│ │ ├── handlers
│ │ │ ├── BatchListEvents.test.ts
│ │ │ ├── BatchRequestHandler.test.ts
│ │ │ ├── CalendarNameResolution.test.ts
│ │ │ ├── create-event-blocking.test.ts
│ │ │ ├── CreateEventHandler.test.ts
│ │ │ ├── datetime-utils.test.ts
│ │ │ ├── duplicate-event-display.test.ts
│ │ │ ├── GetCurrentTimeHandler.test.ts
│ │ │ ├── GetEventHandler.test.ts
│ │ │ ├── list-events-registry.test.ts
│ │ │ ├── ListEventsHandler.test.ts
│ │ │ ├── RecurringEventHelpers.test.ts
│ │ │ ├── UpdateEventHandler.recurring.test.ts
│ │ │ ├── UpdateEventHandler.test.ts
│ │ │ ├── utils-conflict-format.test.ts
│ │ │ └── utils.test.ts
│ │ ├── index.test.ts
│ │ ├── schemas
│ │ │ ├── enhanced-properties.test.ts
│ │ │ ├── no-refs.test.ts
│ │ │ ├── schema-compatibility.test.ts
│ │ │ ├── tool-registration.test.ts
│ │ │ └── validators.test.ts
│ │ ├── services
│ │ │ └── conflict-detection
│ │ │ ├── ConflictAnalyzer.test.ts
│ │ │ └── EventSimilarityChecker.test.ts
│ │ └── utils
│ │ ├── event-id-validator.test.ts
│ │ └── field-mask-builder.test.ts
│ ├── tools
│ │ └── registry.ts
│ ├── transports
│ │ ├── http.ts
│ │ └── stdio.ts
│ ├── types
│ │ └── structured-responses.ts
│ └── utils
│ ├── event-id-validator.ts
│ ├── field-mask-builder.ts
│ └── response-builder.ts
├── tsconfig.json
├── tsconfig.lint.json
└── vitest.config.ts
```
# Files
--------------------------------------------------------------------------------
/src/tests/integration/direct-integration.test.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { spawn, ChildProcess } from 'child_process';
import { TestDataFactory, TestEvent } from './test-data-factory.js';
/**
* Comprehensive Integration Tests for Google Calendar MCP
*
* REQUIREMENTS TO RUN THESE TESTS:
* 1. Valid Google OAuth credentials file at path specified by GOOGLE_OAUTH_CREDENTIALS env var
* 2. Authenticated test account: Run `npm run dev auth:test` first
* 3. TEST_CALENDAR_ID environment variable set to a real Google Calendar ID
* 4. Network access to Google Calendar API
*
* These tests exercise all MCP tools against a real test calendar and will:
* - Create, modify, and delete real calendar events
* - Make actual API calls to Google Calendar
* - Require valid authentication tokens
*
* Test Strategy:
* 1. Create test events first
* 2. Test read operations (list, search, freebusy)
* 3. Test write operations (update)
* 4. Clean up by deleting created events
* 5. Track performance metrics throughout
*/
describe('Google Calendar MCP - Direct Integration Tests', () => {
let client: Client;
let serverProcess: ChildProcess;
let testFactory: TestDataFactory;
let createdEventIds: string[] = [];
const TEST_CALENDAR_ID = process.env.TEST_CALENDAR_ID || 'primary';
const SEND_UPDATES = 'none' as const;
beforeAll(async () => {
// Start the MCP server
console.log('🚀 Starting Google Calendar MCP server...');
// Filter out undefined values from process.env and set NODE_ENV=test
const cleanEnv = Object.fromEntries(
Object.entries(process.env).filter(([_, value]) => value !== undefined)
) as Record<string, string>;
cleanEnv.NODE_ENV = 'test';
serverProcess = spawn('node', ['build/index.js'], {
stdio: ['pipe', 'pipe', 'pipe'],
env: cleanEnv
});
// Wait for server to start
await new Promise(resolve => setTimeout(resolve, 3000));
// Create MCP client
client = new Client({
name: "integration-test-client",
version: "1.0.0"
}, {
capabilities: {
tools: {}
}
});
// Connect to server
const transport = new StdioClientTransport({
command: 'node',
args: ['build/index.js'],
env: cleanEnv
});
await client.connect(transport);
console.log('✅ Connected to MCP server');
// Initialize test factory
testFactory = new TestDataFactory();
}, 30000);
afterAll(async () => {
console.log('\n🏁 Starting final cleanup...');
// Final cleanup - ensure all test events are removed
const allEventIds = testFactory.getCreatedEventIds();
if (allEventIds.length > 0) {
console.log(`📋 Found ${allEventIds.length} total events created during all tests`);
await cleanupAllTestEvents();
} else {
console.log('✨ No additional events to clean up');
}
// Close client connection
if (client) {
await client.close();
console.log('🔌 Closed MCP client connection');
}
// Terminate server process
if (serverProcess && !serverProcess.killed) {
serverProcess.kill();
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('🛑 Terminated MCP server process');
}
// Log performance summary
logPerformanceSummary();
console.log('✅ Integration test cleanup completed successfully\n');
}, 30000);
beforeEach(() => {
testFactory.clearPerformanceMetrics();
createdEventIds = [];
});
afterEach(async () => {
// Cleanup events created in this test
if (createdEventIds.length > 0) {
console.log(`🧹 Cleaning up ${createdEventIds.length} events from test...`);
await cleanupTestEvents(createdEventIds);
createdEventIds = [];
}
});
describe('Tool Availability and Basic Functionality', () => {
it('should list all expected tools', async () => {
const startTime = testFactory.startTimer('list-tools');
try {
const tools = await client.listTools();
testFactory.endTimer('list-tools', startTime, true);
expect(tools.tools).toBeDefined();
expect(tools.tools.length).toBe(10);
const toolNames = tools.tools.map(t => t.name);
expect(toolNames).toContain('get-current-time');
expect(toolNames).toContain('list-calendars');
expect(toolNames).toContain('list-events');
expect(toolNames).toContain('search-events');
expect(toolNames).toContain('list-colors');
expect(toolNames).toContain('create-event');
expect(toolNames).toContain('update-event');
expect(toolNames).toContain('delete-event');
expect(toolNames).toContain('get-freebusy');
expect(toolNames).toContain('get-event');
} catch (error) {
testFactory.endTimer('list-tools', startTime, false, String(error));
throw error;
}
});
it('should list calendars including test calendar', async () => {
const startTime = testFactory.startTimer('list-calendars');
try {
const result = await client.callTool({
name: 'list-calendars',
arguments: {}
});
testFactory.endTimer('list-calendars', startTime, true);
expect(TestDataFactory.validateEventResponse(result)).toBe(true);
const response = JSON.parse((result.content as any)[0].text);
expect(response.calendars).toBeDefined();
expect(Array.isArray(response.calendars)).toBe(true);
expect(response.totalCount).toBeDefined();
expect(typeof response.totalCount).toBe('number');
} catch (error) {
testFactory.endTimer('list-calendars', startTime, false, String(error));
throw error;
}
});
it('should list available colors', async () => {
const startTime = testFactory.startTimer('list-colors');
try {
const result = await client.callTool({
name: 'list-colors',
arguments: {}
});
testFactory.endTimer('list-colors', startTime, true);
expect(TestDataFactory.validateEventResponse(result)).toBe(true);
const response = JSON.parse((result.content as any)[0].text);
expect(response.event).toBeDefined();
expect(response.calendar).toBeDefined();
} catch (error) {
testFactory.endTimer('list-colors', startTime, false, String(error));
throw error;
}
});
it('should get current time without timezone parameter (uses primary calendar timezone)', async () => {
const startTime = testFactory.startTimer('get-current-time');
try {
const result = await client.callTool({
name: 'get-current-time',
arguments: {}
});
testFactory.endTimer('get-current-time', startTime, true);
expect(TestDataFactory.validateEventResponse(result)).toBe(true);
const response = JSON.parse((result.content as any)[0].text);
expect(response.currentTime).toBeDefined();
expect(response.currentTime).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
expect(response.timezone).toBeTypeOf('string');
expect(response.offset).toBeDefined();
expect(response.isDST).toBeTypeOf('boolean');
} catch (error) {
testFactory.endTimer('get-current-time', startTime, false, String(error));
throw error;
}
});
it('should get current time with timezone parameter', async () => {
const startTime = testFactory.startTimer('get-current-time-with-timezone');
try {
const result = await client.callTool({
name: 'get-current-time',
arguments: {
timeZone: 'America/Los_Angeles'
}
});
testFactory.endTimer('get-current-time-with-timezone', startTime, true);
expect(TestDataFactory.validateEventResponse(result)).toBe(true);
const response = JSON.parse((result.content as any)[0].text);
expect(response.currentTime).toBeDefined();
expect(response.currentTime).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
expect(response.timezone).toBe('America/Los_Angeles');
expect(response.offset).toBeDefined();
expect(response.offset).toMatch(/^[+-]\d{2}:\d{2}$/);
expect(response.isDST).toBeTypeOf('boolean');
} catch (error) {
testFactory.endTimer('get-current-time-with-timezone', startTime, false, String(error));
throw error;
}
});
it('should get event by ID', async () => {
const startTime = testFactory.startTimer('get-event');
try {
// First create an event
const eventData = TestDataFactory.createSingleEvent({
summary: `Test Get Event By ID ${Date.now()}`
});
const eventId = await createTestEvent(eventData);
createdEventIds.push(eventId);
// Now get the event by ID
const result = await client.callTool({
name: 'get-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
eventId: eventId
}
});
testFactory.endTimer('get-event', startTime, true);
expect(TestDataFactory.validateEventResponse(result)).toBe(true);
const response = JSON.parse((result.content as any)[0].text);
expect(response.event).toBeDefined();
expect(response.event.summary).toBe(eventData.summary);
expect(response.event.id).toBe(eventId);
} catch (error) {
testFactory.endTimer('get-event', startTime, false, String(error));
throw error;
}
});
it('should return error for non-existent event ID', async () => {
const startTime = testFactory.startTimer('get-event-not-found');
const result = await client.callTool({
name: 'get-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
eventId: 'non-existent-event-id-12345'
}
});
// Errors are returned as text content
const text = (result.content as any)[0]?.text;
if (text && (text.includes('not found') || text.includes('Event with ID'))) {
testFactory.endTimer('get-event-not-found', startTime, true);
// This is expected - test passes
} else {
testFactory.endTimer('get-event-not-found', startTime, false, 'Expected error for non-existent event');
throw new Error('Expected get-event to return error for non-existent event');
}
});
it('should get event with specific fields', async () => {
const startTime = testFactory.startTimer('get-event-with-fields');
try {
// First create an event with extended data
const eventData = TestDataFactory.createColoredEvent('9', {
summary: `Test Get Event With Fields ${Date.now()}`,
description: 'Testing field filtering',
location: 'Test Location'
});
const eventId = await createTestEvent(eventData);
createdEventIds.push(eventId);
// Get event with specific fields
const result = await client.callTool({
name: 'get-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
eventId: eventId,
fields: ['colorId', 'description', 'location', 'created', 'updated']
}
});
testFactory.endTimer('get-event-with-fields', startTime, true);
expect(TestDataFactory.validateEventResponse(result)).toBe(true);
const response = JSON.parse((result.content as any)[0].text);
expect(response.event).toBeDefined();
expect(response.event.summary).toBe(eventData.summary);
expect(response.event.description).toBe(eventData.description);
expect(response.event.location).toBe(eventData.location);
// Color information may not be included when specific fields are requested
// Just verify the event was retrieved with the requested fields
} catch (error) {
testFactory.endTimer('get-event-with-fields', startTime, false, String(error));
throw error;
}
});
});
describe('Event Creation and Management Workflow', () => {
describe('Single Event Operations', () => {
it('should create, list, search, update, and delete a single event', async () => {
// 1. Create event
const eventData = TestDataFactory.createSingleEvent({
summary: `Integration Test - Single Event Workflow ${Date.now()}`
});
const eventId = await createTestEvent(eventData);
createdEventIds.push(eventId);
// 2. List events to verify creation
const timeRanges = TestDataFactory.getTimeRanges();
await verifyEventInList(eventId, timeRanges.nextWeek);
// 3. Search for the event
await verifyEventInSearch(eventData.summary);
// 4. Update the event
await updateTestEvent(eventId, {
summary: 'Updated Integration Test Event',
location: 'Updated Location'
});
// 5. Verify update took effect
await verifyEventInSearch('Integration');
// 6. Delete will happen in afterEach cleanup
});
it('should handle all-day events', async () => {
const allDayEvent = TestDataFactory.createAllDayEvent({
summary: `Integration Test - All Day Event ${Date.now()}`
});
const eventId = await createTestEvent(allDayEvent);
createdEventIds.push(eventId);
// Verify all-day event appears in searches
await verifyEventInSearch(allDayEvent.summary);
});
it('should correctly display all-day events in non-UTC timezones', async () => {
// Create an all-day event for a specific date
// For all-day events, use date-only format (YYYY-MM-DD)
const startDate = '2025-03-15'; // March 15, 2025
const endDate = '2025-03-16'; // March 16, 2025 (exclusive)
// Create all-day event
const createResult = await client.callTool({
name: 'create-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
summary: `All-Day Event Timezone Test ${Date.now()}`,
description: 'Testing all-day event display in different timezones',
start: startDate,
end: endDate
}
});
const eventId = extractEventId(createResult);
expect(eventId).toBeTruthy();
if (eventId) createdEventIds.push(eventId);
// Test 1: List events without timezone (should use calendar's default)
const listDefaultTz = await client.callTool({
name: 'list-events',
arguments: {
calendarId: TEST_CALENDAR_ID,
timeMin: '2025-03-14T00:00:00',
timeMax: '2025-03-17T23:59:59'
}
});
const defaultText = (listDefaultTz.content as any)[0].text;
console.log('Default timezone listing:', defaultText);
// Test 2: List events with UTC timezone
const listUTC = await client.callTool({
name: 'list-events',
arguments: {
calendarId: TEST_CALENDAR_ID,
timeMin: '2025-03-14T00:00:00Z',
timeMax: '2025-03-17T23:59:59Z',
timeZone: 'UTC'
}
});
const utcText = (listUTC.content as any)[0].text;
console.log('UTC listing:', utcText);
// Test 3: List events with Pacific timezone (UTC-7/8)
const listPacific = await client.callTool({
name: 'list-events',
arguments: {
calendarId: TEST_CALENDAR_ID,
timeMin: '2025-03-14T00:00:00-07:00',
timeMax: '2025-03-17T23:59:59-07:00',
timeZone: 'America/Los_Angeles'
}
});
const pacificResponse = JSON.parse((listPacific.content as any)[0].text);
console.log('Pacific timezone listing:', JSON.stringify(pacificResponse, null, 2));
// Parse the other responses too
const defaultResponse = JSON.parse(defaultText);
const utcResponse = JSON.parse(utcText);
// All listings should have events with dates on March 15, 2025
// Check that all responses have events
expect(defaultResponse.events).toBeDefined();
expect(utcResponse.events).toBeDefined();
expect(pacificResponse.events).toBeDefined();
// For all-day events, the date should be 2025-03-15
if (defaultResponse.events.length > 0) {
const event = defaultResponse.events[0];
if (event.start.date) {
expect(event.start.date).toBe('2025-03-15');
}
}
});
it('should handle events with attendees', async () => {
const eventWithAttendees = TestDataFactory.createEventWithAttendees({
summary: `Integration Test - Event with Attendees ${Date.now()}`
});
const eventId = await createTestEvent(eventWithAttendees);
createdEventIds.push(eventId);
await verifyEventInSearch(eventWithAttendees.summary);
});
it('should handle colored events', async () => {
const coloredEvent = TestDataFactory.createColoredEvent('9', {
summary: `Integration Test - Colored Event ${Date.now()}`
});
const eventId = await createTestEvent(coloredEvent);
createdEventIds.push(eventId);
await verifyEventInSearch(coloredEvent.summary);
});
it('should create event without timezone and use calendar default', async () => {
// First, get the calendar details to know the expected default timezone
const calendarResult = await client.callTool({
name: 'list-calendars',
arguments: {}
});
expect(TestDataFactory.validateEventResponse(calendarResult)).toBe(true);
// Create event data without timezone
const eventData = TestDataFactory.createSingleEvent({
summary: `Integration Test - Default Timezone Event ${Date.now()}`
});
// Remove timezone from the event data to test default behavior
const eventDataWithoutTimezone = {
...eventData,
timeZone: undefined
};
delete eventDataWithoutTimezone.timeZone;
// Also convert datetime strings to timezone-naive format
eventDataWithoutTimezone.start = eventDataWithoutTimezone.start.replace(/[+-]\d{2}:\d{2}$|Z$/, '');
eventDataWithoutTimezone.end = eventDataWithoutTimezone.end.replace(/[+-]\d{2}:\d{2}$|Z$/, '');
const startTime = testFactory.startTimer('create-event-default-timezone');
try {
const result = await client.callTool({
name: 'create-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
...eventDataWithoutTimezone
}
});
testFactory.endTimer('create-event-default-timezone', startTime, true);
expect(TestDataFactory.validateEventResponse(result)).toBe(true);
const eventId = extractEventId(result);
expect(eventId).toBeTruthy();
createdEventIds.push(eventId!);
testFactory.addCreatedEventId(eventId!);
// Verify the event was created successfully and shows up in searches
await verifyEventInSearch(eventData.summary);
// Verify the response contains expected event data
const response = JSON.parse((result.content as any)[0].text);
expect(response.event).toBeDefined();
expect(response.event.summary).toBe(eventData.summary);
console.log('✅ Event created successfully without explicit timezone - using calendar default');
} catch (error) {
testFactory.endTimer('create-event-default-timezone', startTime, false, String(error));
throw error;
}
});
});
describe('Recurring Event Operations', () => {
it('should create and manage recurring events', async () => {
// Create recurring event with unique name
const timestamp = Date.now();
const recurringEvent = TestDataFactory.createRecurringEvent({
summary: `Integration Test - Recurring Weekly Meeting ${timestamp}`
});
const eventId = await createTestEvent(recurringEvent);
createdEventIds.push(eventId);
// Verify recurring event
await verifyEventInSearch(recurringEvent.summary);
// Test different update scopes
await testRecurringEventUpdates(eventId);
});
it('should handle update-event with future instances scope (thisAndFollowing)', async () => {
// Create a recurring event with unique name
const timestamp = Date.now();
const recurringEvent = TestDataFactory.createRecurringEvent({
summary: `Weekly Team Meeting - Future Instances Test ${timestamp}`,
description: 'This is a recurring weekly meeting',
location: 'Conference Room A'
});
const eventId = await createTestEvent(recurringEvent);
createdEventIds.push(eventId);
// Wait for event to be searchable
await new Promise(resolve => setTimeout(resolve, 2000));
// Calculate a future date (3 weeks from now)
const futureDate = new Date();
futureDate.setDate(futureDate.getDate() + 21);
const futureStartDate = TestDataFactory.formatDateTimeRFC3339WithTimezone(futureDate);
// Update future instances
const updateResult = await client.callTool({
name: 'update-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
eventId: eventId,
modificationScope: 'thisAndFollowing',
futureStartDate: futureStartDate,
summary: 'Updated Team Meeting - Future Instances',
location: 'New Conference Room',
timeZone: 'America/Los_Angeles',
sendUpdates: SEND_UPDATES
}
});
expect(TestDataFactory.validateEventResponse(updateResult)).toBe(true);
const responseText = (updateResult.content as any)[0].text;
const response = JSON.parse(responseText);
expect(response.event).toBeDefined();
expect(response.event.summary).toBe('Updated Team Meeting - Future Instances');
});
it('should maintain backward compatibility with existing update-event calls', async () => {
// Create a recurring event with unique name
const timestamp = Date.now();
const recurringEvent = TestDataFactory.createRecurringEvent({
summary: `Weekly Team Meeting - Backward Compatibility Test ${timestamp}`
});
const eventId = await createTestEvent(recurringEvent);
createdEventIds.push(eventId);
// Wait for event to be searchable
await new Promise(resolve => setTimeout(resolve, 2000));
// Legacy call format without new parameters (should default to 'all' scope)
const updateResult = await client.callTool({
name: 'update-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
eventId: eventId,
summary: 'Updated Weekly Meeting - All Instances',
location: 'Conference Room B',
timeZone: 'America/Los_Angeles',
sendUpdates: SEND_UPDATES
// No modificationScope, originalStartTime, or futureStartDate
}
});
expect(TestDataFactory.validateEventResponse(updateResult)).toBe(true);
const responseText = (updateResult.content as any)[0].text;
const response = JSON.parse(responseText);
expect(response.event).toBeDefined();
expect(response.event.summary).toBe('Updated Weekly Meeting - All Instances');
// Verify all instances were updated
await verifyEventInSearch('Updated Weekly Meeting - All Instances');
});
it('should handle validation errors for missing required fields', async () => {
// Test case 1: Missing originalStartTime for 'thisEventOnly' scope
const invalidSingleResult = await client.callTool({
name: 'update-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
eventId: 'recurring123',
modificationScope: 'thisEventOnly',
timeZone: 'America/Los_Angeles',
summary: 'Test Update'
// missing originalStartTime
}
});
// Errors are returned as text content
const invalidSingleText = (invalidSingleResult.content as any)[0]?.text;
expect(invalidSingleText).toContain('originalStartTime');
// Test case 2: Missing futureStartDate for 'thisAndFollowing' scope
const invalidFutureResult = await client.callTool({
name: 'update-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
eventId: 'recurring123',
modificationScope: 'thisAndFollowing',
timeZone: 'America/Los_Angeles',
summary: 'Test Update'
// missing futureStartDate
}
});
// Errors are returned as text content
const invalidFutureText = (invalidFutureResult.content as any)[0]?.text;
expect(invalidFutureText).toContain('futureStartDate');
});
it('should reject non-"all" scopes for single (non-recurring) events', async () => {
// Create a single (non-recurring) event
const singleEvent = TestDataFactory.createSingleEvent({
summary: `Single Event - Scope Test ${Date.now()}`
});
const eventId = await createTestEvent(singleEvent);
createdEventIds.push(eventId);
// Wait for event to be created
await new Promise(resolve => setTimeout(resolve, 1000));
// Try to update with 'thisEventOnly' scope (should fail)
const invalidResult = await client.callTool({
name: 'update-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
eventId: eventId,
modificationScope: 'thisEventOnly',
originalStartTime: singleEvent.start,
summary: 'Updated Single Event',
timeZone: 'America/Los_Angeles',
sendUpdates: SEND_UPDATES
}
});
// Errors are returned as text content
const errorText = (invalidResult.content as any)[0]?.text?.toLowerCase() || '';
expect(errorText).toMatch(/scope.*only applies to recurring events|not a recurring event/i);
});
it('should handle complex recurring event updates with all fields', async () => {
// Create a complex recurring event
const complexEvent = TestDataFactory.createRecurringEvent({
summary: `Complex Weekly Meeting ${Date.now()}`,
description: 'Original meeting with all fields',
location: 'Executive Conference Room',
colorId: '9'
});
// Add attendees and reminders
const complexEventWithExtras = {
...complexEvent,
attendees: [
{ email: '[email protected]' },
{ email: '[email protected]' }
],
reminders: {
useDefault: false,
overrides: [
{ method: 'email' as const, minutes: 1440 }, // 1 day before
{ method: 'popup' as const, minutes: 15 }
]
}
};
const eventId = await createTestEvent(complexEventWithExtras);
createdEventIds.push(eventId);
// Wait for event to be searchable
await new Promise(resolve => setTimeout(resolve, 2000));
// Update with all fields
const updateResult = await client.callTool({
name: 'update-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
eventId: eventId,
modificationScope: 'all',
summary: 'Updated Complex Meeting - All Fields',
description: 'Updated meeting with all the bells and whistles',
location: 'New Executive Conference Room',
colorId: '11', // Different color
attendees: [
{ email: '[email protected]' },
{ email: '[email protected]' },
{ email: '[email protected]' } // Added attendee
],
reminders: {
useDefault: false,
overrides: [
{ method: 'email' as const, minutes: 1440 },
{ method: 'popup' as const, minutes: 30 } // Changed from 15 to 30
]
},
timeZone: 'America/Los_Angeles',
sendUpdates: SEND_UPDATES
}
});
expect(TestDataFactory.validateEventResponse(updateResult)).toBe(true);
const updateResponse = JSON.parse((updateResult.content as any)[0].text);
expect(updateResponse.event).toBeDefined();
expect(updateResponse.event.summary).toBe('Updated Complex Meeting - All Fields');
// Verify the update
await verifyEventInSearch('Updated Complex Meeting - All Fields');
});
it('should convert timed event to all-day event and back (Issue #118)', async () => {
console.log('\n🧪 Testing timed ↔ all-day event conversion (Issue #118)...');
// Step 1: Create a timed event
const timedEvent = TestDataFactory.createSingleEvent({
summary: `Conversion Test ${Date.now()}`,
description: 'Testing conversion between timed and all-day formats'
});
const eventId = await createTestEvent(timedEvent);
createdEventIds.push(eventId);
console.log(`✅ Created timed event: ${eventId}`);
// Wait for event to be created
await new Promise(resolve => setTimeout(resolve, 2000));
// Step 2: Convert timed event to all-day event
console.log('🔄 Converting timed event to all-day...');
const toAllDayResult = await client.callTool({
name: 'update-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
eventId: eventId,
start: '2025-10-25',
end: '2025-10-26',
sendUpdates: SEND_UPDATES
}
});
expect(TestDataFactory.validateEventResponse(toAllDayResult)).toBe(true);
const allDayResponse = JSON.parse((toAllDayResult.content as any)[0].text);
expect(allDayResponse.event).toBeDefined();
expect(allDayResponse.event.start.date).toBe('2025-10-25');
expect(allDayResponse.event.end.date).toBe('2025-10-26');
expect(allDayResponse.event.start.dateTime).toBeUndefined();
console.log('✅ Successfully converted to all-day event');
// Wait for update to propagate
await new Promise(resolve => setTimeout(resolve, 2000));
// Step 3: Convert all-day event back to timed event
console.log('🔄 Converting all-day event back to timed...');
const toTimedResult = await client.callTool({
name: 'update-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
eventId: eventId,
start: '2025-10-25T09:00:00',
end: '2025-10-25T10:00:00',
timeZone: 'America/Los_Angeles',
sendUpdates: SEND_UPDATES
}
});
expect(TestDataFactory.validateEventResponse(toTimedResult)).toBe(true);
const timedResponse = JSON.parse((toTimedResult.content as any)[0].text);
expect(timedResponse.event).toBeDefined();
expect(timedResponse.event.start.dateTime).toBeDefined();
expect(timedResponse.event.end.dateTime).toBeDefined();
expect(timedResponse.event.start.date).toBeUndefined();
console.log('✅ Successfully converted back to timed event');
// Step 4: Verify we can create an all-day event directly and convert it
console.log('🔄 Testing direct all-day event creation and conversion...');
const allDayEventData = {
summary: `All-Day Conversion Test ${Date.now()}`,
start: '2025-12-25',
end: '2025-12-26',
description: 'Testing all-day to timed conversion'
};
const allDayEventId = await createTestEvent(allDayEventData);
createdEventIds.push(allDayEventId);
console.log(`✅ Created all-day event: ${allDayEventId}`);
// Wait for event to be created
await new Promise(resolve => setTimeout(resolve, 2000));
// Convert all-day to timed
const directConversionResult = await client.callTool({
name: 'update-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
eventId: allDayEventId,
start: '2025-12-25T10:00:00',
end: '2025-12-25T17:00:00',
timeZone: 'America/Los_Angeles',
sendUpdates: SEND_UPDATES
}
});
expect(TestDataFactory.validateEventResponse(directConversionResult)).toBe(true);
const directResponse = JSON.parse((directConversionResult.content as any)[0].text);
expect(directResponse.event).toBeDefined();
expect(directResponse.event.start.dateTime).toBeDefined();
expect(directResponse.event.end.dateTime).toBeDefined();
console.log('✅ Successfully converted all-day event to timed event');
console.log('✨ All conversion tests passed!');
});
});
describe('Batch and Multi-Calendar Operations', () => {
it('should handle multiple calendar queries', async () => {
const startTime = testFactory.startTimer('list-events-multiple-calendars');
try {
const timeRanges = TestDataFactory.getTimeRanges();
const result = await client.callTool({
name: 'list-events',
arguments: {
calendarId: JSON.stringify(['primary', TEST_CALENDAR_ID]),
timeMin: timeRanges.nextWeek.timeMin,
timeMax: timeRanges.nextWeek.timeMax
}
});
testFactory.endTimer('list-events-multiple-calendars', startTime, true);
expect(TestDataFactory.validateEventResponse(result)).toBe(true);
} catch (error) {
testFactory.endTimer('list-events-multiple-calendars', startTime, false, String(error));
throw error;
}
});
it('should list events with specific fields', async () => {
// Create an event with various fields
const eventData = TestDataFactory.createEventWithAttendees({
summary: `Integration Test - Field Filtering ${Date.now()}`,
description: 'Testing field filtering in list-events',
location: 'Conference Room A'
});
const eventId = await createTestEvent(eventData);
createdEventIds.push(eventId);
const startTime = testFactory.startTimer('list-events-with-fields');
try {
const timeRanges = TestDataFactory.getTimeRanges();
const result = await client.callTool({
name: 'list-events',
arguments: {
calendarId: TEST_CALENDAR_ID,
timeMin: timeRanges.nextWeek.timeMin,
timeMax: timeRanges.nextWeek.timeMax,
fields: ['description', 'location', 'attendees', 'created', 'updated', 'creator', 'organizer']
}
});
testFactory.endTimer('list-events-with-fields', startTime, true);
expect(TestDataFactory.validateEventResponse(result)).toBe(true);
const responseText = (result.content as any)[0].text;
expect(responseText).toContain(eventId);
expect(responseText).toContain(eventData.summary);
// The response should include the additional fields we requested
expect(responseText).toContain(eventData.description!);
expect(responseText).toContain(eventData.location!);
} catch (error) {
testFactory.endTimer('list-events-with-fields', startTime, false, String(error));
throw error;
}
});
it('should filter events by extended properties', async () => {
// Create two events - one with matching properties, one without
const matchingEventData = TestDataFactory.createSingleEvent({
summary: `Integration Test - Matching Extended Props ${Date.now()}`
});
const nonMatchingEventData = TestDataFactory.createSingleEvent({
summary: `Integration Test - Non-Matching Extended Props ${Date.now()}`
});
// Create event with extended properties
const result1 = await client.callTool({
name: 'create-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
...matchingEventData,
extendedProperties: {
private: {
testRun: 'integration-test',
environment: 'test'
},
shared: {
visibility: 'team'
}
}
}
});
const matchingEventId = extractEventId(result1);
createdEventIds.push(matchingEventId!);
// Create event without matching properties
const result2 = await client.callTool({
name: 'create-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
...nonMatchingEventData,
extendedProperties: {
private: {
testRun: 'other-test',
environment: 'production'
}
}
}
});
const nonMatchingEventId = extractEventId(result2);
createdEventIds.push(nonMatchingEventId!);
// Wait for events to be searchable
await new Promise(resolve => setTimeout(resolve, 1000));
const startTime = testFactory.startTimer('list-events-extended-properties');
try {
const timeRanges = TestDataFactory.getTimeRanges();
const result = await client.callTool({
name: 'list-events',
arguments: {
calendarId: TEST_CALENDAR_ID,
timeMin: timeRanges.nextWeek.timeMin,
timeMax: timeRanges.nextWeek.timeMax,
privateExtendedProperty: ['testRun=integration-test', 'environment=test'],
sharedExtendedProperty: ['visibility=team']
}
});
testFactory.endTimer('list-events-extended-properties', startTime, true);
expect(TestDataFactory.validateEventResponse(result)).toBe(true);
const responseText = (result.content as any)[0].text;
// Should find the matching event
expect(responseText).toContain(matchingEventId);
expect(responseText).toContain('Matching Extended Props');
// Should NOT find the non-matching event
expect(responseText).not.toContain(nonMatchingEventId);
expect(responseText).not.toContain('Non-Matching Extended Props');
} catch (error) {
testFactory.endTimer('list-events-extended-properties', startTime, false, String(error));
throw error;
}
});
it('should resolve calendar names to IDs automatically', async () => {
const startTime = testFactory.startTimer('list-events-calendar-name-resolution');
try {
// First, get the list of calendars to find a calendar name
const calendarsResult = await client.callTool({
name: 'list-calendars',
arguments: {}
});
expect(TestDataFactory.validateEventResponse(calendarsResult)).toBe(true);
const calendarsResponse = JSON.parse((calendarsResult.content as any)[0].text);
expect(calendarsResponse.calendars).toBeDefined();
expect(calendarsResponse.calendars.length).toBeGreaterThan(0);
// Get the first calendar's name (summary field)
const firstCalendar = calendarsResponse.calendars[0];
const calendarName = firstCalendar.summary;
const calendarId = firstCalendar.id;
console.log(`🔍 Testing calendar name resolution: "${calendarName}" -> "${calendarId}"`);
// Test 1: Use calendar name instead of ID
const timeRanges = TestDataFactory.getTimeRanges();
const resultWithName = await client.callTool({
name: 'list-events',
arguments: {
calendarId: calendarName, // Using calendar name, not ID
timeMin: timeRanges.nextWeek.timeMin,
timeMax: timeRanges.nextWeek.timeMax
}
});
expect(TestDataFactory.validateEventResponse(resultWithName)).toBe(true);
const responseWithName = JSON.parse((resultWithName.content as any)[0].text);
console.log(`✅ Successfully listed events using calendar name: "${calendarName}"`);
// Test 2: Use calendar ID directly (for comparison)
const resultWithId = await client.callTool({
name: 'list-events',
arguments: {
calendarId: calendarId,
timeMin: timeRanges.nextWeek.timeMin,
timeMax: timeRanges.nextWeek.timeMax
}
});
expect(TestDataFactory.validateEventResponse(resultWithId)).toBe(true);
const responseWithId = JSON.parse((resultWithId.content as any)[0].text);
// Both methods should return the same events
expect(responseWithName.totalCount).toBe(responseWithId.totalCount);
console.log(`✅ Calendar name and ID both return ${responseWithId.totalCount} events`);
// Test 3: Use multiple calendar names in an array
if (calendarsResponse.calendars.length > 1) {
const secondCalendar = calendarsResponse.calendars[1];
const calendarNames = [calendarName, secondCalendar.summary];
console.log(`🔍 Testing multiple calendar names: ${JSON.stringify(calendarNames)}`);
const resultWithMultipleNames = await client.callTool({
name: 'list-events',
arguments: {
calendarId: JSON.stringify(calendarNames),
timeMin: timeRanges.nextWeek.timeMin,
timeMax: timeRanges.nextWeek.timeMax
}
});
expect(TestDataFactory.validateEventResponse(resultWithMultipleNames)).toBe(true);
const responseWithMultipleNames = JSON.parse((resultWithMultipleNames.content as any)[0].text);
console.log(`✅ Successfully listed events from ${calendarNames.length} calendars using names`);
expect(responseWithMultipleNames.calendars).toBeDefined();
expect(responseWithMultipleNames.calendars.length).toBe(2);
}
// Test 4: Invalid calendar name should provide helpful error
// Note: MCP tools return errors as responses (with error content), not as thrown exceptions
const result = await client.callTool({
name: 'list-events',
arguments: {
calendarId: 'ThisCalendarNameDefinitelyDoesNotExist_XYZ123',
timeMin: timeRanges.nextWeek.timeMin,
timeMax: timeRanges.nextWeek.timeMax
}
});
// Extract the error message from the MCP response
const resultText = (result.content as any)[0]?.text || JSON.stringify(result);
// Verify it contains our resolution error message
expect(resultText).toContain('Calendar(s) not found');
expect(resultText).toContain('ThisCalendarNameDefinitelyDoesNotExist_XYZ123');
expect(resultText).toContain('Available calendars');
console.log('✅ Helpful error message provided for invalid calendar name');
console.log(` Error: ${resultText.substring(0, 150)}...`);
testFactory.endTimer('list-events-calendar-name-resolution', startTime, true);
} catch (error) {
testFactory.endTimer('list-events-calendar-name-resolution', startTime, false, String(error));
throw error;
}
});
it('should search events with specific fields', async () => {
// Create an event with rich data
const eventData = TestDataFactory.createColoredEvent('11', {
summary: `Search Test - Field Filtering Event ${Date.now()}`,
description: 'This event tests field filtering in search-events',
location: 'Virtual Meeting Room'
});
const eventId = await createTestEvent(eventData);
createdEventIds.push(eventId);
// Wait for event to be searchable
await new Promise(resolve => setTimeout(resolve, 2000));
const startTime = testFactory.startTimer('search-events-with-fields');
try {
const timeRanges = TestDataFactory.getTimeRanges();
const result = await client.callTool({
name: 'search-events',
arguments: {
calendarId: TEST_CALENDAR_ID,
query: 'Field Filtering',
timeMin: timeRanges.nextWeek.timeMin,
timeMax: timeRanges.nextWeek.timeMax,
fields: ['colorId', 'description', 'location', 'created', 'updated', 'htmlLink']
}
});
testFactory.endTimer('search-events-with-fields', startTime, true);
expect(TestDataFactory.validateEventResponse(result)).toBe(true);
const responseText = (result.content as any)[0].text;
expect(responseText).toContain(eventId);
expect(responseText).toContain(eventData.summary);
expect(responseText).toContain(eventData.description!);
expect(responseText).toContain(eventData.location!);
// Color information may not be included when specific fields are requested
// Just verify the search found the event with the requested fields
} catch (error) {
testFactory.endTimer('search-events-with-fields', startTime, false, String(error));
throw error;
}
});
it('should search events filtered by extended properties', async () => {
// Create event with searchable content and extended properties
const uniqueId = Date.now();
const eventData = TestDataFactory.createSingleEvent({
summary: `Search Extended Props Test Event ${uniqueId}`,
description: 'This event has extended properties for filtering'
});
const result = await client.callTool({
name: 'create-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
...eventData,
allowDuplicates: true, // Add this to handle duplicate events from previous runs
extendedProperties: {
private: {
searchTest: `enabled-${uniqueId}`,
category: 'integration'
},
shared: {
team: 'qa'
}
}
}
});
const eventId = extractEventId(result);
expect(eventId).toBeTruthy(); // Make sure we got an event ID
createdEventIds.push(eventId!);
// Wait for event to be searchable
await new Promise(resolve => setTimeout(resolve, 2000));
const startTime = testFactory.startTimer('search-events-extended-properties');
try {
const timeRanges = TestDataFactory.getTimeRanges();
const searchResult = await client.callTool({
name: 'search-events',
arguments: {
calendarId: TEST_CALENDAR_ID,
query: 'Extended Props',
timeMin: timeRanges.nextWeek.timeMin,
timeMax: timeRanges.nextWeek.timeMax,
privateExtendedProperty: [`searchTest=enabled-${uniqueId}`, 'category=integration'],
sharedExtendedProperty: ['team=qa']
}
});
testFactory.endTimer('search-events-extended-properties', startTime, true);
expect(TestDataFactory.validateEventResponse(searchResult)).toBe(true);
const response = JSON.parse((searchResult.content as any)[0].text);
expect(response.events).toBeDefined();
expect(response.events.length).toBeGreaterThan(0);
expect(response.events[0].id).toBe(eventId);
expect(response.events[0].summary).toContain('Search Extended Props Test Event');
} catch (error) {
testFactory.endTimer('search-events-extended-properties', startTime, false, String(error));
throw error;
}
});
});
describe('Free/Busy Queries', () => {
it('should check availability for test calendar', async () => {
const startTime = testFactory.startTimer('get-freebusy');
try {
const timeRanges = TestDataFactory.getTimeRanges();
const result = await client.callTool({
name: 'get-freebusy',
arguments: {
calendars: [{ id: TEST_CALENDAR_ID }],
timeMin: timeRanges.nextWeek.timeMin,
timeMax: timeRanges.nextWeek.timeMax,
timeZone: 'America/Los_Angeles'
}
});
testFactory.endTimer('get-freebusy', startTime, true);
expect(TestDataFactory.validateEventResponse(result)).toBe(true);
const response = JSON.parse((result.content as any)[0].text);
expect(response.timeMin).toBeDefined();
expect(response.timeMax).toBeDefined();
expect(response.calendars).toBeDefined();
expect(typeof response.calendars).toBe('object');
} catch (error) {
testFactory.endTimer('get-freebusy', startTime, false, String(error));
throw error;
}
});
it('should create event with custom event ID', async () => {
// Google Calendar event IDs must use base32hex encoding: lowercase a-v and 0-9 only
// Generate a valid base32hex ID
const timestamp = Date.now().toString(32).replace(/[w-z]/g, (c) =>
String.fromCharCode(c.charCodeAt(0) - 22)
);
const randomPart = Math.random().toString(32).substring(2, 8).replace(/[w-z]/g, (c) =>
String.fromCharCode(c.charCodeAt(0) - 22)
);
const customEventId = `test${timestamp}${randomPart}`.substring(0, 26);
const eventData = TestDataFactory.createSingleEvent({
summary: `Integration Test - Custom Event ID ${Date.now()}`
});
const startTime = testFactory.startTimer('create-event-custom-id');
try {
const result = await client.callTool({
name: 'create-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
eventId: customEventId,
...eventData
}
});
testFactory.endTimer('create-event-custom-id', startTime, true);
expect(TestDataFactory.validateEventResponse(result)).toBe(true);
const responseText = (result.content as any)[0].text;
expect(responseText).toContain(customEventId);
// Clean up
createdEventIds.push(customEventId);
testFactory.addCreatedEventId(customEventId);
} catch (error) {
testFactory.endTimer('create-event-custom-id', startTime, false, String(error));
throw error;
}
});
it('should handle duplicate custom event ID error', async () => {
// Google Calendar event IDs must use base32hex encoding: lowercase a-v and 0-9 only
// Generate a valid base32hex ID
const timestamp = Date.now().toString(32).replace(/[w-z]/g, (c) =>
String.fromCharCode(c.charCodeAt(0) - 22)
);
const randomPart = Math.random().toString(32).substring(2, 8).replace(/[w-z]/g, (c) =>
String.fromCharCode(c.charCodeAt(0) - 22)
);
const customEventId = `dup${timestamp}${randomPart}`.substring(0, 26);
const eventData = TestDataFactory.createSingleEvent({
summary: `Integration Test - Duplicate ID Test ${Date.now()}`
});
// First create an event with custom ID
const result1 = await client.callTool({
name: 'create-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
eventId: customEventId,
...eventData
}
});
expect(TestDataFactory.validateEventResponse(result1)).toBe(true);
createdEventIds.push(customEventId);
// Wait a moment for Google Calendar to fully process the event
await new Promise(resolve => setTimeout(resolve, 1000));
// Try to create another event with the same ID
const startTime = testFactory.startTimer('create-event-duplicate-id');
try {
await client.callTool({
name: 'create-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
eventId: customEventId,
...eventData
}
});
// If we get here, the duplicate wasn't caught (test should fail)
testFactory.endTimer('create-event-duplicate-id', startTime, false);
expect.fail('Expected error for duplicate event ID');
} catch (error: any) {
testFactory.endTimer('create-event-duplicate-id', startTime, true);
// The error should mention the ID already exists
const errorMessage = error.message || String(error);
expect(errorMessage).toMatch(/already exists|duplicate|conflict|409/i);
}
});
it('should create event with transparency and visibility options', async () => {
const eventData = TestDataFactory.createSingleEvent({
summary: `Integration Test - Transparency and Visibility ${Date.now()}`
});
const startTime = testFactory.startTimer('create-event-transparency-visibility');
try {
const result = await client.callTool({
name: 'create-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
...eventData,
transparency: 'transparent',
visibility: 'private',
guestsCanInviteOthers: false,
guestsCanModify: true,
guestsCanSeeOtherGuests: false
}
});
testFactory.endTimer('create-event-transparency-visibility', startTime, true);
expect(TestDataFactory.validateEventResponse(result)).toBe(true);
const eventId = extractEventId(result);
expect(eventId).toBeTruthy();
createdEventIds.push(eventId!);
testFactory.addCreatedEventId(eventId!);
} catch (error) {
testFactory.endTimer('create-event-transparency-visibility', startTime, false, String(error));
throw error;
}
});
it('should create event with extended properties', async () => {
const eventData = TestDataFactory.createSingleEvent({
summary: `Integration Test - Extended Properties ${Date.now()}`
});
const startTime = testFactory.startTimer('create-event-extended-properties');
try {
const result = await client.callTool({
name: 'create-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
...eventData,
extendedProperties: {
private: {
projectId: 'proj-123',
customerId: 'cust-456',
category: 'meeting'
},
shared: {
department: 'engineering',
team: 'backend'
}
}
}
});
testFactory.endTimer('create-event-extended-properties', startTime, true);
expect(TestDataFactory.validateEventResponse(result)).toBe(true);
const eventId = extractEventId(result);
expect(eventId).toBeTruthy();
createdEventIds.push(eventId!);
testFactory.addCreatedEventId(eventId!);
// Verify the event can be found by extended properties
await new Promise(resolve => setTimeout(resolve, 1000));
const searchResult = await client.callTool({
name: 'list-events',
arguments: {
calendarId: TEST_CALENDAR_ID,
timeMin: eventData.start,
timeMax: eventData.end,
privateExtendedProperty: ['projectId=proj-123', 'customerId=cust-456']
}
});
expect(TestDataFactory.validateEventResponse(searchResult)).toBe(true);
const searchResponse = JSON.parse((searchResult.content as any)[0].text);
expect(searchResponse.events).toBeDefined();
const foundEvent = searchResponse.events.find((e: any) => e.id === eventId);
expect(foundEvent).toBeDefined();
} catch (error) {
testFactory.endTimer('create-event-extended-properties', startTime, false, String(error));
throw error;
}
});
it('should create event with conference data', async () => {
const eventData = TestDataFactory.createSingleEvent({
summary: `Integration Test - Conference Event ${Date.now()}`
});
const startTime = testFactory.startTimer('create-event-conference');
try {
const result = await client.callTool({
name: 'create-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
...eventData,
conferenceData: {
createRequest: {
requestId: `conf-${Date.now()}`,
conferenceSolutionKey: {
type: 'hangoutsMeet'
}
}
}
}
});
testFactory.endTimer('create-event-conference', startTime, true);
expect(TestDataFactory.validateEventResponse(result)).toBe(true);
const eventId = extractEventId(result);
expect(eventId).toBeTruthy();
createdEventIds.push(eventId!);
testFactory.addCreatedEventId(eventId!);
} catch (error) {
testFactory.endTimer('create-event-conference', startTime, false, String(error));
throw error;
}
});
it('should create event with source information', async () => {
const eventData = TestDataFactory.createSingleEvent({
summary: `Integration Test - Event with Source ${Date.now()}`
});
const startTime = testFactory.startTimer('create-event-source');
try {
const result = await client.callTool({
name: 'create-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
...eventData,
source: {
url: 'https://example.com/events/123',
title: 'Original Event Source'
}
}
});
testFactory.endTimer('create-event-source', startTime, true);
expect(TestDataFactory.validateEventResponse(result)).toBe(true);
const eventId = extractEventId(result);
expect(eventId).toBeTruthy();
createdEventIds.push(eventId!);
testFactory.addCreatedEventId(eventId!);
} catch (error) {
testFactory.endTimer('create-event-source', startTime, false, String(error));
throw error;
}
});
it('should create event with complex attendee details', async () => {
const eventData = TestDataFactory.createSingleEvent({
summary: `Integration Test - Complex Attendees ${Date.now()}`
});
const startTime = testFactory.startTimer('create-event-complex-attendees');
try {
const result = await client.callTool({
name: 'create-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
...eventData,
attendees: [
{
email: '[email protected]',
displayName: 'Required Attendee',
optional: false,
responseStatus: 'needsAction',
comment: 'Looking forward to the meeting',
additionalGuests: 2
},
{
email: '[email protected]',
displayName: 'Optional Attendee',
optional: true,
responseStatus: 'tentative'
}
],
sendUpdates: 'none' // Don't send real emails in tests
}
});
testFactory.endTimer('create-event-complex-attendees', startTime, true);
expect(TestDataFactory.validateEventResponse(result)).toBe(true);
const eventId = extractEventId(result);
expect(eventId).toBeTruthy();
createdEventIds.push(eventId!);
testFactory.addCreatedEventId(eventId!);
} catch (error) {
testFactory.endTimer('create-event-complex-attendees', startTime, false, String(error));
throw error;
}
});
});
});
describe('Error Handling and Edge Cases', () => {
it('should handle invalid calendar ID gracefully', async () => {
const invalidData = TestDataFactory.getInvalidTestData();
const now = new Date();
const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000);
try {
await client.callTool({
name: 'list-events',
arguments: {
calendarId: invalidData.invalidCalendarId,
timeMin: TestDataFactory.formatDateTimeRFC3339WithTimezone(now),
timeMax: TestDataFactory.formatDateTimeRFC3339WithTimezone(tomorrow)
}
});
// If we get here, the error wasn't caught (test should fail)
expect.fail('Expected error for invalid calendar ID');
} catch (error: any) {
// Should get an error about invalid calendar ID
const errorMessage = error.message || String(error);
expect(errorMessage.toLowerCase()).toContain('error');
}
});
it('should handle invalid event ID gracefully', async () => {
const invalidData = TestDataFactory.getInvalidTestData();
try {
await client.callTool({
name: 'delete-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
eventId: invalidData.invalidEventId,
sendUpdates: SEND_UPDATES
}
});
// If we get here, the error wasn't caught (test should fail)
expect.fail('Expected error for invalid event ID');
} catch (error: any) {
// Should get an error about invalid event ID
const errorMessage = error.message || String(error);
expect(errorMessage.toLowerCase()).toContain('error');
}
});
it('should handle malformed date formats gracefully', async () => {
const invalidData = TestDataFactory.getInvalidTestData();
try {
await client.callTool({
name: 'create-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
summary: 'Test Event',
start: invalidData.invalidTimeFormat,
end: invalidData.invalidTimeFormat,
timeZone: 'America/Los_Angeles',
sendUpdates: SEND_UPDATES
}
});
// If we get here, the error wasn't caught (test should fail)
expect.fail('Expected error for malformed date format');
} catch (error: any) {
// Should get an error about invalid time value
const errorMessage = error.message || String(error);
expect(errorMessage.toLowerCase()).toMatch(/invalid|error|time/i);
}
});
});
describe('Timezone Handling Validation', () => {
it('should correctly interpret timezone-naive timeMin/timeMax in specified timezone', async () => {
// Test scenario: Create an event at 10:00 AM Los Angeles time,
// then use list-events with timezone-naive timeMin/timeMax and explicit timeZone
// to verify the event is found within a narrow time window.
console.log('🧪 Testing timezone interpretation fix...');
// Step 1: Create an event at 10:00 AM Los Angeles time on a specific date
const testDate = new Date();
testDate.setDate(testDate.getDate() + 7); // Next week to avoid conflicts
const year = testDate.getFullYear();
const month = String(testDate.getMonth() + 1).padStart(2, '0');
const day = String(testDate.getDate()).padStart(2, '0');
const eventStart = `${year}-${month}-${day}T10:00:00-08:00`; // 10:00 AM PST (or PDT)
const eventEnd = `${year}-${month}-${day}T11:00:00-08:00`; // 11:00 AM PST (or PDT)
const eventData: TestEvent = {
summary: 'Timezone Test Event - LA Time',
start: eventStart,
end: eventEnd,
description: 'This event tests timezone interpretation in list-events calls',
timeZone: 'America/Los_Angeles',
sendUpdates: SEND_UPDATES
};
console.log(`📅 Creating event at ${eventStart} (Los Angeles time)`);
const eventId = await createTestEvent(eventData);
createdEventIds.push(eventId);
// Step 2: Use list-events with timezone-naive timeMin/timeMax and explicit timeZone
// This should correctly interpret the times as Los Angeles time, not system time
// Define a narrow time window that includes our event (9:30 AM - 11:30 AM LA time)
const timeMin = `${year}-${month}-${day}T09:30:00`; // Timezone-naive
const timeMax = `${year}-${month}-${day}T11:30:00`; // Timezone-naive
console.log(`🔍 Searching for event using timezone-naive times: ${timeMin} to ${timeMax} (interpreted as Los Angeles time)`);
const startTime = testFactory.startTimer('list-events-timezone-naive');
try {
const listResult = await client.callTool({
name: 'list-events',
arguments: {
calendarId: TEST_CALENDAR_ID,
timeMin: timeMin,
timeMax: timeMax,
timeZone: 'America/Los_Angeles' // This should interpret the timezone-naive times as LA time
}
});
testFactory.endTimer('list-events-timezone-naive', startTime, true);
expect(TestDataFactory.validateEventResponse(listResult)).toBe(true);
const responseText = (listResult.content as any)[0].text;
// The event should be found because:
// - Event is at 10:00-11:00 AM LA time
// - Search window is 9:30-11:30 AM LA time (correctly interpreted)
expect(responseText).toContain(eventId);
expect(responseText).toContain('Timezone Test Event - LA Time');
console.log('✅ Event found in timezone-aware search');
// Step 3: Test the negative case - narrow window that excludes the event
// Search for 8:00-9:00 AM LA time (should NOT find the 10:00 AM event)
const excludingTimeMin = `${year}-${month}-${day}T08:00:00`;
const excludingTimeMax = `${year}-${month}-${day}T09:00:00`;
console.log(`🔍 Testing negative case with excluding time window: ${excludingTimeMin} to ${excludingTimeMax}`);
const excludingResult = await client.callTool({
name: 'list-events',
arguments: {
calendarId: TEST_CALENDAR_ID,
timeMin: excludingTimeMin,
timeMax: excludingTimeMax,
timeZone: 'America/Los_Angeles'
}
});
expect(TestDataFactory.validateEventResponse(excludingResult)).toBe(true);
const excludingResponseText = (excludingResult.content as any)[0].text;
// The event should NOT be found in this time window
expect(excludingResponseText).not.toContain(eventId);
console.log('✅ Event correctly excluded from non-overlapping time window');
} catch (error) {
testFactory.endTimer('list-events-timezone-naive', startTime, false, String(error));
throw error;
}
});
it('should correctly handle DST transitions in timezone interpretation', async () => {
// Test during DST period (July) to ensure DST is handled correctly
console.log('🧪 Testing DST timezone interpretation...');
// Create an event in July (PDT period)
const eventStart = '2024-07-15T10:00:00-07:00'; // 10:00 AM PDT
const eventEnd = '2024-07-15T11:00:00-07:00'; // 11:00 AM PDT
const eventData: TestEvent = {
summary: 'DST Timezone Test Event',
start: eventStart,
end: eventEnd,
description: 'This event tests DST timezone interpretation',
timeZone: 'America/Los_Angeles',
sendUpdates: SEND_UPDATES
};
console.log(`📅 Creating DST event at ${eventStart} (Los Angeles PDT)`);
const eventId = await createTestEvent(eventData);
createdEventIds.push(eventId);
const startTime = testFactory.startTimer('list-events-dst');
try {
// Search with timezone-naive times during DST period
const timeMin = '2024-07-15T09:30:00'; // Should be interpreted as PDT
const timeMax = '2024-07-15T11:30:00'; // Should be interpreted as PDT
console.log(`🔍 Searching during DST period: ${timeMin} to ${timeMax} (PDT)`);
const listResult = await client.callTool({
name: 'list-events',
arguments: {
calendarId: TEST_CALENDAR_ID,
timeMin: timeMin,
timeMax: timeMax,
timeZone: 'America/Los_Angeles'
}
});
testFactory.endTimer('list-events-dst', startTime, true);
expect(TestDataFactory.validateEventResponse(listResult)).toBe(true);
const responseText = (listResult.content as any)[0].text;
expect(responseText).toContain(eventId);
expect(responseText).toContain('DST Timezone Test Event');
console.log('✅ DST timezone interpretation works correctly');
} catch (error) {
testFactory.endTimer('list-events-dst', startTime, false, String(error));
throw error;
}
});
it('should preserve timezone-aware datetime inputs regardless of timeZone parameter', async () => {
// Test that when timeMin/timeMax already have timezone info,
// the timeZone parameter doesn't override them
console.log('🧪 Testing timezone-aware datetime preservation...');
const testDate = new Date();
testDate.setDate(testDate.getDate() + 8);
const year = testDate.getFullYear();
const month = String(testDate.getMonth() + 1).padStart(2, '0');
const day = String(testDate.getDate()).padStart(2, '0');
// Create event in New York time
const eventStart = `${year}-${month}-${day}T14:00:00-05:00`; // 2:00 PM EST
const eventEnd = `${year}-${month}-${day}T15:00:00-05:00`; // 3:00 PM EST
const eventData: TestEvent = {
summary: 'Timezone-Aware Input Test Event',
start: eventStart,
end: eventEnd,
timeZone: 'America/New_York',
sendUpdates: SEND_UPDATES
};
const eventId = await createTestEvent(eventData);
createdEventIds.push(eventId);
const startTime = testFactory.startTimer('list-events-timezone-aware');
try {
// Search using timezone-aware timeMin/timeMax with a different timeZone parameter
// The timezone-aware inputs should be preserved, not converted
const timeMin = `${year}-${month}-${day}T13:30:00-05:00`; // 1:30 PM EST (timezone-aware)
const timeMax = `${year}-${month}-${day}T15:30:00-05:00`; // 3:30 PM EST (timezone-aware)
const listResult = await client.callTool({
name: 'list-events',
arguments: {
calendarId: TEST_CALENDAR_ID,
timeMin: timeMin,
timeMax: timeMax,
timeZone: 'America/Los_Angeles' // Different timezone - should be ignored
}
});
testFactory.endTimer('list-events-timezone-aware', startTime, true);
expect(TestDataFactory.validateEventResponse(listResult)).toBe(true);
const responseText = (listResult.content as any)[0].text;
expect(responseText).toContain(eventId);
expect(responseText).toContain('Timezone-Aware Input Test Event');
console.log('✅ Timezone-aware inputs preserved correctly');
} catch (error) {
testFactory.endTimer('list-events-timezone-aware', startTime, false, String(error));
throw error;
}
});
});
describe('Enhanced Conflict Detection', () => {
describe('Smart Duplicate Detection with Simplified Algorithm', () => {
it('should detect duplicates with rules-based similarity scoring', async () => {
// Create base event with fixed time for consistent duplicate detection
const fixedStart = new Date();
fixedStart.setDate(fixedStart.getDate() + 5); // 5 days from now
fixedStart.setHours(14, 0, 0, 0); // 2 PM
const fixedEnd = new Date(fixedStart);
fixedEnd.setHours(15, 0, 0, 0); // 3 PM
// Pre-check: Clear any existing events in this time window
const timeRangeStart = new Date(fixedStart);
timeRangeStart.setHours(0, 0, 0, 0); // Start of day
const timeRangeEnd = new Date(fixedStart);
timeRangeEnd.setHours(23, 59, 59, 999); // End of day
const existingEventsResult = await client.callTool({
name: 'list-events',
arguments: {
calendarId: TEST_CALENDAR_ID,
timeMin: TestDataFactory.formatDateTimeRFC3339(timeRangeStart),
timeMax: TestDataFactory.formatDateTimeRFC3339(timeRangeEnd)
}
});
// Delete any existing events found
const existingEventIds = TestDataFactory.extractAllEventIds(existingEventsResult);
if (existingEventIds.length > 0) {
console.log(`🧹 Pre-test cleanup: Removing ${existingEventIds.length} existing events from test time window`);
for (const eventId of existingEventIds) {
try {
await client.callTool({
name: 'delete-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
eventId,
sendUpdates: SEND_UPDATES
}
});
} catch (error) {
// Ignore errors - event might be protected or already deleted
}
}
// Wait for deletions to propagate
await new Promise(resolve => setTimeout(resolve, 2000));
}
const timestamp = Date.now();
const baseEvent = TestDataFactory.createSingleEvent({
summary: `Team Meeting ${timestamp}`,
location: 'Conference Room A',
start: TestDataFactory.formatDateTimeRFC3339(fixedStart),
end: TestDataFactory.formatDateTimeRFC3339(fixedEnd)
});
const baseEventId = await createTestEvent(baseEvent);
createdEventIds.push(baseEventId);
// Note: Google Calendar has eventual consistency - events may not immediately
// appear in list queries. This delay helps but doesn't guarantee visibility.
await new Promise(resolve => setTimeout(resolve, 3000));
// Test 1: Exact title + overlapping time = 95% similarity (blocked)
const exactDuplicateResult = await client.callTool({
name: 'create-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
...baseEvent
}
});
// In v2.0, exact duplicates throw an error returned as text
const exactDuplicateText = (exactDuplicateResult.content as any)[0]?.text;
expect(exactDuplicateText).toContain('Duplicate event detected');
// Test 2: Similar title + overlapping time = 70% similarity (warning)
const similarTitleEvent = {
...baseEvent,
summary: `Team Meeting ${timestamp} Discussion` // Contains "Team Meeting"
};
const similarResult = await client.callTool({
name: 'create-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
...similarTitleEvent,
allowDuplicates: true // Allow creation despite warning
}
});
const similarResponse = JSON.parse((similarResult.content as any)[0].text);
expect(similarResponse.event).toBeDefined();
expect(similarResponse.warnings).toBeDefined();
expect(similarResponse.duplicates).toBeDefined();
expect(similarResponse.duplicates.length).toBeGreaterThan(0);
if (similarResponse.duplicates[0]) {
expect(similarResponse.duplicates[0].event.similarity).toBeGreaterThanOrEqual(0.7);
}
const similarEventId = extractEventId(similarResult);
if (similarEventId) createdEventIds.push(similarEventId);
// Test 3: Same title on same day but different time = NO DUPLICATE (different time window)
const laterTime = new Date(baseEvent.start);
laterTime.setHours(laterTime.getHours() + 3);
const laterEndTime = new Date(baseEvent.end);
laterEndTime.setHours(laterEndTime.getHours() + 3);
const sameDayEvent = {
...baseEvent,
start: TestDataFactory.formatDateTimeRFC3339(laterTime),
end: TestDataFactory.formatDateTimeRFC3339(laterEndTime)
};
const sameDayResult = await client.callTool({
name: 'create-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
...sameDayEvent
}
});
// With exact time window search, events at different times are NOT detected as duplicates
const sameDayResponse = JSON.parse((sameDayResult.content as any)[0].text);
expect(sameDayResponse.event).toBeDefined();
expect(sameDayResponse.duplicates).toBeUndefined();
expect(sameDayResponse.warnings).toBeUndefined();
const sameDayEventId = extractEventId(sameDayResult);
if (sameDayEventId) createdEventIds.push(sameDayEventId);
// Test 4: Same title but different day = NO DUPLICATE (different time window)
const nextWeek = new Date(baseEvent.start);
nextWeek.setDate(nextWeek.getDate() + 7);
const nextWeekEnd = new Date(baseEvent.end);
nextWeekEnd.setDate(nextWeekEnd.getDate() + 7);
const differentDayEvent = {
...baseEvent,
start: TestDataFactory.formatDateTimeRFC3339(nextWeek),
end: TestDataFactory.formatDateTimeRFC3339(nextWeekEnd)
};
const differentDayResult = await client.callTool({
name: 'create-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
...differentDayEvent
}
});
// With exact time window search, events on different days are NOT detected as duplicates
const differentDayResponse = JSON.parse((differentDayResult.content as any)[0].text);
expect(differentDayResponse.event).toBeDefined();
expect(differentDayResponse.duplicates).toBeUndefined();
const differentDayEventId = extractEventId(differentDayResult);
if (differentDayEventId) createdEventIds.push(differentDayEventId);
});
});
describe('Adjacent Event Handling (No False Positives)', () => {
it('should not flag back-to-back meetings as conflicts', async () => {
const baseDate = new Date();
baseDate.setDate(baseDate.getDate() + 7); // 7 days from now
baseDate.setHours(9, 0, 0, 0);
// Pre-check: Clear any existing events in this time window
const timeRangeStart = new Date(baseDate);
timeRangeStart.setHours(0, 0, 0, 0); // Start of day
const timeRangeEnd = new Date(baseDate);
timeRangeEnd.setHours(23, 59, 59, 999); // End of day
const existingEventsResult = await client.callTool({
name: 'list-events',
arguments: {
calendarId: TEST_CALENDAR_ID,
timeMin: TestDataFactory.formatDateTimeRFC3339(timeRangeStart),
timeMax: TestDataFactory.formatDateTimeRFC3339(timeRangeEnd)
}
});
// Delete any existing events found
const existingEventIds = TestDataFactory.extractAllEventIds(existingEventsResult);
if (existingEventIds.length > 0) {
console.log(`🧹 Pre-test cleanup: Removing ${existingEventIds.length} existing events from test time window`);
for (const eventId of existingEventIds) {
try {
await client.callTool({
name: 'delete-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
eventId,
sendUpdates: SEND_UPDATES
}
});
} catch (error) {
// Ignore errors - event might be protected or already deleted
}
}
// Wait for deletions to propagate
await new Promise(resolve => setTimeout(resolve, 2000));
}
// Create first meeting 9-10am
const timestamp = Date.now();
const firstStart = new Date(baseDate);
const firstEnd = new Date(firstStart);
firstEnd.setHours(10, 0, 0, 0);
const firstMeeting = TestDataFactory.createSingleEvent({
summary: `Morning Standup ${timestamp}`,
description: 'Daily team sync',
location: 'Room A',
start: TestDataFactory.formatDateTimeRFC3339(firstStart),
end: TestDataFactory.formatDateTimeRFC3339(firstEnd)
});
const firstId = await createTestEvent(firstMeeting);
createdEventIds.push(firstId);
// Note: Google Calendar has eventual consistency - events may not immediately
// appear in list queries. This delay helps but doesn't guarantee visibility.
await new Promise(resolve => setTimeout(resolve, 3000));
// Create second meeting 10-11am (immediately after)
const secondStart = new Date(baseDate);
secondStart.setHours(10, 0, 0, 0);
const secondEnd = new Date(secondStart);
secondEnd.setHours(11, 0, 0, 0);
const secondMeeting = TestDataFactory.createSingleEvent({
summary: `Project Review ${timestamp}`,
description: 'Weekly project status update',
location: 'Room B',
start: TestDataFactory.formatDateTimeRFC3339(secondStart),
end: TestDataFactory.formatDateTimeRFC3339(secondEnd)
});
const result = await client.callTool({
name: 'create-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
...secondMeeting
}
});
// Should not show conflict warning for adjacent events
const resultResponse = JSON.parse((result.content as any)[0].text);
expect(resultResponse.event).toBeDefined();
expect(resultResponse.conflicts).toBeUndefined();
expect(resultResponse.warnings).toBeUndefined();
const secondId = extractEventId(result);
if (secondId) createdEventIds.push(secondId);
// Create third meeting 10:30-11:30am (overlaps with second)
const thirdStart = new Date(baseDate);
thirdStart.setHours(10, 30, 0, 0); // 10:30 AM
const thirdEnd = new Date(thirdStart);
thirdEnd.setHours(11, 30, 0, 0); // 11:30 AM
const thirdMeeting = TestDataFactory.createSingleEvent({
summary: 'Design Discussion',
description: 'UI/UX design review',
location: 'Design Lab',
start: TestDataFactory.formatDateTimeRFC3339(thirdStart),
end: TestDataFactory.formatDateTimeRFC3339(thirdEnd)
});
const conflictResult = await client.callTool({
name: 'create-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
...thirdMeeting
}
});
// Should show conflict for actual overlap
const conflictResponse = JSON.parse((conflictResult.content as any)[0].text);
expect(conflictResponse.event).toBeDefined();
expect(conflictResponse.warnings).toBeDefined();
expect(conflictResponse.conflicts).toBeDefined();
expect(conflictResponse.conflicts.length).toBeGreaterThan(0);
if (conflictResponse.conflicts[0]) {
expect(conflictResponse.conflicts[0].overlap?.duration).toContain('30 minute');
expect(conflictResponse.conflicts[0].overlap?.percentage).toContain('50%');
}
const thirdId = extractEventId(conflictResult);
if (thirdId) createdEventIds.push(thirdId);
});
});
describe('Unified Threshold Configuration', () => {
it('should use configurable duplicate detection threshold', async () => {
// Use fixed time for consistent testing
const fixedStart = new Date();
fixedStart.setDate(fixedStart.getDate() + 8); // 8 days from now
fixedStart.setHours(10, 0, 0, 0); // 10 AM
const fixedEnd = new Date(fixedStart);
fixedEnd.setHours(11, 0, 0, 0); // 11 AM
// Pre-check: Clear any existing events in this time window
const timeRangeStart = new Date(fixedStart);
timeRangeStart.setHours(0, 0, 0, 0); // Start of day
const timeRangeEnd = new Date(fixedStart);
timeRangeEnd.setHours(23, 59, 59, 999); // End of day
const existingEventsResult = await client.callTool({
name: 'list-events',
arguments: {
calendarId: TEST_CALENDAR_ID,
timeMin: TestDataFactory.formatDateTimeRFC3339(timeRangeStart),
timeMax: TestDataFactory.formatDateTimeRFC3339(timeRangeEnd)
}
});
// Delete any existing events found
const existingEventIds = TestDataFactory.extractAllEventIds(existingEventsResult);
if (existingEventIds.length > 0) {
console.log(`🧹 Pre-test cleanup: Removing ${existingEventIds.length} existing events from test time window`);
for (const eventId of existingEventIds) {
try {
await client.callTool({
name: 'delete-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
eventId,
sendUpdates: SEND_UPDATES
}
});
} catch (error) {
// Ignore errors - event might be protected or already deleted
}
}
// Wait for deletions to propagate
await new Promise(resolve => setTimeout(resolve, 2000));
}
const timestamp = Date.now();
const baseEvent = TestDataFactory.createSingleEvent({
summary: `Quarterly Planning ${timestamp}`,
start: TestDataFactory.formatDateTimeRFC3339(fixedStart),
end: TestDataFactory.formatDateTimeRFC3339(fixedEnd)
});
const baseId = await createTestEvent(baseEvent);
createdEventIds.push(baseId);
// Note: Google Calendar has eventual consistency - events may not immediately
// appear in list queries. This delay helps but doesn't guarantee visibility.
await new Promise(resolve => setTimeout(resolve, 3000));
// Test with custom threshold of 0.5 for similar title at same time
const similarEvent = {
...baseEvent,
summary: `Quarterly Planning ${timestamp} Meeting` // Similar but not identical title
};
const lowThresholdResult = await client.callTool({
name: 'create-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
...similarEvent,
duplicateSimilarityThreshold: 0.5,
allowDuplicates: true // Allow creation despite warning
}
});
// Track for cleanup immediately after creation
const lowThresholdId = extractEventId(lowThresholdResult);
if (lowThresholdId) createdEventIds.push(lowThresholdId);
// Should show warning since similarity > 50% threshold
const lowThresholdResponse = JSON.parse((lowThresholdResult.content as any)[0].text);
expect(lowThresholdResponse.event).toBeDefined();
expect(lowThresholdResponse.duplicates).toBeDefined();
expect(lowThresholdResponse.duplicates.length).toBeGreaterThan(0);
// Test with high threshold of 0.9 (should not flag ~70% similarity)
const slightlyDifferentEvent = {
...baseEvent,
summary: 'Q4 Planning' // Different enough title to be below 90% threshold
};
const highThresholdResult = await client.callTool({
name: 'create-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
...slightlyDifferentEvent,
duplicateSimilarityThreshold: 0.9
}
});
// Track for cleanup immediately after creation
const highThresholdId = extractEventId(highThresholdResult);
if (highThresholdId) createdEventIds.push(highThresholdId);
// Should not show DUPLICATE warning since similarity < 90% threshold
// Note: May show conflict warning if events overlap in time
const highThresholdResponse = JSON.parse((highThresholdResult.content as any)[0].text);
expect(highThresholdResponse.event).toBeDefined();
expect(highThresholdResponse.duplicates).toBeUndefined();
});
it('should allow exact duplicates with allowDuplicates flag', async () => {
// Use fixed time for exact duplicate
const fixedStart = new Date();
fixedStart.setDate(fixedStart.getDate() + 9); // 9 days from now
fixedStart.setHours(15, 0, 0, 0); // 3 PM
const fixedEnd = new Date(fixedStart);
fixedEnd.setHours(16, 0, 0, 0); // 4 PM
// Pre-check: Clear any existing events in this time window
const timeRangeStart = new Date(fixedStart);
timeRangeStart.setHours(0, 0, 0, 0); // Start of day
const timeRangeEnd = new Date(fixedStart);
timeRangeEnd.setHours(23, 59, 59, 999); // End of day
const existingEventsResult = await client.callTool({
name: 'list-events',
arguments: {
calendarId: TEST_CALENDAR_ID,
timeMin: TestDataFactory.formatDateTimeRFC3339(timeRangeStart),
timeMax: TestDataFactory.formatDateTimeRFC3339(timeRangeEnd)
}
});
// Delete any existing events found
const existingEventIds = TestDataFactory.extractAllEventIds(existingEventsResult);
if (existingEventIds.length > 0) {
console.log(`🧹 Pre-test cleanup: Removing ${existingEventIds.length} existing events from test time window`);
for (const eventId of existingEventIds) {
try {
await client.callTool({
name: 'delete-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
eventId,
sendUpdates: SEND_UPDATES
}
});
} catch (error) {
// Ignore errors - event might be protected or already deleted
}
}
// Wait for deletions to propagate
await new Promise(resolve => setTimeout(resolve, 2000));
}
const event = TestDataFactory.createSingleEvent({
summary: `Important Presentation ${Date.now()}`,
start: TestDataFactory.formatDateTimeRFC3339(fixedStart),
end: TestDataFactory.formatDateTimeRFC3339(fixedEnd)
});
const firstId = await createTestEvent(event);
createdEventIds.push(firstId);
// Note: Google Calendar has eventual consistency - events may not immediately
// appear in list queries. This delay helps but doesn't guarantee visibility.
await new Promise(resolve => setTimeout(resolve, 3000));
// Try to create exact duplicate with allowDuplicates=true
const duplicateResult = await client.callTool({
name: 'create-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
...event,
allowDuplicates: true
}
});
// Should create with warning but not block
const duplicateResponse = JSON.parse((duplicateResult.content as any)[0].text);
expect(duplicateResponse.event).toBeDefined();
expect(duplicateResponse.warnings).toBeDefined();
expect(duplicateResponse.duplicates).toBeDefined();
expect(duplicateResponse.duplicates.length).toBeGreaterThan(0);
expect(duplicateResponse.duplicates[0].event.similarity).toBeGreaterThan(0.6); // Similarity may vary due to timestamps
const duplicateId = extractEventId(duplicateResult);
if (duplicateId) createdEventIds.push(duplicateId);
});
});
describe('Conflict Detection Performance', () => {
it('should detect conflicts for overlapping events', async () => {
// Create multiple events for conflict checking
const baseTime = new Date();
baseTime.setDate(baseTime.getDate() + 10); // 10 days from now
baseTime.setHours(14, 0, 0, 0); // 2 PM
// Pre-check: Clear any existing events in this time window
const timeRangeStart = new Date(baseTime);
timeRangeStart.setHours(0, 0, 0, 0); // Start of day
const timeRangeEnd = new Date(baseTime);
timeRangeEnd.setHours(23, 59, 59, 999); // End of day
const existingEventsResult = await client.callTool({
name: 'list-events',
arguments: {
calendarId: TEST_CALENDAR_ID,
timeMin: TestDataFactory.formatDateTimeRFC3339(timeRangeStart),
timeMax: TestDataFactory.formatDateTimeRFC3339(timeRangeEnd)
}
});
// Delete any existing events found
const existingEventIds = TestDataFactory.extractAllEventIds(existingEventsResult);
if (existingEventIds.length > 0) {
console.log(`🧹 Pre-test cleanup: Removing ${existingEventIds.length} existing events from test time window`);
for (const eventId of existingEventIds) {
try {
await client.callTool({
name: 'delete-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
eventId,
sendUpdates: SEND_UPDATES
}
});
} catch (error) {
// Ignore errors - event might be protected or already deleted
}
}
// Wait for deletions to propagate
await new Promise(resolve => setTimeout(resolve, 2000));
}
const events = [];
for (let i = 0; i < 3; i++) {
const startTime = new Date(baseTime.getTime() + i * 2 * 60 * 60 * 1000);
const event = TestDataFactory.createSingleEvent({
summary: `Cache Test Event ${i + 1} ${Date.now()}`,
start: TestDataFactory.formatDateTimeRFC3339(startTime),
end: TestDataFactory.formatDateTimeRFC3339(new Date(startTime.getTime() + 60 * 60 * 1000))
});
const id = await createTestEvent(event);
createdEventIds.push(id);
events.push(event);
}
// Longer delay to ensure events are indexed in Google Calendar
await new Promise(resolve => setTimeout(resolve, 3000));
// First conflict check
const overlappingEvent = TestDataFactory.createSingleEvent({
summary: 'Overlapping Meeting',
start: events[1].start, // Same time as second event
end: events[1].end
});
const result1 = await client.callTool({
name: 'create-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
...overlappingEvent,
allowDuplicates: true
}
});
// Should detect a conflict (100% overlap)
const responseText = (result1.content as any)[0].text;
const response1 = JSON.parse(responseText);
expect(response1.conflicts).toBeDefined();
expect(response1.conflicts.length).toBeGreaterThan(0);
expect(response1.conflicts[0].overlap.percentage).toBe('100%');
const overlappingId = response1.event?.id;
if (overlappingId) createdEventIds.push(overlappingId);
// Second conflict check with different event
const anotherOverlapping = TestDataFactory.createSingleEvent({
summary: 'Another Overlapping Meeting',
start: events[1].start,
end: events[1].end
});
const result2 = await client.callTool({
name: 'create-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
...anotherOverlapping,
allowDuplicates: true
}
});
// Should also detect a conflict
const responseText2 = (result2.content as any)[0].text;
const response2 = JSON.parse(responseText2);
expect(response2.conflicts).toBeDefined();
expect(response2.conflicts.length).toBeGreaterThan(0);
// Check that at least one conflict has 100% overlap
const has100PercentOverlap = response2.conflicts.some((c: any) =>
c.overlap && c.overlap.percentage === '100%'
);
expect(has100PercentOverlap).toBe(true);
const anotherId = extractEventId(result2);
if (anotherId) createdEventIds.push(anotherId);
});
});
});
describe('Performance Benchmarks', () => {
it('should complete basic operations within reasonable time limits', async () => {
// Create a test event for performance testing
const eventData = TestDataFactory.createSingleEvent({
summary: `Performance Test Event ${Date.now()}`
});
const eventId = await createTestEvent(eventData);
createdEventIds.push(eventId);
// Test various operations and collect metrics
const timeRanges = TestDataFactory.getTimeRanges();
await verifyEventInList(eventId, timeRanges.nextWeek);
await verifyEventInSearch(eventData.summary);
// Get all performance metrics
const metrics = testFactory.getPerformanceMetrics();
// Log performance results
console.log('\n📊 Performance Metrics:');
metrics.forEach(metric => {
console.log(` ${metric.operation}: ${metric.duration}ms (${metric.success ? '✅' : '❌'})`);
});
// Basic performance assertions
const createMetric = metrics.find(m => m.operation === 'create-event');
const listMetric = metrics.find(m => m.operation === 'list-events');
const searchMetric = metrics.find(m => m.operation === 'search-events');
expect(createMetric?.success).toBe(true);
expect(listMetric?.success).toBe(true);
expect(searchMetric?.success).toBe(true);
// All operations should complete within 30 seconds
metrics.forEach(metric => {
expect(metric.duration).toBeLessThan(30000);
});
});
});
// Helper Functions
function extractEventId(result: any): string | null {
try {
const text = (result.content as any)[0]?.text;
if (!text) return null;
const response = JSON.parse(text);
return response.event?.id || null;
} catch {
return null;
}
}
async function createTestEvent(eventData: TestEvent, allowDuplicates: boolean = true): Promise<string> {
const startTime = testFactory.startTimer('create-event');
try {
const result = await client.callTool({
name: 'create-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
...eventData,
allowDuplicates
}
});
testFactory.endTimer('create-event', startTime, true);
expect(TestDataFactory.validateEventResponse(result)).toBe(true);
// Handle structured JSON response
const text = (result.content as any)[0]?.text;
if (!text) throw new Error('No response text');
// Check if it's an error message (not JSON)
if (text.includes('Duplicate event detected') || text.includes('Error:')) {
throw new Error(text);
}
const response = JSON.parse(text);
const eventId = response.event?.id;
expect(eventId).toBeTruthy();
testFactory.addCreatedEventId(eventId);
return eventId;
} catch (error) {
testFactory.endTimer('create-event', startTime, false, String(error));
throw error;
}
}
async function verifyEventInList(eventId: string, timeRange: { timeMin: string; timeMax: string }): Promise<void> {
const startTime = testFactory.startTimer('list-events');
try {
const result = await client.callTool({
name: 'list-events',
arguments: {
calendarId: TEST_CALENDAR_ID,
timeMin: timeRange.timeMin,
timeMax: timeRange.timeMax
}
});
testFactory.endTimer('list-events', startTime, true);
expect(TestDataFactory.validateEventResponse(result)).toBe(true);
// Handle structured JSON response
const text = (result.content as any)[0]?.text;
const response = JSON.parse(text);
// Check if the event ID is in the list
const eventIds = response.events?.map((e: any) => e.id) || [];
expect(eventIds).toContain(eventId);
} catch (error) {
testFactory.endTimer('list-events', startTime, false, String(error));
throw error;
}
}
async function verifyEventInSearch(query: string): Promise<void> {
// Add small delay to allow Google Calendar search index to update
await new Promise(resolve => setTimeout(resolve, 1000));
const startTime = testFactory.startTimer('search-events');
try {
const timeRanges = TestDataFactory.getTimeRanges();
const result = await client.callTool({
name: 'search-events',
arguments: {
calendarId: TEST_CALENDAR_ID,
query,
timeMin: timeRanges.nextWeek.timeMin,
timeMax: timeRanges.nextWeek.timeMax
}
});
testFactory.endTimer('search-events', startTime, true);
expect(TestDataFactory.validateEventResponse(result)).toBe(true);
// Handle structured JSON response
const text = (result.content as any)[0]?.text;
const response = JSON.parse(text);
// Check if query matches any event in results
const hasMatch = response.events?.some((e: any) =>
e.summary?.toLowerCase().includes(query.toLowerCase()) ||
e.description?.toLowerCase().includes(query.toLowerCase())
);
expect(hasMatch).toBe(true);
} catch (error) {
testFactory.endTimer('search-events', startTime, false, String(error));
throw error;
}
}
async function updateTestEvent(eventId: string, updates: Partial<TestEvent>): Promise<void> {
const startTime = testFactory.startTimer('update-event');
try {
const result = await client.callTool({
name: 'update-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
eventId,
...updates,
timeZone: updates.timeZone || 'America/Los_Angeles',
sendUpdates: SEND_UPDATES
}
});
testFactory.endTimer('update-event', startTime, true);
expect(TestDataFactory.validateEventResponse(result)).toBe(true);
} catch (error) {
testFactory.endTimer('update-event', startTime, false, String(error));
throw error;
}
}
async function testRecurringEventUpdates(eventId: string): Promise<void> {
// Test updating all instances
await updateTestEvent(eventId, {
summary: 'Updated Recurring Meeting - All Instances'
});
// Verify the update
await verifyEventInSearch('Recurring');
}
async function cleanupTestEvents(eventIds: string[]): Promise<void> {
const cleanupResults = { success: 0, failed: 0 };
for (const eventId of eventIds) {
try {
const deleteStartTime = testFactory.startTimer('delete-event');
await client.callTool({
name: 'delete-event',
arguments: {
calendarId: TEST_CALENDAR_ID,
eventId,
sendUpdates: SEND_UPDATES
}
});
testFactory.endTimer('delete-event', deleteStartTime, true);
cleanupResults.success++;
} catch (error: any) {
const deleteStartTime = testFactory.startTimer('delete-event');
testFactory.endTimer('delete-event', deleteStartTime, false, String(error));
// Only warn for non-404 errors (404 means event was already deleted)
const errorMessage = String(error);
if (!errorMessage.includes('404') && !errorMessage.includes('Not Found')) {
console.warn(`⚠️ Failed to cleanup event ${eventId}:`, errorMessage);
}
cleanupResults.failed++;
}
}
if (cleanupResults.success > 0) {
console.log(`✅ Successfully deleted ${cleanupResults.success} test event(s)`);
}
if (cleanupResults.failed > 0 && cleanupResults.failed !== eventIds.length) {
console.log(`⚠️ Failed to delete ${cleanupResults.failed} test event(s) (may have been already deleted)`);
}
}
async function cleanupAllTestEvents(): Promise<void> {
const allEventIds = testFactory.getCreatedEventIds();
await cleanupTestEvents(allEventIds);
testFactory.clearCreatedEventIds();
}
function logPerformanceSummary(): void {
const metrics = testFactory.getPerformanceMetrics();
if (metrics.length === 0) return;
console.log('\n📈 Final Performance Summary:');
const byOperation = metrics.reduce((acc, metric) => {
if (!acc[metric.operation]) {
acc[metric.operation] = {
count: 0,
totalDuration: 0,
successCount: 0,
errors: []
};
}
acc[metric.operation].count++;
acc[metric.operation].totalDuration += metric.duration;
if (metric.success) {
acc[metric.operation].successCount++;
} else if (metric.error) {
acc[metric.operation].errors.push(metric.error);
}
return acc;
}, {} as Record<string, { count: number; totalDuration: number; successCount: number; errors: string[] }>);
Object.entries(byOperation).forEach(([operation, stats]) => {
const avgDuration = Math.round(stats.totalDuration / stats.count);
const successRate = Math.round((stats.successCount / stats.count) * 100);
console.log(` ${operation}:`);
console.log(` Calls: ${stats.count}`);
console.log(` Avg Duration: ${avgDuration}ms`);
console.log(` Success Rate: ${successRate}%`);
if (stats.errors.length > 0) {
console.log(` Errors: ${stats.errors.length}`);
}
});
}
});
```