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');
});
});
});