UNPKG

@bernierllc/retry-policy

Version:

Atomic retry policy utilities with exponential backoff and jitter

321 lines (246 loc) 10.4 kB
/* Copyright (c) 2025 Bernier LLC This file is licensed to the client under a limited-use license. The client may use and modify this code *only within the scope of the project it was delivered for*. Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC. */ import { RetryPolicyConfigurationLoader, loadOptionalRetryPolicyConfig, loadOptionalRetryPolicyConfigWithSources, mergeConfigurations, setGlobalRetryPolicyConfig, getGlobalRetryPolicyConfig, clearGlobalRetryPolicyConfig } from '../src/config/loadOptionalConfig'; describe('Retry Policy Configuration', () => { beforeEach(() => { // Clear global config before each test clearGlobalRetryPolicyConfig(); // Clear environment variables delete process.env.RETRY_MAX_RETRIES; delete process.env.RETRY_INITIAL_DELAY; delete process.env.RETRY_MAX_DELAY; delete process.env.RETRY_BACKOFF_FACTOR; delete process.env.RETRY_JITTER; delete process.env.RETRY_ENABLED; delete process.env.RETRY_BACKOFF_TYPE; delete process.env.RETRY_BACKOFF_BASE_DELAY; delete process.env.RETRY_BACKOFF_MAX_DELAY; delete process.env.RETRY_BACKOFF_MULTIPLIER; delete process.env.RETRY_JITTER_TYPE; delete process.env.RETRY_JITTER_FACTOR; }); describe('Global Configuration', () => { it('should set and get global configuration', () => { const config = { maxRetries: 10, initialDelayMs: 2000 }; setGlobalRetryPolicyConfig(config); const retrieved = getGlobalRetryPolicyConfig(); expect(retrieved).toEqual(config); }); it('should merge global configurations', () => { setGlobalRetryPolicyConfig({ maxRetries: 10 }); setGlobalRetryPolicyConfig({ initialDelayMs: 2000 }); const retrieved = getGlobalRetryPolicyConfig(); expect(retrieved).toEqual({ maxRetries: 10, initialDelayMs: 2000 }); }); it('should clear global configuration', () => { setGlobalRetryPolicyConfig({ maxRetries: 10 }); clearGlobalRetryPolicyConfig(); const retrieved = getGlobalRetryPolicyConfig(); expect(retrieved).toEqual({}); }); }); describe('Environment Variable Parsing', () => { it('should load boolean environment variables', () => { process.env.RETRY_ENABLED = 'true'; process.env.RETRY_JITTER = 'false'; const config = loadOptionalRetryPolicyConfig(); expect(config.enabled).toBe(true); expect(config.jitter).toBe(false); }); it('should load integer environment variables', () => { process.env.RETRY_MAX_RETRIES = '10'; process.env.RETRY_INITIAL_DELAY = '2000'; const config = loadOptionalRetryPolicyConfig(); expect(config.maxRetries).toBe(10); expect(config.initialDelayMs).toBe(2000); }); it('should load float environment variables', () => { process.env.RETRY_BACKOFF_FACTOR = '1.5'; process.env.RETRY_JITTER_FACTOR = '0.25'; const config = loadOptionalRetryPolicyConfig(); expect(config.backoffFactor).toBe(1.5); expect(config.backoff?.jitter?.factor).toBe(0.25); }); it('should load string environment variables', () => { process.env.RETRY_BACKOFF_TYPE = 'linear'; process.env.RETRY_JITTER_TYPE = 'full'; const config = loadOptionalRetryPolicyConfig(); expect(config.backoff?.type).toBe('linear'); expect(config.backoff?.jitter?.type).toBe('full'); }); it('should load nested configuration from environment variables', () => { process.env.RETRY_BACKOFF_BASE_DELAY = '100'; process.env.RETRY_BACKOFF_MAX_DELAY = '5000'; process.env.RETRY_BACKOFF_MULTIPLIER = '2'; const config = loadOptionalRetryPolicyConfig(); expect(config.backoff?.baseDelay).toBe(100); expect(config.backoff?.maxDelay).toBe(5000); expect(config.backoff?.factor).toBe(2); }); }); describe('Configuration Merging', () => { it('should merge multiple configurations', () => { const config1 = { maxRetries: 5 }; const config2 = { initialDelayMs: 1000 }; const config3 = { maxDelayMs: 30000 }; const merged = mergeConfigurations(config1, config2, config3); expect(merged).toEqual({ maxRetries: 5, initialDelayMs: 1000, maxDelayMs: 30000 }); }); it('should merge backoff configurations correctly', () => { const config1 = { backoff: { type: 'exponential' as const, baseDelay: 100 } }; const config2 = { backoff: { maxDelay: 5000 } }; const merged = mergeConfigurations(config1, config2); expect(merged.backoff).toEqual({ type: 'exponential', baseDelay: 100, maxDelay: 5000 }); }); it('should override with later configurations', () => { const config1 = { maxRetries: 5 }; const config2 = { maxRetries: 10 }; const merged = mergeConfigurations(config1, config2); expect(merged.maxRetries).toBe(10); }); it('should handle undefined configurations', () => { const config1 = { maxRetries: 5 }; const merged = mergeConfigurations(config1, undefined as any, { initialDelayMs: 1000 }); expect(merged).toEqual({ maxRetries: 5, initialDelayMs: 1000 }); }); }); describe('Configuration Loader', () => { it('should create loader instance', () => { const loader = new RetryPolicyConfigurationLoader(); expect(loader).toBeInstanceOf(RetryPolicyConfigurationLoader); }); it('should load configuration with sources', () => { process.env.RETRY_MAX_RETRIES = '10'; const loader = new RetryPolicyConfigurationLoader(); const config = loader.loadOptionalConfiguration(); const sources = loader.getConfigurationSources(); expect(config.maxRetries).toBe(10); expect(sources.length).toBeGreaterThan(0); }); it('should track environment variable sources', () => { process.env.RETRY_MAX_RETRIES = '10'; process.env.RETRY_INITIAL_DELAY = '2000'; const loader = new RetryPolicyConfigurationLoader(); loader.loadOptionalConfiguration(); const sources = loader.getConfigurationSources(); const envSources = sources.filter(s => s.source === 'environment'); expect(envSources.length).toBe(2); }); it('should track global configuration source', () => { setGlobalRetryPolicyConfig({ maxRetries: 10 }); const loader = new RetryPolicyConfigurationLoader(); loader.loadOptionalConfiguration(); const sources = loader.getConfigurationSources(); const globalSource = sources.find(s => s.source === 'global'); expect(globalSource).toBeDefined(); expect(globalSource?.description).toContain('service packages'); }); it('should provide environment variable documentation', () => { const loader = new RetryPolicyConfigurationLoader(); const docs = loader.getEnvironmentVariableDocumentation(); expect(docs.RETRY_MAX_RETRIES).toContain('maxRetries'); expect(docs.RETRY_INITIAL_DELAY).toContain('initialDelayMs'); expect(docs.RETRY_BACKOFF_TYPE).toContain('backoff.type'); }); }); describe('loadOptionalRetryPolicyConfig', () => { it('should return empty config when no configuration provided', () => { const config = loadOptionalRetryPolicyConfig(); expect(config).toEqual({}); }); it('should load environment configuration', () => { process.env.RETRY_MAX_RETRIES = '10'; process.env.RETRY_INITIAL_DELAY = '2000'; const config = loadOptionalRetryPolicyConfig(); expect(config.maxRetries).toBe(10); expect(config.initialDelayMs).toBe(2000); }); it('should merge global and environment configuration', () => { setGlobalRetryPolicyConfig({ maxRetries: 5 }); process.env.RETRY_INITIAL_DELAY = '2000'; const config = loadOptionalRetryPolicyConfig(); expect(config.maxRetries).toBe(5); expect(config.initialDelayMs).toBe(2000); }); it('should handle errors gracefully', () => { // Force an error by making setNestedValue fail const originalProcessEnv = process.env; Object.defineProperty(process, 'env', { get: () => { throw new Error('Test error'); }, configurable: true }); const config = loadOptionalRetryPolicyConfig(); // Should return empty config on error expect(config).toEqual({}); // Restore process.env Object.defineProperty(process, 'env', { value: originalProcessEnv, configurable: true }); }); }); describe('loadOptionalRetryPolicyConfigWithSources', () => { it('should return config and sources', () => { process.env.RETRY_MAX_RETRIES = '10'; const result = loadOptionalRetryPolicyConfigWithSources(); expect(result.config.maxRetries).toBe(10); expect(result.sources.length).toBeGreaterThan(0); }); it('should handle errors gracefully', () => { // Force an error const originalProcessEnv = process.env; Object.defineProperty(process, 'env', { get: () => { throw new Error('Test error'); }, configurable: true }); const result = loadOptionalRetryPolicyConfigWithSources(); expect(result.config).toEqual({}); expect(result.sources).toEqual([]); // Restore process.env Object.defineProperty(process, 'env', { value: originalProcessEnv, configurable: true }); }); }); describe('Configuration Precedence', () => { it('should follow precedence: global < environment', () => { setGlobalRetryPolicyConfig({ maxRetries: 5 }); process.env.RETRY_MAX_RETRIES = '10'; const config = loadOptionalRetryPolicyConfig(); // Environment should override global expect(config.maxRetries).toBe(10); }); it('should allow constructor options to override all', () => { setGlobalRetryPolicyConfig({ maxRetries: 5 }); process.env.RETRY_MAX_RETRIES = '10'; const runtimeConfig = loadOptionalRetryPolicyConfig(); const constructorConfig = { maxRetries: 15 }; const final = mergeConfigurations(runtimeConfig, constructorConfig); expect(final.maxRetries).toBe(15); }); }); });