/** * DC-048 — disk-settings-loader unit tests * * Covers: * - applies persisted values to process.env (happy path) * - explicit process.env wins over persisted file * - missing file → no-op, no throw * - malformed JSON → no throw, engine defaults preserved * - non-numeric values rejected, not silently applied * - empty/null/undefined values skipped * - idempotent across calls (once-guard) * - all six mapped keys land in env when persisted * * Run with: npx jest __tests__/disk-settings-loader.test.js */ 'use strict'; const fs = require('fs'); const path = require('path'); // Snapshot env at module load so we can restore in afterEach. We always // UNSET the loader-managed keys (HEALTH_*, AUDIT_*, BACKUP_*, CONTAINER_STATS_*) // at the start of each test, regardless of whether they were set at // snapshot time, because the loader mutates process.env and stale values // from prior tests would silently change behavior. const LOADER_KEYS = [ 'HEALTH_CHECK_INTERVAL', 'HEALTH_MAX_ENTRIES', 'HEALTH_HISTORY_RETENTION', 'AUDIT_MAX_ENTRIES', 'BACKUP_MAX_STORAGE_BYTES', 'CONTAINER_STATS_MAX_ENTRIES', ]; const ORIGINAL_ENV = Object.fromEntries( Object.entries(process.env).filter(([k]) => LOADER_KEYS.includes(k) || k === 'DATA_DIR'), ); function restoreEnv() { // Loader-managed keys: ALWAYS reset to ORIGINAL_ENV state (or undefined). // This is critical — without it, env vars set by a prior test would leak // into the next test as "env-already-set" and the loader would skip // values that the test expects to be applied. for (const k of LOADER_KEYS) { if (ORIGINAL_ENV[k] === undefined) { delete process.env[k]; } else { process.env[k] = ORIGINAL_ENV[k]; } } delete process.env.DATA_DIR; } // Temp data dir for filesystem-driven tests. const TMP_DATA_DIR = '/tmp/dashcaddy-disk-settings-loader-test'; function makeDataDir() { try { fs.rmSync(TMP_DATA_DIR, { recursive: true, force: true }); } catch (_) { /* ok */ } fs.mkdirSync(TMP_DATA_DIR, { recursive: true }); } function writePersisted(obj) { fs.writeFileSync(path.join(TMP_DATA_DIR, 'disk-settings.json'), JSON.stringify(obj)); } describe('disk-settings-loader', () => { beforeEach(() => { restoreEnv(); makeDataDir(); // Wipe the once-guard between tests so each case sees a fresh loader run. // We must require the module AFTER clearing the cache. delete require.cache[require.resolve('../src/config/disk-settings-loader')]; const loader = require('../src/config/disk-settings-loader'); loader._resetForTesting(); // Force hasRun reset (jest's module loader is not always cleared by the // require.cache delete — explicit call is the contract for the loader). // Note: loader._resetForTesting is the authoritative reset path. }); afterAll(() => { restoreEnv(); try { fs.rmSync(TMP_DATA_DIR, { recursive: true, force: true }); } catch (_) { /* ok */ } }); it('applies all six persisted values to process.env', () => { writePersisted({ healthCheckInterval: 45000, healthMaxEntries: 750, healthRetentionDays: 14, statsMaxEntries: 800, auditMaxEntries: 1500, backupMaxStorageBytes: 2147483648, }); const loader = require('../src/config/disk-settings-loader'); const result = loader({ dataDir: TMP_DATA_DIR }); expect(result.applied).toHaveLength(6); expect(process.env.HEALTH_CHECK_INTERVAL).toBe('45000'); expect(process.env.HEALTH_MAX_ENTRIES).toBe('750'); expect(process.env.HEALTH_HISTORY_RETENTION).toBe('14'); expect(process.env.CONTAINER_STATS_MAX_ENTRIES).toBe('800'); expect(process.env.AUDIT_MAX_ENTRIES).toBe('1500'); expect(process.env.BACKUP_MAX_STORAGE_BYTES).toBe('2147483648'); expect(result.skipped).toEqual([]); }); it('does not throw when disk-settings.json is missing', () => { // TMP_DATA_DIR exists but no disk-settings.json inside it. const loader = require('../src/config/disk-settings-loader'); expect(() => loader({ dataDir: TMP_DATA_DIR })).not.toThrow(); const result = loader({ dataDir: TMP_DATA_DIR }); // idempotent expect(result.applied).toEqual([]); }); it('does not throw on malformed JSON; logs to stderr', () => { fs.writeFileSync(path.join(TMP_DATA_DIR, 'disk-settings.json'), '{ this is not json'); const stderrSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => true); const loader = require('../src/config/disk-settings-loader'); expect(() => loader({ dataDir: TMP_DATA_DIR })).not.toThrow(); const result = loader({ dataDir: TMP_DATA_DIR }); expect(result.applied).toEqual([]); expect(stderrSpy).toHaveBeenCalledWith( expect.stringContaining('WARN: failed to parse'), ); stderrSpy.mockRestore(); }); it('explicit process.env wins over persisted file', () => { process.env.HEALTH_HISTORY_RETENTION = '90'; writePersisted({ healthRetentionDays: 7, healthMaxEntries: 999, }); const loader = require('../src/config/disk-settings-loader'); const result = loader({ dataDir: TMP_DATA_DIR }); expect(process.env.HEALTH_HISTORY_RETENTION).toBe('90'); // unchanged expect(process.env.HEALTH_MAX_ENTRIES).toBe('999'); // applied expect(result.skipped).toEqual([ expect.objectContaining({ envKey: 'HEALTH_HISTORY_RETENTION', reason: 'env-already-set' }), ]); }); it('rejects non-numeric values for numeric fields', () => { writePersisted({ healthCheckInterval: 'fast', // not numeric healthMaxEntries: '500x', // not numeric healthRetentionDays: 14, // valid auditMaxEntries: null, // silently skipped (null) backupMaxStorageBytes: '', // silently skipped (empty) }); const loader = require('../src/config/disk-settings-loader'); const result = loader({ dataDir: TMP_DATA_DIR }); // Only the valid value lands in `applied`. expect(result.applied.map((a) => a.envKey)).toEqual(['HEALTH_HISTORY_RETENTION']); // Non-numeric values appear in `skipped` with reason='non-numeric'. // null and '' are silently filtered (treated as "field not present"). expect(result.skipped.map((s) => s.envKey).sort()).toEqual( ['HEALTH_CHECK_INTERVAL', 'HEALTH_MAX_ENTRIES'].sort(), ); expect(result.skipped.every((s) => s.reason === 'non-numeric')).toBe(true); }); it('coerces numeric strings (e.g. "14") to integer strings', () => { writePersisted({ healthRetentionDays: '14' }); const loader = require('../src/config/disk-settings-loader'); loader({ dataDir: TMP_DATA_DIR }); expect(process.env.HEALTH_HISTORY_RETENTION).toBe('14'); // Must be an integer-formatted string (not "14.7", "14x", etc.) expect(Number.isInteger(parseInt(process.env.HEALTH_HISTORY_RETENTION, 10))).toBe(true); }); it('is idempotent across multiple calls (once-guard)', () => { writePersisted({ healthRetentionDays: 7 }); const loader = require('../src/config/disk-settings-loader'); const first = loader({ dataDir: TMP_DATA_DIR }); const second = loader({ dataDir: TMP_DATA_DIR }); expect(first.applied).toHaveLength(1); expect(second.applied).toEqual([]); expect(second.alreadyRun).toBe(true); }); it('skips unknown fields without crashing', () => { writePersisted({ healthRetentionDays: 14, unknownField: 'whatever', anotherUnknown: { nested: true }, }); const loader = require('../src/config/disk-settings-loader'); expect(() => loader({ dataDir: TMP_DATA_DIR })).not.toThrow(); expect(process.env.HEALTH_HISTORY_RETENTION).toBe('14'); }); it('returns a summary object with source path', () => { writePersisted({ healthRetentionDays: 14 }); const loader = require('../src/config/disk-settings-loader'); const result = loader({ dataDir: TMP_DATA_DIR }); expect(result.source).toBe(path.join(TMP_DATA_DIR, 'disk-settings.json')); expect(result.alreadyRun).toBe(false); }); it('writes a boot summary to stderr when no logger is provided', () => { writePersisted({ healthRetentionDays: 14, healthMaxEntries: 999 }); const stderrSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => true); const loader = require('../src/config/disk-settings-loader'); loader({ dataDir: TMP_DATA_DIR }); // no logger passed expect(stderrSpy).toHaveBeenCalledWith( expect.stringMatching(/^\[disk-settings-loader\] rehydrated 2 setting/), ); stderrSpy.mockRestore(); }); });