/** * DC-096 regression tests: the `monitoring: { public: false }` config option * actually gates the monitoring endpoints. * * WHY THIS EXISTS: * The middleware comment documented `monitoring: { public: false }` in * config.json as the way to require auth for /api/v1/monitoring/stats and * /api/v1/health-checks/status on internet-exposed deployments. But the * option was dead three ways: * 1. applyConfigFields() never copied `monitoring` out of raw config — * siteConfig.monitoring stayed undefined forever. * 2. `monitoring` was not in config-schema KNOWN_KEYS — saving it via * POST /api/v1/config produced "Unknown config key" warnings (save * still succeeded, so users saw a warning for a real feature). * 3. MONITORING_PUBLIC was a const frozen at mount time AND re-required * the config/site singleton — POST /config changes never took effect * without a full process restart. * * Net effect: an operator who set the documented hardening option on an * exposed box kept serving monitoring data unauthenticated, with only a * cosmetic warning. Classic "config option that never worked". * * These tests pin the fixed behavior: * - applyConfigFields copies monitoring through to siteConfig * - isPublicRoute honors the gate LIVE (no restart) * - env override still wins over config * - schema accepts `monitoring` and validates its shape * - typo keys setupCompleted/setupMode no longer silently allowlisted */ const express = require('express'); const request = require('supertest'); // The config/site module exports the siteConfig singleton + loaders. const { siteConfig, loadSiteConfig } = require('../src/config/site'); const { validateConfig } = require('../src/utilities/config-schema'); // Build a minimal app mounting ONLY the middleware under test, with the // same dependency shape app.js passes. This mirrors how configureMiddleware // is used in production without booting the whole app (routes, docker, etc). function buildMiddlewareApp(configOverrides = {}) { const configureMiddleware = require('../src/utilities/middleware'); const app = express(); const siteConfigDep = { tld: '.sami', dashboardHost: 'status.sami', ...configOverrides }; const deps = { siteConfig: siteConfigDep, totpConfig: { enabled: true }, // force the auth path to actually run tailscaleConfig: { enabled: false, requireAuth: false }, metrics: { recordRequest: () => {} }, auditLogger: { middleware: () => (req, res, next) => next() }, authManager: { verifyJWT: async () => null, verifyAPIKey: async () => null }, log: { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} }, cryptoUtils: { loadOrCreateKey: () => 'test-key-not-a-real-secret' }, isValidContainerId: () => true, isTailscaleIP: () => false, getTailscaleStatus: async () => ({}) }; configureMiddleware(app, deps); // Probe route AFTER middleware so it exercises the auth chain. app.get('/api/v1/monitoring/stats', (req, res) => res.json({ ok: true })); app.get('/api/v1/health-checks/status', (req, res) => res.json({ ok: true })); return app; } describe('DC-096: monitoring.public config gate (middleware + site config)', () => { const ENV_KEY = 'MONITORING_PUBLIC'; afterEach(() => { delete process.env[ENV_KEY]; // Reset the singleton to a clean default for other suites siteConfig.monitoring = null; }); test('applyConfigFields copies monitoring through to siteConfig (the original dead option)', () => { loadSiteConfig(null, null); // no CONFIG_FILE arg → falls to catch, keeps defaults siteConfig.monitoring = undefined; // Directly exercise applyConfigFields via the public loader with a real temp file const fs = require('fs'); const os = require('os'); const path = require('path'); const tmp = path.join(os.tmpdir(), `dc096-config-${Date.now()}.json`); fs.writeFileSync(tmp, JSON.stringify({ tld: '.sami', monitoring: { public: false } })); try { const noopLog = { info: () => {}, warn: () => {}, error: () => {} }; loadSiteConfig(tmp, noopLog); expect(siteConfig.monitoring).toEqual({ public: false }); } finally { fs.unlinkSync(tmp); } }); test('monitoring endpoints are PUBLIC by default (no monitoring config)', async () => { const app = buildMiddlewareApp(); const res = await request(app).get('/api/v1/monitoring/stats'); expect(res.status).toBe(200); expect(res.body.ok).toBe(true); }); test('monitoring: { public: false } in config → endpoints require auth (401) — LIVE, no restart', async () => { const app = buildMiddlewareApp({ monitoring: { public: false } }); const res = await request(app).get('/api/v1/monitoring/stats'); expect(res.status).toBe(401); const res2 = await request(app).get('/api/v1/health-checks/status'); expect(res2.status).toBe(401); }); test('gate reads config LIVE: flipping siteConfig.monitoring.public at runtime flips the gate', async () => { const cfg = { monitoring: { public: true } }; const app = buildMiddlewareApp(cfg); let res = await request(app).get('/api/v1/monitoring/stats'); expect(res.status).toBe(200); // Simulate POST /api/v1/config refreshing the singleton in place — // the same object the middleware holds a reference to. cfg.monitoring.public = false; res = await request(app).get('/api/v1/monitoring/stats'); expect(res.status).toBe(401); }); test('env override MONITORING_PUBLIC=true beats config monitoring.public=false', async () => { process.env.MONITORING_PUBLIC = 'true'; const app = buildMiddlewareApp({ monitoring: { public: false } }); const res = await request(app).get('/api/v1/monitoring/stats'); expect(res.status).toBe(200); }); test('env override MONITORING_PUBLIC=false beats config monitoring.public=true', async () => { process.env.MONITORING_PUBLIC = 'false'; const app = buildMiddlewareApp({ monitoring: { public: true } }); const res = await request(app).get('/api/v1/monitoring/stats'); expect(res.status).toBe(401); }); test('non-monitoring public routes stay public when monitoring gate closes', async () => { const app = buildMiddlewareApp({ monitoring: { public: false } }); // /api/v1/version is public unconditionally const res = await request(app).get('/api/v1/version'); // No route mounted at that path in this harness → 404 from express, // NOT 401 — proving the auth middleware let it through. expect(res.status).toBe(404); }); }); describe('DC-096: config-schema accepts monitoring', () => { test('monitoring: { public: boolean } passes with zero warnings', () => { const result = validateConfig({ monitoring: { public: false } }); expect(result.warnings).toEqual([]); expect(result.valid).toBe(true); }); test('monitoring.public non-boolean is an ERROR (not silent)', () => { const result = validateConfig({ monitoring: { public: 'false' } }); expect(result.valid).toBe(false); expect(result.errors).toContain('monitoring.public must be a boolean'); }); test('monitoring non-object is an ERROR', () => { const result = validateConfig({ monitoring: 'private' }); expect(result.valid).toBe(false); expect(result.errors).toContain('monitoring must be an object'); }); test('typo keys setupCompleted/setupMode now WARN (no longer silently allowlisted)', () => { const result = validateConfig({ setupCompleted: true, setupMode: 'simple' }); expect(result.warnings).toEqual([ 'Unknown config key "setupCompleted" — possible typo?', 'Unknown config key "setupMode" — possible typo?' ]); }); });