const test = require("node:test"); const assert = require("node:assert/strict"); const { ConfigError, loadConfig, redactedConfig } = require("../src/config"); function validEnvironment(overrides = {}) { return { NODE_ENV: "production", HOST: "127.0.0.1", PORT: "7532", DATABASE_PATH: "/data/db/announcements.sqlite", APP_TIMEZONE: "America/Chicago", S3_ENDPOINT: "http://seaweedfs:8333", S3_REGION: "us-east-1", S3_BUCKET: "announcements", S3_ACCESS_KEY_ID: "test-access-key", S3_SECRET_ACCESS_KEY: "test-secret-key", S3_FORCE_PATH_STYLE: "true", MQTT_URL: "mqtt://broker:1883", MQTT_USERNAME: "mqtt-user", MQTT_PASSWORD: "mqtt-password", HA_WEBHOOK_URL: "http://home-assistant:8123/api/webhook/example", HA_WEBHOOK_SECRET: "webhook-secret", ...overrides, }; } test("loadConfig validates and normalizes the supported environment", () => { const config = loadConfig(validEnvironment()); assert.equal(config.environment, "production"); assert.equal(config.port, 7532); assert.equal(config.timeZone, "America/Chicago"); assert.equal(config.storage.endpoint, "http://seaweedfs:8333"); assert.equal(config.storage.forcePathStyle, true); assert.equal(config.storage.healthProbeIntervalMs, 30_000); assert.equal(config.storage.connectionTimeoutMs, 2_000); assert.equal(config.storage.requestTimeoutMs, 5_000); assert.equal(config.storage.maxAttempts, 2); assert.equal(config.processing.tempDirectory, "/tmp/announcements-uploads"); assert.equal(config.processing.maxFileBytes, 25 * 1024 * 1024); assert.equal(config.processing.maxFiles, 20); assert.equal(config.processing.maxInputPixels, 40_000_000); }); test("production requires private S3 credentials", () => { assert.throws( () => loadConfig(validEnvironment({ S3_ACCESS_KEY_ID: "" })), (error) => error instanceof ConfigError && error.field === "S3_ACCESS_KEY_ID", ); const development = loadConfig( validEnvironment({ NODE_ENV: "development", S3_ACCESS_KEY_ID: "", S3_SECRET_ACCESS_KEY: "", }), ); assert.equal(development.storage.accessKeyId, null); assert.equal(development.storage.secretAccessKey, null); }); test("loadConfig rejects invalid typed values", () => { assert.throws( () => loadConfig(validEnvironment({ PORT: "7532.5" })), /PORT must be an integer/, ); assert.throws( () => loadConfig(validEnvironment({ APP_TIMEZONE: "Church/Local" })), /valid IANA time zone/, ); assert.throws( () => loadConfig(validEnvironment({ S3_FORCE_PATH_STYLE: "sometimes" })), /must be a boolean/, ); assert.throws( () => loadConfig(validEnvironment({ S3_HEALTH_PROBE_INTERVAL_MS: "999" })), /S3_HEALTH_PROBE_INTERVAL_MS must be at least 1000/, ); assert.throws( () => loadConfig(validEnvironment({ S3_REQUEST_TIMEOUT_MS: "999" })), /S3_REQUEST_TIMEOUT_MS must be at least 1000/, ); assert.throws( () => loadConfig(validEnvironment({ S3_CONNECT_TIMEOUT_MS: "249" })), /S3_CONNECT_TIMEOUT_MS must be at least 250/, ); assert.throws( () => loadConfig(validEnvironment({ S3_MAX_ATTEMPTS: "6" })), /S3_MAX_ATTEMPTS must be at most 5/, ); assert.throws( () => loadConfig(validEnvironment({ DATABASE_PATH: "relative.sqlite" })), /must be an absolute path/, ); assert.throws( () => loadConfig(validEnvironment({ UPLOAD_TEMP_DIRECTORY: "relative" })), /UPLOAD_TEMP_DIRECTORY must be an absolute path/, ); assert.throws( () => loadConfig(validEnvironment({ UPLOAD_MAX_FILE_BYTES: "0" })), /UPLOAD_MAX_FILE_BYTES must be at least 1/, ); assert.throws( () => loadConfig(validEnvironment({ UPLOAD_MAX_FILES: "101" })), /UPLOAD_MAX_FILES must be at most 100/, ); assert.throws( () => loadConfig(validEnvironment({ IMAGE_MAX_INPUT_PIXELS: "no-limit" })), /IMAGE_MAX_INPUT_PIXELS must be an integer/, ); }); test("redactedConfig never returns credentials, passwords, paths, or webhook URLs", () => { const summary = redactedConfig(loadConfig(validEnvironment())); const serialized = JSON.stringify(summary); assert.equal(summary.storage.configured, true); assert.equal(summary.mqtt.configured, true); assert.equal(summary.notifications.configured, true); assert.doesNotMatch(serialized, /test-access-key/); assert.doesNotMatch(serialized, /test-secret-key/); assert.doesNotMatch(serialized, /mqtt-password/); assert.doesNotMatch(serialized, /webhook-secret/); assert.doesNotMatch(serialized, /announcements\.sqlite/); assert.doesNotMatch(serialized, /seaweedfs/); assert.doesNotMatch(serialized, /home-assistant/); });