UNPKG

qapinterface

Version:

Comprehensive API utilities for Node.js applications including authentication, security, request processing, and response handling with zero external dependencies

562 lines (455 loc) 18.1 kB
/** * Comprehensive unit tests for services modules * Tests service health checks, retry logic, and error classification with mocked dependencies */ const { expect } = require('chai'); const sinon = require('sinon'); // Import modules to test const { checkServiceHealth } = require('../services/health-check'); const { validateServiceConnection } = require('../services/retry-handler'); const { validateServiceAsync } = require('../services/async-validator'); const { validateMultipleServices } = require('../services/multi-validator'); const { validateServiceWithErrorClassification } = require('../services/error-classifier'); const { connectWithRetry } = require('../service'); describe('Services Module Tests', () => { describe('Service Health Check', () => { it('should check healthy service', async () => { const mockHealthyService = { url: 'https://api.example.com/health', timeout: 5000, expectedStatus: 200 }; // Mock successful health check const originalFetch = global.fetch; global.fetch = sinon.stub().mockResolvedValue({ ok: true, status: 200, json: () => Promise.resolve({ status: 'healthy' }) }); const result = await checkServiceHealth(mockHealthyService); expect(result.healthy).to.equal(true); expect(typeof result.responseTime).to.equal('number'); expect(result.responseTime >= 0).to.equal(true); global.fetch = originalFetch; }); it('should detect unhealthy service', async () => { const mockUnhealthyService = { url: 'https://api.example.com/health', timeout: 5000, expectedStatus: 200 }; // Mock failed health check const originalFetch = global.fetch; global.fetch = sinon.stub().mockResolvedValue({ ok: false, status: 503, statusText: 'Service Unavailable' }); const result = await checkServiceHealth(mockUnhealthyService); expect(result.healthy).to.equal(false); expect(typeof result.error).to.equal('string'); expect(result.error.includes('503')).to.equal(true); global.fetch = originalFetch; }); it('should handle network errors', async () => { const mockService = { url: 'https://nonexistent.example.com/health', timeout: 1000 }; // Mock network error const originalFetch = global.fetch; global.fetch = sinon.stub().mockRejectedValue(new Error('ECONNREFUSED')); const result = await checkServiceHealth(mockService); expect(result.healthy).to.equal(false); expect(typeof result.error).to.equal('string'); expect(result.error.includes('ECONNREFUSED')).to.equal(true); global.fetch = originalFetch; }); it('should handle timeout errors', async () => { const mockService = { url: 'https://slow.example.com/health', timeout: 100 // Very short timeout }; // Mock slow response const originalFetch = global.fetch; global.fetch = sinon.stub().mockImplementation(() => new Promise(resolve => setTimeout(() => resolve({ ok: true }), 200)) ); const result = await checkServiceHealth(mockService); expect(result.healthy).to.equal(false); expect(typeof result.error).to.equal('string'); global.fetch = originalFetch; }); it('should validate service configuration', async () => { try { await checkServiceHealth(null); expect(false).to.equal(true); // Should not reach here } catch (error) { expect(error instanceof Error).to.equal(true); expect(error.message.includes('Service configuration')).to.equal(true); } try { await checkServiceHealth({ timeout: 5000 }); // Missing URL expect(false).to.equal(true); // Should not reach here } catch (error) { expect(error instanceof Error).to.equal(true); } }); }); describe('Service Retry Handler', () => { it('should retry on transient failures', async () => { let attemptCount = 0; const mockServiceCall = sinon.stub().mockImplementation(() => { attemptCount++; if (attemptCount < 3) { throw new Error('Service temporarily unavailable'); } return Promise.resolve({ success: true, data: 'Service restored' }); }); const result = await validateServiceConnection(mockServiceCall, { maxRetries: 3, baseDelay: 100 }); expect(result.success).to.equal(true); expect(result.data).to.equal('Service restored'); expect(mockServiceCall.callCount).to.equal(3); }); it('should fail after max retries', async () => { const mockServiceCall = sinon.stub().mockRejectedValue(new Error('Persistent failure')); try { await validateServiceConnection(mockServiceCall, { maxRetries: 2, baseDelay: 50 }); expect(false).to.equal(true); // Should not reach here } catch (error) { expect(error.message).to.equal('Persistent failure'); expect(mockServiceCall.callCount).to.equal(2); } }); it('should apply exponential backoff', async () => { const delays = []; const originalSetTimeout = global.setTimeout; global.setTimeout = jest.fn((callback, delay) => { delays.push(delay); callback(); }); const mockServiceCall = sinon.stub().mockRejectedValue(new Error('Always fails')); try { await validateServiceConnection(mockServiceCall, { maxRetries: 3, baseDelay: 100 }); } catch (error) { // Expected to fail } global.setTimeout = originalSetTimeout; expect(delays.length).to.equal(2); // 3 attempts = 2 delays expect(delays[0]).to.equal(100); // First delay expect(delays[1]).to.equal(200); // Second delay (exponential) }); it('should not retry on non-retryable errors', async () => { const mockServiceCall = sinon.stub().mockRejectedValue(new Error('Authentication failed')); // Mock error classifier to mark as non-retryable const originalRequire = require; require = jest.fn((path) => { if (path.includes('error-classifier')) { return { classifyError: () => ({ retryable: false, category: 'auth' }) }; } return originalRequire(path); }); try { await validateServiceConnection(mockServiceCall, { maxRetries: 3 }); expect(false).to.equal(true); // Should not reach here } catch (error) { expect(mockServiceCall.callCount).to.equal(1); // No retries } require = originalRequire; }); }); describe('Async Service Validator', () => { it('should validate service asynchronously', async () => { const mockService = { name: 'test-service', healthCheck: () => Promise.resolve({ healthy: true }) }; const result = await validateServiceAsync(mockService); expect(result.valid).to.equal(true); expect(result.serviceName).to.equal('test-service'); expect(typeof result.validatedAt).to.equal('string'); }); it('should handle async validation failures', async () => { const mockService = { name: 'failing-service', healthCheck: () => Promise.reject(new Error('Service down')) }; const result = await validateServiceAsync(mockService); expect(result.valid).to.equal(false); expect(result.serviceName).to.equal('failing-service'); expect(typeof result.error).to.equal('string'); }); it('should apply timeout to async validation', async () => { const mockService = { name: 'slow-service', healthCheck: () => new Promise(resolve => setTimeout(resolve, 2000)) }; const result = await validateServiceAsync(mockService, { timeout: 500 }); expect(result.valid).to.equal(false); expect(result.error.includes('timeout')).to.equal(true); }); it('should validate multiple services concurrently', async () => { const mockServices = [ { name: 'service-1', healthCheck: () => Promise.resolve({ healthy: true }) }, { name: 'service-2', healthCheck: () => Promise.resolve({ healthy: true }) }, { name: 'service-3', healthCheck: () => Promise.reject(new Error('Service 3 down')) } ]; const results = await Promise.all( mockServices.map(service => validateServiceAsync(service)) ); expect(results.length).to.equal(3); expect(results[0].valid).to.equal(true); expect(results[1].valid).to.equal(true); expect(results[2].valid).to.equal(false); }); }); describe('Multi-Service Validator', () => { it('should validate multiple services', async () => { const mockServices = [ { name: 'database', url: 'https://db.example.com/health' }, { name: 'cache', url: 'https://cache.example.com/health' } ]; // Mock successful health checks const originalFetch = global.fetch; global.fetch = sinon.stub().mockResolvedValue({ ok: true, status: 200, json: () => Promise.resolve({ status: 'healthy' }) }); const result = await validateMultipleServices(mockServices); expect(result.overallHealth).to.equal('healthy'); expect(Object.keys(result.services).length).to.equal(2); expect(result.services.database.healthy).to.equal(true); expect(result.services.cache.healthy).to.equal(true); expect(result.unhealthyCount).to.equal(0); global.fetch = originalFetch; }); it('should detect degraded system health', async () => { const mockServices = [ { name: 'database', url: 'https://db.example.com/health' }, { name: 'cache', url: 'https://cache.example.com/health' } ]; // Mock mixed health responses const originalFetch = global.fetch; global.fetch = sinon.stub() .mockResolvedValueOnce({ ok: true, status: 200, json: () => Promise.resolve({ status: 'healthy' }) }) .mockResolvedValueOnce({ ok: false, status: 503, statusText: 'Service Unavailable' }); const result = await validateMultipleServices(mockServices); expect(result.overallHealth).to.equal('degraded'); expect(result.services.database.healthy).to.equal(true); expect(result.services.cache.healthy).to.equal(false); expect(result.unhealthyCount).to.equal(1); global.fetch = originalFetch; }); it('should detect unhealthy system', async () => { const mockServices = [ { name: 'database', url: 'https://db.example.com/health' }, { name: 'cache', url: 'https://cache.example.com/health' } ]; // Mock all services down const originalFetch = global.fetch; global.fetch = sinon.stub().mockRejectedValue(new Error('Connection refused')); const result = await validateMultipleServices(mockServices); expect(result.overallHealth).to.equal('unhealthy'); expect(result.services.database.healthy).to.equal(false); expect(result.services.cache.healthy).to.equal(false); expect(result.unhealthyCount).to.equal(2); global.fetch = originalFetch; }); it('should handle empty service list', async () => { const result = await validateMultipleServices([]); expect(result.overallHealth).to.equal('healthy'); expect(Object.keys(result.services).length).to.equal(0); expect(result.unhealthyCount).to.equal(0); }); }); describe('Error Classification Service', () => { it('should classify network errors as retryable', () => { const networkError = new Error('ECONNREFUSED'); networkError.code = 'ECONNREFUSED'; const result = validateServiceWithErrorClassification(networkError); expect(result.retryable).to.equal(true); expect(result.category).to.equal('network'); expect(typeof result.recommendedDelay).to.equal('number'); }); it('should classify server errors as retryable', () => { const serverError = new Error('Internal Server Error'); serverError.status = 500; const result = validateServiceWithErrorClassification(serverError); expect(result.retryable).to.equal(true); expect(result.category).to.equal('server'); }); it('should classify client errors as non-retryable', () => { const clientError = new Error('Bad Request'); clientError.status = 400; const result = validateServiceWithErrorClassification(clientError); expect(result.retryable).to.equal(false); expect(result.category).to.equal('client'); }); it('should classify authentication errors as non-retryable', () => { const authError = new Error('Unauthorized'); authError.status = 401; const result = validateServiceWithErrorClassification(authError); expect(result.retryable).to.equal(false); expect(result.category).to.equal('auth'); }); it('should handle rate limiting with longer delay', () => { const rateLimitError = new Error('Too Many Requests'); rateLimitError.status = 429; const result = validateServiceWithErrorClassification(rateLimitError); expect(result.retryable).to.equal(true); expect(result.category).to.equal('rate_limit'); expect(result.recommendedDelay > 5000).to.equal(true); // Longer delay for rate limiting }); it('should handle unknown errors', () => { const unknownError = new Error('Unknown error'); const result = validateServiceWithErrorClassification(unknownError); expect(result.category).to.equal('unknown'); expect(typeof result.retryable).to.equal('boolean'); }); }); describe('Service Connection with Retry', () => { it('should connect successfully on first attempt', async () => { const mockConnectionFn = sinon.stub().mockResolvedValue({ connected: true, data: 'Connection established' }); const result = await connectWithRetry(mockConnectionFn); expect(result.connected).to.equal(true); expect(result.data).to.equal('Connection established'); expect(mockConnectionFn.callCount).to.equal(1); }); it('should retry with exponential backoff', async () => { let attemptCount = 0; const mockConnectionFn = sinon.stub().mockImplementation(() => { attemptCount++; if (attemptCount < 3) { throw new Error('Connection failed'); } return Promise.resolve({ connected: true }); }); const result = await connectWithRetry(mockConnectionFn, { retries: 3 }); expect(result.connected).to.equal(true); expect(mockConnectionFn.callCount).to.equal(3); }); it('should fail after exhausting retries', async () => { const mockConnectionFn = sinon.stub().mockRejectedValue(new Error('Persistent connection failure')); try { await connectWithRetry(mockConnectionFn, { retries: 2 }); expect(false).to.equal(true); // Should not reach here } catch (error) { expect(error.message).to.equal('Persistent connection failure'); expect(mockConnectionFn.callCount).to.equal(2); } }); it('should use custom retry options', async () => { const delays = []; const originalSetTimeout = global.setTimeout; global.setTimeout = jest.fn((callback, delay) => { delays.push(delay); callback(); }); const mockConnectionFn = sinon.stub().mockRejectedValue(new Error('Custom retry test')); try { await connectWithRetry(mockConnectionFn, { retries: 2 }); } catch (error) { // Expected to fail } global.setTimeout = originalSetTimeout; expect(delays.length).to.equal(1); // 2 attempts = 1 delay expect(delays[0]).to.equal(1000); // First delay is 1 second }); }); describe('Service Integration Tests', () => { it('should integrate health check with retry logic', async () => { let healthCheckCount = 0; const mockService = { name: 'integration-test-service', healthCheck: () => { healthCheckCount++; if (healthCheckCount < 2) { throw new Error('Service starting up'); } return Promise.resolve({ healthy: true, status: 'operational' }); } }; const result = await validateServiceConnection( () => mockService.healthCheck(), { maxRetries: 3, baseDelay: 50 } ); expect(result.healthy).to.equal(true); expect(result.status).to.equal('operational'); expect(healthCheckCount).to.equal(2); }); it('should handle service dependency chain', async () => { const serviceChain = [ { name: 'database', dependency: null }, { name: 'cache', dependency: 'database' }, { name: 'api', dependency: 'cache' } ]; // Mock health checks for dependency chain const healthChecks = { database: () => Promise.resolve({ healthy: true }), cache: () => Promise.resolve({ healthy: true }), api: () => Promise.resolve({ healthy: true }) }; const results = []; for (const service of serviceChain) { const result = await validateServiceAsync({ name: service.name, healthCheck: healthChecks[service.name] }); results.push(result); } expect(results.every(r => r.valid)).to.equal(true); expect(results.length).to.equal(3); }); }); }); module.exports = { runServicesTests: () => expect().to.be.undefined };