Files
cve-dashboard/frontend/src/components/pages/__tests__/atlasMetricsAggregation.property.test.js

207 lines
6.5 KiB
JavaScript

// Feature: atlas-metrics-report, Property 1: Metrics aggregation correctness
import fc from 'fast-check';
// ---------------------------------------------------------------------------
// Mock backend dependencies so we can import the pure function
// without pulling in Express, SQLite, etc.
// ---------------------------------------------------------------------------
jest.mock('express', () => ({ Router: jest.fn(() => ({ get: jest.fn(), post: jest.fn(), put: jest.fn(), patch: jest.fn() })) }));
jest.mock('../../../../../backend/middleware/auth', () => ({ requireGroup: jest.fn() }), { virtual: true });
jest.mock('../../../../../backend/helpers/auditLog', () => jest.fn(), { virtual: true });
jest.mock('../../../../../backend/helpers/atlasApi', () => ({
isConfigured: false,
atlasGet: jest.fn(),
atlasPut: jest.fn(),
atlasPatch: jest.fn(),
atlasPost: jest.fn(),
}), { virtual: true });
// Now import the pure function
const { aggregateAtlasMetrics } = require('../../../../../backend/routes/atlas');
// ---------------------------------------------------------------------------
// Generators
// ---------------------------------------------------------------------------
const PLAN_TYPES = ['decommission', 'remediation', 'false_positive', 'risk_acceptance', 'scan_exclusion'];
const PLAN_STATUSES = ['active', 'expired', 'completed'];
/** Generate a single plan object with plan_type and status */
const planArb = fc.record({
plan_type: fc.constantFrom(...PLAN_TYPES),
status: fc.constantFrom(...PLAN_STATUSES),
});
/** Generate a valid plans_json string (JSON array of plan objects) */
const validPlansJsonArb = fc
.array(planArb, { minLength: 0, maxLength: 10 })
.map((plans) => JSON.stringify(plans));
/** Generate an invalid JSON string that will fail JSON.parse */
const invalidPlansJsonArb = fc.constantFrom(
'{bad json',
'not json at all',
'{{[',
'',
'undefined',
);
/** Generate a plans_json value — either valid JSON or invalid */
const plansJsonArb = fc.oneof(
{ weight: 3, arbitrary: validPlansJsonArb },
{ weight: 1, arbitrary: invalidPlansJsonArb },
);
/** Generate a single cache row */
const cacheRowArb = fc.record({
has_action_plan: fc.constantFrom(0, 1),
plans_json: plansJsonArb,
});
// ---------------------------------------------------------------------------
// Helper: manually compute expected metrics for comparison
// ---------------------------------------------------------------------------
function computeExpected(rows) {
const expected = {
totalHosts: rows.length,
hostsWithPlans: 0,
hostsWithoutPlans: 0,
plansByType: {},
plansByStatus: {},
totalPlans: 0,
};
for (const row of rows) {
if (row.has_action_plan === 1) {
expected.hostsWithPlans++;
} else {
expected.hostsWithoutPlans++;
}
let plans;
try {
plans = JSON.parse(row.plans_json);
} catch (e) {
continue;
}
if (!Array.isArray(plans)) continue;
for (const plan of plans) {
expected.totalPlans++;
if (plan.plan_type) {
expected.plansByType[plan.plan_type] = (expected.plansByType[plan.plan_type] || 0) + 1;
}
if (plan.status) {
expected.plansByStatus[plan.status] = (expected.plansByStatus[plan.status] || 0) + 1;
}
}
}
return expected;
}
// ---------------------------------------------------------------------------
// Property 1: Metrics aggregation correctness
// Validates: Requirements 1.3, 1.4, 1.5
// ---------------------------------------------------------------------------
describe('Property 1: Metrics aggregation correctness', () => {
test('totalHosts equals rows.length', () => {
fc.assert(
fc.property(
fc.array(cacheRowArb, { minLength: 0, maxLength: 30 }),
(rows) => {
const result = aggregateAtlasMetrics(rows);
expect(result.totalHosts).toBe(rows.length);
},
),
{ numRuns: 100 },
);
});
test('hostsWithPlans + hostsWithoutPlans equals totalHosts', () => {
fc.assert(
fc.property(
fc.array(cacheRowArb, { minLength: 0, maxLength: 30 }),
(rows) => {
const result = aggregateAtlasMetrics(rows);
expect(result.hostsWithPlans + result.hostsWithoutPlans).toBe(result.totalHosts);
},
),
{ numRuns: 100 },
);
});
test('hostsWithPlans equals count of rows where has_action_plan === 1', () => {
fc.assert(
fc.property(
fc.array(cacheRowArb, { minLength: 0, maxLength: 30 }),
(rows) => {
const result = aggregateAtlasMetrics(rows);
const expectedWithPlans = rows.filter((r) => r.has_action_plan === 1).length;
expect(result.hostsWithPlans).toBe(expectedWithPlans);
},
),
{ numRuns: 100 },
);
});
test('totalPlans equals sum of valid plan array lengths', () => {
fc.assert(
fc.property(
fc.array(cacheRowArb, { minLength: 0, maxLength: 30 }),
(rows) => {
const result = aggregateAtlasMetrics(rows);
const expected = computeExpected(rows);
expect(result.totalPlans).toBe(expected.totalPlans);
},
),
{ numRuns: 100 },
);
});
test('plansByType and plansByStatus counts match individual plan fields', () => {
fc.assert(
fc.property(
fc.array(cacheRowArb, { minLength: 0, maxLength: 30 }),
(rows) => {
const result = aggregateAtlasMetrics(rows);
const expected = computeExpected(rows);
expect(result.plansByType).toEqual(expected.plansByType);
expect(result.plansByStatus).toEqual(expected.plansByStatus);
},
),
{ numRuns: 100 },
);
});
test('rows with invalid JSON are counted in host totals but excluded from plan counts', () => {
fc.assert(
fc.property(
fc.array(
fc.record({
has_action_plan: fc.constantFrom(0, 1),
plans_json: invalidPlansJsonArb,
}),
{ minLength: 1, maxLength: 20 },
),
(rows) => {
const result = aggregateAtlasMetrics(rows);
// Host totals should still be correct
expect(result.totalHosts).toBe(rows.length);
expect(result.hostsWithPlans + result.hostsWithoutPlans).toBe(rows.length);
// No plans should be counted since all JSON is invalid
expect(result.totalPlans).toBe(0);
expect(result.plansByType).toEqual({});
expect(result.plansByStatus).toEqual({});
},
),
{ numRuns: 100 },
);
});
});