import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { HealthRouteService } from '@/lib/services/health/HealthRouteService'; import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl'; import { isProductionEnvironment } from '@/lib/config/env'; // Mock the dependencies vi.mock('@/lib/config/apiBaseUrl', () => ({ getWebsiteApiBaseUrl: () => 'https://api.example.com', })); vi.mock('@/lib/config/env', () => ({ isProductionEnvironment: () => false, })); describe('HealthRouteService', () => { let service: HealthRouteService; let originalFetch: typeof global.fetch; let mockFetch: any; beforeEach(() => { vi.clearAllMocks(); service = new HealthRouteService(); originalFetch = global.fetch; mockFetch = vi.fn(); global.fetch = mockFetch as any; }); afterEach(() => { global.fetch = originalFetch; }); describe('happy paths', () => { it('should return ok status with timestamp when all dependencies are healthy', async () => { // Mock successful responses for all dependencies mockFetch.mockResolvedValueOnce({ ok: true, status: 200, }); // Mock database and external service to be healthy vi.spyOn(service as any, 'checkDatabaseHealth').mockResolvedValue({ status: 'healthy', latency: 50, }); vi.spyOn(service as any, 'checkExternalServiceHealth').mockResolvedValue({ status: 'healthy', latency: 100, }); const result = await service.getHealth(); expect(result.isOk()).toBe(true); const health = result.unwrap(); expect(health.status).toBe('healthy'); expect(health.timestamp).toBeDefined(); expect(health.dependencies.api.status).toBe('healthy'); expect(health.dependencies.database.status).toBe('healthy'); expect(health.dependencies.externalService.status).toBe('healthy'); }); it('should return degraded status when external service is slow', async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, }); vi.spyOn(service as any, 'checkDatabaseHealth').mockResolvedValue({ status: 'healthy', latency: 50, }); vi.spyOn(service as any, 'checkExternalServiceHealth').mockResolvedValue({ status: 'degraded', latency: 1500, error: 'High latency', }); const result = await service.getHealth(); expect(result.isOk()).toBe(true); const health = result.unwrap(); expect(health.status).toBe('degraded'); expect(health.dependencies.externalService.status).toBe('degraded'); }); }); describe('failure modes', () => { it('should handle API server errors gracefully', async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 500, }); vi.spyOn(service as any, 'checkDatabaseHealth').mockResolvedValue({ status: 'healthy', latency: 50, }); vi.spyOn(service as any, 'checkExternalServiceHealth').mockResolvedValue({ status: 'healthy', latency: 100, }); const result = await service.getHealth(); expect(result.isOk()).toBe(true); const health = result.unwrap(); expect(health.status).toBe('unhealthy'); expect(health.dependencies.api.status).toBe('unhealthy'); expect(health.dependencies.api.error).toContain('500'); }); it('should handle network errors gracefully', async () => { mockFetch.mockRejectedValueOnce(new Error('Network connection failed')); vi.spyOn(service as any, 'checkDatabaseHealth').mockResolvedValue({ status: 'healthy', latency: 50, }); vi.spyOn(service as any, 'checkExternalServiceHealth').mockResolvedValue({ status: 'healthy', latency: 100, }); const result = await service.getHealth(); expect(result.isOk()).toBe(true); const health = result.unwrap(); expect(health.status).toBe('unhealthy'); expect(health.dependencies.api.status).toBe('unhealthy'); expect(health.dependencies.api.error).toContain('Network connection failed'); }); it('should handle database connection failures', async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, }); vi.spyOn(service as any, 'checkDatabaseHealth').mockResolvedValue({ status: 'unhealthy', latency: 100, error: 'Connection timeout', }); vi.spyOn(service as any, 'checkExternalServiceHealth').mockResolvedValue({ status: 'healthy', latency: 100, }); const result = await service.getHealth(); expect(result.isOk()).toBe(true); const health = result.unwrap(); expect(health.status).toBe('unhealthy'); expect(health.dependencies.database.status).toBe('unhealthy'); }); it('should handle external service failures gracefully', async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, }); vi.spyOn(service as any, 'checkDatabaseHealth').mockResolvedValue({ status: 'healthy', latency: 50, }); vi.spyOn(service as any, 'checkExternalServiceHealth').mockResolvedValue({ status: 'degraded', latency: 200, error: 'Service unavailable', }); const result = await service.getHealth(); expect(result.isOk()).toBe(true); const health = result.unwrap(); expect(health.status).toBe('degraded'); expect(health.dependencies.externalService.status).toBe('degraded'); }); it('should handle all dependencies failing', async () => { mockFetch.mockRejectedValueOnce(new Error('API unavailable')); vi.spyOn(service as any, 'checkDatabaseHealth').mockResolvedValue({ status: 'unhealthy', latency: 100, error: 'DB connection failed', }); vi.spyOn(service as any, 'checkExternalServiceHealth').mockResolvedValue({ status: 'degraded', latency: 150, error: 'External service timeout', }); const result = await service.getHealth(); expect(result.isOk()).toBe(true); const health = result.unwrap(); expect(health.status).toBe('unhealthy'); expect(health.dependencies.api.status).toBe('unhealthy'); expect(health.dependencies.database.status).toBe('unhealthy'); expect(health.dependencies.externalService.status).toBe('degraded'); }); }); describe('retries', () => { it('should retry on transient API failures', async () => { // First call fails, second succeeds mockFetch .mockRejectedValueOnce(new Error('Network timeout')) .mockResolvedValueOnce({ ok: true, status: 200, }); vi.spyOn(service as any, 'checkDatabaseHealth').mockResolvedValue({ status: 'healthy', latency: 50, }); vi.spyOn(service as any, 'checkExternalServiceHealth').mockResolvedValue({ status: 'healthy', latency: 100, }); const result = await service.getHealth(); expect(result.isOk()).toBe(true); const health = result.unwrap(); expect(health.status).toBe('healthy'); expect(mockFetch).toHaveBeenCalledTimes(2); }); it('should retry database health check on transient failures', async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, }); // Mock database to fail first, then succeed const checkDatabaseHealthSpy = vi.spyOn(service as any, 'checkDatabaseHealth'); checkDatabaseHealthSpy .mockRejectedValueOnce(new Error('Connection timeout')) .mockResolvedValueOnce({ status: 'healthy', latency: 50, }); vi.spyOn(service as any, 'checkExternalServiceHealth').mockResolvedValue({ status: 'healthy', latency: 100, }); const result = await service.getHealth(); expect(result.isOk()).toBe(true); const health = result.unwrap(); expect(health.status).toBe('healthy'); expect(health.dependencies.database.status).toBe('healthy'); }); it('should exhaust retries and return unhealthy after max attempts', async () => { // Mock all retries to fail mockFetch.mockRejectedValue(new Error('Persistent network error')); vi.spyOn(service as any, 'checkDatabaseHealth').mockResolvedValue({ status: 'healthy', latency: 50, }); vi.spyOn(service as any, 'checkExternalServiceHealth').mockResolvedValue({ status: 'healthy', latency: 100, }); const result = await service.getHealth(); expect(result.isOk()).toBe(true); const health = result.unwrap(); expect(health.status).toBe('unhealthy'); expect(mockFetch).toHaveBeenCalledTimes(3); // Max retries }); it('should handle mixed retry scenarios', async () => { // API succeeds on second attempt mockFetch .mockRejectedValueOnce(new Error('Timeout')) .mockResolvedValueOnce({ ok: true, status: 200, }); // Database fails all attempts const checkDatabaseHealthSpy = vi.spyOn(service as any, 'checkDatabaseHealth'); checkDatabaseHealthSpy.mockRejectedValue(new Error('DB connection failed')); // External service succeeds vi.spyOn(service as any, 'checkExternalServiceHealth').mockResolvedValue({ status: 'healthy', latency: 100, }); const result = await service.getHealth(); expect(result.isOk()).toBe(true); const health = result.unwrap(); expect(health.status).toBe('unhealthy'); // Database failure makes overall unhealthy expect(mockFetch).toHaveBeenCalledTimes(2); // API retried once expect(checkDatabaseHealthSpy).toHaveBeenCalledTimes(3); // Database retried max times }); }); describe('fallback logic', () => { it('should continue with degraded status when external service fails', async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, }); vi.spyOn(service as any, 'checkDatabaseHealth').mockResolvedValue({ status: 'healthy', latency: 50, }); vi.spyOn(service as any, 'checkExternalServiceHealth').mockResolvedValue({ status: 'degraded', latency: 2000, error: 'External service timeout', }); const result = await service.getHealth(); expect(result.isOk()).toBe(true); const health = result.unwrap(); expect(health.status).toBe('degraded'); expect(health.dependencies.externalService.status).toBe('degraded'); expect(health.dependencies.api.status).toBe('healthy'); expect(health.dependencies.database.status).toBe('healthy'); }); it('should handle partial failures without complete system failure', async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, }); vi.spyOn(service as any, 'checkDatabaseHealth').mockResolvedValue({ status: 'healthy', latency: 50, }); vi.spyOn(service as any, 'checkExternalServiceHealth').mockResolvedValue({ status: 'degraded', latency: 1500, error: 'High latency', }); const result = await service.getHealth(); expect(result.isOk()).toBe(true); const health = result.unwrap(); // System should be degraded but not completely down expect(health.status).toBe('degraded'); expect(health.dependencies.api.status).toBe('healthy'); expect(health.dependencies.database.status).toBe('healthy'); }); it('should provide fallback information in details', async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, }); vi.spyOn(service as any, 'checkDatabaseHealth').mockResolvedValue({ status: 'healthy', latency: 50, }); vi.spyOn(service as any, 'checkExternalServiceHealth').mockResolvedValue({ status: 'degraded', latency: 1200, error: 'External service degraded', }); const result = await service.getHealth(); expect(result.isOk()).toBe(true); const health = result.unwrap(); expect(health.dependencies.externalService.error).toBe('External service degraded'); }); }); describe('aggregation logic', () => { it('should aggregate health status from multiple dependencies correctly', async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, }); vi.spyOn(service as any, 'checkDatabaseHealth').mockResolvedValue({ status: 'healthy', latency: 45, }); vi.spyOn(service as any, 'checkExternalServiceHealth').mockResolvedValue({ status: 'healthy', latency: 95, }); const result = await service.getHealth(); expect(result.isOk()).toBe(true); const health = result.unwrap(); // Verify all dependencies are checked expect(health.dependencies.api.status).toBe('healthy'); expect(health.dependencies.database.status).toBe('healthy'); expect(health.dependencies.externalService.status).toBe('healthy'); // Verify latency aggregation (max of all latencies) expect(health.dependencies.api.latency).toBeGreaterThan(0); expect(health.dependencies.database.latency).toBeGreaterThan(0); expect(health.dependencies.externalService.latency).toBeGreaterThan(0); }); it('should correctly aggregate when one dependency is degraded', async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, }); vi.spyOn(service as any, 'checkDatabaseHealth').mockResolvedValue({ status: 'healthy', latency: 50, }); vi.spyOn(service as any, 'checkExternalServiceHealth').mockResolvedValue({ status: 'degraded', latency: 1500, error: 'Slow response', }); const result = await service.getHealth(); expect(result.isOk()).toBe(true); const health = result.unwrap(); // Aggregation should result in degraded status expect(health.status).toBe('degraded'); expect(health.dependencies.api.status).toBe('healthy'); expect(health.dependencies.database.status).toBe('healthy'); expect(health.dependencies.externalService.status).toBe('degraded'); }); it('should handle critical dependency failures in aggregation', async () => { mockFetch.mockRejectedValueOnce(new Error('API down')); vi.spyOn(service as any, 'checkDatabaseHealth').mockResolvedValue({ status: 'healthy', latency: 50, }); vi.spyOn(service as any, 'checkExternalServiceHealth').mockResolvedValue({ status: 'healthy', latency: 100, }); const result = await service.getHealth(); expect(result.isOk()).toBe(true); const health = result.unwrap(); // API failure should make overall status unhealthy expect(health.status).toBe('unhealthy'); expect(health.dependencies.api.status).toBe('unhealthy'); }); it('should aggregate latency values correctly', async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, }); vi.spyOn(service as any, 'checkDatabaseHealth').mockResolvedValue({ status: 'healthy', latency: 150, }); vi.spyOn(service as any, 'checkExternalServiceHealth').mockResolvedValue({ status: 'healthy', latency: 200, }); const result = await service.getHealth(); expect(result.isOk()).toBe(true); const health = result.unwrap(); // Should take the maximum latency expect(health.dependencies.api.latency).toBeGreaterThan(0); expect(health.dependencies.database.latency).toBe(150); expect(health.dependencies.externalService.latency).toBe(200); }); }); describe('decision branches', () => { it('should return healthy when all dependencies are healthy and fast', async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, }); vi.spyOn(service as any, 'checkDatabaseHealth').mockResolvedValue({ status: 'healthy', latency: 50, }); vi.spyOn(service as any, 'checkExternalServiceHealth').mockResolvedValue({ status: 'healthy', latency: 100, }); const result = await service.getHealth(); expect(result.isOk()).toBe(true); expect(result.unwrap().status).toBe('healthy'); }); it('should return degraded when dependencies are healthy but slow', async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, }); vi.spyOn(service as any, 'checkDatabaseHealth').mockResolvedValue({ status: 'healthy', latency: 50, }); vi.spyOn(service as any, 'checkExternalServiceHealth').mockResolvedValue({ status: 'healthy', latency: 1200, // Exceeds threshold }); const result = await service.getHealth(); expect(result.isOk()).toBe(true); expect(result.unwrap().status).toBe('degraded'); }); it('should return unhealthy when critical dependencies fail', async () => { mockFetch.mockRejectedValueOnce(new Error('API unavailable')); vi.spyOn(service as any, 'checkDatabaseHealth').mockResolvedValue({ status: 'healthy', latency: 50, }); vi.spyOn(service as any, 'checkExternalServiceHealth').mockResolvedValue({ status: 'healthy', latency: 100, }); const result = await service.getHealth(); expect(result.isOk()).toBe(true); expect(result.unwrap().status).toBe('unhealthy'); }); it('should handle different error types based on retryability', async () => { // Test retryable error (timeout) mockFetch.mockRejectedValueOnce(new Error('Connection timeout')); vi.spyOn(service as any, 'checkDatabaseHealth').mockResolvedValue({ status: 'healthy', latency: 50, }); vi.spyOn(service as any, 'checkExternalServiceHealth').mockResolvedValue({ status: 'healthy', latency: 100, }); const result1 = await service.getHealth(); expect(result1.isOk()).toBe(true); expect(mockFetch).toHaveBeenCalledTimes(2); // Should retry // Reset mocks mockFetch.mockClear(); vi.clearAllMocks(); // Test non-retryable error (400) mockFetch.mockResolvedValueOnce({ ok: false, status: 400, }); vi.spyOn(service as any, 'checkDatabaseHealth').mockResolvedValue({ status: 'healthy', latency: 50, }); vi.spyOn(service as any, 'checkExternalServiceHealth').mockResolvedValue({ status: 'healthy', latency: 100, }); const result2 = await service.getHealth(); expect(result2.isOk()).toBe(true); expect(mockFetch).toHaveBeenCalledTimes(1); // Should not retry }); it('should handle mixed dependency states correctly', async () => { // API: healthy, Database: unhealthy, External: degraded mockFetch.mockResolvedValueOnce({ ok: true, status: 200, }); vi.spyOn(service as any, 'checkDatabaseHealth').mockResolvedValue({ status: 'unhealthy', latency: 100, error: 'DB connection failed', }); vi.spyOn(service as any, 'checkExternalServiceHealth').mockResolvedValue({ status: 'degraded', latency: 1500, error: 'Slow response', }); const result = await service.getHealth(); expect(result.isOk()).toBe(true); const health = result.unwrap(); // Database failure should make overall unhealthy expect(health.status).toBe('unhealthy'); expect(health.dependencies.api.status).toBe('healthy'); expect(health.dependencies.database.status).toBe('unhealthy'); expect(health.dependencies.externalService.status).toBe('degraded'); }); it('should handle edge case where all dependencies are degraded', async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, }); vi.spyOn(service as any, 'checkDatabaseHealth').mockResolvedValue({ status: 'degraded', latency: 800, error: 'Slow query', }); vi.spyOn(service as any, 'checkExternalServiceHealth').mockResolvedValue({ status: 'degraded', latency: 1200, error: 'External timeout', }); const result = await service.getHealth(); expect(result.isOk()).toBe(true); const health = result.unwrap(); // All degraded should result in degraded overall expect(health.status).toBe('degraded'); expect(health.dependencies.api.status).toBe('healthy'); expect(health.dependencies.database.status).toBe('degraded'); expect(health.dependencies.externalService.status).toBe('degraded'); }); it('should handle timeout aborts correctly', async () => { // Mock fetch to simulate timeout const abortError = new Error('The operation was aborted.'); abortError.name = 'AbortError'; mockFetch.mockRejectedValueOnce(abortError); vi.spyOn(service as any, 'checkDatabaseHealth').mockResolvedValue({ status: 'healthy', latency: 50, }); vi.spyOn(service as any, 'checkExternalServiceHealth').mockResolvedValue({ status: 'healthy', latency: 100, }); const result = await service.getHealth(); expect(result.isOk()).toBe(true); const health = result.unwrap(); expect(health.status).toBe('unhealthy'); expect(health.dependencies.api.status).toBe('unhealthy'); }); }); });