/** * Unit and Integration Tests: VCL Aggregated Burndown * * Feature: vcl-aggregated-burndown * * Tests cover: * - deduplicateByHostname edge cases * - computeAggregatedBurndown edge cases * - GET /burndown endpoint with mocked DB * - Empty DB returns zero/empty response * - All-blocker scenario * - Auth middleware enforcement */ const http = require('http'); const express = require('express'); // Mock auth middleware jest.mock('../middleware/auth', () => ({ requireAuth: () => (req, res, next) => { req.user = { id: 1, username: 'testuser', group: 'Admin' }; next(); }, requireGroup: () => (req, res, next) => next(), })); jest.mock('../helpers/auditLog', () => jest.fn()); jest.mock('../helpers/ivantiApi', () => ({ ivantiFormPost: jest.fn(), ivantiPost: jest.fn(), })); // Mock driftChecker jest.mock('../helpers/driftChecker', () => ({ loadConfig: jest.fn(() => ({})), compareSchemaToDrift: jest.fn(() => null), reconcileConfig: jest.fn(() => ({ changes: [] })), })); const mockPool = { query: jest.fn(() => Promise.resolve({ rows: [], rowCount: 0 })), connect: jest.fn(() => Promise.resolve({ query: jest.fn(() => Promise.resolve({ rows: [], rowCount: 0 })), release: jest.fn(), })), }; jest.mock('../db', () => mockPool); const { deduplicateByHostname, computeAggregatedBurndown, } = require('../helpers/vclHelpers'); const { createVCLMultiVerticalRouter } = require('../routes/vclMultiVertical'); // --- HTTP helper --- function request(server, method, path, body) { return new Promise((resolve, reject) => { const addr = server.address(); const options = { hostname: '127.0.0.1', port: addr.port, path, method, headers: { 'Content-Type': 'application/json' }, }; const req = http.request(options, (res) => { const chunks = []; res.on('data', (chunk) => chunks.push(chunk)); res.on('end', () => { const rawBody = Buffer.concat(chunks).toString(); let json; try { json = JSON.parse(rawBody); } catch (e) { json = null; } resolve({ statusCode: res.statusCode, body: json }); }); }); req.on('error', reject); if (body) req.write(JSON.stringify(body)); req.end(); }); } // --- Setup --- let app, server; beforeAll((done) => { app = express(); app.use(express.json()); const mockUpload = { array: () => (req, res, next) => next() }; const router = createVCLMultiVerticalRouter(mockUpload); app.use('/api/compliance/vcl-multi', router); server = app.listen(0, '127.0.0.1', done); }); afterAll((done) => { server.close(done); }); beforeEach(() => { mockPool.query.mockReset(); mockPool.connect.mockReset(); mockPool.query.mockResolvedValue({ rows: [], rowCount: 0 }); }); // --- deduplicateByHostname unit tests --- describe('deduplicateByHostname', () => { it('returns empty array for empty input', () => { expect(deduplicateByHostname([])).toEqual([]); }); it('passes through single item unchanged', () => { const items = [{ hostname: 'srv-001', resolution_date: '2026-06-15', vertical: 'NTS_AEO' }]; const result = deduplicateByHostname(items); expect(result).toEqual([{ hostname: 'srv-001', resolution_date: '2026-06-15', vertical: 'NTS_AEO' }]); }); it('deduplicates by hostname keeping earliest non-null date', () => { const items = [ { hostname: 'srv-001', resolution_date: '2026-08-15', vertical: 'NTS_AEO' }, { hostname: 'srv-001', resolution_date: '2026-06-01', vertical: 'SDIT_CISO' }, { hostname: 'srv-001', resolution_date: '2026-07-10', vertical: 'TSI' }, ]; const result = deduplicateByHostname(items); expect(result).toHaveLength(1); expect(result[0].hostname).toBe('srv-001'); expect(result[0].resolution_date).toBe('2026-06-01'); }); it('returns null date when all entries for a hostname have null dates', () => { const items = [ { hostname: 'srv-001', resolution_date: null, vertical: 'NTS_AEO' }, { hostname: 'srv-001', resolution_date: null, vertical: 'SDIT_CISO' }, ]; const result = deduplicateByHostname(items); expect(result).toHaveLength(1); expect(result[0].resolution_date).toBeNull(); }); it('picks earliest non-null date even when some entries are null', () => { const items = [ { hostname: 'srv-001', resolution_date: null, vertical: 'NTS_AEO' }, { hostname: 'srv-001', resolution_date: '2026-09-01', vertical: 'SDIT_CISO' }, { hostname: 'srv-001', resolution_date: '2026-06-15', vertical: 'TSI' }, ]; const result = deduplicateByHostname(items); expect(result).toHaveLength(1); expect(result[0].resolution_date).toBe('2026-06-15'); }); it('preserves vertical from the first entry', () => { const items = [ { hostname: 'srv-001', resolution_date: '2026-08-01', vertical: 'NTS_AEO' }, { hostname: 'srv-001', resolution_date: '2026-06-01', vertical: 'SDIT_CISO' }, ]; const result = deduplicateByHostname(items); expect(result[0].vertical).toBe('NTS_AEO'); }); }); // --- computeAggregatedBurndown unit tests --- describe('computeAggregatedBurndown', () => { it('returns zero/empty for empty input', () => { const result = computeAggregatedBurndown([]); expect(result.total).toBe(0); expect(result.blockers).toBe(0); expect(result.with_dates).toBe(0); expect(result.monthly).toEqual({}); expect(result.projection).toEqual({}); expect(result.projected_clear_date).toBeNull(); expect(result.by_vertical).toEqual([]); }); it('all blockers — with_dates=0, monthly={}, projected_clear_date=null', () => { const devices = [ { hostname: 'srv-001', resolution_date: null, vertical: 'NTS_AEO' }, { hostname: 'srv-002', resolution_date: null, vertical: 'NTS_AEO' }, { hostname: 'srv-003', resolution_date: null, vertical: 'SDIT_CISO' }, ]; const result = computeAggregatedBurndown(devices); expect(result.total).toBe(3); expect(result.blockers).toBe(3); expect(result.with_dates).toBe(0); expect(result.monthly).toEqual({}); expect(result.projected_clear_date).toBeNull(); }); it('single device with date — correct monthly bucket and projection', () => { const devices = [ { hostname: 'srv-001', resolution_date: '2026-06-15', vertical: 'NTS_AEO' }, ]; const result = computeAggregatedBurndown(devices); expect(result.total).toBe(1); expect(result.blockers).toBe(0); expect(result.with_dates).toBe(1); expect(result.monthly).toEqual({ '2026-06': 1 }); expect(result.projection).toEqual({ '2026-06': { remediated: 1, remaining: 0 } }); expect(result.projected_clear_date).toBe('2026-06'); }); it('mixed blockers and in-progress — projected_clear_date is null', () => { const devices = [ { hostname: 'srv-001', resolution_date: '2026-06-15', vertical: 'NTS_AEO' }, { hostname: 'srv-002', resolution_date: null, vertical: 'NTS_AEO' }, ]; const result = computeAggregatedBurndown(devices); expect(result.total).toBe(2); expect(result.blockers).toBe(1); expect(result.with_dates).toBe(1); expect(result.projected_clear_date).toBeNull(); }); it('multiple months — correct cumulative projection', () => { const devices = [ { hostname: 'srv-001', resolution_date: '2026-06-15', vertical: 'NTS_AEO' }, { hostname: 'srv-002', resolution_date: '2026-06-20', vertical: 'NTS_AEO' }, { hostname: 'srv-003', resolution_date: '2026-07-10', vertical: 'SDIT_CISO' }, { hostname: 'srv-004', resolution_date: '2026-08-01', vertical: 'TSI' }, ]; const result = computeAggregatedBurndown(devices); expect(result.total).toBe(4); expect(result.monthly).toEqual({ '2026-06': 2, '2026-07': 1, '2026-08': 1 }); expect(result.projection['2026-06'].remaining).toBe(2); // 4 - 2 expect(result.projection['2026-07'].remaining).toBe(1); // 4 - 3 expect(result.projection['2026-08'].remaining).toBe(0); // 4 - 4 expect(result.projected_clear_date).toBe('2026-08'); }); it('by_vertical sorted descending by total, omits zero-total verticals', () => { const devices = [ { hostname: 'srv-001', resolution_date: '2026-06-15', vertical: 'TSI' }, { hostname: 'srv-002', resolution_date: null, vertical: 'NTS_AEO' }, { hostname: 'srv-003', resolution_date: null, vertical: 'NTS_AEO' }, { hostname: 'srv-004', resolution_date: '2026-07-01', vertical: 'NTS_AEO' }, ]; const result = computeAggregatedBurndown(devices); expect(result.by_vertical[0].vertical).toBe('NTS_AEO'); expect(result.by_vertical[0].total).toBe(3); expect(result.by_vertical[1].vertical).toBe('TSI'); expect(result.by_vertical[1].total).toBe(1); }); }); // --- GET /burndown endpoint tests --- describe('GET /api/compliance/vcl-multi/burndown', () => { it('returns zero/empty response when no active devices exist', async () => { mockPool.query.mockResolvedValueOnce({ rows: [] }); const res = await request(server, 'GET', '/api/compliance/vcl-multi/burndown'); expect(res.statusCode).toBe(200); expect(res.body.total_non_compliant).toBe(0); expect(res.body.blockers).toBe(0); expect(res.body.with_dates).toBe(0); expect(res.body.monthly_forecast).toEqual({}); expect(res.body.projected_clear_date).toBeNull(); expect(res.body.by_vertical).toEqual([]); }); it('returns correct burndown data with mocked DB rows', async () => { mockPool.query.mockResolvedValueOnce({ rows: [ { hostname: 'srv-001', resolution_date: '2026-06-15', vertical: 'NTS_AEO' }, { hostname: 'srv-002', resolution_date: '2026-07-01', vertical: 'NTS_AEO' }, { hostname: 'srv-003', resolution_date: null, vertical: 'SDIT_CISO' }, { hostname: 'srv-001', resolution_date: '2026-08-01', vertical: 'SDIT_CISO' }, // duplicate hostname ], }); const res = await request(server, 'GET', '/api/compliance/vcl-multi/burndown'); expect(res.statusCode).toBe(200); // srv-001 deduplicated: earliest date is 2026-06-15 expect(res.body.total_non_compliant).toBe(3); // srv-001, srv-002, srv-003 expect(res.body.blockers).toBe(1); // srv-003 expect(res.body.with_dates).toBe(2); // srv-001, srv-002 expect(res.body.monthly_forecast['2026-06']).toBe(1); expect(res.body.monthly_forecast['2026-07']).toBe(1); expect(res.body.projected_clear_date).toBeNull(); // blockers > 0 }); it('returns all-blocker response correctly', async () => { mockPool.query.mockResolvedValueOnce({ rows: [ { hostname: 'srv-001', resolution_date: null, vertical: 'NTS_AEO' }, { hostname: 'srv-002', resolution_date: null, vertical: 'SDIT_CISO' }, ], }); const res = await request(server, 'GET', '/api/compliance/vcl-multi/burndown'); expect(res.statusCode).toBe(200); expect(res.body.total_non_compliant).toBe(2); expect(res.body.blockers).toBe(2); expect(res.body.with_dates).toBe(0); expect(res.body.monthly_forecast).toEqual({}); expect(res.body.projected_clear_date).toBeNull(); }); it('returns 500 on database error', async () => { mockPool.query.mockRejectedValueOnce(new Error('Connection refused')); const res = await request(server, 'GET', '/api/compliance/vcl-multi/burndown'); expect(res.statusCode).toBe(500); expect(res.body.error).toBe('Database error'); }); it('response shape matches API contract', async () => { mockPool.query.mockResolvedValueOnce({ rows: [ { hostname: 'srv-001', resolution_date: '2026-06-15', vertical: 'NTS_AEO' }, ], }); const res = await request(server, 'GET', '/api/compliance/vcl-multi/burndown'); expect(res.statusCode).toBe(200); expect(res.body).toHaveProperty('total_non_compliant'); expect(res.body).toHaveProperty('blockers'); expect(res.body).toHaveProperty('with_dates'); expect(res.body).toHaveProperty('monthly_forecast'); expect(res.body).toHaveProperty('projected_clear_date'); expect(res.body).toHaveProperty('by_vertical'); expect(Array.isArray(res.body.by_vertical)).toBe(true); }); }); // --- Auth enforcement test --- describe('GET /burndown — auth enforcement', () => { it('returns 401 when auth middleware rejects', async () => { // Create a separate app with rejecting auth const rejectApp = express(); rejectApp.use(express.json()); // Override requireAuth to reject jest.resetModules(); jest.doMock('../middleware/auth', () => ({ requireAuth: () => (req, res, next) => { res.status(401).json({ error: 'Authentication required' }); }, requireGroup: () => (req, res, next) => next(), })); const { createVCLMultiVerticalRouter: createRouter } = require('../routes/vclMultiVertical'); const mockUpload = { array: () => (req, res, next) => next() }; const router = createRouter(mockUpload); rejectApp.use('/api/compliance/vcl-multi', router); const rejectServer = await new Promise((resolve) => { const s = rejectApp.listen(0, '127.0.0.1', () => resolve(s)); }); try { const res = await request(rejectServer, 'GET', '/api/compliance/vcl-multi/burndown'); expect(res.statusCode).toBe(401); expect(res.body.error).toBe('Authentication required'); } finally { await new Promise((resolve) => rejectServer.close(resolve)); } }); });