import { CircuitBreaker, CircuitState } from '../../utils/CircuitBreaker'; describe('CircuitBreaker', () => { let circuitBreaker: CircuitBreaker; let mockEventHandlers: any; beforeEach(() => { mockEventHandlers = { stateChange: jest.fn(), failure: jest.fn(), success: jest.fn(), reject: jest.fn() }; circuitBreaker = new CircuitBreaker({ failureThreshold: 3, successThreshold: 2, timeout: 1000, monitoringWindow: 5000 }); // Register event handlers Object.keys(mockEventHandlers).forEach(event => { circuitBreaker.on(event as any, mockEventHandlers[event]); }); jest.useFakeTimers(); }); afterEach(() => { jest.useRealTimers(); }); describe('initial state', () => { it('should start in CLOSED state', () => { expect(circuitBreaker.getState()).toBe(CircuitState.CLOSED); expect(circuitBreaker.isAvailable()).toBe(true); }); it('should have clean initial metrics', () => { const metrics = circuitBreaker.getMetrics(); expect(metrics.failures).toBe(0); expect(metrics.successes).toBe(0); expect(metrics.requests).toBe(0); expect(metrics.lastFailureTime).toBeNull(); expect(metrics.lastSuccessTime).toBeNull(); }); }); describe('successful operations', () => { it('should track successful operations', async () => { const operation = jest.fn().mockResolvedValue('success'); const result = await circuitBreaker.execute(operation); expect(result).toBe('success'); expect(mockEventHandlers.success).toHaveBeenCalled(); const metrics = circuitBreaker.getMetrics(); expect(metrics.successes).toBe(1); expect(metrics.requests).toBe(1); expect(metrics.lastSuccessTime).toBeGreaterThan(0); }); it('should stay in CLOSED state for successful operations', async () => { const operation = jest.fn().mockResolvedValue('success'); await circuitBreaker.execute(operation); await circuitBreaker.execute(operation); expect(circuitBreaker.getState()).toBe(CircuitState.CLOSED); expect(mockEventHandlers.stateChange).not.toHaveBeenCalled(); }); }); describe('failed operations', () => { it('should track failed operations', async () => { const error = new Error('Operation failed'); const operation = jest.fn().mockRejectedValue(error); await expect(circuitBreaker.execute(operation)).rejects.toThrow('Operation failed'); expect(mockEventHandlers.failure).toHaveBeenCalledWith(error); const metrics = circuitBreaker.getMetrics(); expect(metrics.failures).toBe(1); expect(metrics.requests).toBe(1); expect(metrics.lastFailureTime).toBeGreaterThan(0); }); it('should open circuit after failure threshold', async () => { const error = new Error('Operation failed'); const operation = jest.fn().mockRejectedValue(error); // Execute operations to exceed failure threshold for (let i = 0; i < 3; i++) { await expect(circuitBreaker.execute(operation)).rejects.toThrow(); } expect(circuitBreaker.getState()).toBe(CircuitState.OPEN); expect(mockEventHandlers.stateChange).toHaveBeenCalledWith( CircuitState.OPEN, CircuitState.CLOSED ); }); }); describe('OPEN state behavior', () => { beforeEach(async () => { // Force circuit to OPEN state const error = new Error('Operation failed'); const operation = jest.fn().mockRejectedValue(error); for (let i = 0; i < 3; i++) { await expect(circuitBreaker.execute(operation)).rejects.toThrow(); } expect(circuitBreaker.getState()).toBe(CircuitState.OPEN); }); it('should reject requests immediately when OPEN', async () => { const operation = jest.fn().mockResolvedValue('success'); await expect(circuitBreaker.execute(operation)).rejects.toThrow( 'Circuit breaker is OPEN' ); expect(operation).not.toHaveBeenCalled(); expect(mockEventHandlers.reject).toHaveBeenCalled(); expect(circuitBreaker.isAvailable()).toBe(false); }); it('should transition to HALF_OPEN after timeout', async () => { expect(circuitBreaker.getState()).toBe(CircuitState.OPEN); // Advance time beyond timeout jest.advanceTimersByTime(1100); const operation = jest.fn().mockResolvedValue('success'); await circuitBreaker.execute(operation); expect(mockEventHandlers.stateChange).toHaveBeenCalledWith( CircuitState.HALF_OPEN, CircuitState.OPEN ); }); }); describe('HALF_OPEN state behavior', () => { beforeEach(async () => { // Force circuit to OPEN state then wait for timeout const error = new Error('Operation failed'); const failingOperation = jest.fn().mockRejectedValue(error); for (let i = 0; i < 3; i++) { await expect(circuitBreaker.execute(failingOperation)).rejects.toThrow(); } jest.advanceTimersByTime(1100); }); it('should close circuit after sufficient successes in HALF_OPEN', async () => { const operation = jest.fn().mockResolvedValue('success'); // Execute successful operations to meet success threshold await circuitBreaker.execute(operation); // Transition to HALF_OPEN await circuitBreaker.execute(operation); // Should close circuit expect(circuitBreaker.getState()).toBe(CircuitState.CLOSED); expect(mockEventHandlers.stateChange).toHaveBeenCalledWith( CircuitState.CLOSED, CircuitState.HALF_OPEN ); }); it('should open circuit immediately on failure in HALF_OPEN', async () => { const successOperation = jest.fn().mockResolvedValue('success'); const failingOperation = jest.fn().mockRejectedValue(new Error('Failed again')); // Transition to HALF_OPEN await circuitBreaker.execute(successOperation); expect(circuitBreaker.getState()).toBe(CircuitState.HALF_OPEN); // Single failure should open circuit again await expect(circuitBreaker.execute(failingOperation)).rejects.toThrow(); expect(circuitBreaker.getState()).toBe(CircuitState.OPEN); }); }); describe('error filtering', () => { it('should ignore specified error types', async () => { circuitBreaker = new CircuitBreaker({ failureThreshold: 2, ignoredErrors: ['Timeout', /Network/] }); const timeoutError = new Error('Request timeout occurred'); const networkError = new Error('Network connection failed'); const serverError = new Error('Server internal error'); const timeoutOperation = jest.fn().mockRejectedValue(timeoutError); const networkOperation = jest.fn().mockRejectedValue(networkError); const serverOperation = jest.fn().mockRejectedValue(serverError); // These should be ignored and not count toward failure threshold await expect(circuitBreaker.execute(timeoutOperation)).rejects.toThrow(); await expect(circuitBreaker.execute(networkOperation)).rejects.toThrow(); expect(circuitBreaker.getState()).toBe(CircuitState.CLOSED); // This should count and open circuit after threshold await expect(circuitBreaker.execute(serverOperation)).rejects.toThrow(); await expect(circuitBreaker.execute(serverOperation)).rejects.toThrow(); expect(circuitBreaker.getState()).toBe(CircuitState.OPEN); }); it('should only count expected error types when configured', async () => { circuitBreaker = new CircuitBreaker({ failureThreshold: 2, expectedErrors: ['Server error'] }); const timeoutError = new Error('Request timeout'); const serverError = new Error('Server error occurred'); const timeoutOperation = jest.fn().mockRejectedValue(timeoutError); const serverOperation = jest.fn().mockRejectedValue(serverError); // Timeout errors should be ignored await expect(circuitBreaker.execute(timeoutOperation)).rejects.toThrow(); await expect(circuitBreaker.execute(timeoutOperation)).rejects.toThrow(); expect(circuitBreaker.getState()).toBe(CircuitState.CLOSED); // Server errors should count await expect(circuitBreaker.execute(serverOperation)).rejects.toThrow(); await expect(circuitBreaker.execute(serverOperation)).rejects.toThrow(); expect(circuitBreaker.getState()).toBe(CircuitState.OPEN); }); }); describe('metrics and monitoring', () => { it('should clean up old metrics outside monitoring window', async () => { const operation = jest.fn().mockRejectedValue(new Error('Failed')); // Generate some failures await expect(circuitBreaker.execute(operation)).rejects.toThrow(); let metrics = circuitBreaker.getMetrics(); expect(metrics.failures).toBe(1); // Advance time beyond monitoring window jest.advanceTimersByTime(6000); // Check metrics again - should be cleaned up metrics = circuitBreaker.getMetrics(); expect(metrics.failures).toBe(0); expect(metrics.requests).toBe(0); }); it('should provide current metrics', () => { const metrics = circuitBreaker.getMetrics(); expect(metrics).toMatchObject({ failures: expect.any(Number), successes: expect.any(Number), requests: expect.any(Number), lastFailureTime: expect.any(Object), // null or number lastSuccessTime: expect.any(Object) // null or number }); }); }); describe('manual control', () => { it('should allow manual reset', async () => { // Force circuit to open const operation = jest.fn().mockRejectedValue(new Error('Failed')); for (let i = 0; i < 3; i++) { await expect(circuitBreaker.execute(operation)).rejects.toThrow(); } expect(circuitBreaker.getState()).toBe(CircuitState.OPEN); // Reset manually circuitBreaker.reset(); expect(circuitBreaker.getState()).toBe(CircuitState.CLOSED); expect(circuitBreaker.isAvailable()).toBe(true); const metrics = circuitBreaker.getMetrics(); expect(metrics.failures).toBe(0); expect(metrics.successes).toBe(0); }); it('should allow manual opening', () => { expect(circuitBreaker.getState()).toBe(CircuitState.CLOSED); circuitBreaker.open(); expect(circuitBreaker.getState()).toBe(CircuitState.OPEN); expect(circuitBreaker.isAvailable()).toBe(false); }); }); describe('availability checks', () => { it('should report availability correctly in different states', async () => { // CLOSED - available expect(circuitBreaker.isAvailable()).toBe(true); // Force to OPEN const operation = jest.fn().mockRejectedValue(new Error('Failed')); for (let i = 0; i < 3; i++) { await expect(circuitBreaker.execute(operation)).rejects.toThrow(); } // OPEN - not available expect(circuitBreaker.isAvailable()).toBe(false); // After timeout - available again jest.advanceTimersByTime(1100); expect(circuitBreaker.isAvailable()).toBe(true); }); }); });