217 lines
8.4 KiB
JavaScript
217 lines
8.4 KiB
JavaScript
|
|
/**
|
||
|
|
* Bug Condition Exploration Property Test: Compliance Remediation Display Fix
|
||
|
|
*
|
||
|
|
* Spec: .kiro/specs/compliance-remediation-display-fix/ (bugfix)
|
||
|
|
*
|
||
|
|
* BUG CONDITION:
|
||
|
|
* isBugCondition(row) = row.resolution_date != null OR row.remediation_plan != null
|
||
|
|
* Any row with metadata set will lose it through groupByHostname() because the
|
||
|
|
* function does not propagate resolution_date or remediation_plan into device objects.
|
||
|
|
*
|
||
|
|
* THIS TEST IS EXPECTED TO FAIL ON UNFIXED CODE.
|
||
|
|
* Failure confirms the bug exists — resolution_date and remediation_plan are undefined
|
||
|
|
* in the grouped device objects returned by groupByHostname().
|
||
|
|
*
|
||
|
|
* **Validates: Requirements 1.1, 1.2, 2.2**
|
||
|
|
*/
|
||
|
|
|
||
|
|
const fc = require('fast-check');
|
||
|
|
|
||
|
|
// --- Mocks (must be installed BEFORE requiring the route module) ---
|
||
|
|
|
||
|
|
jest.mock('../middleware/auth', () => ({
|
||
|
|
requireAuth: () => (req, res, next) => next(),
|
||
|
|
requireGroup: () => (req, res, next) => next(),
|
||
|
|
}));
|
||
|
|
|
||
|
|
jest.mock('../helpers/auditLog', () => jest.fn());
|
||
|
|
|
||
|
|
jest.mock('../helpers/driftChecker', () => ({
|
||
|
|
loadConfig: jest.fn(() => ({})),
|
||
|
|
compareSchemaToDrift: jest.fn(() => null),
|
||
|
|
reconcileConfig: jest.fn(() => ({ changes: [] })),
|
||
|
|
}));
|
||
|
|
|
||
|
|
jest.mock('../helpers/vclHelpers', () => ({
|
||
|
|
isValidDateString: jest.fn(() => true),
|
||
|
|
validateRemediationPlan: jest.fn(() => ({ valid: true })),
|
||
|
|
computeVCLStats: jest.fn(() => ({})),
|
||
|
|
categorizeNonCompliant: jest.fn(() => []),
|
||
|
|
rankHeavyHitters: jest.fn(() => []),
|
||
|
|
computeForecastBurndown: jest.fn(() => ({})),
|
||
|
|
matchByHostname: jest.fn(() => []),
|
||
|
|
computeBulkDiff: jest.fn(() => ({})),
|
||
|
|
mapColumnHeaders: jest.fn(() => ({})),
|
||
|
|
}));
|
||
|
|
|
||
|
|
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 { groupByHostname } = require('../routes/compliance');
|
||
|
|
|
||
|
|
// --- Generators ---
|
||
|
|
|
||
|
|
/** Generate a date string in YYYY-MM-DD format (avoid toISOString on shrunk invalid dates) */
|
||
|
|
const arbDateString = fc.tuple(
|
||
|
|
fc.integer({ min: 2020, max: 2030 }),
|
||
|
|
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 non-empty remediation plan string */
|
||
|
|
const arbRemediationPlan = fc.string({ minLength: 1, maxLength: 200 }).filter(s => s.trim().length > 0);
|
||
|
|
|
||
|
|
/** Generate a hostname (alphanumeric with dashes, realistic) */
|
||
|
|
const arbHostname = fc.stringMatching(/^[A-Z][A-Z0-9\-]{2,20}$/);
|
||
|
|
|
||
|
|
/** Generate a metric_id like "7.1.1" or "3.2" */
|
||
|
|
const arbMetricId = 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 compliance row with non-null resolution_date and/or remediation_plan.
|
||
|
|
* This is the bug condition: rows that have metadata which should be propagated.
|
||
|
|
*/
|
||
|
|
const arbComplianceRowWithMetadata = fc.record({
|
||
|
|
hostname: arbHostname,
|
||
|
|
ip_address: fc.ipV4(),
|
||
|
|
device_type: fc.constantFrom('Switch', 'Router', 'Firewall', 'Server'),
|
||
|
|
team: fc.constantFrom('STEAM', 'ACCESS-ENG'),
|
||
|
|
status: fc.constant('active'),
|
||
|
|
metric_id: arbMetricId,
|
||
|
|
metric_desc: fc.string({ minLength: 3, maxLength: 50 }),
|
||
|
|
category: fc.constantFrom('Configuration', 'Patching', 'Access Control'),
|
||
|
|
seen_count: fc.integer({ min: 1, max: 20 }),
|
||
|
|
first_seen: arbDateString,
|
||
|
|
last_seen: arbDateString,
|
||
|
|
resolved_on: fc.constant(null),
|
||
|
|
resolution_date: arbDateString,
|
||
|
|
remediation_plan: arbRemediationPlan,
|
||
|
|
});
|
||
|
|
|
||
|
|
// --- Property Test ---
|
||
|
|
|
||
|
|
describe('Bug Condition Exploration: resolution_date and remediation_plan in groupByHostname()', () => {
|
||
|
|
it('Property 1: groupByHostname() should propagate resolution_date from rows to device objects', () => {
|
||
|
|
fc.assert(
|
||
|
|
fc.property(arbComplianceRowWithMetadata, (row) => {
|
||
|
|
const rows = [row];
|
||
|
|
const noteHostnames = new Set();
|
||
|
|
const devices = groupByHostname(rows, noteHostnames);
|
||
|
|
|
||
|
|
// There should be exactly one device for the single hostname
|
||
|
|
expect(devices).toHaveLength(1);
|
||
|
|
const device = devices[0];
|
||
|
|
|
||
|
|
// BUG CONDITION: resolution_date should be propagated but is undefined on unfixed code
|
||
|
|
expect(device.resolution_date).toBe(row.resolution_date);
|
||
|
|
}),
|
||
|
|
{ numRuns: 100 }
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('Property 2: groupByHostname() should propagate remediation_plan from rows to device objects', () => {
|
||
|
|
fc.assert(
|
||
|
|
fc.property(arbComplianceRowWithMetadata, (row) => {
|
||
|
|
const rows = [row];
|
||
|
|
const noteHostnames = new Set();
|
||
|
|
const devices = groupByHostname(rows, noteHostnames);
|
||
|
|
|
||
|
|
expect(devices).toHaveLength(1);
|
||
|
|
const device = devices[0];
|
||
|
|
|
||
|
|
// BUG CONDITION: remediation_plan should be propagated but is undefined on unfixed code
|
||
|
|
expect(device.remediation_plan).toBe(row.remediation_plan);
|
||
|
|
}),
|
||
|
|
{ numRuns: 100 }
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('Property 3: groupByHostname() should pick first non-null resolution_date across multiple rows for same hostname', () => {
|
||
|
|
fc.assert(
|
||
|
|
fc.property(
|
||
|
|
arbHostname,
|
||
|
|
fc.array(arbMetricId, { minLength: 2, maxLength: 5 }),
|
||
|
|
arbDateString,
|
||
|
|
(hostname, metricIds, resolutionDate) => {
|
||
|
|
// Create multiple rows for the same hostname, first row has resolution_date
|
||
|
|
const rows = metricIds.map((mid, idx) => ({
|
||
|
|
hostname,
|
||
|
|
ip_address: '10.0.0.1',
|
||
|
|
device_type: 'Switch',
|
||
|
|
team: 'STEAM',
|
||
|
|
status: 'active',
|
||
|
|
metric_id: mid,
|
||
|
|
metric_desc: `Metric ${mid}`,
|
||
|
|
category: 'Configuration',
|
||
|
|
seen_count: 1,
|
||
|
|
first_seen: '2025-01-01',
|
||
|
|
last_seen: '2025-06-01',
|
||
|
|
resolved_on: null,
|
||
|
|
resolution_date: idx === 0 ? resolutionDate : null,
|
||
|
|
remediation_plan: null,
|
||
|
|
}));
|
||
|
|
|
||
|
|
const noteHostnames = new Set();
|
||
|
|
const devices = groupByHostname(rows, noteHostnames);
|
||
|
|
|
||
|
|
expect(devices).toHaveLength(1);
|
||
|
|
const device = devices[0];
|
||
|
|
|
||
|
|
// The first non-null resolution_date should be propagated
|
||
|
|
expect(device.resolution_date).toBe(resolutionDate);
|
||
|
|
}
|
||
|
|
),
|
||
|
|
{ numRuns: 100 }
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('Property 4: groupByHostname() should pick first non-null remediation_plan across multiple rows for same hostname', () => {
|
||
|
|
fc.assert(
|
||
|
|
fc.property(
|
||
|
|
arbHostname,
|
||
|
|
fc.array(arbMetricId, { minLength: 2, maxLength: 5 }),
|
||
|
|
arbRemediationPlan,
|
||
|
|
(hostname, metricIds, plan) => {
|
||
|
|
// Create multiple rows for the same hostname, first row has remediation_plan
|
||
|
|
const rows = metricIds.map((mid, idx) => ({
|
||
|
|
hostname,
|
||
|
|
ip_address: '10.0.0.1',
|
||
|
|
device_type: 'Switch',
|
||
|
|
team: 'STEAM',
|
||
|
|
status: 'active',
|
||
|
|
metric_id: mid,
|
||
|
|
metric_desc: `Metric ${mid}`,
|
||
|
|
category: 'Configuration',
|
||
|
|
seen_count: 1,
|
||
|
|
first_seen: '2025-01-01',
|
||
|
|
last_seen: '2025-06-01',
|
||
|
|
resolved_on: null,
|
||
|
|
resolution_date: null,
|
||
|
|
remediation_plan: idx === 0 ? plan : null,
|
||
|
|
}));
|
||
|
|
|
||
|
|
const noteHostnames = new Set();
|
||
|
|
const devices = groupByHostname(rows, noteHostnames);
|
||
|
|
|
||
|
|
expect(devices).toHaveLength(1);
|
||
|
|
const device = devices[0];
|
||
|
|
|
||
|
|
// The first non-null remediation_plan should be propagated
|
||
|
|
expect(device.remediation_plan).toBe(plan);
|
||
|
|
}
|
||
|
|
),
|
||
|
|
{ numRuns: 100 }
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|