/** * DC-Shield route tests: /security/shield endpoint. * * The route reads the sec-dashboard snapshot from an absolute path * (/var/lib/sec-dashboard/snapshot.json) and services.json. Those constants * live inside the module, so these tests exercise the REAL module against * the REAL host files — same approach as app-startup-smoke — plus * behavior-level assertions for the 503 no-snapshot case using a bound * copy of the handler with a stubbed fs (dependency seam via require cache). * * On a dev host without the snapshot the 503 path is the testable truth; on * DNS2 the snapshot exists, so both shapes are covered. */ const path = require('path'); const express = require('express'); const request = require('supertest'); const buildShield = require(path.resolve(__dirname, '..', 'routes', 'shield')); function buildApp() { const e = express(); e.use('/api/v1/security', buildShield({ log: { info() {}, error() {}, warn() {} } })); return e; } describe('DC-Shield: GET /api/v1/security/shield', () => { it('mounts and responds without crashing (200 with snapshot, else 503)', async () => { const res = await request(buildApp()).get('/api/v1/security/shield'); expect([200, 503]).toContain(res.status); }); it('response shape matches the frontend contract on 200', async () => { const res = await request(buildApp()).get('/api/v1/security/shield'); if (res.status === 503) return; // no snapshot on this host — covered below const b = res.body.data || res.body; expect(b).toHaveProperty('generated_utc'); expect(b.fleet).toHaveProperty('verdict'); expect(['secure', 'watch', 'incident']).toContain(b.fleet.verdict); expect(b.services).toBeDefined(); expect(Array.isArray(b.unmapped_hosts)).toBe(true); }); it('503 + SNAPSHOT_UNAVAILABLE when snapshot missing (fs stubbed)', async () => { // Stub fs.readFileSync for the snapshot path only, via the module cache. const fsPath = require.resolve('fs'); const realFs = require('fs'); const orig = realFs.readFileSync; realFs.readFileSync = function (p, enc) { if (String(p).includes('sec-dashboard')) { throw new Error('ENOENT (test stub)'); } return orig.call(realFs, p, enc); }; try { const res = await request(buildApp()).get('/api/v1/security/shield'); expect(res.status).toBe(503); expect(JSON.stringify(res.body)).toContain('SNAPSHOT_UNAVAILABLE'); } finally { realFs.readFileSync = orig; } }); it('route module loads with the composition-root mount (regression: prod-only drift)', () => { // src/app.js must mount routes/shield (was prod-only for weeks). const appjs = require('fs').readFileSync(path.resolve(__dirname, '..', 'src', 'app.js'), 'utf8'); expect(appjs).toMatch(/apiRouter\.use\(\x27\/security\x27, require\(\x27\.\.\/routes\/shield\x27\)/); }); });