UNPKG

@the_cfdude/productboard-mcp

Version:

Model Context Protocol server for Productboard REST API with dynamic tool loading

173 lines (172 loc) 6.56 kB
/** * Unit tests for retry logic */ import { describe, it, expect } from '@jest/globals'; import { withRetry, CircuitBreaker } from '../utils/retry.js'; import { NetworkError, RateLimitError } from '../errors/index.js'; describe('Retry Logic', () => { describe('withRetry', () => { it('should succeed on first attempt', async () => { let attempts = 0; const fn = async () => { attempts++; return 'success'; }; const result = await withRetry(fn); expect(result).toBe('success'); expect(attempts).toBe(1); }); it('should retry on retryable errors', async () => { let attempts = 0; const fn = async () => { attempts++; if (attempts < 3) { throw new NetworkError('Connection failed'); } return 'success'; }; const result = await withRetry(fn, { maxRetries: 3, initialDelay: 10, }); expect(result).toBe('success'); expect(attempts).toBe(3); }); it('should respect rate limit retry-after', async () => { let attempts = 0; const fn = async () => { attempts++; if (attempts === 1) { throw new RateLimitError(1); // 1 second } return 'success'; }; const start = Date.now(); const result = await withRetry(fn, { maxRetries: 2, initialDelay: 10000, // Would be 10s without retry-after }); const duration = Date.now() - start; expect(result).toBe('success'); expect(attempts).toBe(2); expect(duration).toBeGreaterThanOrEqual(900); // At least 900ms expect(duration).toBeLessThan(2000); // But less than 2s }); it('should fail after max retries', async () => { let attempts = 0; const error = new NetworkError('Persistent failure'); const fn = async () => { attempts++; throw error; }; await expect(withRetry(fn, { maxRetries: 2, initialDelay: 10 })).rejects.toThrow(error); expect(attempts).toBe(2); }); it('should not retry non-retryable errors', async () => { let attempts = 0; const error = new Error('Not retryable'); const fn = async () => { attempts++; throw error; }; await expect(withRetry(fn)).rejects.toThrow(error); expect(attempts).toBe(1); }); it('should handle axios-like errors', async () => { let attempts = 0; const fn = async () => { attempts++; if (attempts === 1) { throw { response: { status: 503 } }; } return 'success'; }; const result = await withRetry(fn, { maxRetries: 2, initialDelay: 10, }); expect(result).toBe('success'); expect(attempts).toBe(2); }); }); describe('CircuitBreaker', () => { it('should allow calls when closed', async () => { const breaker = new CircuitBreaker(3, 100); let calls = 0; const fn = async () => { calls++; return 'success'; }; const result = await breaker.execute(fn); expect(result).toBe('success'); expect(calls).toBe(1); }); it('should open after threshold failures', async () => { const breaker = new CircuitBreaker(3, 100); let calls = 0; const fn = async () => { calls++; throw new Error('fail'); }; // Fail 3 times to open the breaker for (let i = 0; i < 3; i++) { await expect(breaker.execute(fn)).rejects.toThrow('fail'); } expect(calls).toBe(3); // Circuit should be open now await expect(breaker.execute(fn)).rejects.toThrow('Circuit breaker is open'); expect(calls).toBe(3); // Not called on 4th attempt }); it('should enter half-open state after timeout', async () => { const breaker = new CircuitBreaker(2, 50); // 50ms timeout let calls = 0; let shouldFail = true; const fn = async () => { calls++; if (shouldFail) { throw new Error('fail'); } return 'success'; }; // Open the breaker await expect(breaker.execute(fn)).rejects.toThrow('fail'); await expect(breaker.execute(fn)).rejects.toThrow('fail'); expect(calls).toBe(2); // Should be open await expect(breaker.execute(fn)).rejects.toThrow('Circuit breaker is open'); expect(calls).toBe(2); // Wait for timeout await new Promise(resolve => setTimeout(resolve, 60)); // Should allow one attempt (half-open) shouldFail = false; const result = await breaker.execute(fn); expect(result).toBe('success'); expect(calls).toBe(3); }); it('should reset on successful half-open call', async () => { const breaker = new CircuitBreaker(2, 50); let calls = 0; let failCount = 0; const fn = async () => { calls++; if (failCount > 0) { failCount--; throw new Error('fail'); } return 'success'; }; // Open the breaker failCount = 2; await expect(breaker.execute(fn)).rejects.toThrow(); await expect(breaker.execute(fn)).rejects.toThrow(); // Wait and succeed in half-open await new Promise(resolve => setTimeout(resolve, 60)); await breaker.execute(fn); // Should be fully closed now - reset call count calls = 0; await breaker.execute(fn); await breaker.execute(fn); expect(calls).toBe(2); }); }); });