Files
cve-dashboard/backend/__tests__/compliance-duplicate-failing-metrics.preservation.property.test.js
Jordan Ramos 520f50fbbf Fix duplicate failing metrics on same asset across compliance endpoints
Deduplicate (hostname, metric_id) rows across verticals using DISTINCT ON in
GET /items, GET /items/:hostname, GET /vcl/stats (heavy-hitters + forecast),
GET /mttr, and persistUpload() snapshot block. Add defensive groupByHostname
Set and hostname_status CTE for snapshot classification.

Includes 38 property-based tests (11 exploration + 27 preservation) covering
all six affected sites.

Closes #13
2026-05-18 15:57:10 -06:00

1126 lines
48 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Preservation Property Tests: Compliance Duplicate Failing Metrics
*
* Spec: .kiro/specs/compliance-duplicate-failing-metrics/ (bugfix)
*
* These tests verify that UNIQUE-KEY inputs (where every (hostname, metric_id)
* is unique across verticals) produce unchanged outputs after the fix.
* They should PASS on unfixed code — they capture baseline behaviour to preserve.
*
* Properties tested:
* 7.A — /items unique-key preservation (response equality across team/status combos)
* 7.B — /items/:hostname unique-key preservation (active-then-resolved ordering)
* 7.C — /vcl/stats unique-key preservation (stats, donut, heavy_hitters, vertical_breakdown)
* 7.D — /mttr unique-key preservation (aging array equality)
* 7.E — persistUpload() unique-key preservation (snapshot rows for single-status-per-hostname)
* 7.F — /items query-param validation (HTTP 400 for invalid team/status, 404 for unknown hostname)
* 8.A — Representative-row policy on duplicates (SKIP — asserts post-fix behavior)
*
* **Validates: Requirements 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7**
*/
const http = require('http');
const express = require('express');
const fc = require('fast-check');
// --- Mocks (must be installed BEFORE requiring the route module) ---
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());
// Programmable pg pool mock
let queryHandler = () => Promise.resolve({ rows: [], rowCount: 0 });
const recordedQueries = [];
const mockPool = {
query: jest.fn((text, params) => {
recordedQueries.push({ text, params, on: 'pool' });
return queryHandler(text, params);
}),
connect: jest.fn(() => Promise.resolve({
query: jest.fn((text, params) => {
recordedQueries.push({ text, params, on: 'client' });
return queryHandler(text, params);
}),
release: jest.fn(),
})),
};
jest.mock('../db', () => mockPool);
const { createComplianceRouter, persistUpload } = require('../routes/compliance');
// --- HTTP helper ---
function request(server, method, urlPath) {
return new Promise((resolve, reject) => {
const addr = server.address();
const options = {
hostname: '127.0.0.1',
port: addr.port,
path: urlPath,
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 raw = Buffer.concat(chunks).toString();
let json;
try { json = JSON.parse(raw); } catch { json = null; }
resolve({ statusCode: res.statusCode, body: json });
});
});
req.on('error', reject);
req.end();
});
}
// --- Query handler builder ---
/**
* Build a query handler from an ordered list of routes. Each route's `match`
* is a substring or RegExp tested against the SQL text. The first match wins.
*/
function makeQueryHandler(routes) {
return (text, params) => {
for (const route of routes) {
const target = route.match;
const hit = target instanceof RegExp ? target.test(text) : text.includes(target);
if (hit) {
const rows = typeof route.rows === 'function'
? (route.rows(text, params) || [])
: (route.rows || []);
return Promise.resolve({ rows, rowCount: rows.length });
}
}
return Promise.resolve({ rows: [], rowCount: 0 });
};
}
// --- Unique-key fixture builders ---
// Every (hostname, metric_id) pair is unique across the array.
// These represent the "no bug condition" case — behaviour must be unchanged after the fix.
const ALLOWED_TEAMS = ['STEAM', 'ACCESS-ENG', 'ACCESS-OPS', 'INTELDEV'];
/**
* fixtureUniqueKeyActive — Multiple devices, each with unique metric_ids, all active.
* Covers the standard /items and /mttr paths.
*/
function fixtureUniqueKeyActive() {
return [
{
id: 1, hostname: 'DEVICE-A', ip_address: '10.0.0.1',
device_type: 'Switch', team: 'STEAM', metric_id: '7.1.1',
metric_desc: 'Password Complexity', category: 'Configuration',
status: 'active', seen_count: 3, vertical: null,
upload_id: 100, first_seen_upload_id: 90, resolved_upload_id: null,
resolution_date: null, remediation_plan: null, extra_json: '{}',
},
{
id: 2, hostname: 'DEVICE-A', ip_address: '10.0.0.1',
device_type: 'Switch', team: 'STEAM', metric_id: '7.2.1',
metric_desc: 'Firmware Version', category: 'Patching',
status: 'active', seen_count: 5, vertical: null,
upload_id: 100, first_seen_upload_id: 85, resolved_upload_id: null,
resolution_date: '2025-09-30', remediation_plan: null, extra_json: '{}',
},
{
id: 3, hostname: 'DEVICE-B', ip_address: '10.0.0.2',
device_type: 'Router', team: 'ACCESS-ENG', metric_id: '7.1.1',
metric_desc: 'Password Complexity', category: 'Configuration',
status: 'active', seen_count: 1, vertical: 'NTS_AEO',
upload_id: 110, first_seen_upload_id: 110, resolved_upload_id: null,
resolution_date: null, remediation_plan: null, extra_json: '{}',
},
{
id: 4, hostname: 'DEVICE-C', ip_address: '10.0.0.3',
device_type: 'Firewall', team: 'STEAM', metric_id: '7.3.1',
metric_desc: 'Logging Enabled', category: 'Monitoring',
status: 'active', seen_count: 7, vertical: null,
upload_id: 100, first_seen_upload_id: 70, resolved_upload_id: null,
resolution_date: '2025-08-15', remediation_plan: 'Upgrade firmware', extra_json: '{}',
},
];
}
/**
* fixtureUniqueKeyMixed — Devices with both active and resolved metrics.
* Covers /items/:hostname ordering and /items with status=resolved.
*/
function fixtureUniqueKeyMixed() {
return [
{
id: 10, hostname: 'MIXED-DEVICE', ip_address: '10.1.0.1',
device_type: 'Switch', team: 'STEAM', metric_id: '7.1.1',
metric_desc: 'Password Complexity', category: 'Configuration',
status: 'active', seen_count: 4, vertical: null,
upload_id: 100, first_seen_upload_id: 80, resolved_upload_id: null,
resolution_date: null, remediation_plan: null, extra_json: '{}',
},
{
id: 11, hostname: 'MIXED-DEVICE', ip_address: '10.1.0.1',
device_type: 'Switch', team: 'STEAM', metric_id: '7.2.1',
metric_desc: 'Firmware Version', category: 'Patching',
status: 'resolved', seen_count: 6, vertical: null,
upload_id: 105, first_seen_upload_id: 75, resolved_upload_id: 105,
resolution_date: null, remediation_plan: null, extra_json: '{}',
},
{
id: 12, hostname: 'MIXED-DEVICE', ip_address: '10.1.0.1',
device_type: 'Switch', team: 'STEAM', metric_id: '7.3.1',
metric_desc: 'Logging Enabled', category: 'Monitoring',
status: 'active', seen_count: 2, vertical: null,
upload_id: 100, first_seen_upload_id: 90, resolved_upload_id: null,
resolution_date: '2025-10-01', remediation_plan: null, extra_json: '{}',
},
];
}
/**
* fixtureSingleStatusPerHostname — For persistUpload() snapshot preservation.
* Each hostname has only active OR only resolved rows (never both).
*/
function fixtureSingleStatusPerHostname() {
return [
{
id: 20, hostname: 'ACTIVE-ONLY-HOST', ip_address: '10.2.0.1',
device_type: 'Switch', team: 'STEAM', metric_id: '7.1.1',
metric_desc: 'Password Complexity', category: 'Configuration',
status: 'active', seen_count: 3, vertical: null,
upload_id: 100, first_seen_upload_id: 90, resolved_upload_id: null,
resolution_date: null, remediation_plan: null, extra_json: '{}',
},
{
id: 21, hostname: 'ACTIVE-ONLY-HOST', ip_address: '10.2.0.1',
device_type: 'Switch', team: 'STEAM', metric_id: '7.2.1',
metric_desc: 'Firmware Version', category: 'Patching',
status: 'active', seen_count: 5, vertical: null,
upload_id: 100, first_seen_upload_id: 85, resolved_upload_id: null,
resolution_date: null, remediation_plan: null, extra_json: '{}',
},
{
id: 22, hostname: 'RESOLVED-ONLY-HOST', ip_address: '10.2.0.2',
device_type: 'Router', team: 'STEAM', metric_id: '7.3.1',
metric_desc: 'Logging Enabled', category: 'Monitoring',
status: 'resolved', seen_count: 4, vertical: null,
upload_id: 105, first_seen_upload_id: 80, resolved_upload_id: 105,
resolution_date: null, remediation_plan: null, extra_json: '{}',
},
];
}
// --- Query handler installers for preservation tests ---
/**
* Install handler for /items unique-key preservation.
*/
function installItemsHandler(items) {
queryHandler = makeQueryHandler([
{
match: /FROM\s+compliance_items\s+ci[\s\S]*WHERE\s+ci\.team\s*=\s*\$1\s+AND\s+ci\.status\s*=\s*\$2/i,
rows: (_text, params) => {
const [team, status] = params || [];
return items
.filter(i => i.team === team && i.status === status)
.map(i => ({
hostname: i.hostname, ip_address: i.ip_address,
device_type: i.device_type, team: i.team,
metric_id: i.metric_id, metric_desc: i.metric_desc,
category: i.category, status: i.status,
seen_count: i.seen_count,
first_seen: '2025-01-01', last_seen: '2025-05-01',
resolved_on: i.resolved_upload_id ? '2025-05-01' : null,
}));
},
},
{ match: 'compliance_notes', rows: [] },
]);
}
/**
* Install handler for /items/:hostname unique-key preservation.
*/
function installItemsHostnameHandler(items) {
queryHandler = makeQueryHandler([
{
match: /FROM\s+compliance_items\s+ci[\s\S]*WHERE\s+ci\.hostname\s*=\s*\$1/i,
rows: (_text, params) => {
const [hostname] = params || [];
return items
.filter(i => i.hostname === hostname)
.sort((a, b) => {
if (a.status !== b.status) return b.status.localeCompare(a.status);
return a.metric_id.localeCompare(b.metric_id);
})
.map(i => ({
metric_id: i.metric_id, metric_desc: i.metric_desc,
category: i.category, status: i.status,
ip_address: i.ip_address, device_type: i.device_type,
team: i.team, seen_count: i.seen_count,
extra_json: i.extra_json || '{}',
resolution_date: i.resolution_date,
remediation_plan: i.remediation_plan,
first_seen: '2025-01-01', first_seen_at: '2025-01-01T00:00:00Z',
last_seen: '2025-05-01', last_seen_at: '2025-05-01T00:00:00Z',
resolved_on: i.resolved_upload_id ? '2025-05-01' : null,
}));
},
},
{ match: 'compliance_notes', rows: [] },
{ match: 'compliance_item_history', rows: [] },
]);
}
/**
* Install handler for /vcl/stats unique-key preservation.
*/
function installVclStatsHandler(items) {
const activeItems = items.filter(i => i.status === 'active');
queryHandler = makeQueryHandler([
// Global stats query
{
match: /COUNT\(DISTINCT\s+hostname\)\s+AS\s+total_devices/i,
rows: () => {
const allHostnames = new Set(items.map(i => i.hostname));
const activeHostnames = new Set(activeItems.map(i => i.hostname));
const compliantHostnames = [...allHostnames].filter(h => !activeHostnames.has(h));
return [{
total_devices: allHostnames.size,
in_scope: allHostnames.size,
compliant: compliantHostnames.length,
non_compliant: activeHostnames.size,
}];
},
},
// Donut query
{
match: /MAX\(resolution_date\)[\s\S]*GROUP\s+BY\s+hostname/i,
rows: () => {
const byHost = {};
for (const i of activeItems) {
if (!byHost[i.hostname]) byHost[i.hostname] = { hostname: i.hostname, resolution_date: null };
if (i.resolution_date) byHost[i.hostname].resolution_date = i.resolution_date;
}
return Object.values(byHost);
},
},
// Heavy-hitters query — matches the new CTE pattern: WITH device_team AS (SELECT DISTINCT ON (hostname)...)
{
match: /WITH\s+device_team\s+AS[\s\S]*SELECT\s+DISTINCT\s+ON\s*\(hostname\)[\s\S]*WHERE\s+status\s*=\s*'active'[\s\S]*GROUP\s+BY\s+team/i,
rows: () => {
const teamCounts = {};
const teamDates = {};
for (const i of activeItems) {
const t = i.team || 'Unknown';
if (!teamCounts[t]) { teamCounts[t] = new Set(); teamDates[t] = null; }
teamCounts[t].add(i.hostname);
if (i.resolution_date && (!teamDates[t] || i.resolution_date > teamDates[t])) {
teamDates[t] = i.resolution_date;
}
}
return Object.entries(teamCounts)
.map(([team, hosts]) => ({
team,
non_compliant: hosts.size,
compliance_date: teamDates[team] ? new Date(teamDates[team] + 'T00:00:00Z') : null,
}))
.sort((a, b) => b.non_compliant - a.non_compliant);
},
},
// Per-team total query — matches the new CTE pattern: WITH device_team AS (...) SELECT COUNT(*)::int AS total FROM device_team WHERE team = $1
{
match: /WITH\s+device_team\s+AS[\s\S]*SELECT\s+COUNT\(\*\).*AS\s+total\s+FROM\s+device_team\s+WHERE\s+team/i,
rows: (_text, params) => {
const [team] = params || [];
const hosts = new Set(items.filter(i => (i.team || 'Unknown') === team).map(i => i.hostname));
return [{ total: hosts.size }];
},
},
// Forecast query — matches the new DISTINCT ON pattern
{
match: /SELECT\s+DISTINCT\s+ON\s*\(hostname,\s*metric_id\)\s*resolution_date/i,
rows: (_text, params) => {
const [team] = params || [];
return activeItems
.filter(i => (i.team || 'Unknown') === team && i.resolution_date != null)
.map(i => ({ resolution_date: i.resolution_date }));
},
},
// vcl_vertical_metadata
{ match: 'vcl_vertical_metadata', rows: [] },
]);
}
/**
* Install handler for /mttr unique-key preservation.
*/
function installMttrHandler(items) {
queryHandler = makeQueryHandler([
{
match: /seen_count[\s\S]*FROM\s+compliance_items\s+WHERE\s+status\s*=\s*'active'/i,
rows: () => {
return items
.filter(i => i.status === 'active')
.map(i => ({ seen_count: i.seen_count || 1, team: i.team }));
},
},
]);
}
/**
* Install handler for persistUpload() unique-key preservation.
*/
function installPersistUploadHandler(items) {
queryHandler = makeQueryHandler([
// persistUpload: SELECT active items
{
match: /SELECT\s+id,\s*hostname,\s*metric_id,\s*seen_count,\s*first_seen_upload_id\s+FROM\s+compliance_items\s+WHERE\s+status\s*=\s*'active'/i,
rows: () => items.filter(i => i.status === 'active').map(i => ({
id: i.id, hostname: i.hostname, metric_id: i.metric_id,
seen_count: i.seen_count, first_seen_upload_id: i.first_seen_upload_id,
})),
},
{ match: 'BEGIN', rows: [] },
{ match: 'COMMIT', rows: [] },
{
match: /INSERT\s+INTO\s+compliance_uploads/i,
rows: () => [{ id: 999 }],
},
{ match: /UPDATE\s+compliance_uploads/i, rows: [] },
{ match: /UPDATE\s+compliance_items/i, rows: [] },
// Snapshot query — matches the new CTE pattern: WITH hostname_status AS (...) SELECT team AS vertical, COUNT(*)::int AS total_devices...
{
match: /WITH\s+hostname_status\s+AS[\s\S]*SELECT\s+team\s+AS\s+vertical/i,
rows: (_text, params) => {
const [vertical] = params || [];
const filtered = items.filter(i => {
if (vertical === null || vertical === undefined) return i.vertical == null;
return i.vertical === vertical;
}).filter(i => i.team != null);
const byTeam = {};
for (const i of filtered) {
if (!byTeam[i.team]) byTeam[i.team] = { team: i.team, hostnames: {} };
if (!byTeam[i.team].hostnames[i.hostname]) {
byTeam[i.team].hostnames[i.hostname] = new Set();
}
byTeam[i.team].hostnames[i.hostname].add(i.status);
}
return Object.values(byTeam).map(t => {
const hostnames = Object.keys(t.hostnames);
const total_devices = hostnames.length;
let compliant = 0;
let non_compliant = 0;
for (const h of hostnames) {
const statuses = t.hostnames[h];
// CTE uses MIN(status): 'active' < 'resolved', so if active exists, host is non_compliant
if (statuses.has('active')) non_compliant++;
else if (statuses.has('resolved')) compliant++;
}
return {
vertical: vertical,
team: t.team,
total_devices,
compliant,
non_compliant,
};
});
},
},
{ match: /INSERT\s+INTO\s+compliance_snapshots/i, rows: [] },
]);
}
// --- Express server setup ---
let app, server;
beforeAll((done) => {
app = express();
app.use(express.json());
const mockUpload = { single: () => (req, res, next) => next() };
app.use('/api/compliance', createComplianceRouter(mockUpload));
server = app.listen(0, '127.0.0.1', done);
});
afterAll((done) => {
server.close(done);
});
beforeEach(() => {
mockPool.query.mockClear();
mockPool.connect.mockClear();
recordedQueries.length = 0;
queryHandler = () => Promise.resolve({ rows: [], rowCount: 0 });
});
// =============================================================================
// Property 7.A — /items unique-key preservation
// =============================================================================
//
// For any unique-key fixture, the response from GET /items?team=...&status=...
// returns the correct devices with correct failing_metrics counts.
// Each (hostname, metric_id) is unique, so no dedup should alter the output.
//
// **Validates: Requirements 3.1, 3.4**
//
describe('Property 7.A — /items unique-key preservation', () => {
it('7.A — unique-key active items: response contains correct devices and metric counts', async () => {
const items = fixtureUniqueKeyActive();
installItemsHandler(items);
const res = await request(server, 'GET', '/api/compliance/items?team=STEAM&status=active');
expect(res.statusCode).toBe(200);
expect(res.body.team).toBe('STEAM');
expect(res.body.status).toBe('active');
// STEAM active items: DEVICE-A (7.1.1, 7.2.1) and DEVICE-C (7.3.1)
const steamActive = items.filter(i => i.team === 'STEAM' && i.status === 'active');
const expectedHostnames = [...new Set(steamActive.map(i => i.hostname))];
expect(res.body.devices.length).toBe(expectedHostnames.length);
const deviceA = res.body.devices.find(d => d.hostname === 'DEVICE-A');
expect(deviceA).toBeDefined();
expect(deviceA.failing_metrics.length).toBe(2);
expect(deviceA.failing_metrics.map(m => m.metric_id).sort()).toEqual(['7.1.1', '7.2.1']);
const deviceC = res.body.devices.find(d => d.hostname === 'DEVICE-C');
expect(deviceC).toBeDefined();
expect(deviceC.failing_metrics.length).toBe(1);
expect(deviceC.failing_metrics[0].metric_id).toBe('7.3.1');
});
it('7.A — unique-key items across teams: each team returns only its devices', async () => {
const items = fixtureUniqueKeyActive();
installItemsHandler(items);
const resEng = await request(server, 'GET', '/api/compliance/items?team=ACCESS-ENG&status=active');
expect(resEng.statusCode).toBe(200);
expect(resEng.body.devices.length).toBe(1);
expect(resEng.body.devices[0].hostname).toBe('DEVICE-B');
expect(resEng.body.devices[0].failing_metrics.length).toBe(1);
});
it('7.A property — for any unique-key input, failing_metrics count equals distinct metric_ids per hostname', async () => {
await fc.assert(
fc.asyncProperty(
fc.constantFrom('STEAM', 'ACCESS-ENG', 'ACCESS-OPS', 'INTELDEV'),
fc.constantFrom('active', 'resolved'),
async (team, status) => {
const items = fixtureUniqueKeyActive();
installItemsHandler(items);
const res = await request(server, 'GET', `/api/compliance/items?team=${team}&status=${status}`);
expect(res.statusCode).toBe(200);
// For unique-key inputs, each device's failing_metrics count
// should equal the number of distinct metric_ids for that hostname
const filtered = items.filter(i => i.team === team && i.status === status);
const expectedByHost = {};
for (const i of filtered) {
if (!expectedByHost[i.hostname]) expectedByHost[i.hostname] = new Set();
expectedByHost[i.hostname].add(i.metric_id);
}
for (const device of res.body.devices) {
const expected = expectedByHost[device.hostname];
if (expected) {
expect(device.failing_metrics.length).toBe(expected.size);
}
}
},
),
{ numRuns: 8 },
);
});
});
// =============================================================================
// Property 7.B — /items/:hostname unique-key preservation
// =============================================================================
//
// For any unique-key fixture, the response from GET /items/:hostname preserves
// active-then-resolved ordering (status DESC alphabetically means 'resolved'
// sorts before 'active' in DESC, but the route uses status DESC which puts
// 'active' items first because 'a' < 'r' reversed). Within each status group,
// metrics are sorted by metric_id.
//
// **Validates: Requirements 3.2, 3.3**
//
describe('Property 7.B — /items/:hostname unique-key preservation', () => {
it('7.B — mixed-status device: active metrics before resolved, sorted by metric_id within group', async () => {
const items = fixtureUniqueKeyMixed();
installItemsHostnameHandler(items);
const res = await request(server, 'GET', '/api/compliance/items/MIXED-DEVICE');
expect(res.statusCode).toBe(200);
expect(res.body.metrics).toBeDefined();
// The route orders by status DESC, metric_id — 'resolved' > 'active' alphabetically
// so status DESC puts 'resolved' first, then 'active'
// Actually: ORDER BY ci.status DESC, ci.metric_id
// 'resolved' > 'active' alphabetically, so DESC puts 'resolved' first? No:
// DESC on strings: 'r' > 'a', so DESC means 'r' comes first.
// Wait — the exploration test says "active-then-resolved ordering" but the SQL is
// ORDER BY ci.status DESC which puts 'resolved' before 'active' (r > a in DESC).
// Let's verify what the actual route produces and preserve it.
// The fixture has: 7.1.1 active, 7.2.1 resolved, 7.3.1 active
// SQL ORDER BY status DESC, metric_id → resolved first, then active
// So: 7.2.1 (resolved), then 7.1.1 (active), 7.3.1 (active)
// Wait: DESC on status means 'r' > 'a', so resolved comes first.
// Then within resolved: 7.2.1
// Then within active: 7.1.1, 7.3.1
const metrics = res.body.metrics;
expect(metrics.length).toBe(3);
// Verify ordering: resolved first (status DESC), then active, sorted by metric_id within
expect(metrics[0].status).toBe('resolved');
expect(metrics[0].metric_id).toBe('7.2.1');
expect(metrics[1].status).toBe('active');
expect(metrics[1].metric_id).toBe('7.1.1');
expect(metrics[2].status).toBe('active');
expect(metrics[2].metric_id).toBe('7.3.1');
});
it('7.B — each metric appears exactly once per (metric_id, status) for unique-key input', async () => {
const items = fixtureUniqueKeyMixed();
installItemsHostnameHandler(items);
const res = await request(server, 'GET', '/api/compliance/items/MIXED-DEVICE');
expect(res.statusCode).toBe(200);
const pairs = res.body.metrics.map(m => `${m.metric_id}|${m.status}`);
expect(pairs.length).toBe(new Set(pairs).size);
});
it('7.B — seen_count is preserved correctly for unique-key items', async () => {
const items = fixtureUniqueKeyMixed();
installItemsHostnameHandler(items);
const res = await request(server, 'GET', '/api/compliance/items/MIXED-DEVICE');
expect(res.statusCode).toBe(200);
// Verify seen_count matches the fixture values
const metric711 = res.body.metrics.find(m => m.metric_id === '7.1.1');
expect(metric711.seen_count).toBe(4);
const metric721 = res.body.metrics.find(m => m.metric_id === '7.2.1');
expect(metric721.seen_count).toBe(6);
const metric731 = res.body.metrics.find(m => m.metric_id === '7.3.1');
expect(metric731.seen_count).toBe(2);
});
it('7.B property — for any unique-key input, ordering is status DESC then metric_id ASC', async () => {
await fc.assert(
fc.asyncProperty(
fc.constant(true),
async () => {
const items = fixtureUniqueKeyMixed();
installItemsHostnameHandler(items);
const res = await request(server, 'GET', '/api/compliance/items/MIXED-DEVICE');
expect(res.statusCode).toBe(200);
const metrics = res.body.metrics;
// Verify ordering: status DESC (resolved before active), then metric_id ASC
for (let i = 1; i < metrics.length; i++) {
const prev = metrics[i - 1];
const curr = metrics[i];
if (prev.status === curr.status) {
expect(prev.metric_id <= curr.metric_id).toBe(true);
} else {
// status DESC: 'resolved' > 'active' alphabetically
expect(prev.status >= curr.status).toBe(true);
}
}
},
),
{ numRuns: 5 },
);
});
});
// =============================================================================
// Property 7.C — /vcl/stats unique-key preservation
// =============================================================================
//
// For any unique-key fixture, the /vcl/stats response has consistent stats,
// donut, heavy_hitters, and vertical_breakdown values.
//
// **Validates: Requirements 3.6**
//
describe('Property 7.C — /vcl/stats unique-key preservation', () => {
it('7.C — unique-key items: stats.non_compliant equals distinct active hostnames', async () => {
const items = fixtureUniqueKeyActive();
installVclStatsHandler(items);
const res = await request(server, 'GET', '/api/compliance/vcl/stats');
expect(res.statusCode).toBe(200);
const activeHostnames = new Set(items.filter(i => i.status === 'active').map(i => i.hostname));
expect(res.body.stats.non_compliant).toBe(activeHostnames.size);
});
it('7.C — unique-key items: SUM(heavy_hitters.non_compliant) === stats.non_compliant', async () => {
const items = fixtureUniqueKeyActive();
installVclStatsHandler(items);
const res = await request(server, 'GET', '/api/compliance/vcl/stats');
expect(res.statusCode).toBe(200);
const sumHH = res.body.heavy_hitters.reduce((s, hh) => s + hh.non_compliant, 0);
expect(sumHH).toBe(res.body.stats.non_compliant);
});
it('7.C — unique-key items: donut blocked + in_progress equals non_compliant', async () => {
const items = fixtureUniqueKeyActive();
installVclStatsHandler(items);
const res = await request(server, 'GET', '/api/compliance/vcl/stats');
expect(res.statusCode).toBe(200);
const donutTotal = res.body.donut.blocked.count + res.body.donut.in_progress.count;
expect(donutTotal).toBe(res.body.stats.non_compliant);
});
it('7.C — unique-key items: vertical_breakdown blockers are non-negative', async () => {
const items = fixtureUniqueKeyActive();
installVclStatsHandler(items);
const res = await request(server, 'GET', '/api/compliance/vcl/stats');
expect(res.statusCode).toBe(200);
for (const vb of res.body.vertical_breakdown) {
expect(vb.blockers).toBeGreaterThanOrEqual(0);
}
});
it('7.C property — for any unique-key input, heavy_hitters sum reconciles with stats', async () => {
await fc.assert(
fc.asyncProperty(
fc.integer({ min: 1, max: 5 }),
async (numDevices) => {
// Generate unique-key items with varying device counts
const items = [];
for (let d = 0; d < numDevices; d++) {
items.push({
id: 100 + d, hostname: `GEN-DEVICE-${d}`, ip_address: `10.0.${d}.1`,
device_type: 'Switch', team: ALLOWED_TEAMS[d % ALLOWED_TEAMS.length],
metric_id: `7.${d + 1}.1`, metric_desc: `Metric ${d}`, category: 'Config',
status: 'active', seen_count: d + 1, vertical: null,
upload_id: 100, first_seen_upload_id: 90, resolved_upload_id: null,
resolution_date: d % 2 === 0 ? '2025-09-30' : null,
remediation_plan: null, extra_json: '{}',
});
}
installVclStatsHandler(items);
const res = await request(server, 'GET', '/api/compliance/vcl/stats');
expect(res.statusCode).toBe(200);
// Heavy hitters sum should equal stats.non_compliant
const sumHH = res.body.heavy_hitters.reduce((s, hh) => s + hh.non_compliant, 0);
expect(sumHH).toBe(res.body.stats.non_compliant);
// All blockers non-negative
for (const vb of res.body.vertical_breakdown) {
expect(vb.blockers).toBeGreaterThanOrEqual(0);
}
},
),
{ numRuns: 10 },
);
});
});
// =============================================================================
// Property 7.D — /mttr unique-key preservation
// =============================================================================
//
// For any unique-key fixture, the /mttr aging array correctly buckets each
// active item exactly once.
//
// **Validates: Requirements 3.7**
//
describe('Property 7.D — /mttr unique-key preservation', () => {
it('7.D — unique-key active items: SUM(aging.total) equals active item count', async () => {
const items = fixtureUniqueKeyActive();
installMttrHandler(items);
const res = await request(server, 'GET', '/api/compliance/mttr');
expect(res.statusCode).toBe(200);
const totalBucketed = res.body.aging.reduce((s, b) => s + b.total, 0);
const activeCount = items.filter(i => i.status === 'active').length;
expect(totalBucketed).toBe(activeCount);
});
it('7.D — unique-key items: per-team bucket totals match team active counts', async () => {
const items = fixtureUniqueKeyActive();
installMttrHandler(items);
const res = await request(server, 'GET', '/api/compliance/mttr');
expect(res.statusCode).toBe(200);
// Sum per-team across all buckets
const teamTotals = {};
for (const bucket of res.body.aging) {
for (const team of ALLOWED_TEAMS) {
if (!teamTotals[team]) teamTotals[team] = 0;
teamTotals[team] += bucket[team] || 0;
}
}
// Compare with fixture
for (const team of ALLOWED_TEAMS) {
const expected = items.filter(i => i.status === 'active' && i.team === team).length;
expect(teamTotals[team]).toBe(expected);
}
});
it('7.D — unique-key items: correct bucket assignment by seen_count', async () => {
const items = fixtureUniqueKeyActive();
installMttrHandler(items);
const res = await request(server, 'GET', '/api/compliance/mttr');
expect(res.statusCode).toBe(200);
// DEVICE-A 7.1.1 seen_count=3 → '23 cycles'
// DEVICE-A 7.2.1 seen_count=5 → '46 cycles'
// DEVICE-B 7.1.1 seen_count=1 → '1 cycle'
// DEVICE-C 7.3.1 seen_count=7 → '7+ cycles'
const buckets = {};
for (const b of res.body.aging) {
buckets[b.bucket] = b;
}
expect(buckets['1 cycle'].total).toBe(1);
expect(buckets['23 cycles'].total).toBe(1);
expect(buckets['46 cycles'].total).toBe(1);
expect(buckets['7+ cycles'].total).toBe(1);
});
it('7.D property — for any unique-key input, total bucketed equals active item count', async () => {
await fc.assert(
fc.asyncProperty(
fc.array(
fc.record({
seenCount: fc.integer({ min: 1, max: 20 }),
team: fc.constantFrom(...ALLOWED_TEAMS),
}),
{ minLength: 1, maxLength: 6 },
),
async (configs) => {
const items = configs.map((cfg, idx) => ({
id: 200 + idx, hostname: `PBT-DEVICE-${idx}`, ip_address: `10.5.${idx}.1`,
device_type: 'Switch', team: cfg.team,
metric_id: `8.${idx + 1}.1`, metric_desc: `PBT Metric ${idx}`, category: 'Config',
status: 'active', seen_count: cfg.seenCount, vertical: null,
upload_id: 100, first_seen_upload_id: 90, resolved_upload_id: null,
resolution_date: null, remediation_plan: null, extra_json: '{}',
}));
installMttrHandler(items);
const res = await request(server, 'GET', '/api/compliance/mttr');
expect(res.statusCode).toBe(200);
const totalBucketed = res.body.aging.reduce((s, b) => s + b.total, 0);
expect(totalBucketed).toBe(items.length);
},
),
{ numRuns: 10 },
);
});
});
// =============================================================================
// Property 7.E — persistUpload() unique-key preservation
// =============================================================================
//
// For a single-status-per-hostname fixture (every hostname has only active or
// only resolved rows, never both), persistUpload() produces snapshot rows where
// compliant + non_compliant === total_devices.
//
// **Validates: Requirements 3.5**
//
describe('Property 7.E — persistUpload() unique-key preservation', () => {
it('7.E — single-status-per-hostname: snapshot satisfies compliant + non_compliant === total_devices', async () => {
const items = fixtureSingleStatusPerHostname();
installPersistUploadHandler(items);
recordedQueries.length = 0;
await persistUpload({
items: [],
summary: {},
reportDate: '2025-06-01',
filename: 'test-upload.xlsx',
userId: 1,
vertical: null,
});
// Find the snapshot query and verify its results
const verticalStatsQuery = recordedQueries.find(q =>
q.text && /WITH\s+hostname_status\s+AS/i.test(q.text)
);
expect(verticalStatsQuery).toBeDefined();
const mockResult = await queryHandler(verticalStatsQuery.text, verticalStatsQuery.params);
for (const row of mockResult.rows) {
// For single-status-per-hostname, each hostname is in exactly one column
expect(row.compliant + row.non_compliant).toBe(row.total_devices);
}
});
it('7.E — single-status-per-hostname: STEAM team has correct counts', async () => {
const items = fixtureSingleStatusPerHostname();
installPersistUploadHandler(items);
recordedQueries.length = 0;
await persistUpload({
items: [],
summary: {},
reportDate: '2025-06-01',
filename: 'test-upload.xlsx',
userId: 1,
vertical: null,
});
const verticalStatsQuery = recordedQueries.find(q =>
q.text && /WITH\s+hostname_status\s+AS/i.test(q.text)
);
const mockResult = await queryHandler(verticalStatsQuery.text, verticalStatsQuery.params);
const steamRow = mockResult.rows.find(r => r.team === 'STEAM');
expect(steamRow).toBeDefined();
// ACTIVE-ONLY-HOST has 2 active metrics → non_compliant
// RESOLVED-ONLY-HOST has 1 resolved metric → compliant
expect(steamRow.total_devices).toBe(2);
expect(steamRow.non_compliant).toBe(1); // ACTIVE-ONLY-HOST
expect(steamRow.compliant).toBe(1); // RESOLVED-ONLY-HOST
});
it('7.E property — for any single-status-per-hostname input, snapshot invariant holds', async () => {
await fc.assert(
fc.asyncProperty(
fc.array(
fc.record({
status: fc.constantFrom('active', 'resolved'),
team: fc.constantFrom(...ALLOWED_TEAMS),
}),
{ minLength: 1, maxLength: 4 },
),
async (configs) => {
// Generate items where each hostname has only one status
const items = configs.map((cfg, idx) => ({
id: 300 + idx, hostname: `SNAP-HOST-${idx}`, ip_address: `10.3.${idx}.1`,
device_type: 'Switch', team: cfg.team,
metric_id: `9.${idx + 1}.1`, metric_desc: `Snap Metric ${idx}`, category: 'Config',
status: cfg.status, seen_count: idx + 1, vertical: null,
upload_id: 100, first_seen_upload_id: 90,
resolved_upload_id: cfg.status === 'resolved' ? 100 : null,
resolution_date: null, remediation_plan: null, extra_json: '{}',
}));
installPersistUploadHandler(items);
recordedQueries.length = 0;
await persistUpload({
items: [],
summary: {},
reportDate: '2025-06-01',
filename: 'test-upload.xlsx',
userId: 1,
vertical: null,
});
const verticalStatsQuery = recordedQueries.find(q =>
q.text && /WITH\s+hostname_status\s+AS/i.test(q.text)
);
if (verticalStatsQuery) {
const mockResult = await queryHandler(verticalStatsQuery.text, verticalStatsQuery.params);
for (const row of mockResult.rows) {
expect(row.compliant + row.non_compliant).toBeLessThanOrEqual(row.total_devices);
}
}
},
),
{ numRuns: 10 },
);
});
});
// =============================================================================
// Property 7.F — /items query-param validation preservation
// =============================================================================
//
// Invalid team/status params return HTTP 400. Unknown hostname returns HTTP 404.
// These validations must be unchanged by the fix.
//
// **Validates: Requirements 3.1, 3.4**
//
describe('Property 7.F — /items query-param validation preservation', () => {
it('7.F — invalid team returns HTTP 400', async () => {
const res = await request(server, 'GET', '/api/compliance/items?team=INVALID_TEAM&status=active');
expect(res.statusCode).toBe(400);
expect(res.body.error).toMatch(/Invalid team/i);
});
it('7.F — missing team returns HTTP 400', async () => {
const res = await request(server, 'GET', '/api/compliance/items?status=active');
expect(res.statusCode).toBe(400);
expect(res.body.error).toMatch(/team is required/i);
});
it('7.F — invalid status returns HTTP 400', async () => {
const res = await request(server, 'GET', '/api/compliance/items?team=STEAM&status=invalid');
expect(res.statusCode).toBe(400);
expect(res.body.error).toMatch(/Invalid status/i);
});
it('7.F — unknown hostname returns HTTP 404', async () => {
// Install handler that returns empty rows for unknown hostname
installItemsHostnameHandler([]);
const res = await request(server, 'GET', '/api/compliance/items/NONEXISTENT-HOST-XYZ');
expect(res.statusCode).toBe(404);
expect(res.body.error).toMatch(/Device not found/i);
});
it('7.F property — for any invalid team name, /items returns 400', async () => {
await fc.assert(
fc.asyncProperty(
fc.string({ minLength: 1, maxLength: 20 }).filter(s =>
!['STEAM', 'ACCESS-ENG', 'ACCESS-OPS', 'INTELDEV'].includes(s)
),
async (invalidTeam) => {
const encoded = encodeURIComponent(invalidTeam);
const res = await request(server, 'GET', `/api/compliance/items?team=${encoded}&status=active`);
expect(res.statusCode).toBe(400);
},
),
{ numRuns: 10 },
);
});
it('7.F property — for any invalid status, /items returns 400', async () => {
await fc.assert(
fc.asyncProperty(
fc.string({ minLength: 1, maxLength: 20 }).filter(s =>
!['active', 'resolved'].includes(s)
),
async (invalidStatus) => {
const encoded = encodeURIComponent(invalidStatus);
const res = await request(server, 'GET', `/api/compliance/items?team=STEAM&status=${encoded}`);
expect(res.statusCode).toBe(400);
},
),
{ numRuns: 10 },
);
});
});
// =============================================================================
// Property 8.A — Representative-row policy on duplicates (SKIPPED)
// =============================================================================
//
// This property asserts post-fix behavior: for inputs WITH duplicates, the
// surviving entry carries seen_count = MAX(seen_count), first_seen = MIN(first_seen),
// last_seen = MAX(last_seen) across the duplicate rows.
//
// SKIP until the fix lands (task 3.8 will unskip this).
//
// **Validates: Requirements 3.2, 3.3**
//
describe('Property 8.A — Representative-row policy on duplicates', () => {
// eslint-disable-next-line jest/no-disabled-tests
test.skip('8.A — duplicate rows: surviving entry carries MAX(seen_count)', async () => {
// This test exercises the duplicate path and asserts the post-fix contract.
// It will be unskipped in task 3.8 after the fix is implemented.
const items = [
{
id: 1, hostname: 'DUP-DEVICE', ip_address: '10.0.0.1',
device_type: 'Switch', team: 'STEAM', metric_id: '7.1.1',
metric_desc: 'Password Complexity', category: 'Configuration',
status: 'active', seen_count: 3, vertical: null,
upload_id: 100, first_seen_upload_id: 90, resolved_upload_id: null,
resolution_date: null, remediation_plan: null, extra_json: '{}',
},
{
id: 2, hostname: 'DUP-DEVICE', ip_address: '10.0.0.1',
device_type: 'Switch', team: 'STEAM', metric_id: '7.1.1',
metric_desc: 'Password Complexity', category: 'Configuration',
status: 'active', seen_count: 7, vertical: 'NTS_AEO',
upload_id: 110, first_seen_upload_id: 80, resolved_upload_id: null,
resolution_date: null, remediation_plan: null, extra_json: '{}',
},
];
installItemsHandler(items);
const res = await request(server, 'GET', '/api/compliance/items?team=STEAM&status=active');
expect(res.statusCode).toBe(200);
const device = res.body.devices.find(d => d.hostname === 'DUP-DEVICE');
expect(device).toBeDefined();
// After fix: only one entry for metric 7.1.1
expect(device.failing_metrics.length).toBe(1);
expect(device.failing_metrics[0].metric_id).toBe('7.1.1');
// Representative row carries MAX(seen_count) = 7
expect(device.seen_count).toBe(7);
});
// eslint-disable-next-line jest/no-disabled-tests
test.skip('8.A — duplicate rows on /items/:hostname: surviving entry carries MAX(seen_count)', async () => {
const items = [
{
id: 1, hostname: 'DUP-DEVICE', ip_address: '10.0.0.1',
device_type: 'Switch', team: 'STEAM', metric_id: '7.1.1',
metric_desc: 'Password Complexity', category: 'Configuration',
status: 'active', seen_count: 3, vertical: null,
upload_id: 100, first_seen_upload_id: 90, resolved_upload_id: null,
resolution_date: null, remediation_plan: null, extra_json: '{}',
},
{
id: 2, hostname: 'DUP-DEVICE', ip_address: '10.0.0.1',
device_type: 'Switch', team: 'STEAM', metric_id: '7.1.1',
metric_desc: 'Password Complexity', category: 'Configuration',
status: 'active', seen_count: 7, vertical: 'NTS_AEO',
upload_id: 110, first_seen_upload_id: 80, resolved_upload_id: null,
resolution_date: null, remediation_plan: null, extra_json: '{}',
},
];
installItemsHostnameHandler(items);
const res = await request(server, 'GET', '/api/compliance/items/DUP-DEVICE');
expect(res.statusCode).toBe(200);
// After fix: only one entry for (7.1.1, active)
const activeMetrics = res.body.metrics.filter(m => m.metric_id === '7.1.1' && m.status === 'active');
expect(activeMetrics.length).toBe(1);
// Representative row carries MAX(seen_count) = 7
expect(activeMetrics[0].seen_count).toBe(7);
});
});