// health-checker.js exports a singleton that reads config/history from disk on construction. // The jest.setup.js suppresses console and the files don't exist in test env, so it falls back to defaults. const healthChecker = require('../health-checker'); beforeEach(() => { // Reset singleton state between tests healthChecker.currentStatus = new Map(); healthChecker.incidents = []; healthChecker.history = {}; healthChecker.config = { services: {} }; healthChecker.checking = false; if (healthChecker.checkInterval) { clearInterval(healthChecker.checkInterval); healthChecker.checkInterval = null; } }); afterAll(() => { healthChecker.stop(); }); describe('evaluateHealth', () => { test('returns true for status code in expectedStatusCodes', () => { expect(healthChecker.evaluateHealth(200, '', { expectedStatusCodes: [200, 201] })).toBe(true); }); test('returns false for status code not in expectedStatusCodes', () => { expect(healthChecker.evaluateHealth(500, '', { expectedStatusCodes: [200] })).toBe(false); }); test('uses default expected codes when not configured', () => { expect(healthChecker.evaluateHealth(200, '', {})).toBe(true); expect(healthChecker.evaluateHealth(301, '', {})).toBe(true); expect(healthChecker.evaluateHealth(500, '', {})).toBe(false); }); test('returns false when expectedBodyPattern regex does not match', () => { expect(healthChecker.evaluateHealth(200, 'error occurred', { expectedBodyPattern: 'ok|healthy' })).toBe(false); }); test('returns true when expectedBodyPattern regex matches', () => { expect(healthChecker.evaluateHealth(200, 'status: healthy', { expectedBodyPattern: 'healthy' })).toBe(true); }); test('returns false when expectedBodyContains text is missing', () => { expect(healthChecker.evaluateHealth(200, 'some response', { expectedBodyContains: 'healthy' })).toBe(false); }); test('returns true when expectedBodyContains text is present', () => { expect(healthChecker.evaluateHealth(200, 'service is healthy', { expectedBodyContains: 'healthy' })).toBe(true); }); test('checks all conditions: status code AND body pattern AND body contains', () => { // All pass expect(healthChecker.evaluateHealth(200, 'healthy ok', { expectedStatusCodes: [200], expectedBodyPattern: 'healthy', expectedBodyContains: 'ok' })).toBe(true); // Status fails expect(healthChecker.evaluateHealth(500, 'healthy ok', { expectedStatusCodes: [200], expectedBodyPattern: 'healthy', expectedBodyContains: 'ok' })).toBe(false); // Body pattern fails expect(healthChecker.evaluateHealth(200, 'error', { expectedStatusCodes: [200], expectedBodyPattern: 'healthy', expectedBodyContains: 'error' })).toBe(false); }); }); describe('calculateSeverity', () => { test('returns critical for outage', () => { expect(healthChecker.calculateSeverity('outage')).toBe('critical'); }); test('returns high for sla-violation', () => { expect(healthChecker.calculateSeverity('sla-violation')).toBe('high'); }); test('returns medium for slow-response', () => { expect(healthChecker.calculateSeverity('slow-response')).toBe('medium'); }); test('returns low for unknown type', () => { expect(healthChecker.calculateSeverity('unknown')).toBe('low'); }); }); describe('calculateUptime', () => { test('returns 100 when no history', () => { expect(healthChecker.calculateUptime('svc1')).toBe(100); }); test('returns 100 when all checks are up', () => { const now = new Date().toISOString(); healthChecker.history['svc1'] = [ { status: 'up', timestamp: now }, { status: 'up', timestamp: now }, { status: 'up', timestamp: now }, ]; expect(healthChecker.calculateUptime('svc1')).toBe(100); }); test('returns 0 when all checks are down', () => { const now = new Date().toISOString(); healthChecker.history['svc1'] = [ { status: 'down', timestamp: now }, { status: 'down', timestamp: now }, ]; expect(healthChecker.calculateUptime('svc1')).toBe(0); }); test('returns 50 when half are up', () => { const now = new Date().toISOString(); healthChecker.history['svc1'] = [ { status: 'up', timestamp: now }, { status: 'down', timestamp: now }, ]; expect(healthChecker.calculateUptime('svc1')).toBe(50); }); }); describe('calculateAverageResponseTime', () => { test('returns 0 when no history', () => { expect(healthChecker.calculateAverageResponseTime('svc1')).toBe(0); }); test('calculates correct average', () => { const now = new Date().toISOString(); healthChecker.history['svc1'] = [ { responseTime: 100, timestamp: now }, { responseTime: 200, timestamp: now }, { responseTime: 300, timestamp: now }, ]; expect(healthChecker.calculateAverageResponseTime('svc1')).toBe(200); }); }); describe('calculatePercentile', () => { test('returns p95 correctly', () => { const values = Array.from({ length: 100 }, (_, i) => i + 1); expect(healthChecker.calculatePercentile(values, 95)).toBe(95); }); test('returns p99 correctly', () => { const values = Array.from({ length: 100 }, (_, i) => i + 1); expect(healthChecker.calculatePercentile(values, 99)).toBe(99); }); test('returns 0 for empty array', () => { expect(healthChecker.calculatePercentile([], 95)).toBe(0); }); test('handles single-element array', () => { expect(healthChecker.calculatePercentile([42], 95)).toBe(42); }); test('sorts values before calculating', () => { const unsorted = [50, 10, 90, 30, 70, 20, 80, 40, 60, 100]; expect(healthChecker.calculatePercentile(unsorted, 50)).toBe(50); }); }); describe('recordStatus', () => { test('adds status to currentStatus map', () => { const status = { serviceId: 'svc1', status: 'up', timestamp: new Date().toISOString() }; healthChecker.recordStatus('svc1', status); expect(healthChecker.currentStatus.get('svc1')).toEqual(status); }); test('creates history array for new serviceId', () => { const status = { serviceId: 'new-svc', status: 'up', timestamp: new Date().toISOString() }; healthChecker.recordStatus('new-svc', status); expect(healthChecker.history['new-svc']).toHaveLength(1); }); test('appends to existing history', () => { healthChecker.history['svc1'] = [{ status: 'up', timestamp: new Date().toISOString() }]; const status = { status: 'down', timestamp: new Date().toISOString() }; healthChecker.recordStatus('svc1', status); expect(healthChecker.history['svc1']).toHaveLength(2); }); test('emits status-check event', () => { const handler = jest.fn(); healthChecker.on('status-check', handler); healthChecker.recordStatus('svc1', { status: 'up', timestamp: new Date().toISOString() }); expect(handler).toHaveBeenCalled(); healthChecker.removeListener('status-check', handler); }); }); describe('createIncident', () => { test('creates incident with correct structure', () => { const status = { timestamp: new Date().toISOString() }; healthChecker.createIncident('svc1', 'outage', 'Service down', status); expect(healthChecker.incidents).toHaveLength(1); expect(healthChecker.incidents[0].serviceId).toBe('svc1'); expect(healthChecker.incidents[0].type).toBe('outage'); expect(healthChecker.incidents[0].status).toBe('open'); expect(healthChecker.incidents[0].severity).toBe('critical'); expect(healthChecker.incidents[0].occurrences).toBe(1); }); test('emits incident-created event', () => { const handler = jest.fn(); healthChecker.on('incident-created', handler); healthChecker.createIncident('svc1', 'outage', 'Down', { timestamp: new Date().toISOString() }); expect(handler).toHaveBeenCalledWith(expect.objectContaining({ serviceId: 'svc1' })); healthChecker.removeListener('incident-created', handler); }); test('does not duplicate open incidents of same type', () => { const status = { timestamp: new Date().toISOString() }; healthChecker.createIncident('svc1', 'outage', 'Down', status); healthChecker.createIncident('svc1', 'outage', 'Still down', status); expect(healthChecker.incidents).toHaveLength(1); }); test('increments occurrences on existing open incident', () => { const status = { timestamp: new Date().toISOString() }; healthChecker.createIncident('svc1', 'outage', 'Down', status); healthChecker.createIncident('svc1', 'outage', 'Still down', status); expect(healthChecker.incidents[0].occurrences).toBe(2); }); }); describe('resolveIncident', () => { test('marks incident as resolved with duration', () => { const created = new Date(Date.now() - 60000).toISOString(); const resolved = new Date().toISOString(); healthChecker.createIncident('svc1', 'outage', 'Down', { timestamp: created }); healthChecker.resolveIncident('svc1', 'outage', { timestamp: resolved }); expect(healthChecker.incidents[0].status).toBe('resolved'); expect(healthChecker.incidents[0].resolvedAt).toBe(resolved); expect(healthChecker.incidents[0].duration).toBeGreaterThan(0); }); test('emits incident-resolved event', () => { const handler = jest.fn(); healthChecker.on('incident-resolved', handler); const ts = new Date().toISOString(); healthChecker.createIncident('svc1', 'outage', 'Down', { timestamp: ts }); healthChecker.resolveIncident('svc1', 'outage', { timestamp: ts }); expect(handler).toHaveBeenCalled(); healthChecker.removeListener('incident-resolved', handler); }); test('handles no matching incident gracefully', () => { // Should not throw healthChecker.resolveIncident('nonexistent', 'outage', { timestamp: new Date().toISOString() }); expect(healthChecker.incidents).toHaveLength(0); }); }); describe('configureService / removeService', () => { test('adds service config with defaults', () => { healthChecker.configureService('svc1', { url: 'http://localhost:3000', name: 'Test' }); expect(healthChecker.config.services['svc1']).toBeDefined(); expect(healthChecker.config.services['svc1'].method).toBe('GET'); expect(healthChecker.config.services['svc1'].timeout).toBe(10000); }); test('removes service and cleans up', () => { healthChecker.configureService('svc1', { url: 'http://localhost:3000' }); healthChecker.currentStatus.set('svc1', { status: 'up' }); healthChecker.history['svc1'] = [{ status: 'up' }]; healthChecker.removeService('svc1'); expect(healthChecker.config.services['svc1']).toBeUndefined(); expect(healthChecker.currentStatus.has('svc1')).toBe(false); expect(healthChecker.history['svc1']).toBeUndefined(); }); }); describe('getOpenIncidents / getIncidentHistory', () => { test('getOpenIncidents returns only open incidents', () => { const ts = new Date().toISOString(); healthChecker.createIncident('svc1', 'outage', 'Down', { timestamp: ts }); healthChecker.createIncident('svc2', 'slow-response', 'Slow', { timestamp: ts }); healthChecker.resolveIncident('svc1', 'outage', { timestamp: ts }); expect(healthChecker.getOpenIncidents()).toHaveLength(1); expect(healthChecker.getOpenIncidents()[0].serviceId).toBe('svc2'); }); test('getIncidentHistory returns reverse chronological order', () => { const ts = new Date().toISOString(); healthChecker.createIncident('svc1', 'outage', 'First', { timestamp: ts }); healthChecker.createIncident('svc2', 'outage', 'Second', { timestamp: ts }); const history = healthChecker.getIncidentHistory(); expect(history[0].serviceId).toBe('svc2'); }); }); describe('getServiceStats', () => { test('returns null for service with no history', () => { expect(healthChecker.getServiceStats('nonexistent')).toBeNull(); }); test('returns correct stats structure', () => { const now = new Date().toISOString(); healthChecker.history['svc1'] = [ { status: 'up', responseTime: 100, timestamp: now }, { status: 'up', responseTime: 200, timestamp: now }, { status: 'down', responseTime: 0, timestamp: now }, ]; const stats = healthChecker.getServiceStats('svc1'); expect(stats.totalChecks).toBe(3); expect(stats.upChecks).toBe(2); expect(stats.downChecks).toBe(1); expect(stats.responseTime.avg).toBe(100); expect(stats.responseTime.min).toBe(0); expect(stats.responseTime.max).toBe(200); expect(stats.responseTime).toHaveProperty('p95'); expect(stats.responseTime).toHaveProperty('p99'); }); }); describe('start / stop', () => { test('start sets checking flag', () => { jest.useFakeTimers(); healthChecker.start(); expect(healthChecker.checking).toBe(true); healthChecker.stop(); jest.useRealTimers(); }); test('stop clears interval and checking flag', () => { jest.useFakeTimers(); healthChecker.start(); healthChecker.stop(); expect(healthChecker.checking).toBe(false); expect(healthChecker.checkInterval).toBeNull(); jest.useRealTimers(); }); test('start is idempotent', () => { jest.useFakeTimers(); healthChecker.start(); const firstInterval = healthChecker.checkInterval; healthChecker.start(); expect(healthChecker.checkInterval).toBe(firstInterval); healthChecker.stop(); jest.useRealTimers(); }); });