Files
julian e1c6f59948 Realign processor stream-name default to telemetry:teltonika
Stage discovered the wrong default at runtime: tcp-ingestion's compiled
default REDIS_TELEMETRY_STREAM is 'telemetry:teltonika' but processor's
was 'telemetry:t', so the two services were talking past each other —
tcp-ingestion publishing to one stream, processor reading another empty
one. The deploy stack now pins both to the same value via a shared env
var, but the processor's compiled default should also match so local
development and the integration test stay aligned with reality.

Changes:
- src/config/load.ts — default changed to 'telemetry:teltonika'
- .env.example — same
- test/config.test.ts — default-value assertion updated
- planning docs (ROADMAP, phase-1 README, tasks 03/08/10, phase-3 README) —
  occurrences of 'telemetry:t' replaced with 'telemetry:teltonika'

The deploy stack remains the single source of truth via the shared
REDIS_TELEMETRY_STREAM env var. Compiled defaults are belt-and-braces.
2026-05-01 11:43:31 +02:00

248 lines
8.4 KiB
TypeScript

/**
* 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<string, string> = {}): Record<string, string> {
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/);
});
});