circuit-breaker.test.js
1 /** 2 * Circuit Breaker Tests 3 * Verify circuit breaker behavior for API protection 4 */ 5 6 import { test } from 'node:test'; 7 import assert from 'node:assert'; 8 import { 9 createOpenRouterBreaker, 10 createZenRowsBreaker, 11 createTwilioBreaker, 12 createResendBreaker, 13 getBreakerStats, 14 } from '../../src/utils/circuit-breaker.js'; 15 16 test('Circuit breaker should be created with correct name', () => { 17 const breaker = createOpenRouterBreaker(); 18 assert.strictEqual(breaker.name, 'OpenRouter'); 19 }); 20 21 test('Circuit breaker should track successful requests', async () => { 22 const breaker = createOpenRouterBreaker(); 23 24 // Fire a successful request 25 const result = await breaker.fire(async () => { 26 return 'success'; 27 }); 28 29 assert.strictEqual(result, 'success'); 30 31 const stats = getBreakerStats(breaker); 32 assert.strictEqual(stats.name, 'OpenRouter'); 33 assert.ok(stats.fires >= 1); 34 assert.ok(stats.successes >= 1); 35 }); 36 37 test('Circuit breaker should track failed requests', async () => { 38 const breaker = createZenRowsBreaker(); 39 40 // Fire a failing request 41 try { 42 await breaker.fire(async () => { 43 throw new Error('API failure'); 44 }); 45 assert.fail('Should have thrown error'); 46 } catch (error) { 47 assert.strictEqual(error.message, 'API failure'); 48 } 49 50 const stats = getBreakerStats(breaker); 51 assert.strictEqual(stats.name, 'ZenRows'); 52 assert.ok(stats.fires >= 1); 53 assert.ok(stats.failures >= 1); 54 }); 55 56 test('Circuit breaker should open after too many failures', async () => { 57 const breaker = createTwilioBreaker(); 58 const failCount = 10; 59 60 // Cause multiple failures to open the circuit 61 for (let i = 0; i < failCount; i++) { 62 try { 63 await breaker.fire(async () => { 64 throw new Error('API failure'); 65 }); 66 } catch (error) { 67 // Expected to fail 68 } 69 } 70 71 const stats = getBreakerStats(breaker); 72 assert.strictEqual(stats.name, 'Twilio'); 73 74 // After many failures, circuit should be open or have high failure rate 75 if (stats.fires >= 5) { 76 // volumeThreshold is 5 77 const failureRate = parseFloat(stats.failureRate); 78 assert.ok(failureRate > 0, 'Failure rate should be greater than 0%'); 79 } 80 }); 81 82 test('Circuit breaker should have correct timeout settings', () => { 83 const breaker = createResendBreaker(); 84 assert.strictEqual(breaker.name, 'Resend'); 85 assert.strictEqual(breaker.options.timeout, 30000); // 30s 86 }); 87 88 test('getBreakerStats should return correct structure', async () => { 89 const breaker = createOpenRouterBreaker(); 90 91 // Fire a request to populate stats 92 await breaker.fire(async () => 'test'); 93 94 const stats = getBreakerStats(breaker); 95 96 assert.ok(stats.name); 97 assert.ok(stats.state); 98 assert.ok(typeof stats.fires === 'number'); 99 assert.ok(typeof stats.successes === 'number'); 100 assert.ok(typeof stats.failures === 'number'); 101 assert.ok(typeof stats.rejects === 'number'); 102 assert.ok(typeof stats.timeouts === 'number'); 103 assert.ok(typeof stats.failureRate === 'string'); 104 }); 105 106 test('Circuit breaker should timeout long requests', async () => { 107 const breaker = createResendBreaker(); 108 109 // Override timeout for testing (original is 30s) 110 breaker.options.timeout = 100; // 100ms 111 112 try { 113 await breaker.fire(async () => { 114 // Simulate slow request 115 await new Promise(resolve => setTimeout(resolve, 200)); 116 return 'should timeout'; 117 }); 118 assert.fail('Should have timed out'); 119 } catch (error) { 120 assert.ok( 121 error.message.includes('Timed out') || error.code === 'ETIMEDOUT', 122 'Should timeout long requests' 123 ); 124 } 125 126 const stats = getBreakerStats(breaker); 127 assert.ok(stats.timeouts >= 1 || stats.failures >= 1); 128 });