// @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'] }); // After a reimport replaced the captured scan run, the decision stays listed and needs review. const replaced = riskRegisterEntries(dataset([], record('duplicate', 'none', 'Unused', 'Defender')), {}, today, [{ ...acceptance, capturedScopeReplaced: true }]); expect(replaced).toHaveLength(1); expect(replaced[0]).toMatchObject({ id: 'snapshot-1', state: 'Review required', revisions: acceptance.revisions }); }); 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('.risk-register-actions .primary')!.click()); expect(openFinding).toHaveBeenCalledWith(second.id); const groups = [...container.querySelectorAll('.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('.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('.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('.risk-register-detail-group')].every(group => !group.open)).toBe(true); const history = container.querySelector('.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()); }); });