/** * Unit tests for src/config/load.ts * * Covers: * - Parses all defaults correctly when only required vars are provided * - Missing required vars throw with the right message * - Invalid URLs throw (wrong protocol, not a URL) * - Bounded numerics throw on out-of-range values * - REDIS_CONSUMER_NAME defaults to INSTANCE_ID * - Explicit REDIS_CONSUMER_NAME overrides INSTANCE_ID */ import { describe, it, expect } from 'vitest'; import { loadConfig } from '../src/config/load.js'; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- /** Minimal valid env — only required fields. */ function validEnv(overrides: Record = {}): Record { return { REDIS_URL: 'redis://localhost:6379', POSTGRES_URL: 'postgres://postgres:pass@localhost:5432/trm', ...overrides, }; } // --------------------------------------------------------------------------- // 1. Happy path — defaults // --------------------------------------------------------------------------- describe('loadConfig — defaults', () => { it('parses successfully with only required vars', () => { const config = loadConfig(validEnv()); expect(config.REDIS_URL).toBe('redis://localhost:6379'); expect(config.POSTGRES_URL).toBe('postgres://postgres:pass@localhost:5432/trm'); }); it('applies default NODE_ENV=production', () => { const config = loadConfig(validEnv()); expect(config.NODE_ENV).toBe('production'); }); it('applies default INSTANCE_ID=processor-1', () => { const config = loadConfig(validEnv()); expect(config.INSTANCE_ID).toBe('processor-1'); }); it('applies default LOG_LEVEL=info', () => { const config = loadConfig(validEnv()); expect(config.LOG_LEVEL).toBe('info'); }); it('applies default REDIS_TELEMETRY_STREAM=telemetry:teltonika', () => { const config = loadConfig(validEnv()); expect(config.REDIS_TELEMETRY_STREAM).toBe('telemetry:teltonika'); }); it('applies default REDIS_CONSUMER_GROUP=processor', () => { const config = loadConfig(validEnv()); expect(config.REDIS_CONSUMER_GROUP).toBe('processor'); }); it('defaults REDIS_CONSUMER_NAME to INSTANCE_ID', () => { const config = loadConfig(validEnv({ INSTANCE_ID: 'my-instance' })); expect(config.REDIS_CONSUMER_NAME).toBe('my-instance'); }); it('respects explicit REDIS_CONSUMER_NAME override', () => { const config = loadConfig( validEnv({ INSTANCE_ID: 'instance-a', REDIS_CONSUMER_NAME: 'consumer-override' }), ); expect(config.REDIS_CONSUMER_NAME).toBe('consumer-override'); }); it('applies default METRICS_PORT=9090', () => { const config = loadConfig(validEnv()); expect(config.METRICS_PORT).toBe(9090); }); it('applies default BATCH_SIZE=100', () => { const config = loadConfig(validEnv()); expect(config.BATCH_SIZE).toBe(100); }); it('applies default BATCH_BLOCK_MS=5000', () => { const config = loadConfig(validEnv()); expect(config.BATCH_BLOCK_MS).toBe(5_000); }); it('applies default WRITE_BATCH_SIZE=50', () => { const config = loadConfig(validEnv()); expect(config.WRITE_BATCH_SIZE).toBe(50); }); it('applies default DEVICE_STATE_LRU_CAP=10000', () => { const config = loadConfig(validEnv()); expect(config.DEVICE_STATE_LRU_CAP).toBe(10_000); }); }); // --------------------------------------------------------------------------- // 2. Missing required vars // --------------------------------------------------------------------------- describe('loadConfig — missing required vars', () => { it('throws when REDIS_URL is missing', () => { expect(() => loadConfig({ POSTGRES_URL: 'postgres://localhost:5432/trm' })).toThrow( /REDIS_URL/, ); }); it('throws when POSTGRES_URL is missing', () => { expect(() => loadConfig({ REDIS_URL: 'redis://localhost:6379' })).toThrow(/POSTGRES_URL/); }); it('throws when both required vars are missing', () => { expect(() => loadConfig({})).toThrow(/Configuration error/); }); it('error message mentions every failing field', () => { let message = ''; try { loadConfig({}); } catch (err) { message = err instanceof Error ? err.message : ''; } expect(message).toMatch(/REDIS_URL/); expect(message).toMatch(/POSTGRES_URL/); }); }); // --------------------------------------------------------------------------- // 3. URL validation // --------------------------------------------------------------------------- describe('loadConfig — URL validation', () => { it('accepts redis:// URLs', () => { expect(() => loadConfig(validEnv({ REDIS_URL: 'redis://redis:6379' }))).not.toThrow(); }); it('accepts rediss:// (TLS) URLs', () => { expect(() => loadConfig(validEnv({ REDIS_URL: 'rediss://redis:6380' }))).not.toThrow(); }); it('rejects REDIS_URL with wrong protocol (http)', () => { expect(() => loadConfig(validEnv({ REDIS_URL: 'http://localhost:6379' }))).toThrow( /REDIS_URL/, ); }); it('rejects REDIS_URL that is not a URL at all', () => { expect(() => loadConfig(validEnv({ REDIS_URL: 'not-a-url' }))).toThrow(/REDIS_URL/); }); it('accepts postgres:// URLs', () => { expect(() => loadConfig(validEnv({ POSTGRES_URL: 'postgres://user:pass@db:5432/mydb' })), ).not.toThrow(); }); it('accepts postgresql:// URLs', () => { expect(() => loadConfig(validEnv({ POSTGRES_URL: 'postgresql://user:pass@db:5432/mydb' })), ).not.toThrow(); }); it('rejects POSTGRES_URL with wrong protocol (mysql)', () => { expect(() => loadConfig(validEnv({ POSTGRES_URL: 'mysql://localhost:3306/db' })), ).toThrow(/POSTGRES_URL/); }); it('rejects POSTGRES_URL that is not a URL at all', () => { expect(() => loadConfig(validEnv({ POSTGRES_URL: 'localhost/db' }))).toThrow(/POSTGRES_URL/); }); }); // --------------------------------------------------------------------------- // 4. Bounded numerics // --------------------------------------------------------------------------- describe('loadConfig — bounded numerics', () => { it('rejects BATCH_SIZE below minimum (0)', () => { expect(() => loadConfig(validEnv({ BATCH_SIZE: '0' }))).toThrow(/BATCH_SIZE/); }); it('rejects BATCH_SIZE above maximum (10001)', () => { expect(() => loadConfig(validEnv({ BATCH_SIZE: '10001' }))).toThrow(/BATCH_SIZE/); }); it('accepts BATCH_SIZE at boundary values (1, 10000)', () => { expect(() => loadConfig(validEnv({ BATCH_SIZE: '1' }))).not.toThrow(); expect(() => loadConfig(validEnv({ BATCH_SIZE: '10000' }))).not.toThrow(); }); it('rejects BATCH_BLOCK_MS above maximum (60001)', () => { expect(() => loadConfig(validEnv({ BATCH_BLOCK_MS: '60001' }))).toThrow(/BATCH_BLOCK_MS/); }); it('accepts BATCH_BLOCK_MS=0 (no blocking)', () => { const config = loadConfig(validEnv({ BATCH_BLOCK_MS: '0' })); expect(config.BATCH_BLOCK_MS).toBe(0); }); it('rejects WRITE_BATCH_SIZE below minimum (0)', () => { expect(() => loadConfig(validEnv({ WRITE_BATCH_SIZE: '0' }))).toThrow(/WRITE_BATCH_SIZE/); }); it('rejects WRITE_BATCH_SIZE above maximum (1001)', () => { expect(() => loadConfig(validEnv({ WRITE_BATCH_SIZE: '1001' }))).toThrow(/WRITE_BATCH_SIZE/); }); it('rejects DEVICE_STATE_LRU_CAP below minimum (99)', () => { expect(() => loadConfig(validEnv({ DEVICE_STATE_LRU_CAP: '99' }))).toThrow( /DEVICE_STATE_LRU_CAP/, ); }); it('rejects DEVICE_STATE_LRU_CAP above maximum (1000001)', () => { expect(() => loadConfig(validEnv({ DEVICE_STATE_LRU_CAP: '1000001' }))).toThrow( /DEVICE_STATE_LRU_CAP/, ); }); it('rejects non-numeric METRICS_PORT', () => { expect(() => loadConfig(validEnv({ METRICS_PORT: 'abc' }))).toThrow(/METRICS_PORT/); }); }); // --------------------------------------------------------------------------- // 5. LOG_LEVEL validation // --------------------------------------------------------------------------- describe('loadConfig — LOG_LEVEL', () => { it('accepts all valid pino levels', () => { const levels = ['fatal', 'error', 'warn', 'info', 'debug', 'trace'] as const; for (const level of levels) { expect(() => loadConfig(validEnv({ LOG_LEVEL: level }))).not.toThrow(); } }); it('rejects an invalid log level', () => { expect(() => loadConfig(validEnv({ LOG_LEVEL: 'verbose' }))).toThrow(/LOG_LEVEL/); }); });