/** * Integration Test: Health Check Use Case Orchestration * * Tests the orchestration logic of health check-related Use Cases: * - CheckApiHealthUseCase: Executes health checks and returns status * - GetConnectionStatusUseCase: Retrieves current connection status * - Validates that Use Cases correctly interact with their Ports (Health Check Adapter, Event Publisher) * - Uses In-Memory adapters for fast, deterministic testing * * Focus: Business logic orchestration, NOT UI rendering */ import { describe, it, expect, beforeAll, beforeEach } from 'vitest'; import { InMemoryHealthCheckAdapter } from '../../../adapters/health/persistence/inmemory/InMemoryHealthCheckAdapter'; import { InMemoryHealthEventPublisher } from '../../../adapters/events/InMemoryHealthEventPublisher'; import { CheckApiHealthUseCase } from '../../../core/health/use-cases/CheckApiHealthUseCase'; import { GetConnectionStatusUseCase } from '../../../core/health/use-cases/GetConnectionStatusUseCase'; describe('Health Check Use Case Orchestration', () => { let healthCheckAdapter: InMemoryHealthCheckAdapter; let eventPublisher: InMemoryHealthEventPublisher; let checkApiHealthUseCase: CheckApiHealthUseCase; let getConnectionStatusUseCase: GetConnectionStatusUseCase; beforeAll(() => { // Initialize In-Memory adapters and event publisher healthCheckAdapter = new InMemoryHealthCheckAdapter(); eventPublisher = new InMemoryHealthEventPublisher(); checkApiHealthUseCase = new CheckApiHealthUseCase({ healthCheckAdapter, eventPublisher, }); getConnectionStatusUseCase = new GetConnectionStatusUseCase({ healthCheckAdapter, }); }); beforeEach(() => { // Clear all In-Memory repositories before each test healthCheckAdapter.clear(); eventPublisher.clear(); }); describe('CheckApiHealthUseCase - Success Path', () => { it('should perform health check and return healthy status', async () => { // Scenario: API is healthy and responsive // Given: HealthCheckAdapter returns successful response // And: Response time is 50ms healthCheckAdapter.setResponseTime(50); // When: CheckApiHealthUseCase.execute() is called const result = await checkApiHealthUseCase.execute(); // Then: Result should show healthy=true expect(result.healthy).toBe(true); // And: Response time should be 50ms expect(result.responseTime).toBeGreaterThanOrEqual(50); // And: Timestamp should be present expect(result.timestamp).toBeInstanceOf(Date); // And: EventPublisher should emit HealthCheckCompletedEvent expect(eventPublisher.getEventCountByType('HealthCheckCompleted')).toBe(1); }); it('should perform health check with slow response time', async () => { // Scenario: API is healthy but slow // Given: HealthCheckAdapter returns successful response // And: Response time is 500ms healthCheckAdapter.setResponseTime(500); // When: CheckApiHealthUseCase.execute() is called const result = await checkApiHealthUseCase.execute(); // Then: Result should show healthy=true expect(result.healthy).toBe(true); // And: Response time should be 500ms expect(result.responseTime).toBeGreaterThanOrEqual(500); // And: EventPublisher should emit HealthCheckCompletedEvent expect(eventPublisher.getEventCountByType('HealthCheckCompleted')).toBe(1); }); it('should handle health check with custom endpoint', async () => { // Scenario: Health check on custom endpoint // Given: HealthCheckAdapter returns success for /custom/health healthCheckAdapter.configureResponse('/custom/health', { healthy: true, responseTime: 50, timestamp: new Date(), }); // When: CheckApiHealthUseCase.execute() is called const result = await checkApiHealthUseCase.execute(); // Then: Result should show healthy=true expect(result.healthy).toBe(true); // And: EventPublisher should emit HealthCheckCompletedEvent expect(eventPublisher.getEventCountByType('HealthCheckCompleted')).toBe(1); }); }); describe('CheckApiHealthUseCase - Failure Path', () => { it('should handle failed health check and return unhealthy status', async () => { // Scenario: API is unreachable // Given: HealthCheckAdapter throws network error healthCheckAdapter.setShouldFail(true, 'ECONNREFUSED'); // When: CheckApiHealthUseCase.execute() is called const result = await checkApiHealthUseCase.execute(); // Then: Result should show healthy=false expect(result.healthy).toBe(false); // And: Error message should be present expect(result.error).toBeDefined(); // And: EventPublisher should emit HealthCheckFailedEvent expect(eventPublisher.getEventCountByType('HealthCheckFailed')).toBe(1); }); it('should handle timeout during health check', async () => { // Scenario: Health check times out // Given: HealthCheckAdapter times out after 30 seconds healthCheckAdapter.setShouldFail(true, 'Timeout'); // When: CheckApiHealthUseCase.execute() is called const result = await checkApiHealthUseCase.execute(); // Then: Result should show healthy=false expect(result.healthy).toBe(false); // And: Error should indicate timeout expect(result.error).toContain('Timeout'); // And: EventPublisher should emit HealthCheckTimeoutEvent expect(eventPublisher.getEventCountByType('HealthCheckTimeout')).toBe(1); }); it('should handle malformed response from health endpoint', async () => { // Scenario: Health endpoint returns invalid JSON // Given: HealthCheckAdapter returns malformed response healthCheckAdapter.setShouldFail(true, 'Invalid JSON'); // When: CheckApiHealthUseCase.execute() is called const result = await checkApiHealthUseCase.execute(); // Then: Result should show healthy=false expect(result.healthy).toBe(false); // And: Error should indicate parsing failure expect(result.error).toContain('Invalid JSON'); // And: EventPublisher should emit HealthCheckFailedEvent expect(eventPublisher.getEventCountByType('HealthCheckFailed')).toBe(1); }); }); describe('GetConnectionStatusUseCase - Success Path', () => { it('should retrieve connection status when healthy', async () => { // Scenario: Connection is healthy // Given: HealthCheckAdapter has successful checks // And: Connection status is 'connected' healthCheckAdapter.setResponseTime(50); // Perform successful health check await checkApiHealthUseCase.execute(); // When: GetConnectionStatusUseCase.execute() is called const result = await getConnectionStatusUseCase.execute(); // Then: Result should show status='connected' expect(result.status).toBe('connected'); // And: Reliability should be 100% expect(result.reliability).toBe(100); // And: Last check timestamp should be present expect(result.lastCheck).toBeInstanceOf(Date); }); it('should retrieve connection status when degraded', async () => { // Scenario: Connection is degraded // Given: HealthCheckAdapter has mixed results (5 success, 3 fail) // And: Connection status is 'degraded' healthCheckAdapter.setResponseTime(50); // Perform 5 successful checks for (let i = 0; i < 5; i++) { await checkApiHealthUseCase.execute(); } // Now start failing healthCheckAdapter.setShouldFail(true, 'ECONNREFUSED'); // Perform 3 failed checks for (let i = 0; i < 3; i++) { await checkApiHealthUseCase.execute(); } // When: GetConnectionStatusUseCase.execute() is called const result = await getConnectionStatusUseCase.execute(); // Then: Result should show status='degraded' expect(result.status).toBe('degraded'); // And: Reliability should be 62.5% expect(result.reliability).toBeCloseTo(62.5, 1); // And: Consecutive failures should be 0 expect(result.consecutiveFailures).toBe(0); }); it('should retrieve connection status when disconnected', async () => { // Scenario: Connection is disconnected // Given: HealthCheckAdapter has 3 consecutive failures // And: Connection status is 'disconnected' healthCheckAdapter.setShouldFail(true, 'ECONNREFUSED'); // Perform 3 failed checks for (let i = 0; i < 3; i++) { await checkApiHealthUseCase.execute(); } // When: GetConnectionStatusUseCase.execute() is called const result = await getConnectionStatusUseCase.execute(); // Then: Result should show status='disconnected' expect(result.status).toBe('disconnected'); // And: Consecutive failures should be 3 expect(result.consecutiveFailures).toBe(3); // And: Last failure timestamp should be present expect(result.lastFailure).toBeInstanceOf(Date); }); it('should retrieve connection status when checking', async () => { // Scenario: Connection status is checking // Given: No health checks performed yet // And: Connection status is 'checking' // When: GetConnectionStatusUseCase.execute() is called const result = await getConnectionStatusUseCase.execute(); // Then: Result should show status='checking' expect(result.status).toBe('checking'); // And: Reliability should be 0 expect(result.reliability).toBe(0); }); }); describe('GetConnectionStatusUseCase - Metrics', () => { it('should calculate reliability correctly', async () => { // Scenario: Calculate reliability from mixed results // Given: 7 successful requests and 3 failed requests healthCheckAdapter.setResponseTime(50); // Perform 7 successful checks for (let i = 0; i < 7; i++) { await checkApiHealthUseCase.execute(); } // Now start failing healthCheckAdapter.setShouldFail(true, 'ECONNREFUSED'); // Perform 3 failed checks for (let i = 0; i < 3; i++) { await checkApiHealthUseCase.execute(); } // When: GetConnectionStatusUseCase.execute() is called const result = await getConnectionStatusUseCase.execute(); // Then: Result should show reliability=70% expect(result.reliability).toBeCloseTo(70, 1); // And: Total requests should be 10 expect(result.totalRequests).toBe(10); // And: Successful requests should be 7 expect(result.successfulRequests).toBe(7); // And: Failed requests should be 3 expect(result.failedRequests).toBe(3); }); it('should calculate average response time correctly', async () => { // Scenario: Calculate average from varying response times // Given: Response times of 50ms, 100ms, 150ms const responseTimes = [50, 100, 150]; // Mock different response times let callCount = 0; const originalPerformHealthCheck = healthCheckAdapter.performHealthCheck.bind(healthCheckAdapter); healthCheckAdapter.performHealthCheck = async () => { const time = responseTimes[callCount] || 50; callCount++; await new Promise(resolve => setTimeout(resolve, time)); return { healthy: true, responseTime: time, timestamp: new Date(), }; }; // Perform 3 health checks await checkApiHealthUseCase.execute(); await checkApiHealthUseCase.execute(); await checkApiHealthUseCase.execute(); // When: GetConnectionStatusUseCase.execute() is called const result = await getConnectionStatusUseCase.execute(); // Then: Result should show averageResponseTime=100ms expect(result.averageResponseTime).toBeCloseTo(100, 1); }); it('should handle zero requests for metrics calculation', async () => { // Scenario: No requests made yet // Given: No health checks performed // When: GetConnectionStatusUseCase.execute() is called const result = await getConnectionStatusUseCase.execute(); // Then: Result should show reliability=0 expect(result.reliability).toBe(0); // And: Average response time should be 0 expect(result.averageResponseTime).toBe(0); // And: Total requests should be 0 expect(result.totalRequests).toBe(0); }); }); describe('Health Check Data Orchestration', () => { it('should correctly format health check result with all fields', async () => { // Scenario: Complete health check result // Given: HealthCheckAdapter returns successful response // And: Response time is 75ms healthCheckAdapter.setResponseTime(75); // When: CheckApiHealthUseCase.execute() is called const result = await checkApiHealthUseCase.execute(); // Then: Result should contain: expect(result.healthy).toBe(true); expect(result.responseTime).toBeGreaterThanOrEqual(75); expect(result.timestamp).toBeInstanceOf(Date); expect(result.error).toBeUndefined(); }); it('should correctly format connection status with all fields', async () => { // Scenario: Complete connection status // Given: HealthCheckAdapter has 5 success, 3 fail healthCheckAdapter.setResponseTime(50); // Perform 5 successful checks for (let i = 0; i < 5; i++) { await checkApiHealthUseCase.execute(); } // Now start failing healthCheckAdapter.setShouldFail(true, 'ECONNREFUSED'); // Perform 3 failed checks for (let i = 0; i < 3; i++) { await checkApiHealthUseCase.execute(); } // When: GetConnectionStatusUseCase.execute() is called const result = await getConnectionStatusUseCase.execute(); // Then: Result should contain: expect(result.status).toBe('degraded'); expect(result.reliability).toBeCloseTo(62.5, 1); expect(result.totalRequests).toBe(8); expect(result.successfulRequests).toBe(5); expect(result.failedRequests).toBe(3); expect(result.consecutiveFailures).toBe(0); expect(result.averageResponseTime).toBeGreaterThanOrEqual(50); expect(result.lastCheck).toBeInstanceOf(Date); expect(result.lastSuccess).toBeInstanceOf(Date); expect(result.lastFailure).toBeInstanceOf(Date); }); it('should correctly format connection status when disconnected', async () => { // Scenario: Connection is disconnected // Given: HealthCheckAdapter has 3 consecutive failures healthCheckAdapter.setShouldFail(true, 'ECONNREFUSED'); // Perform 3 failed checks for (let i = 0; i < 3; i++) { await checkApiHealthUseCase.execute(); } // When: GetConnectionStatusUseCase.execute() is called const result = await getConnectionStatusUseCase.execute(); // Then: Result should contain: expect(result.status).toBe('disconnected'); expect(result.consecutiveFailures).toBe(3); expect(result.lastFailure).toBeInstanceOf(Date); expect(result.lastSuccess).toBeInstanceOf(Date); }); }); describe('Event Emission Patterns', () => { it('should emit HealthCheckCompletedEvent on successful check', async () => { // Scenario: Successful health check // Given: HealthCheckAdapter returns success healthCheckAdapter.setResponseTime(50); // When: CheckApiHealthUseCase.execute() is called await checkApiHealthUseCase.execute(); // Then: EventPublisher should emit HealthCheckCompletedEvent expect(eventPublisher.getEventCountByType('HealthCheckCompleted')).toBe(1); // And: Event should include health check result const events = eventPublisher.getEventsByType('HealthCheckCompleted'); expect(events[0].healthy).toBe(true); expect(events[0].responseTime).toBeGreaterThanOrEqual(50); expect(events[0].timestamp).toBeInstanceOf(Date); }); it('should emit HealthCheckFailedEvent on failed check', async () => { // Scenario: Failed health check // Given: HealthCheckAdapter throws error healthCheckAdapter.setShouldFail(true, 'ECONNREFUSED'); // When: CheckApiHealthUseCase.execute() is called await checkApiHealthUseCase.execute(); // Then: EventPublisher should emit HealthCheckFailedEvent expect(eventPublisher.getEventCountByType('HealthCheckFailed')).toBe(1); // And: Event should include error details const events = eventPublisher.getEventsByType('HealthCheckFailed'); expect(events[0].error).toBe('ECONNREFUSED'); expect(events[0].timestamp).toBeInstanceOf(Date); }); it('should emit ConnectionStatusChangedEvent on status change', async () => { // Scenario: Connection status changes // Given: Current status is 'disconnected' // And: HealthCheckAdapter returns success healthCheckAdapter.setShouldFail(true, 'ECONNREFUSED'); // Perform 3 failed checks to get disconnected status for (let i = 0; i < 3; i++) { await checkApiHealthUseCase.execute(); } // Now start succeeding healthCheckAdapter.setShouldFail(false); healthCheckAdapter.setResponseTime(50); // When: CheckApiHealthUseCase.execute() is called await checkApiHealthUseCase.execute(); // Then: EventPublisher should emit ConnectedEvent expect(eventPublisher.getEventCountByType('Connected')).toBe(1); // And: Event should include timestamp and response time const events = eventPublisher.getEventsByType('Connected'); expect(events[0].timestamp).toBeInstanceOf(Date); expect(events[0].responseTime).toBeGreaterThanOrEqual(50); }); }); describe('Error Handling', () => { it('should handle adapter errors gracefully', async () => { // Scenario: HealthCheckAdapter throws unexpected error // Given: HealthCheckAdapter throws generic error healthCheckAdapter.setShouldFail(true, 'Unexpected error'); // When: CheckApiHealthUseCase.execute() is called const result = await checkApiHealthUseCase.execute(); // Then: Should not throw unhandled error expect(result).toBeDefined(); // And: Should return unhealthy status expect(result.healthy).toBe(false); // And: Should include error message expect(result.error).toBe('Unexpected error'); }); it('should handle invalid endpoint configuration', async () => { // Scenario: Invalid endpoint provided // Given: Invalid endpoint string healthCheckAdapter.setShouldFail(true, 'Invalid endpoint'); // When: CheckApiHealthUseCase.execute() is called const result = await checkApiHealthUseCase.execute(); // Then: Should handle validation error expect(result).toBeDefined(); // And: Should return error status expect(result.healthy).toBe(false); expect(result.error).toBe('Invalid endpoint'); }); it('should handle concurrent health check calls', async () => { // Scenario: Multiple simultaneous health checks // Given: CheckApiHealthUseCase.execute() is already running let resolveFirst: (value: any) => void; const firstPromise = new Promise((resolve) => { resolveFirst = resolve; }); const originalPerformHealthCheck = healthCheckAdapter.performHealthCheck.bind(healthCheckAdapter); healthCheckAdapter.performHealthCheck = async () => firstPromise; // Start first health check const firstCheck = checkApiHealthUseCase.execute(); // When: CheckApiHealthUseCase.execute() is called again const secondCheck = checkApiHealthUseCase.execute(); // Resolve the first check resolveFirst!({ healthy: true, responseTime: 50, timestamp: new Date(), }); // Wait for both checks to complete const [result1, result2] = await Promise.all([firstCheck, secondCheck]); // Then: Should return existing result expect(result1.healthy).toBe(true); expect(result2.healthy).toBe(true); }); }); });