Skip to main content
Glama
api-clients.test.tsโ€ข16.5 kB
import { describe, it } from 'node:test'; import { strict as assert } from 'node:assert'; /** * Unit tests for API client classes * * Tests the following classes: * - ZebrunnerApiClient * - EnhancedZebrunnerClient * - ReportingApiClient */ describe('API Clients Unit Tests', () => { describe('ZebrunnerApiClient', () => { it('should validate constructor parameters', () => { const validConfig = { baseUrl: 'https://test.zebrunner.com', username: 'testuser', password: 'testpass' }; assert.ok(validConfig.baseUrl, 'baseUrl should be required'); assert.ok(validConfig.username, 'username should be required'); assert.ok(validConfig.password, 'password should be required'); assert.ok(validConfig.baseUrl.startsWith('http'), 'baseUrl should be valid URL'); }); it('should validate URL construction', () => { const baseUrl = 'https://test.zebrunner.com'; const endpoint = '/api/tcm/v1/test-suites'; const expectedUrl = `${baseUrl}${endpoint}`; assert.equal(expectedUrl, 'https://test.zebrunner.com/api/tcm/v1/test-suites', 'should construct correct URL'); }); it('should validate authentication headers', () => { const credentials = { username: 'testuser', password: 'testpass' }; const basicAuth = Buffer.from(`${credentials.username}:${credentials.password}`).toString('base64'); const expectedHeader = `Basic ${basicAuth}`; assert.ok(basicAuth.length > 0, 'should generate base64 auth'); assert.ok(expectedHeader.startsWith('Basic '), 'should format auth header correctly'); }); it('should validate request parameters', () => { const validParams = { projectKey: 'MCP', page: 0, size: 50 }; assert.ok(validParams.projectKey.length > 0, 'projectKey should not be empty'); assert.ok(validParams.page >= 0, 'page should be non-negative'); assert.ok(validParams.size > 0, 'size should be positive'); assert.ok(validParams.size <= 1000, 'size should not exceed reasonable limit'); }); it('should validate response structure', () => { const mockApiResponse = { items: [ { id: 18815, title: 'Treatment ON', parentSuiteId: 18814 } ], _meta: { nextPageToken: 'token123', totalElements: 1188, currentPage: 0, pageSize: 50 } }; assert.ok(Array.isArray(mockApiResponse.items), 'response should have items array'); assert.ok(mockApiResponse._meta, 'response should have metadata'); assert.ok(typeof mockApiResponse._meta.totalElements === 'number', 'totalElements should be number'); }); it('should handle error responses', () => { const errorScenarios = [ { status: 401, message: 'Unauthorized' }, { status: 403, message: 'Forbidden' }, { status: 404, message: 'Not Found' }, { status: 500, message: 'Internal Server Error' } ]; errorScenarios.forEach(scenario => { assert.ok(scenario.status >= 400, `Status ${scenario.status} should be error status`); assert.ok(scenario.message.length > 0, 'Error should have message'); }); }); }); describe('EnhancedZebrunnerClient', () => { it('should extend base client functionality', () => { const baseClientMethods = ['getTestSuites', 'getTestCases', 'getTestCaseById']; const enhancedMethods = ['getAllTestSuites', 'getAllTestCases', 'testConnection']; // Simulate method availability check const allMethods = [...baseClientMethods, ...enhancedMethods]; assert.ok(allMethods.includes('getTestSuites'), 'should have base methods'); assert.ok(allMethods.includes('getAllTestSuites'), 'should have enhanced methods'); assert.ok(allMethods.includes('testConnection'), 'should have connection testing'); }); it('should validate pagination handling', () => { const paginationConfig = { maxPages: 50, defaultPageSize: 50, maxPageSize: 100 }; assert.ok(paginationConfig.maxPages > 0, 'maxPages should be positive'); assert.ok(paginationConfig.defaultPageSize > 0, 'defaultPageSize should be positive'); assert.ok(paginationConfig.maxPageSize >= paginationConfig.defaultPageSize, 'maxPageSize should be >= defaultPageSize'); }); it('should validate token-based pagination logic', () => { const paginationState = { pageToken: 'abc123', pageCount: 5, maxPages: 50, hasMore: true }; const shouldContinue = !!(paginationState.pageToken && paginationState.pageCount < paginationState.maxPages && paginationState.hasMore); assert.equal(typeof shouldContinue, 'boolean', 'shouldContinue should be boolean'); assert.ok(shouldContinue, 'should continue when conditions are met'); }); it('should validate connection testing', () => { const connectionTestEndpoints = [ '/api/tcm/v1/test-suites', '/api/tcm/v1/projects', '/api/iam/v1/users/profile' ]; connectionTestEndpoints.forEach(endpoint => { assert.ok(endpoint.startsWith('/api/'), 'endpoint should start with /api/'); assert.ok(endpoint.includes('/v1/'), 'endpoint should include version'); }); }); it('should handle batch processing', () => { const BATCH_SIZE = 100; const TOTAL_ITEMS = 4579; const expectedBatches = Math.ceil(TOTAL_ITEMS / BATCH_SIZE); assert.ok(expectedBatches > 1, 'should require multiple batches'); assert.ok(expectedBatches <= 50, 'should not require excessive batches'); assert.equal(expectedBatches, 46, 'should calculate correct batch count for MCP'); }); it('should validate suite enrichment', () => { const mockSuite = { id: 17470, title: 'Budget', parentSuiteId: 17468 }; const enrichedSuite = { ...mockSuite, rootSuiteId: 17441, rootSuiteName: '10. Meal Planner', parentSuiteName: 'Settings', treeNames: '10. Meal Planner > Settings > Budget', level: 2 }; assert.ok(enrichedSuite.rootSuiteId, 'enriched suite should have rootSuiteId'); assert.ok(enrichedSuite.treeNames, 'enriched suite should have tree path'); assert.ok(typeof enrichedSuite.level === 'number', 'level should be number'); assert.ok(enrichedSuite.treeNames.includes(' > '), 'tree path should use separator'); }); }); describe('ReportingApiClient', () => { it('should validate reporting endpoints', () => { const reportingEndpoints = [ '/api/reporting/v1/test-runs', '/api/reporting/v1/launchers', '/api/reporting/v1/platforms' ]; reportingEndpoints.forEach(endpoint => { assert.ok(endpoint.startsWith('/api/reporting/'), 'reporting endpoint should start with /api/reporting/'); assert.ok(endpoint.includes('/v1/'), 'endpoint should include version'); }); }); it('should validate date range parameters', () => { const dateRange = { fromDate: '2024-01-01', toDate: '2024-01-31' }; const fromDate = new Date(dateRange.fromDate); const toDate = new Date(dateRange.toDate); assert.ok(!isNaN(fromDate.getTime()), 'fromDate should be valid date'); assert.ok(!isNaN(toDate.getTime()), 'toDate should be valid date'); assert.ok(toDate >= fromDate, 'toDate should be after fromDate'); }); it('should validate launcher parameters', () => { const launcherParams = { launcherId: 12345, projectKey: 'MCP', includeDetails: true }; assert.ok(launcherParams.launcherId > 0, 'launcherId should be positive'); assert.ok(launcherParams.projectKey.length > 0, 'projectKey should not be empty'); assert.equal(typeof launcherParams.includeDetails, 'boolean', 'includeDetails should be boolean'); }); it('should validate platform results structure', () => { const mockPlatformResult = { platform: 'Android', version: '14.0', testResults: { total: 100, passed: 85, failed: 10, skipped: 5 }, executionTime: 3600000, // milliseconds timestamp: '2024-01-15T10:30:00Z' }; assert.ok(mockPlatformResult.platform, 'result should have platform'); assert.ok(mockPlatformResult.testResults, 'result should have test results'); assert.ok(typeof mockPlatformResult.testResults.total === 'number', 'total should be number'); assert.equal( mockPlatformResult.testResults.total, mockPlatformResult.testResults.passed + mockPlatformResult.testResults.failed + mockPlatformResult.testResults.skipped, 'test counts should add up' ); }); }); describe('HTTP Client Behavior', () => { it('should validate request timeout handling', () => { const timeoutConfig = { connectionTimeout: 30000, // 30 seconds requestTimeout: 120000, // 2 minutes retryTimeout: 5000 // 5 seconds }; assert.ok(timeoutConfig.connectionTimeout > 0, 'connection timeout should be positive'); assert.ok(timeoutConfig.requestTimeout > timeoutConfig.connectionTimeout, 'request timeout should be longer than connection timeout'); assert.ok(timeoutConfig.retryTimeout < timeoutConfig.connectionTimeout, 'retry timeout should be shorter than connection timeout'); }); it('should validate retry logic', () => { const retryConfig = { maxRetries: 3, retryableStatusCodes: [429, 500, 502, 503, 504], backoffMultiplier: 2 }; assert.ok(retryConfig.maxRetries > 0, 'should have positive max retries'); assert.ok(retryConfig.maxRetries <= 5, 'should not retry excessively'); assert.ok(retryConfig.retryableStatusCodes.includes(500), 'should retry on server errors'); assert.ok(retryConfig.retryableStatusCodes.includes(429), 'should retry on rate limiting'); assert.ok(!retryConfig.retryableStatusCodes.includes(404), 'should not retry on client errors'); }); it('should validate request headers', () => { const defaultHeaders = { 'Content-Type': 'application/json', 'Accept': 'application/json', 'User-Agent': 'mcp-zebrunner/1.0.0' }; assert.equal(defaultHeaders['Content-Type'], 'application/json', 'should set JSON content type'); assert.equal(defaultHeaders['Accept'], 'application/json', 'should accept JSON responses'); assert.ok(defaultHeaders['User-Agent'].includes('mcp-zebrunner'), 'should identify client'); }); it('should validate response parsing', () => { const mockJsonResponse = '{"items": [{"id": 1, "title": "Test"}], "_meta": {"totalElements": 1}}'; const mockEmptyResponse = ''; const mockInvalidJson = '{"invalid": json}'; // Simulate JSON parsing try { const parsed = JSON.parse(mockJsonResponse); assert.ok(parsed.items, 'should parse valid JSON'); assert.ok(Array.isArray(parsed.items), 'items should be array'); } catch (error) { assert.fail('Should parse valid JSON without error'); } // Test empty response handling const isEmpty = mockEmptyResponse.length === 0; assert.ok(isEmpty, 'should detect empty response'); // Test invalid JSON handling try { JSON.parse(mockInvalidJson); assert.fail('Should throw error for invalid JSON'); } catch (error) { assert.ok(error instanceof SyntaxError, 'Should throw SyntaxError for invalid JSON'); } }); }); describe('Error Handling', () => { it('should validate API error structure', () => { const mockApiError = { status: 400, statusText: 'Bad Request', message: 'Invalid project key', details: { field: 'projectKey', value: 'INVALID', reason: 'Project not found' } }; assert.ok(mockApiError.status >= 400, 'error should have error status code'); assert.ok(mockApiError.message, 'error should have message'); assert.ok(mockApiError.statusText, 'error should have status text'); }); it('should validate network error handling', () => { const networkErrors = [ 'ECONNREFUSED', 'ENOTFOUND', 'ETIMEDOUT', 'ECONNRESET' ]; networkErrors.forEach(errorCode => { assert.ok(errorCode.startsWith('E'), 'network error codes should start with E'); assert.ok(errorCode.length > 1, 'error codes should be meaningful'); }); }); it('should validate authentication error handling', () => { const authErrors = [ { status: 401, message: 'Invalid credentials' }, { status: 403, message: 'Access denied' }, { status: 401, message: 'Token expired' } ]; authErrors.forEach(error => { assert.ok(error.status === 401 || error.status === 403, 'should be auth error status'); assert.ok(error.message.length > 0, 'should have error message'); }); }); it('should validate rate limiting handling', () => { const rateLimitError = { status: 429, message: 'Too Many Requests', headers: { 'Retry-After': '60', 'X-RateLimit-Remaining': '0', 'X-RateLimit-Reset': '1640995200' } }; assert.equal(rateLimitError.status, 429, 'should be rate limit status'); assert.ok(rateLimitError.headers['Retry-After'], 'should have retry after header'); assert.ok(parseInt(rateLimitError.headers['Retry-After']) > 0, 'retry after should be positive'); }); }); describe('Performance Considerations', () => { it('should validate connection pooling', () => { const poolConfig = { maxConnections: 10, keepAlive: true, keepAliveMsecs: 30000 }; assert.ok(poolConfig.maxConnections > 0, 'should have positive max connections'); assert.ok(poolConfig.maxConnections <= 20, 'should not have excessive connections'); assert.equal(poolConfig.keepAlive, true, 'should use keep-alive'); assert.ok(poolConfig.keepAliveMsecs > 0, 'keep-alive timeout should be positive'); }); it('should validate request batching', () => { const batchConfig = { batchSize: 100, maxConcurrentRequests: 5, batchDelay: 100 // milliseconds }; assert.ok(batchConfig.batchSize > 0, 'batch size should be positive'); assert.ok(batchConfig.batchSize <= 1000, 'batch size should be reasonable'); assert.ok(batchConfig.maxConcurrentRequests > 0, 'should allow concurrent requests'); assert.ok(batchConfig.maxConcurrentRequests <= 10, 'should limit concurrent requests'); }); it('should validate memory usage for large datasets', () => { const MCP_TOTAL_CASES = 4579; const BATCH_SIZE = 100; const MEMORY_PER_CASE = 1024; // bytes const totalMemory = MCP_TOTAL_CASES * MEMORY_PER_CASE; const batchMemory = BATCH_SIZE * MEMORY_PER_CASE; assert.ok(batchMemory < totalMemory, 'batch processing should use less memory'); assert.ok(batchMemory < 1024 * 1024, 'batch memory should be under 1MB'); }); it('should validate caching strategy', () => { const cacheConfig = { enableCache: true, cacheTTL: 300000, // 5 minutes maxCacheSize: 100, cacheableEndpoints: ['/test-suites', '/projects'] }; assert.equal(cacheConfig.enableCache, true, 'should enable caching'); assert.ok(cacheConfig.cacheTTL > 0, 'cache TTL should be positive'); assert.ok(cacheConfig.maxCacheSize > 0, 'cache size should be positive'); assert.ok(Array.isArray(cacheConfig.cacheableEndpoints), 'cacheable endpoints should be array'); }); }); });

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/maksimsarychau/mcp-zebrunner'

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