/ tests / utils / circuit-breaker.test.js
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  });