Files
cve-dashboard/backend/__tests__/vcl-aggregated-burndown.test.js

372 lines
14 KiB
JavaScript
Raw Normal View History

/**
* 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));
}
});
});