334 lines
13 KiB
JavaScript
334 lines
13 KiB
JavaScript
|
|
/**
|
||
|
|
* Preservation Property Tests: Compliance Remediation Display Fix
|
||
|
|
*
|
||
|
|
* Spec: .kiro/specs/compliance-remediation-display-fix/ (bugfix)
|
||
|
|
*
|
||
|
|
* These tests verify that groupByHostname() correctly aggregates existing fields
|
||
|
|
* on UNFIXED code. They should PASS on unfixed code — they capture baseline
|
||
|
|
* behaviour that must be preserved through the fix.
|
||
|
|
*
|
||
|
|
* Properties tested:
|
||
|
|
* P2.A — Each device.hostname appears exactly once in output
|
||
|
|
* P2.B — device.failing_metrics contains no duplicate metric_ids
|
||
|
|
* P2.C — device.seen_count >= every row's seen_count for that hostname
|
||
|
|
* P2.D — device.first_seen <= every row's first_seen for that hostname
|
||
|
|
* P2.E — device.last_seen >= every row's last_seen for that hostname
|
||
|
|
* P2.F — device.has_notes matches noteHostnames membership
|
||
|
|
*
|
||
|
|
* **Validates: Requirements 3.3, 3.5**
|
||
|
|
*/
|
||
|
|
|
||
|
|
const fc = require('fast-check');
|
||
|
|
|
||
|
|
// Mock dependencies required by the compliance module
|
||
|
|
jest.mock('../middleware/auth', () => ({
|
||
|
|
requireAuth: () => (req, res, next) => next(),
|
||
|
|
requireGroup: () => (req, res, next) => next(),
|
||
|
|
}));
|
||
|
|
jest.mock('../helpers/auditLog', () => jest.fn());
|
||
|
|
jest.mock('../db', () => ({
|
||
|
|
query: jest.fn(() => Promise.resolve({ rows: [], rowCount: 0 })),
|
||
|
|
connect: jest.fn(() => Promise.resolve({ query: jest.fn(), release: jest.fn() })),
|
||
|
|
}));
|
||
|
|
|
||
|
|
const { groupByHostname } = require('../routes/compliance');
|
||
|
|
|
||
|
|
// --- Generators ---
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Generate a valid hostname string (alphanumeric + hyphens, 1-20 chars).
|
||
|
|
*/
|
||
|
|
const hostnameArb = fc.stringMatching(/^[A-Z][A-Z0-9\-]{0,14}$/);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Generate a metric_id string like "7.1.1", "7.2.3", etc.
|
||
|
|
*/
|
||
|
|
const metricIdArb = fc.tuple(
|
||
|
|
fc.integer({ min: 1, max: 9 }),
|
||
|
|
fc.integer({ min: 1, max: 9 }),
|
||
|
|
fc.integer({ min: 1, max: 9 })
|
||
|
|
).map(([a, b, c]) => `${a}.${b}.${c}`);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Generate a date string in YYYY-MM-DD format for first_seen/last_seen.
|
||
|
|
*/
|
||
|
|
const dateArb = fc.tuple(
|
||
|
|
fc.integer({ min: 2023, max: 2025 }),
|
||
|
|
fc.integer({ min: 1, max: 12 }),
|
||
|
|
fc.integer({ min: 1, max: 28 })
|
||
|
|
).map(([y, m, d]) => `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Generate a compliance row with resolution_date = null and remediation_plan = null
|
||
|
|
* (non-bug-condition inputs — these are the rows that should work correctly on unfixed code).
|
||
|
|
*/
|
||
|
|
function complianceRowArb(hostname, metricId) {
|
||
|
|
return fc.record({
|
||
|
|
hostname: fc.constant(hostname),
|
||
|
|
ip_address: fc.constantFrom('10.0.0.1', '10.0.0.2', '192.168.1.1', ''),
|
||
|
|
device_type: fc.constantFrom('Switch', 'Router', 'Firewall', ''),
|
||
|
|
team: fc.constantFrom('STEAM', 'ACCESS-ENG'),
|
||
|
|
status: fc.constantFrom('active', 'resolved'),
|
||
|
|
metric_id: fc.constant(metricId),
|
||
|
|
metric_desc: fc.constantFrom('Password Complexity', 'Firmware Version', 'Logging Enabled'),
|
||
|
|
category: fc.constantFrom('Configuration', 'Patching', 'Monitoring'),
|
||
|
|
seen_count: fc.integer({ min: 1, max: 20 }),
|
||
|
|
first_seen: dateArb,
|
||
|
|
last_seen: dateArb,
|
||
|
|
resolved_on: fc.constant(null),
|
||
|
|
resolution_date: fc.constant(null),
|
||
|
|
remediation_plan: fc.constant(null),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Generate an array of compliance rows with varying hostnames and metric_ids.
|
||
|
|
* Ensures at least 1 row, with 1-4 hostnames and 1-5 metrics per hostname.
|
||
|
|
*/
|
||
|
|
const complianceRowsArb = fc.tuple(
|
||
|
|
fc.array(hostnameArb, { minLength: 1, maxLength: 4 }),
|
||
|
|
fc.array(metricIdArb, { minLength: 1, maxLength: 5 })
|
||
|
|
).chain(([hostnames, metricIds]) => {
|
||
|
|
// Ensure unique hostnames and metric_ids
|
||
|
|
const uniqueHostnames = [...new Set(hostnames)];
|
||
|
|
const uniqueMetricIds = [...new Set(metricIds)];
|
||
|
|
|
||
|
|
if (uniqueHostnames.length === 0 || uniqueMetricIds.length === 0) {
|
||
|
|
// Fallback: generate at least one row
|
||
|
|
return complianceRowArb('HOST-A', '1.1.1').map(row => [row]);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Generate rows: each hostname gets some subset of metric_ids
|
||
|
|
// Some hostnames may share metric_ids (same metric failing on different devices)
|
||
|
|
const rowArbs = [];
|
||
|
|
for (const hostname of uniqueHostnames) {
|
||
|
|
// Each hostname gets 1 to all metric_ids
|
||
|
|
const metricsForHost = uniqueMetricIds.slice(0, Math.max(1, Math.ceil(Math.random() * uniqueMetricIds.length)));
|
||
|
|
for (const metricId of metricsForHost) {
|
||
|
|
rowArbs.push(complianceRowArb(hostname, metricId));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return fc.tuple(...rowArbs).map(rows => rows);
|
||
|
|
});
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Better generator: explicitly controls the structure to ensure good coverage.
|
||
|
|
* Generates 1-3 hostnames, each with 1-4 rows (possibly duplicate metric_ids to test dedup).
|
||
|
|
*/
|
||
|
|
const structuredRowsArb = fc.record({
|
||
|
|
numHostnames: fc.integer({ min: 1, max: 3 }),
|
||
|
|
numMetricsPerHost: fc.integer({ min: 1, max: 4 }),
|
||
|
|
allowDuplicateMetrics: fc.boolean(),
|
||
|
|
}).chain(({ numHostnames, numMetricsPerHost, allowDuplicateMetrics }) => {
|
||
|
|
const hostnameArbs = fc.array(hostnameArb, { minLength: numHostnames, maxLength: numHostnames });
|
||
|
|
const metricArbs = fc.array(metricIdArb, { minLength: numMetricsPerHost, maxLength: numMetricsPerHost });
|
||
|
|
|
||
|
|
return fc.tuple(hostnameArbs, metricArbs).chain(([hostnames, metrics]) => {
|
||
|
|
const uniqueHostnames = [...new Set(hostnames)];
|
||
|
|
if (uniqueHostnames.length === 0) return fc.constant([]);
|
||
|
|
|
||
|
|
const rowArbs = [];
|
||
|
|
for (const hostname of uniqueHostnames) {
|
||
|
|
const metricsToUse = allowDuplicateMetrics
|
||
|
|
? metrics // May have duplicates
|
||
|
|
: [...new Set(metrics)];
|
||
|
|
|
||
|
|
for (const metricId of metricsToUse) {
|
||
|
|
rowArbs.push(complianceRowArb(hostname, metricId));
|
||
|
|
}
|
||
|
|
// Add an extra duplicate row for the first metric to test dedup
|
||
|
|
if (allowDuplicateMetrics && metricsToUse.length > 0) {
|
||
|
|
rowArbs.push(complianceRowArb(hostname, metricsToUse[0]));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (rowArbs.length === 0) return fc.constant([]);
|
||
|
|
return fc.tuple(...rowArbs).map(rows => rows);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
|
||
|
|
// =============================================================================
|
||
|
|
// Property P2.A — Each device.hostname appears exactly once in output
|
||
|
|
// =============================================================================
|
||
|
|
//
|
||
|
|
// **Validates: Requirements 3.3, 3.5**
|
||
|
|
//
|
||
|
|
describe('Property P2.A — Each device.hostname appears exactly once in output', () => {
|
||
|
|
it('P2.A — groupByHostname produces one device per unique hostname', () => {
|
||
|
|
fc.assert(
|
||
|
|
fc.property(structuredRowsArb, (rows) => {
|
||
|
|
if (rows.length === 0) return;
|
||
|
|
|
||
|
|
const noteHostnames = new Set();
|
||
|
|
const devices = groupByHostname(rows, noteHostnames);
|
||
|
|
|
||
|
|
// Each hostname in the output should appear exactly once
|
||
|
|
const outputHostnames = devices.map(d => d.hostname);
|
||
|
|
const uniqueOutputHostnames = new Set(outputHostnames);
|
||
|
|
expect(outputHostnames.length).toBe(uniqueOutputHostnames.size);
|
||
|
|
|
||
|
|
// Every hostname from input should appear in output
|
||
|
|
const inputHostnames = new Set(rows.map(r => r.hostname));
|
||
|
|
for (const hostname of inputHostnames) {
|
||
|
|
expect(uniqueOutputHostnames.has(hostname)).toBe(true);
|
||
|
|
}
|
||
|
|
}),
|
||
|
|
{ numRuns: 100 },
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
|
||
|
|
// =============================================================================
|
||
|
|
// Property P2.B — device.failing_metrics contains no duplicate metric_ids
|
||
|
|
// =============================================================================
|
||
|
|
//
|
||
|
|
// **Validates: Requirements 3.3, 3.5**
|
||
|
|
//
|
||
|
|
describe('Property P2.B — device.failing_metrics contains no duplicate metric_ids', () => {
|
||
|
|
it('P2.B — groupByHostname deduplicates metrics by metric_id', () => {
|
||
|
|
fc.assert(
|
||
|
|
fc.property(structuredRowsArb, (rows) => {
|
||
|
|
if (rows.length === 0) return;
|
||
|
|
|
||
|
|
const noteHostnames = new Set();
|
||
|
|
const devices = groupByHostname(rows, noteHostnames);
|
||
|
|
|
||
|
|
for (const device of devices) {
|
||
|
|
const metricIds = device.failing_metrics.map(m => m.metric_id);
|
||
|
|
const uniqueMetricIds = new Set(metricIds);
|
||
|
|
expect(metricIds.length).toBe(uniqueMetricIds.size);
|
||
|
|
}
|
||
|
|
}),
|
||
|
|
{ numRuns: 100 },
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
|
||
|
|
// =============================================================================
|
||
|
|
// Property P2.C — device.seen_count >= every row's seen_count for that hostname
|
||
|
|
// =============================================================================
|
||
|
|
//
|
||
|
|
// **Validates: Requirements 3.3, 3.5**
|
||
|
|
//
|
||
|
|
describe('Property P2.C — device.seen_count >= every row seen_count for that hostname', () => {
|
||
|
|
it('P2.C — groupByHostname picks the maximum seen_count across rows', () => {
|
||
|
|
fc.assert(
|
||
|
|
fc.property(structuredRowsArb, (rows) => {
|
||
|
|
if (rows.length === 0) return;
|
||
|
|
|
||
|
|
const noteHostnames = new Set();
|
||
|
|
const devices = groupByHostname(rows, noteHostnames);
|
||
|
|
|
||
|
|
for (const device of devices) {
|
||
|
|
const rowsForHost = rows.filter(r => r.hostname === device.hostname);
|
||
|
|
for (const row of rowsForHost) {
|
||
|
|
expect(device.seen_count).toBeGreaterThanOrEqual(row.seen_count);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}),
|
||
|
|
{ numRuns: 100 },
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
|
||
|
|
// =============================================================================
|
||
|
|
// Property P2.D — device.first_seen <= every row's first_seen for that hostname
|
||
|
|
// =============================================================================
|
||
|
|
//
|
||
|
|
// **Validates: Requirements 3.3, 3.5**
|
||
|
|
//
|
||
|
|
describe('Property P2.D — device.first_seen <= every row first_seen for that hostname', () => {
|
||
|
|
it('P2.D — groupByHostname picks the earliest first_seen across rows', () => {
|
||
|
|
fc.assert(
|
||
|
|
fc.property(structuredRowsArb, (rows) => {
|
||
|
|
if (rows.length === 0) return;
|
||
|
|
|
||
|
|
const noteHostnames = new Set();
|
||
|
|
const devices = groupByHostname(rows, noteHostnames);
|
||
|
|
|
||
|
|
for (const device of devices) {
|
||
|
|
const rowsForHost = rows.filter(r => r.hostname === device.hostname);
|
||
|
|
for (const row of rowsForHost) {
|
||
|
|
if (row.first_seen && device.first_seen) {
|
||
|
|
expect(device.first_seen <= row.first_seen).toBe(true);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}),
|
||
|
|
{ numRuns: 100 },
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
|
||
|
|
// =============================================================================
|
||
|
|
// Property P2.E — device.last_seen >= every row's last_seen for that hostname
|
||
|
|
// =============================================================================
|
||
|
|
//
|
||
|
|
// **Validates: Requirements 3.3, 3.5**
|
||
|
|
//
|
||
|
|
describe('Property P2.E — device.last_seen >= every row last_seen for that hostname', () => {
|
||
|
|
it('P2.E — groupByHostname picks the latest last_seen across rows', () => {
|
||
|
|
fc.assert(
|
||
|
|
fc.property(structuredRowsArb, (rows) => {
|
||
|
|
if (rows.length === 0) return;
|
||
|
|
|
||
|
|
const noteHostnames = new Set();
|
||
|
|
const devices = groupByHostname(rows, noteHostnames);
|
||
|
|
|
||
|
|
for (const device of devices) {
|
||
|
|
const rowsForHost = rows.filter(r => r.hostname === device.hostname);
|
||
|
|
for (const row of rowsForHost) {
|
||
|
|
if (row.last_seen && device.last_seen) {
|
||
|
|
expect(device.last_seen >= row.last_seen).toBe(true);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}),
|
||
|
|
{ numRuns: 100 },
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
|
||
|
|
// =============================================================================
|
||
|
|
// Property P2.F — device.has_notes matches noteHostnames membership
|
||
|
|
// =============================================================================
|
||
|
|
//
|
||
|
|
// **Validates: Requirements 3.3, 3.5**
|
||
|
|
//
|
||
|
|
describe('Property P2.F — device.has_notes matches noteHostnames membership', () => {
|
||
|
|
it('P2.F — groupByHostname sets has_notes based on noteHostnames Set', () => {
|
||
|
|
fc.assert(
|
||
|
|
fc.property(
|
||
|
|
structuredRowsArb,
|
||
|
|
fc.array(hostnameArb, { minLength: 0, maxLength: 3 }),
|
||
|
|
(rows, noteHosts) => {
|
||
|
|
if (rows.length === 0) return;
|
||
|
|
|
||
|
|
// Build noteHostnames set — include some from input, some random
|
||
|
|
const inputHostnames = [...new Set(rows.map(r => r.hostname))];
|
||
|
|
const noteHostnames = new Set([
|
||
|
|
...noteHosts,
|
||
|
|
// Include some actual hostnames from input to test true case
|
||
|
|
...inputHostnames.slice(0, Math.ceil(inputHostnames.length / 2)),
|
||
|
|
]);
|
||
|
|
|
||
|
|
const devices = groupByHostname(rows, noteHostnames);
|
||
|
|
|
||
|
|
for (const device of devices) {
|
||
|
|
const expected = noteHostnames.has(device.hostname);
|
||
|
|
expect(device.has_notes).toBe(expected);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
),
|
||
|
|
{ numRuns: 100 },
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|