diff --git a/src/config.test.ts b/src/config.test.ts index 775fce1..f9e9ee5 100644 --- a/src/config.test.ts +++ b/src/config.test.ts @@ -3,43 +3,84 @@ import { loadConfig } from "./config"; describe("loadConfig", () => { test("returns defaults when env vars are unset", () => { - const config = loadConfig({}); - expect(config).toEqual({ + expect(loadConfig({})).toEqual({ host: "0.0.0.0", port: 22, maxLoop: 5, + playbackMode: "loop", + allowUserControl: true, + switchDebounceMs: 120, loginDelay: 1500, maxConnections: 10, + maxTotalConnections: 1000, + maxAuthAttempts: 6, + handshakeTimeout: 10000, + maxDimension: 512, + frameResolution: 360, brightnessThreshold: 40, + charset: "detailed", + invert: false, logCredentials: false, + videoPath: undefined, }); }); test("overrides defaults from env vars", () => { - const config = loadConfig({ - HOST: "127.0.0.1", - PORT: "2222", - MAX_LOOP: "3", - LOGIN_DELAY: "500", - MAX_CONNECTIONS: "20", - BRIGHTNESS_THRESHOLD: "60", - LOG_CREDENTIALS: "true", - }); - expect(config).toEqual({ + expect( + loadConfig({ + HOST: "127.0.0.1", + PORT: "2222", + MAX_LOOP: "3", + PLAYBACK_MODE: "random", + ALLOW_USER_CONTROL: "false", + SWITCH_DEBOUNCE_MS: "200", + LOGIN_DELAY: "500", + MAX_CONNECTIONS: "20", + MAX_TOTAL_CONNECTIONS: "50", + MAX_AUTH_ATTEMPTS: "3", + HANDSHAKE_TIMEOUT: "5000", + MAX_DIMENSION: "200", + FRAME_RESOLUTION: "480", + BRIGHTNESS_THRESHOLD: "60", + CHARSET: "blocks", + INVERT: "true", + LOG_CREDENTIALS: "true", + VIDEO_PATH: "/videos/clip.mkv", + }) + ).toEqual({ host: "127.0.0.1", port: 2222, maxLoop: 3, + playbackMode: "random", + allowUserControl: false, + switchDebounceMs: 200, loginDelay: 500, maxConnections: 20, + maxTotalConnections: 50, + maxAuthAttempts: 3, + handshakeTimeout: 5000, + maxDimension: 200, + frameResolution: 480, brightnessThreshold: 60, + charset: "blocks", + invert: true, logCredentials: true, + videoPath: "/videos/clip.mkv", }); }); - test("logCredentials is false for any value other than the string 'true'", () => { + test("boolean env vars are true only for the exact string 'true'", () => { expect(loadConfig({ LOG_CREDENTIALS: "1" }).logCredentials).toBe(false); - expect(loadConfig({ LOG_CREDENTIALS: "false" }).logCredentials).toBe( - false - ); + expect(loadConfig({ INVERT: "yes" }).invert).toBe(false); + expect(loadConfig({ INVERT: "true" }).invert).toBe(true); + }); + + test("falls back to defaults for non-numeric and out-of-range values", () => { + expect(loadConfig({ PORT: "not-a-number" }).port).toBe(22); + expect(loadConfig({ PORT: "99999" }).port).toBe(65535); + expect(loadConfig({ MAX_DIMENSION: "0" }).maxDimension).toBe(1); + expect( + loadConfig({ BRIGHTNESS_THRESHOLD: "999" }).brightnessThreshold + ).toBe(100); }); }); diff --git a/src/config.ts b/src/config.ts index aaff6eb..e570b6c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -4,25 +4,77 @@ export interface Config { host: string; port: number; maxLoop: number; + playbackMode: "loop" | "random"; + allowUserControl: boolean; + switchDebounceMs: number; loginDelay: number; maxConnections: number; + maxTotalConnections: number; + maxAuthAttempts: number; + handshakeTimeout: number; + maxDimension: number; + frameResolution: number; brightnessThreshold: number; + charset: string; + invert: boolean; logCredentials: boolean; + videoPath?: string; +} + +function parseIntEnv( + value: string | undefined, + fallback: number, + { min, max }: { min?: number; max?: number } = {} +): number { + if (value === undefined || value.trim() === "") return fallback; + const parsed = parseInt(value, 10); + if (!Number.isFinite(parsed)) return fallback; + let result = parsed; + if (typeof min === "number") result = Math.max(min, result); + if (typeof max === "number") result = Math.min(max, result); + return result; +} + +function parseBoolEnv(value: string | undefined): boolean { + return value === "true"; } export function loadConfig(env: NodeJS.ProcessEnv = process.env): Config { + const videoPath = env.VIDEO_PATH?.trim(); + return { host: env.HOST ?? "0.0.0.0", - port: env.PORT ? parseInt(env.PORT, 10) : 22, - maxLoop: env.MAX_LOOP ? parseInt(env.MAX_LOOP, 10) : 5, - loginDelay: env.LOGIN_DELAY ? parseInt(env.LOGIN_DELAY, 10) : 1500, - maxConnections: env.MAX_CONNECTIONS - ? parseInt(env.MAX_CONNECTIONS, 10) - : 10, - brightnessThreshold: env.BRIGHTNESS_THRESHOLD - ? parseInt(env.BRIGHTNESS_THRESHOLD, 10) - : 40, - logCredentials: env.LOG_CREDENTIALS === "true", + port: parseIntEnv(env.PORT, 22, { min: 1, max: 65535 }), + maxLoop: parseIntEnv(env.MAX_LOOP, 5, { min: 1 }), + playbackMode: + env.PLAYBACK_MODE?.trim().toLowerCase() === "random" + ? "random" + : "loop", + allowUserControl: env.ALLOW_USER_CONTROL !== "false", + switchDebounceMs: parseIntEnv(env.SWITCH_DEBOUNCE_MS, 120, { min: 0 }), + loginDelay: parseIntEnv(env.LOGIN_DELAY, 1500, { min: 0 }), + maxConnections: parseIntEnv(env.MAX_CONNECTIONS, 10, { min: 1 }), + maxTotalConnections: parseIntEnv(env.MAX_TOTAL_CONNECTIONS, 1000, { + min: 1, + }), + maxAuthAttempts: parseIntEnv(env.MAX_AUTH_ATTEMPTS, 6, { min: 1 }), + handshakeTimeout: parseIntEnv(env.HANDSHAKE_TIMEOUT, 10000, { min: 0 }), + maxDimension: parseIntEnv(env.MAX_DIMENSION, 512, { + min: 1, + max: 4096, + }), + frameResolution: parseIntEnv(env.FRAME_RESOLUTION, 360, { + min: 16, + max: 1080, + }), + brightnessThreshold: parseIntEnv(env.BRIGHTNESS_THRESHOLD, 40, { + min: 0, + max: 100, + }), + charset: env.CHARSET?.trim() || "detailed", + invert: parseBoolEnv(env.INVERT), + logCredentials: parseBoolEnv(env.LOG_CREDENTIALS), + videoPath: videoPath ? videoPath : undefined, }; }