Files
cve-dashboard/frontend/src/__tests__/ivanti-queue-remediation-grouping.property.test.js
Jordan Ramos 79f98414c4 Add Remediate workflow type to Ivanti Queue with remediation notes
- Add 'Remediate' as a valid workflow type (vendor-required, like FP/Archer)
- Create queue_remediation_notes table with FK cascade and 5000 char limit
- Add POST/GET /api/ivanti/todo-queue/:id/notes endpoints
- Include remediation_notes_count in queue item GET response
- Add RemediationModal component for viewing/adding notes
- Add notes count badge on Remediate queue items (purple #A855F7 theme)
- Add delete confirmation warning when removing items with notes
- Append remediation notes to Jira ticket descriptions
- Add property-based tests for all correctness properties
2026-06-08 14:07:59 -06:00

181 lines
7.7 KiB
JavaScript

/**
* Property-Based Test: Remediate Queue Grouping
*
* Feature: ivanti-queue-remediation
* Property 2: Remediate items grouped into vendor sections, never Inventory
*
* For any queue item with workflow_type "Remediate", the groupQueueItems function
* SHALL place it in a vendor-grouped section (using the item's vendor field, or
* "Unknown" if vendor is empty/null) and SHALL NOT place it in the Inventory section.
*
* **Validates: Requirements 2.1, 2.2, 2.4**
*/
import fc from 'fast-check';
import { groupQueueItems } from '../utils/queueGrouping';
// ---------------------------------------------------------------------------
// Arbitraries
// ---------------------------------------------------------------------------
const arbVendor = fc.oneof(
fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0),
fc.constantFrom('Microsoft', 'Cisco', 'Juniper', 'ADTRAN', 'VMware')
);
const arbEmptyVendor = fc.oneof(
fc.constant(''),
fc.constant(null),
fc.constant(undefined),
fc.constant(' ') // whitespace only
);
const arbRemediateItemWithVendor = fc.record({
id: fc.integer({ min: 1, max: 100000 }),
workflow_type: fc.constant('Remediate'),
vendor: arbVendor,
status: fc.constant('pending'),
finding_id: fc.string({ minLength: 1, maxLength: 20 }),
finding_title: fc.string({ minLength: 1, maxLength: 100 }),
});
const arbRemediateItemNoVendor = fc.record({
id: fc.integer({ min: 1, max: 100000 }),
workflow_type: fc.constant('Remediate'),
vendor: arbEmptyVendor,
status: fc.constant('pending'),
finding_id: fc.string({ minLength: 1, maxLength: 20 }),
finding_title: fc.string({ minLength: 1, maxLength: 100 }),
});
const arbInventoryItem = fc.record({
id: fc.integer({ min: 100001, max: 200000 }),
workflow_type: fc.constantFrom('CARD', 'GRANITE', 'DECOM'),
vendor: fc.constant(''),
status: fc.constant('pending'),
finding_id: fc.string({ minLength: 1, maxLength: 20 }),
finding_title: fc.string({ minLength: 1, maxLength: 100 }),
});
// ---------------------------------------------------------------------------
// Property 2: Remediate items grouped into vendor sections, never Inventory
// ---------------------------------------------------------------------------
describe('Feature: ivanti-queue-remediation, Property 2: Remediate items grouped into vendor sections, never Inventory', () => {
it('Remediate items with a vendor are placed in vendor-grouped sections, never Inventory', () => {
fc.assert(
fc.property(
fc.array(arbRemediateItemWithVendor, { minLength: 1, maxLength: 20 })
.map(items => {
// Ensure unique IDs
const seen = new Set();
return items.filter(item => {
if (seen.has(item.id)) return false;
seen.add(item.id);
return true;
});
})
.filter(items => items.length > 0),
(items) => {
const sections = groupQueueItems(items);
// No Inventory section should exist (Remediate items never go there)
const inventorySection = sections.find(s => s.type === 'inventory');
expect(inventorySection).toBeUndefined();
// All items should be in vendor sections
const vendorSections = sections.filter(s => s.type === 'vendor');
const allGroupedItems = vendorSections.flatMap(s => s.items);
expect(allGroupedItems.length).toBe(items.length);
// Each item should be in its vendor's section
for (const item of items) {
const expectedVendor = item.vendor?.trim() || 'Unknown';
const section = vendorSections.find(s => s.label === expectedVendor);
expect(section).toBeDefined();
expect(section.items.some(i => i.id === item.id)).toBe(true);
}
}
),
{ numRuns: 100 }
);
});
it('Remediate items with empty/null/whitespace-only vendor land in "Unknown" section', () => {
fc.assert(
fc.property(
fc.array(arbRemediateItemNoVendor, { minLength: 1, maxLength: 10 })
.map(items => {
const seen = new Set();
return items.filter(item => {
if (seen.has(item.id)) return false;
seen.add(item.id);
return true;
});
})
.filter(items => items.length > 0),
(items) => {
const sections = groupQueueItems(items);
// No Inventory section
const inventorySection = sections.find(s => s.type === 'inventory');
expect(inventorySection).toBeUndefined();
// All items should be in the "Unknown" vendor section
const unknownSection = sections.find(s => s.label === 'Unknown');
expect(unknownSection).toBeDefined();
expect(unknownSection.items.length).toBe(items.length);
}
),
{ numRuns: 100 }
);
});
it('Remediate items are never placed in the Inventory section even when mixed with inventory items', () => {
fc.assert(
fc.property(
fc.array(arbRemediateItemWithVendor, { minLength: 1, maxLength: 10 })
.map(items => {
const seen = new Set();
return items.filter(item => {
if (seen.has(item.id)) return false;
seen.add(item.id);
return true;
});
})
.filter(items => items.length > 0),
fc.array(arbInventoryItem, { minLength: 1, maxLength: 5 })
.map(items => {
const seen = new Set();
return items.filter(item => {
if (seen.has(item.id)) return false;
seen.add(item.id);
return true;
});
})
.filter(items => items.length > 0),
(remediateItems, inventoryItems) => {
const allItems = [...remediateItems, ...inventoryItems];
const sections = groupQueueItems(allItems);
// Inventory section exists (from inventory items)
const inventorySection = sections.find(s => s.type === 'inventory');
expect(inventorySection).toBeDefined();
// No Remediate items in the inventory section
const remediateInInventory = inventorySection.items.filter(
i => i.workflow_type === 'Remediate'
);
expect(remediateInInventory.length).toBe(0);
// All Remediate items are in vendor sections
const vendorSections = sections.filter(s => s.type === 'vendor');
const allVendorItems = vendorSections.flatMap(s => s.items);
for (const item of remediateItems) {
expect(allVendorItems.some(i => i.id === item.id)).toBe(true);
}
}
),
{ numRuns: 100 }
);
});
});