181 lines
7.7 KiB
JavaScript
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 }
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|