119 lines
8.2 KiB
TypeScript
119 lines
8.2 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { act, createElement } from 'react';
|
|
import { createRoot } from 'react-dom/client';
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
import type { ImportedDataset } from '../../src/domain/dataset';
|
|
import type { FindingRecord } from '../../src/domain/finding';
|
|
import { appendRiskAcceptanceReview } from '../../src/domain/risk-acceptance';
|
|
import { appendScopedRiskAcceptance, type ScopedRiskAcceptanceTarget } from '../../src/domain/scoped-risk-acceptance';
|
|
import { defaultReview, type Review } from '../../src/domain/review';
|
|
import { RiskRegister, riskRegisterEntries } from '../../src/features/dashboard/RiskRegister';
|
|
import { riskCommand } from '../domain/risk-acceptance-fixture';
|
|
|
|
const today = new Date().toISOString().slice(0, 10);
|
|
|
|
function record(id: string, occurrenceId: string, title: string, source: string): FindingRecord {
|
|
return { id, findingOccurrenceId: occurrenceId, index: 0, title,
|
|
fields: [{ index: 0, label: 'Source: Scan', value: source }], filled: [], meta: [] };
|
|
}
|
|
|
|
function dataset(records: FindingRecord[], duplicate: FindingRecord): ImportedDataset {
|
|
const sheet = (id: string, sheetRecords: FindingRecord[]) => ({ id, name: id, rows: [], raw: '',
|
|
analysis: { headerIndex: 0, headers: [], records: sheetRecords, columns: [], signals: [], title: id } });
|
|
return { fileName: 'Synthetic client', fileSize: 0, lastModified: 0, format: 'synthetic', rawText: '',
|
|
profileId: 'generic', scanTool: 'Synthetic', sheets: [sheet('all', records), sheet('source', [duplicate])] };
|
|
}
|
|
|
|
describe('Client Info risk register', () => {
|
|
it('shows one register entry for one grouped device snapshot', () => {
|
|
const target: ScopedRiskAcceptanceTarget = {
|
|
kind: 'device-snapshot',
|
|
device: { key: 'device-id:dev-a', displayName: 'Device A', ip: '192.0.2.10', site: 'Synthetic site', identityMethod: 'device-id', identityConfidence: 'high' },
|
|
members: [
|
|
{ findingDefinitionId: 'definition-1', cve: 'CVE-2026-1001', capturedOccurrences: [{ occurrenceId: 'occurrence-1', scanRunId: 'run-1', importFileId: 'file-1', sourcePosition: { rowIndex: 1 } }] },
|
|
{ findingDefinitionId: 'definition-2', cve: 'CVE-2026-1002', capturedOccurrences: [{ occurrenceId: 'occurrence-2', scanRunId: 'run-1', importFileId: 'file-1', sourcePosition: { rowIndex: 2 } }] }
|
|
],
|
|
capturedScanRunIds: ['run-1'], capturedAt: `${today}T08:00:00.000Z`
|
|
};
|
|
const acceptance = appendScopedRiskAcceptance(undefined, 'project-1', 'snapshot-1', target, riskCommand('accept', today), `${today}T08:00:00.000Z`);
|
|
const entries = riskRegisterEntries(dataset([], record('duplicate', 'none', 'Unused', 'Defender')), {}, today, [acceptance]);
|
|
|
|
expect(entries).toHaveLength(1);
|
|
expect(entries[0]).toMatchObject({ id: 'snapshot-1', title: 'All current findings on Device A', state: 'Accepted', coveredCves: ['CVE-2026-1001', 'CVE-2026-1002'] });
|
|
});
|
|
|
|
it('deduplicates projected occurrences and keeps their current governance state', () => {
|
|
const first = record('record-1', 'occurrence-1', 'Synthetic identity risk', 'Purple Knight');
|
|
const duplicate = record('record-1-copy', 'occurrence-1', 'Synthetic identity risk', 'Purple Knight');
|
|
const command = riskCommand('accept', today);
|
|
command.details.scope = 'Synthetic directory scope';
|
|
const accepted = appendRiskAcceptanceReview(undefined, first.findingOccurrenceId!, command, `${today}T08:00:00.000Z`);
|
|
const review = { ...defaultReview(), riskAcceptance: accepted.riskAcceptance } satisfies Review;
|
|
const entries = riskRegisterEntries(dataset([first], duplicate), { [first.id]: review, [duplicate.id]: review }, today);
|
|
|
|
expect(entries).toHaveLength(1);
|
|
expect(entries[0]).toMatchObject({ title: 'Synthetic identity risk', source: 'Purple Knight', state: 'Accepted' });
|
|
expect(entries[0].latest.details.scope).toBe('Synthetic directory scope');
|
|
});
|
|
|
|
it('selects a risk on the left and replaces the complete detail view on the right', () => {
|
|
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
const first = record('record-1', 'occurrence-1', 'Synthetic identity risk', 'Purple Knight');
|
|
const second = record('record-2', 'occurrence-2', 'Synthetic policy risk', 'GPO Analysis');
|
|
const duplicate = record('record-1-copy', 'occurrence-1', 'Synthetic identity risk', 'Purple Knight');
|
|
const firstCommand = riskCommand('accept', today);
|
|
firstCommand.details.scope = 'First synthetic scope';
|
|
const secondCommand = riskCommand('accept', today);
|
|
secondCommand.details.scope = 'Second synthetic scope';
|
|
const firstAcceptance = appendRiskAcceptanceReview(undefined, 'occurrence-1', firstCommand, `${today}T08:00:00.000Z`);
|
|
const secondAcceptance = appendRiskAcceptanceReview(undefined, 'occurrence-2', secondCommand, `${today}T09:00:00.000Z`);
|
|
const reviews = {
|
|
[first.id]: { ...defaultReview(), riskAcceptance: firstAcceptance.riskAcceptance },
|
|
[duplicate.id]: { ...defaultReview(), riskAcceptance: firstAcceptance.riskAcceptance },
|
|
[second.id]: { ...defaultReview(), riskAcceptance: secondAcceptance.riskAcceptance }
|
|
};
|
|
const container = document.createElement('div');
|
|
const root = createRoot(container);
|
|
|
|
const openFinding = vi.fn();
|
|
act(() => root.render(createElement(RiskRegister, { dataset: dataset([first, second], duplicate), reviews, onOpenFinding: openFinding })));
|
|
expect(container.querySelectorAll('.client-risk-register-list button')).toHaveLength(2);
|
|
expect(container.querySelector('.client-risk-register-detail > header')?.textContent).toContain('Risk Information');
|
|
expect(container.querySelector('.client-risk-register-detail h2')?.textContent).toBe('Synthetic policy risk');
|
|
expect(container.querySelector('.client-risk-register-detail')?.textContent).toContain('Second synthetic scope');
|
|
act(() => container.querySelector<HTMLButtonElement>('.risk-register-actions .primary')!.click());
|
|
expect(openFinding).toHaveBeenCalledWith(second.id);
|
|
const groups = [...container.querySelectorAll<HTMLDetailsElement>('.risk-register-detail-group')];
|
|
expect(groups).toHaveLength(4);
|
|
expect(groups.every(group => !group.open)).toBe(true);
|
|
act(() => groups[0].querySelector('summary')!.click());
|
|
expect(groups[0].open).toBe(true);
|
|
expect(groups[0].querySelector('dl')?.textContent).toContain('Second synthetic scope');
|
|
act(() => groups[0].querySelector('summary')!.click());
|
|
expect(groups[0].open).toBe(false);
|
|
act(() => groups[1].querySelector('summary')!.click());
|
|
expect(groups[1].open).toBe(true);
|
|
|
|
const firstButton = [...container.querySelectorAll<HTMLButtonElement>('.client-risk-register-list button')]
|
|
.find(button => button.textContent?.includes('Synthetic identity risk'))!;
|
|
act(() => firstButton.click());
|
|
expect(firstButton.getAttribute('aria-pressed')).toBe('true');
|
|
expect(container.querySelector('.client-risk-register-detail h2')?.textContent).toBe('Synthetic identity risk');
|
|
expect(container.querySelector('.client-risk-register-detail')?.textContent).toContain('First synthetic scope');
|
|
act(() => firstButton.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true })));
|
|
expect(container.querySelector('.client-risk-register-detail h2')?.textContent).toBe('Synthetic policy risk');
|
|
const secondButton = [...container.querySelectorAll<HTMLButtonElement>('.client-risk-register-list button')]
|
|
.find(button => button.textContent?.includes('Synthetic policy risk'))!;
|
|
act(() => secondButton.dispatchEvent(new KeyboardEvent('keydown', { key: 'End', bubbles: true })));
|
|
expect(container.querySelector('.client-risk-register-detail h2')?.textContent).toBe('Synthetic identity risk');
|
|
expect([...container.querySelectorAll<HTMLDetailsElement>('.risk-register-detail-group')].every(group => !group.open)).toBe(true);
|
|
const history = container.querySelector<HTMLDetailsElement>('.risk-register-history')!;
|
|
expect(history.open).toBe(false);
|
|
act(() => history.querySelector('summary')!.click());
|
|
expect(history.open).toBe(true);
|
|
expect(container.querySelector('.risk-register-history')?.textContent).toContain('Revision 1');
|
|
act(() => root.unmount());
|
|
});
|
|
});
|