115 lines
10 KiB
TypeScript
115 lines
10 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { act, useState } from 'react';
|
|
import { createRoot } from 'react-dom/client';
|
|
import { expect, it, vi } from 'vitest';
|
|
import { useApp } from '../../src/app/AppProvider';
|
|
import { SidebarSecurityDomains } from '../../src/features/sidebar/SidebarSecurityDomains';
|
|
import { comparePublishedClusters, contextClusterNumber } from '../../src/features/sidebar/context-cluster-display';
|
|
import { contextClusterSeverities } from '../../src/assessment-context/context-cluster-severity';
|
|
import { makeSheet } from '../../src/importers/sheet-factory';
|
|
import { genericProfile } from '../../src/profiles/generic/generic-profile';
|
|
|
|
vi.mock('../../src/app/AppProvider', () => ({ useApp: vi.fn() }));
|
|
vi.mock('../../src/app/engine-preferences', () => ({ useEnginePreferences: () => ({ context: true, remediation: true, report: false }) }));
|
|
const fixtures = vi.hoisted(() => ({
|
|
packages: [ { id: 'p1', code: 'WP-01', clusterLabel: 'AD Persistence Indicators', clusterId: 'ad-persistence-indicators' }, { id: 'p2', code: 'WP-02', clusterLabel: 'AD Persistence Indicators', clusterId: 'ad-persistence-indicators' },
|
|
{ id: 'p3', code: 'WP-01', clusterLabel: 'AD Privileged Account Exposure', clusterId: 'ad-privileged-account-exposure' },
|
|
{ id: 'p0', code: 'WP-01', clusterLabel: 'Credential Relay Exposure', clusterId: 'credential-relay-exposure' } ],
|
|
clusters: [ { id: 'ad-privileged-account-exposure', severity: 'critical', hasEvidence: true },
|
|
{ id: 'ad-persistence-indicators', severity: 'high', hasEvidence: true } ],
|
|
facets: [ { id: 'derived:security-domain', values: [] }, { id: 'context:cluster', values: [ { value: 'ad-persistence-indicators', label: 'AD Persistence Indicators', count: 8 },
|
|
{ value: 'ad-privileged-account-exposure', label: 'AD Privileged Account Exposure', count: 9 } ] },
|
|
{ id: 'remediation:work-package', values: [ { value: 'p2', label: 'AD Persistence Indicators → WP-02', count: 2 },
|
|
{ value: 'p1', label: 'AD Persistence Indicators → WP-01', count: 6 },
|
|
{ value: 'p3', label: 'AD Privileged Account Exposure → WP-01', count: 9 },
|
|
{ value: 'p0', label: 'Credential Relay Exposure → WP-01', count: 1 },
|
|
{ value: '__unassigned__', label: 'Unassigned', count: 3 } ] } ]
|
|
}));
|
|
vi.mock('../../src/features/context-layer/use-context-cluster-index', () => ({ useContextClusterIndex: () => ({ publication: { clusters: fixtures.clusters } }) }));
|
|
vi.mock('../../src/remediation/use-work-packages', () => ({ useWorkPackages: () => ({ status: 'ready', index: { packages: fixtures.packages } }) }));
|
|
vi.mock('../../src/features/search-filter/work-facets', () => ({ selectWorkFacets: () => fixtures.facets }));
|
|
|
|
it('numbers the complete published heatmap in order and renumbers when its scope order changes', () => {
|
|
const publication = contextClusterSeverities.map(cluster => ({ id: cluster.clusterId })).reverse();
|
|
const numbers = publication.map(cluster => contextClusterNumber(cluster.id, publication));
|
|
expect(numbers.every(Boolean)).toBe(true);
|
|
expect(new Set(numbers).size).toBe(numbers.length);
|
|
expect(numbers).toEqual(publication.map((_, index) => String(index + 1).padStart(2, '0')));
|
|
expect(contextClusterNumber(publication[0].id, publication)).toBe('01');
|
|
expect(contextClusterNumber(publication[0].id, [...publication].reverse())).toBe('22');
|
|
expect(contextClusterNumber('ad-persistence-indicators')).toBeUndefined();
|
|
});
|
|
|
|
it.each(['light', 'dark'])('orders both actual filters by displayed Context severity, retaining numeric AP order and stable identifiers (%s)', async theme => {
|
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); document.documentElement.dataset.theme = theme;
|
|
const saved = structuredClone(fixtures);
|
|
const medium = 'ad-reconnaissance-exposure', neutral = 'cloud-privileged-access-exposure';
|
|
fixtures.clusters.push({id:'credential-relay-exposure',severity:'high',hasEvidence:true},
|
|
{id:medium,severity:'medium',hasEvidence:true},{id:neutral,severity:'critical',hasEvidence:false});
|
|
// Published heatmap order is authoritative, including equal-severity ties.
|
|
[fixtures.clusters[1], fixtures.clusters[2]] = [fixtures.clusters[2], fixtures.clusters[1]];
|
|
fixtures.packages.push({id:'p4',code:'WP-01',clusterId:medium,clusterLabel:'AD Reconnaissance Exposure'},
|
|
{id:'p5',code:'WP-01',clusterId:neutral,clusterLabel:'Cloud Privileged Access Exposure'});
|
|
fixtures.facets[1].values.push({value:'credential-relay-exposure',label:'Credential Relay Exposure',count:1},
|
|
{value:medium,label:'AD Reconnaissance Exposure',count:4},{value:neutral,label:'Cloud Privileged Access Exposure',count:1});
|
|
fixtures.facets[2].values.push({value:'p4',label:'AD Reconnaissance Exposure → WP-01',count:4},
|
|
{value:'p5',label:'Cloud Privileged Access Exposure → WP-01',count:1});
|
|
const host=document.createElement('div');document.body.append(host);const root=createRoot(host);
|
|
const select=vi.fn();const sheet=makeSheet('Synthetic',[['Name'],['Original']],'','synthetic.csv');
|
|
vi.mocked(useApp).mockReturnValue({dataset:null,activeProfile:genericProfile,reviews:{},statusFilter:'all',facetFilters:{},search:'',actionOnly:false,setFacetFilter:select} as unknown as ReturnType<typeof useApp>);
|
|
try {
|
|
await act(async()=>root.render(<SidebarSecurityDomains sheet={sheet}/>));
|
|
const expected=['01 · AD Privileged Account Exposure','02 · Credential Relay Exposure','03 · AD Persistence Indicators','04 · AD Reconnaissance Exposure','05 · Cloud Privileged Access Exposure'];
|
|
expect([...host.querySelectorAll('[data-filter-id="context:cluster"] .work-filter-value > span')].map(node=>node.textContent)).toEqual(expected);
|
|
expect([...host.querySelectorAll('.work-package-cluster-head > span:first-child')].map(node=>node.textContent)).toEqual(expected);
|
|
const rows=[...host.querySelectorAll<HTMLButtonElement>('[data-filter-id="remediation:work-package"] .work-filter-value')];
|
|
expect(rows.map(row=>row.getAttribute('aria-label'))).toEqual([
|
|
expected[0]+' → WP-01: 9',expected[1]+' → WP-01: 1',expected[2]+' → WP-01: 6',expected[2]+' → WP-02: 2',expected[3]+' → WP-01: 4',expected[4]+' → WP-01: 1','Unassigned: 3']);
|
|
await act(async()=>rows[3].click());expect(select).toHaveBeenLastCalledWith('remediation:work-package',{values:['p2']});
|
|
expect([...host.querySelectorAll('.work-package-cluster-colour')].map(node=>node.className)).toEqual(['critical','high','high','medium','undetermined'].map(value=>'work-package-cluster-colour severity-'+value));
|
|
expect(comparePublishedClusters('credential-relay-exposure','ad-persistence-indicators',fixtures.clusters as never)).toBeLessThan(0);
|
|
}finally{await act(async()=>root.unmount());host.remove();Object.assign(fixtures,saved);vi.unstubAllGlobals();delete document.documentElement.dataset.theme;}
|
|
});
|
|
|
|
it.each(['light', 'dark'])('shows one published cluster colour and matching cluster numbers across package selection and floating panels (%s)', async theme => {
|
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); document.documentElement.dataset.theme = theme;
|
|
const sheet = makeSheet('Synthetic', [['Name', 'Severity'], ['Declaration', 'Critical'], ['Observation', 'Low']], '', 'synthetic.csv');
|
|
const original = JSON.stringify(sheet.analysis.records);
|
|
const host = document.createElement('div'); document.body.append(host); const root = createRoot(host);
|
|
function Harness() {
|
|
const [facetFilters, setFilters] = useState<ReturnType<typeof useApp>['facetFilters']>({});
|
|
vi.mocked(useApp).mockReturnValue({ dataset: null, activeProfile: genericProfile, reviews: {}, statusFilter: 'all',
|
|
facetFilters, search: '', actionOnly: false, setFacetFilter: (id, value) => setFilters(current => {
|
|
const next = { ...current }; if (value) next[id] = value; else delete next[id]; return next;
|
|
}) } as ReturnType<typeof useApp>);
|
|
return <SidebarSecurityDomains sheet={sheet}/>;
|
|
}
|
|
try {
|
|
await act(async () => root.render(<Harness/>));
|
|
const packageRows = () => Array.from(document.querySelectorAll<HTMLButtonElement>('[data-filter-id="remediation:work-package"] .work-filter-value'));
|
|
expect(packageRows().map(row => row.querySelector('span')?.textContent)).toEqual([
|
|
'WP-01', 'WP-01', 'WP-02', 'WP-01', 'Unassigned']);
|
|
expect(Array.from(document.querySelectorAll('.work-package-cluster-colour')).map(marker => marker.className)).toEqual([
|
|
'work-package-cluster-colour severity-critical', 'work-package-cluster-colour severity-high', 'work-package-cluster-colour severity-undetermined']);
|
|
expect(document.querySelectorAll('.work-package-heatmap-segment')).toHaveLength(0);
|
|
expect(document.querySelector('[data-filter-id="context:cluster"] .work-filter-value span')?.textContent).toBe('01 · AD Privileged Account Exposure');
|
|
expect(packageRows()[1].querySelector('strong')?.textContent).toBe('6');
|
|
await act(async () => packageRows()[0].click());
|
|
expect(packageRows()[0].getAttribute('aria-pressed')).toBe('true');
|
|
expect(packageRows()[0].querySelector('span')?.textContent).toBe('WP-01');
|
|
await act(async () => document.querySelector<HTMLButtonElement>('[aria-label="Detach filters"]')!.click());
|
|
expect(document.querySelector('.work-filter-panel.is-floating')).not.toBeNull();
|
|
expect(packageRows()[0].getAttribute('aria-pressed')).toBe('true');
|
|
expect(packageRows().every(row => row.querySelectorAll('.work-package-cluster-colour').length === 0)).toBe(true);
|
|
expect(Array.from(document.querySelectorAll('.work-package-cluster-head > span:first-child')).map(node => node.textContent)).toEqual(['01 · AD Privileged Account Exposure', '02 · AD Persistence Indicators', 'Credential Relay Exposure']);
|
|
expect(packageRows()[0].getAttribute('aria-label')).toBe('01 · AD Privileged Account Exposure → WP-01: 9');
|
|
await act(async () => packageRows()[0].click());
|
|
expect(packageRows().every(row => row.getAttribute('aria-pressed') === 'false')).toBe(true);
|
|
await act(async () => packageRows()[4].click());
|
|
expect(packageRows()[4].getAttribute('aria-pressed')).toBe('true');
|
|
await act(async () => document.querySelector<HTMLButtonElement>('[aria-label="Dock filters"]')!.click());
|
|
expect(packageRows()[4].getAttribute('aria-pressed')).toBe('true');
|
|
expect(JSON.stringify(sheet.analysis.records)).toBe(original);
|
|
} finally { await act(async () => root.unmount()); host.remove(); vi.unstubAllGlobals(); delete document.documentElement.dataset.theme; }
|
|
});
|