Adds a CategoryFilterBar with pill-shaped FilterChip components below the metric health cards. Non-metric categories (Missing_AppID, Aging Vulns, Missing_DF, etc.) are derived dynamically from device data and displayed as color-coded filterable chips with device counts. Unified filter state replaces the old metricFilter array, ensuring mutual exclusivity between metric card filters and non-metric chip filters. Includes 4 property-based tests validating derivation, filter predicate, mutual exclusivity, and color resolution correctness. Closes #26
113 lines
4.6 KiB
JavaScript
113 lines
4.6 KiB
JavaScript
/**
|
|
* Property-Based Test: Non-metric category derivation is the set difference with accurate counts
|
|
*
|
|
* Feature: compliance-nonmetric-filters, Property 1: Non-metric category derivation
|
|
* **Validates: Requirements 1.1, 1.2, 2.2, 2.4**
|
|
*
|
|
* For any set of devices and any set of summary entries for a team,
|
|
* deriveNonMetricCategories returns exactly the metric_ids present in at least
|
|
* one device's failing_metrics array that do not appear in any summary entry's
|
|
* metric_id — deduplicated, sorted alphabetically by metricId, and each with a
|
|
* count equal to the number of devices whose failing_metrics contains that metric_id.
|
|
*/
|
|
import fc from 'fast-check';
|
|
import { deriveNonMetricCategories } from '../components/pages/CompliancePage';
|
|
|
|
// Generators
|
|
const metricIdArb = fc.stringMatching(/^[A-Za-z0-9_.]{1,20}$/);
|
|
|
|
const failingMetricArb = fc.record({
|
|
metric_id: metricIdArb,
|
|
metric_desc: fc.constant(''),
|
|
category: fc.constant(''),
|
|
});
|
|
|
|
const deviceArb = fc.record({
|
|
hostname: fc.string({ minLength: 1, maxLength: 30 }),
|
|
failing_metrics: fc.array(failingMetricArb, { minLength: 0, maxLength: 8 }),
|
|
});
|
|
|
|
const summaryEntryArb = fc.record({
|
|
metric_id: metricIdArb,
|
|
team: fc.constant('STEAM'),
|
|
compliance_pct: fc.double({ min: 0, max: 1 }),
|
|
status: fc.constantFrom('Meets/Exceeds Target', 'Within 15% of Target', 'Below 15% of Target'),
|
|
});
|
|
|
|
const categoriesConfigArb = fc.dictionary(metricIdArb, fc.constantFrom(
|
|
'Vulnerability Management', 'Access & MFA', 'Logging & Monitoring',
|
|
'Asset Data Quality', 'Endpoint Protection', 'Application Security',
|
|
'Disaster Recovery', 'Decommissioned Assets'
|
|
));
|
|
|
|
describe('Compliance Non-Metric Derivation — Property 1: Set difference with accurate counts', () => {
|
|
it('returned metric_ids are exactly the set difference of device metric_ids minus summary metric_ids', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.array(deviceArb, { minLength: 0, maxLength: 20 }),
|
|
fc.array(summaryEntryArb, { minLength: 0, maxLength: 10 }),
|
|
categoriesConfigArb,
|
|
(devices, summaryEntries, config) => {
|
|
const result = deriveNonMetricCategories(devices, summaryEntries, config);
|
|
|
|
const summaryIds = new Set(summaryEntries.map(e => e.metric_id));
|
|
const deviceMetricIds = new Set();
|
|
for (const d of devices) {
|
|
for (const m of (d.failing_metrics || [])) {
|
|
if (m.metric_id) deviceMetricIds.add(m.metric_id);
|
|
}
|
|
}
|
|
const expectedIds = new Set([...deviceMetricIds].filter(id => !summaryIds.has(id)));
|
|
|
|
const resultIds = new Set(result.map(r => r.metricId));
|
|
expect(resultIds).toEqual(expectedIds);
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
);
|
|
});
|
|
|
|
it('returned list is deduplicated and sorted alphabetically by metricId', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.array(deviceArb, { minLength: 0, maxLength: 20 }),
|
|
fc.array(summaryEntryArb, { minLength: 0, maxLength: 10 }),
|
|
categoriesConfigArb,
|
|
(devices, summaryEntries, config) => {
|
|
const result = deriveNonMetricCategories(devices, summaryEntries, config);
|
|
|
|
// Deduplicated
|
|
const ids = result.map(r => r.metricId);
|
|
expect(ids.length).toBe(new Set(ids).size);
|
|
|
|
// Sorted alphabetically
|
|
const sorted = [...ids].sort((a, b) => a.localeCompare(b));
|
|
expect(ids).toEqual(sorted);
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
);
|
|
});
|
|
|
|
it('each count equals the number of devices whose failing_metrics contains that metric_id', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.array(deviceArb, { minLength: 0, maxLength: 20 }),
|
|
fc.array(summaryEntryArb, { minLength: 0, maxLength: 10 }),
|
|
categoriesConfigArb,
|
|
(devices, summaryEntries, config) => {
|
|
const result = deriveNonMetricCategories(devices, summaryEntries, config);
|
|
|
|
for (const { metricId, count } of result) {
|
|
const expectedCount = devices.filter(d =>
|
|
(d.failing_metrics || []).some(m => m.metric_id === metricId)
|
|
).length;
|
|
expect(count).toBe(expectedCount);
|
|
}
|
|
}
|
|
),
|
|
{ numRuns: 100 }
|
|
);
|
|
});
|
|
});
|