Skip to main content
Glama
test-helpers.ts11.9 kB
/** * Reusable test utilities and assertion helpers for universal tool tests * * This file provides common testing utilities, assertion helpers, and * convenience functions used across multiple universal test files. * * Follows the mock factory pattern and maintains clean separation from * production code. */ import { expect } from 'vitest'; import { MockRecord } from './mock-data.js'; import { PERFORMANCE_BUDGETS, TEST_LOGGING } from './test-constants.js'; /** * Assertion Helpers * Common assertion patterns used across tests */ export const assertionHelpers = { /** * Assert that a record has the expected structure */ assertValidRecord: (record: MockRecord, expectedId?: string) => { expect(record).toBeDefined(); expect(record.id).toBeDefined(); expect(record.id.record_id).toBeDefined(); expect(record.values).toBeDefined(); expect(typeof record.values).toBe('object'); if (expectedId) { expect(record.id.record_id).toBe(expectedId); } }, /** * Assert that a company record has expected fields */ assertValidCompanyRecord: (record: MockRecord, expectedName?: string) => { assertionHelpers.assertValidRecord(record); expect(record.values.name).toBeDefined(); expect(Array.isArray(record.values.name)).toBe(true); expect(record.values.name[0].value).toBeDefined(); if (expectedName) { expect(record.values.name[0].value).toBe(expectedName); } }, /** * Assert that a person record has expected fields */ assertValidPersonRecord: (record: MockRecord, expectedEmail?: string) => { assertionHelpers.assertValidRecord(record); expect(record.values.email_addresses).toBeDefined(); expect(Array.isArray(record.values.email_addresses)).toBe(true); expect(record.values.email_addresses[0].email_address).toBeDefined(); if (expectedEmail) { expect(record.values.email_addresses[0].email_address).toBe( expectedEmail ); } }, /** * Assert that a task record has expected fields */ assertValidTaskRecord: (record: MockRecord, expectedTitle?: string) => { assertionHelpers.assertValidRecord(record); expect(record.values.title).toBeDefined(); expect(Array.isArray(record.values.title)).toBe(true); expect(record.values.title[0].value).toBeDefined(); if (expectedTitle) { expect(record.values.title[0].value).toBe(expectedTitle); } }, /** * Assert that a search result array is valid */ assertValidSearchResults: (results: MockRecord[], minCount: number = 0) => { expect(results).toBeDefined(); expect(Array.isArray(results)).toBe(true); expect(results.length).toBeGreaterThanOrEqual(minCount); // Validate each record in the results results.forEach((record) => { assertionHelpers.assertValidRecord(record); }); }, /** * Assert that a delete response is valid */ assertValidDeleteResponse: ( response: { success: boolean; record_id: string }, expectedId?: string ) => { expect(response).toBeDefined(); expect(response.success).toBe(true); expect(response.record_id).toBeDefined(); if (expectedId) { expect(response.record_id).toBe(expectedId); } }, /** * Assert that a batch operation response is valid */ assertValidBatchResponse: ( response: Array<{ success: boolean; result?: any; error?: string }>, expectedCount: number, minSuccessCount?: number ) => { expect(response).toBeDefined(); expect(Array.isArray(response)).toBe(true); expect(response).toHaveLength(expectedCount); if (minSuccessCount !== undefined) { const successCount = response.filter((r) => r.success).length; expect(successCount).toBeGreaterThanOrEqual(minSuccessCount); } }, }; /** * Performance Testing Helpers * Utilities for measuring and asserting performance */ export const performanceHelpers = { /** * Measure execution time of a function */ measureTime: async <T>( fn: () => Promise<T> ): Promise<{ result: T; duration: number }> => { const startTime = Date.now(); const result = await fn(); const endTime = Date.now(); const duration = endTime - startTime; return { result, duration }; }, /** * Assert that an operation completes within a performance budget */ assertWithinBudget: ( duration: number, budgetType: keyof typeof PERFORMANCE_BUDGETS ) => { const budget = PERFORMANCE_BUDGETS[budgetType]; expect(duration).toBeLessThan(budget); if (TEST_LOGGING.debugEnabled) { TEST_LOGGING.logPerformance(`${budgetType} operation`, duration); } }, /** * Log performance metrics for analysis */ logPerformance: (operation: string, duration: number, budget?: number) => { console.log( `${operation}: ${duration}ms${budget ? ` (budget: ${budget}ms)` : ''}` ); if (budget && duration > budget * 0.8) { console.warn( `⚠️ ${operation} took ${duration}ms (${((duration / budget) * 100).toFixed(1)}% of budget)` ); } }, /** * Create performance test wrapper */ createPerformanceTest: <T>( operation: string, budgetType: keyof typeof PERFORMANCE_BUDGETS, testFn: () => Promise<T> ) => { return async (): Promise<T> => { const { result, duration } = await performanceHelpers.measureTime(testFn); performanceHelpers.logPerformance( operation, duration, PERFORMANCE_BUDGETS[budgetType] ); performanceHelpers.assertWithinBudget(duration, budgetType); return result; }; }, }; /** * Test Data Helpers * Utilities for managing test data and cleanup */ export const testDataHelpers = { /** * Extract record IDs from results for cleanup */ extractRecordIds: ( results: Array<{ success: boolean; result?: any }> ): string[] => { return results .filter((r) => r.success && r.result?.id?.record_id) .map((r) => r.result.id.record_id); }, /** * Create unique test identifiers */ createTestIdentifiers: (prefix: string = 'test') => { const timestamp = Date.now(); const randomId = Math.random().toString(36).substring(7); return { timestamp, randomId, id: `${prefix}-${timestamp}-${randomId}`, name: `${prefix} ${timestamp}-${randomId}`, email: `${prefix}-${timestamp}-${randomId}@example.com`, domain: `${prefix}-${timestamp}-${randomId}.com`, }; }, /** * Wait for API indexing (integration tests) */ waitForIndexing: (ms: number = 2000): Promise<void> => { return new Promise((resolve) => setTimeout(resolve, ms)); }, /** * Retry operation with exponential backoff */ retryOperation: async <T>( operation: () => Promise<T>, maxRetries: number = 3, baseDelayMs: number = 1000 ): Promise<T> => { let lastError: any; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { return await operation(); } catch (error) { lastError = error; if (attempt === maxRetries) { throw error; } // Exponential backoff: 1s, 2s, 4s, etc. const delay = baseDelayMs * Math.pow(2, attempt - 1); await new Promise((resolve) => setTimeout(resolve, delay)); console.log( `Retry attempt ${attempt}/${maxRetries} after ${delay}ms delay` ); } } throw lastError; }, }; /** * Error Testing Helpers * Utilities for testing error scenarios */ export const errorTestHelpers = { /** * Assert that an error has expected properties */ assertError: ( error: any, expectedMessage?: string, expectedType?: string ) => { expect(error).toBeDefined(); expect(error.message).toBeDefined(); if (expectedMessage) { expect(error.message).toContain(expectedMessage); } if (expectedType) { expect(error.constructor.name).toBe(expectedType); } }, /** * Create error expectation helper */ expectError: (promise: Promise<any>, expectedMessage?: string) => { return expect(promise).rejects.toThrow(expectedMessage); }, /** * Test error propagation through handlers */ testErrorPropagation: async ( handler: (params: any) => Promise<any>, params: any, mockError: Error, expectedErrorMessage?: string ) => { // This would typically be used with a mocked function that throws await errorTestHelpers.expectError( handler(params), expectedErrorMessage || mockError.message ); }, }; /** * Mock Verification Helpers * Utilities for verifying mock calls and behavior */ export const mockVerificationHelpers = { /** * Assert that a mock was called with expected parameters */ assertMockCalledWith: (mockFn: any, expectedParams: any) => { expect(mockFn).toHaveBeenCalledWith(expectedParams); }, /** * Assert that multiple mocks were called in correct order */ assertMockCallOrder: (mocks: Array<{ mock: any; params?: any }>) => { mocks.forEach(({ mock, params }, index) => { expect(mock).toHaveBeenCalled(); if (params) { expect(mock).toHaveBeenCalledWith(params); } }); }, /** * Get call arguments from a mock for inspection */ getMockCallArgs: (mockFn: any, callIndex: number = 0) => { expect(mockFn).toHaveBeenCalled(); return mockFn.mock.calls[callIndex]; }, /** * Assert mock was called specified number of times */ assertMockCallCount: (mockFn: any, expectedCount: number) => { expect(mockFn).toHaveBeenCalledTimes(expectedCount); }, }; /** * Integration Test Helpers * Specific utilities for integration and API tests */ export const integrationHelpers = { /** * Check if integration tests should be skipped */ shouldSkipIntegration: (): boolean => { return !process.env.ATTIO_API_KEY; }, /** * Check if performance tests should be skipped */ shouldSkipPerformance: (): boolean => { return ( !process.env.ATTIO_API_KEY || process.env.SKIP_PERFORMANCE_TESTS === 'true' ); }, /** * Create skip message for tests */ getSkipMessage: (testType: 'integration' | 'performance'): string => { if (testType === 'integration') { return 'Skipping integration tests - no API key found'; } else { return 'Skipping performance tests - no API key found or explicitly skipped'; } }, /** * Setup API client for integration tests */ setupApiClient: async (apiKey?: string) => { const { initializeAttioClient } = await import( '../../../../../src/api/attio-client.js' ); const key = apiKey || process.env.ATTIO_API_KEY; if (!key) { throw new Error('No API key available for integration tests'); } initializeAttioClient(key); }, /** * Cleanup test records in batches */ cleanupTestRecords: async ( recordIds: string[], resourceType: string, deleteHandler: (params: any) => Promise<any> ) => { if (recordIds.length === 0) return; const CLEANUP_BATCH_SIZE = 45; // Stay under 50 limit const batches = []; for (let i = 0; i < recordIds.length; i += CLEANUP_BATCH_SIZE) { batches.push(recordIds.slice(i, i + CLEANUP_BATCH_SIZE)); } console.log( `Cleaning up ${recordIds.length} test records in ${batches.length} batches...` ); const cleanupPromises = batches.map(async (batch, index) => { // Stagger requests to avoid overwhelming the API if (index > 0) { await new Promise((resolve) => setTimeout(resolve, index * 100)); } return deleteHandler({ resource_type: resourceType, operation_type: 'DELETE', record_ids: batch, }); }); await Promise.all(cleanupPromises); console.log('Test cleanup completed successfully'); }, };

Latest Blog Posts

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/kesslerio/attio-mcp-server'

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