// Feature: atlas-metrics-report // Property tests for Atlas donut chart data correctness and color assignment import fc from 'fast-check'; // --------------------------------------------------------------------------- // Since the donut components and getStatusColor are defined inline in // ReportingPage.js and not exported, we replicate the exact data // transformation logic here and test the mathematical properties directly. // This validates that the formulas used in the components are correct // for all valid inputs. // --------------------------------------------------------------------------- // --------------------------------------------------------------------------- // Replicated logic from ReportingPage.js // --------------------------------------------------------------------------- /** * Coverage donut segment computation — mirrors AtlasCoverageDonut logic. */ function computeCoverageDonutData(hostsWithPlans, hostsWithoutPlans) { const totalHosts = hostsWithPlans + hostsWithoutPlans; if (totalHosts === 0) return { totalHosts, segments: [] }; const segments = [ { label: 'With Plans', count: hostsWithPlans, color: '#10B981', percentage: ((hostsWithPlans / totalHosts) * 100).toFixed(1) }, { label: 'Without Plans', count: hostsWithoutPlans, color: '#F59E0B', percentage: ((hostsWithoutPlans / totalHosts) * 100).toFixed(1) }, ].filter((s) => s.count > 0); return { totalHosts, segments }; } /** * Plan type definitions — mirrors PLAN_TYPE_DEFS in ReportingPage.js. */ const PLAN_TYPE_DEFS = [ { key: 'decommission', label: 'Decommission', color: '#EF4444' }, { key: 'remediation', label: 'Remediation', color: '#0EA5E9' }, { key: 'false_positive', label: 'False Positive', color: '#A855F7' }, { key: 'risk_acceptance', label: 'Risk Acceptance', color: '#F59E0B' }, { key: 'scan_exclusion', label: 'Scan Exclusion', color: '#64748B' }, ]; /** * Plan type donut segment computation — mirrors AtlasPlanTypeDonut logic. */ function computePlanTypeDonutData(plansByType) { const totalPlans = Object.values(plansByType).reduce((sum, c) => sum + c, 0); if (totalPlans === 0) return { totalPlans, segments: [] }; const segments = PLAN_TYPE_DEFS.map((def) => { const count = plansByType[def.key] || 0; return { ...def, count, percentage: ((count / totalPlans) * 100).toFixed(0) }; }).filter((s) => s.count > 0); return { totalPlans, segments }; } /** * Plan status color assignment — mirrors getStatusColor in ReportingPage.js. */ function getStatusColor(status) { if (status === 'active') return '#10B981'; if (status === 'expired') return '#EF4444'; if (status === 'completed') return '#0EA5E9'; return '#64748B'; } /** * Plan status donut segment computation — mirrors AtlasPlanStatusDonut logic. */ function computePlanStatusDonutData(plansByStatus) { const totalPlans = Object.values(plansByStatus).reduce((sum, c) => sum + c, 0); if (totalPlans === 0) return { totalPlans, segments: [] }; const entries = Object.entries(plansByStatus).filter(([, count]) => count > 0); const segments = entries.map(([status, count]) => ({ key: status, label: status.charAt(0).toUpperCase() + status.slice(1), color: getStatusColor(status), count, percentage: ((count / totalPlans) * 100).toFixed(0), })); return { totalPlans, segments }; } // --------------------------------------------------------------------------- // Generators // --------------------------------------------------------------------------- const KNOWN_PLAN_TYPES = ['decommission', 'remediation', 'false_positive', 'risk_acceptance', 'scan_exclusion']; const KNOWN_STATUSES = ['active', 'expired', 'completed']; /** * Generate a pair of non-negative integers where at least one is > 0. */ const coveragePairArb = fc .tuple( fc.nat({ max: 10000 }), fc.nat({ max: 10000 }), ) .filter(([a, b]) => a + b > 0); /** * Generate a plansByType object with 1–5 known plan type keys mapped to positive integers. */ const plansByTypeArb = fc .subarray(KNOWN_PLAN_TYPES, { minLength: 1, maxLength: 5 }) .chain((keys) => fc.tuple(...keys.map(() => fc.integer({ min: 1, max: 5000 }))).map((counts) => { const obj = {}; keys.forEach((key, i) => { obj[key] = counts[i]; }); return obj; }), ); /** * Generate a plansByStatus object with 1–3 known status keys mapped to positive integers. * Also allows arbitrary unknown status strings. */ const statusKeyArb = fc.oneof( { weight: 3, arbitrary: fc.constantFrom(...KNOWN_STATUSES) }, { weight: 1, arbitrary: fc.stringMatching(/^[a-z_]{2,15}$/).filter((s) => !KNOWN_STATUSES.includes(s)) }, ); const plansByStatusArb = fc .array( fc.tuple(statusKeyArb, fc.integer({ min: 1, max: 5000 })), { minLength: 1, maxLength: 4 }, ) .map((pairs) => { const obj = {}; for (const [key, count] of pairs) { // Use first occurrence if duplicate keys generated if (!(key in obj)) obj[key] = count; } return obj; }) .filter((obj) => Object.keys(obj).length >= 1); // --------------------------------------------------------------------------- // Property 2: Coverage donut data correctness // Validates: Requirements 3.3, 3.4 // --------------------------------------------------------------------------- describe('Property 2: Coverage donut data correctness', () => { test('center text (totalHosts) equals hostsWithPlans + hostsWithoutPlans', () => { fc.assert( fc.property(coveragePairArb, ([hostsWithPlans, hostsWithoutPlans]) => { const { totalHosts } = computeCoverageDonutData(hostsWithPlans, hostsWithoutPlans); expect(totalHosts).toBe(hostsWithPlans + hostsWithoutPlans); }), { numRuns: 100 }, ); }); test('legend percentages equal (count / totalHosts) * 100 for each segment', () => { fc.assert( fc.property(coveragePairArb, ([hostsWithPlans, hostsWithoutPlans]) => { const totalHosts = hostsWithPlans + hostsWithoutPlans; const { segments } = computeCoverageDonutData(hostsWithPlans, hostsWithoutPlans); for (const seg of segments) { const expectedPct = ((seg.count / totalHosts) * 100).toFixed(1); expect(seg.percentage).toBe(expectedPct); } }), { numRuns: 100 }, ); }); test('segments only include entries with count > 0', () => { fc.assert( fc.property(coveragePairArb, ([hostsWithPlans, hostsWithoutPlans]) => { const { segments } = computeCoverageDonutData(hostsWithPlans, hostsWithoutPlans); for (const seg of segments) { expect(seg.count).toBeGreaterThan(0); } // If one value is 0, only one segment should appear if (hostsWithPlans === 0) { expect(segments.length).toBe(1); expect(segments[0].label).toBe('Without Plans'); } else if (hostsWithoutPlans === 0) { expect(segments.length).toBe(1); expect(segments[0].label).toBe('With Plans'); } else { expect(segments.length).toBe(2); } }), { numRuns: 100 }, ); }); test('segment percentages sum to approximately 100', () => { fc.assert( fc.property(coveragePairArb, ([hostsWithPlans, hostsWithoutPlans]) => { const { segments } = computeCoverageDonutData(hostsWithPlans, hostsWithoutPlans); const totalPct = segments.reduce((sum, s) => sum + parseFloat(s.percentage), 0); // Allow small rounding tolerance due to toFixed(1) expect(totalPct).toBeCloseTo(100, 0); }), { numRuns: 100 }, ); }); }); // --------------------------------------------------------------------------- // Property 3: Plan type donut data correctness // Validates: Requirements 4.3, 4.4 // --------------------------------------------------------------------------- describe('Property 3: Plan type donut data correctness', () => { test('center text (totalPlans) equals sum of all plan type counts', () => { fc.assert( fc.property(plansByTypeArb, (plansByType) => { const expectedTotal = Object.values(plansByType).reduce((s, c) => s + c, 0); const { totalPlans } = computePlanTypeDonutData(plansByType); expect(totalPlans).toBe(expectedTotal); }), { numRuns: 100 }, ); }); test('legend entries match input — only types with count > 0 appear', () => { fc.assert( fc.property(plansByTypeArb, (plansByType) => { const { segments } = computePlanTypeDonutData(plansByType); // Every segment should have count > 0 for (const seg of segments) { expect(seg.count).toBeGreaterThan(0); } // Every key in plansByType with count > 0 should appear in segments const segmentKeys = new Set(segments.map((s) => s.key)); for (const [key, count] of Object.entries(plansByType)) { if (count > 0 && KNOWN_PLAN_TYPES.includes(key)) { expect(segmentKeys.has(key)).toBe(true); } } // Every segment key should be in the input for (const seg of segments) { expect(plansByType[seg.key]).toBeDefined(); expect(plansByType[seg.key]).toBe(seg.count); } }), { numRuns: 100 }, ); }); test('legend percentages equal (count / totalPlans) * 100 for each entry', () => { fc.assert( fc.property(plansByTypeArb, (plansByType) => { const { totalPlans, segments } = computePlanTypeDonutData(plansByType); for (const seg of segments) { const expectedPct = ((seg.count / totalPlans) * 100).toFixed(0); expect(seg.percentage).toBe(expectedPct); } }), { numRuns: 100 }, ); }); }); // --------------------------------------------------------------------------- // Property 4: Plan status donut data correctness // Validates: Requirements 5.3, 5.4 // --------------------------------------------------------------------------- describe('Property 4: Plan status donut data correctness', () => { test('center text (totalPlans) equals sum of all status counts', () => { fc.assert( fc.property(plansByStatusArb, (plansByStatus) => { const expectedTotal = Object.values(plansByStatus).reduce((s, c) => s + c, 0); const { totalPlans } = computePlanStatusDonutData(plansByStatus); expect(totalPlans).toBe(expectedTotal); }), { numRuns: 100 }, ); }); test('legend entries match input — only statuses with count > 0 appear', () => { fc.assert( fc.property(plansByStatusArb, (plansByStatus) => { const { segments } = computePlanStatusDonutData(plansByStatus); // Every segment should have count > 0 for (const seg of segments) { expect(seg.count).toBeGreaterThan(0); } // Every key in plansByStatus with count > 0 should appear in segments const segmentKeys = new Set(segments.map((s) => s.key)); for (const [key, count] of Object.entries(plansByStatus)) { if (count > 0) { expect(segmentKeys.has(key)).toBe(true); } } // Every segment key should be in the input with matching count for (const seg of segments) { expect(plansByStatus[seg.key]).toBeDefined(); expect(plansByStatus[seg.key]).toBe(seg.count); } }), { numRuns: 100 }, ); }); test('legend percentages equal (count / totalPlans) * 100 for each entry', () => { fc.assert( fc.property(plansByStatusArb, (plansByStatus) => { const { totalPlans, segments } = computePlanStatusDonutData(plansByStatus); for (const seg of segments) { const expectedPct = ((seg.count / totalPlans) * 100).toFixed(0); expect(seg.percentage).toBe(expectedPct); } }), { numRuns: 100 }, ); }); test('segment labels are capitalized versions of status keys', () => { fc.assert( fc.property(plansByStatusArb, (plansByStatus) => { const { segments } = computePlanStatusDonutData(plansByStatus); for (const seg of segments) { const expectedLabel = seg.key.charAt(0).toUpperCase() + seg.key.slice(1); expect(seg.label).toBe(expectedLabel); } }), { numRuns: 100 }, ); }); }); // --------------------------------------------------------------------------- // Property 5: Plan status color assignment // Validates: Requirements 5.2 // --------------------------------------------------------------------------- describe('Property 5: Plan status color assignment', () => { test('known statuses return their specified colors', () => { fc.assert( fc.property( fc.constantFrom('active', 'expired', 'completed'), (status) => { const color = getStatusColor(status); if (status === 'active') expect(color).toBe('#10B981'); if (status === 'expired') expect(color).toBe('#EF4444'); if (status === 'completed') expect(color).toBe('#0EA5E9'); }, ), { numRuns: 100 }, ); }); test('arbitrary unknown strings return the fallback color #64748B', () => { fc.assert( fc.property( fc.string({ minLength: 0, maxLength: 50 }).filter( (s) => s !== 'active' && s !== 'expired' && s !== 'completed', ), (status) => { const color = getStatusColor(status); expect(color).toBe('#64748B'); }, ), { numRuns: 100 }, ); }); test('mixed known and unknown statuses all return correct colors', () => { fc.assert( fc.property( fc.oneof( fc.constantFrom('active', 'expired', 'completed'), fc.string({ minLength: 0, maxLength: 30 }), ), (status) => { const color = getStatusColor(status); const expected = status === 'active' ? '#10B981' : status === 'expired' ? '#EF4444' : status === 'completed' ? '#0EA5E9' : '#64748B'; expect(color).toBe(expected); }, ), { numRuns: 100 }, ); }); });