// @vitest-environment jsdom import { act } from 'react'; import { createRoot } from 'react-dom/client'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { useApp } from '../../src/app/AppProvider'; import { useFilteredRecords } from '../../src/features/search-filter/use-filtered-records'; import { DatasetViewer } from '../../src/features/dataset-viewer/DatasetViewer'; import { FloatingResultsWindow } from '../../src/features/linked-results/FloatingResultsWindow'; import { makeSheet } from '../../src/importers/sheet-factory'; import { resultSheetRowToneFor } from '../../src/features/linked-results/result-message'; vi.mock('../../src/app/AppProvider', () => { const useApp = vi.fn(); return { useApp, useOptionalApp: useApp }; }); vi.mock('../../src/features/search-filter/use-filtered-records', () => ({ useFilteredRecords: vi.fn() })); afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); Reflect.deleteProperty(HTMLElement.prototype, 'scrollIntoView'); }); function smbFixture() { const findings = makeSheet('Synthetic Work', [ ['Title', 'Status', 'Source Rule ID', 'Verwandte Daten', 'Source: Scan'], ['SMB v1 activated on 1 DC', 'Matched', 'S-SMB-v1', 'synthetic-file::rule-details-S-SMB-v1,synthetic-file::domaincontrollersdetail', 'PingCastle · synthetic'] ], '', 'synthetic.html', { toolId: 'pingcastle', isProjectSheet: true }); const finding = findings.analysis.records[0]; finding.importFileId = 'synthetic-file'; const detail = makeSheet('PingCastle · Details', [['Domain controller'], ['DUMMY-DC']], '', 'synthetic.html', { id: 'pingcastle-section-synthetic-file::rule-details-S-SMB-v1' }); const inventory = makeSheet('PingCastle · Domain controllers', [['Domain controller', 'SMB v1'], ['DUMMY-OTHER', 'NO']], '', 'synthetic.html', { id: 'pingcastle-section-synthetic-file::domaincontrollersdetail' }); return { findings, finding, detail, inventory }; } describe.each(['dark', 'light'])('Results affected evidence (%s)', theme => { it('opens SMB Results through Work, marks only the exact affected list and retains proof when Work filters hide the finding', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); vi.stubGlobal('ResizeObserver', class { observe() {} disconnect() {} }); Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', { value: vi.fn(), configurable: true }); document.documentElement.dataset.theme = theme; const { findings, finding, detail, inventory } = smbFixture(); const before = JSON.stringify([finding, detail, inventory]); vi.mocked(useApp).mockReturnValue({ dataset: { sheets: [findings, detail, inventory], rawText: '', profileId: 'pingcastle' }, activeSheet: findings, view: 'records', search: '', titleColumn: 'Title', selectedRecordId: finding.id, reviews: {}, facetFilters: {}, statusFilter: 'all', actionOnly: false, setView: vi.fn(), setSelectedRecord: vi.fn(), updateReview: vi.fn(), resetFilters: vi.fn() } as unknown as ReturnType); vi.mocked(useFilteredRecords).mockReturnValue([finding]); const host = document.createElement('div'); document.body.append(host); const root = createRoot(host); try { await act(async () => root.render()); await act(async () => [...host.querySelectorAll('button')].find(button => button.textContent === 'Results')!.click()); const red = host.querySelectorAll('.rich-text-table-row--violation'); expect(red).toHaveLength(1); expect(red[0].textContent).toBe('DUMMY-DC'); const other = [...host.querySelectorAll('tbody tr')].find(row => row.textContent?.includes('DUMMY-OTHER'))!; expect(other.classList.contains('rich-text-table-row--violation')).toBe(false); vi.mocked(useFilteredRecords).mockReturnValue([]); await act(async () => root.render()); expect(host.querySelector('.rich-text-table-row--violation')!.textContent).toBe('DUMMY-DC'); expect(JSON.stringify([finding, detail, inventory])).toBe(before); } finally { await act(async () => root.unmount()); host.remove(); } }); it('colors source-marked Maester inline rows and leaves missing/conflicting status or misleading names neutral', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); vi.stubGlobal('ResizeObserver', class { observe() {} disconnect() {} }); const host = document.createElement('div'); document.body.append(host); const root = createRoot(host); const value = '| Object | Status |\n| --- | --- |\n| DUMMY-BAD | ❌ Failed |\n| DUMMY-GOOD | ✅ Passed |\n| Fail | Unknown |\n| DUMMY-UNKNOWN | Investigate |\n| ✅ DUMMY-CONFLICT | ❌ Failed |'; try { await act(async () => root.render()); expect(host.querySelectorAll('.rich-text-table-row--violation')).toHaveLength(1); expect(host.querySelectorAll('.rich-text-table-row--match')).toHaveLength(1); expect(host.querySelectorAll('tbody tr:not([class])')).toHaveLength(3); } finally { await act(async () => root.unmount()); host.remove(); } }); }); it('fails closed on unknown rules, foreign imports, changed layouts, missing scope and non-fail outcomes', () => { const { finding, detail, inventory } = smbFixture(); const row = detail.analysis.records[0]; const tone = (f = finding, s = detail, outcome: 'fail' | 'pass' | 'warning' = 'fail') => resultSheetRowToneFor('pingcastle', row, outcome, { finding: f, sheet: s }); expect(tone()).toBe('violation'); expect(tone(finding, detail, 'pass')).toBeUndefined(); expect(tone(finding, detail, 'warning')).toBeUndefined(); expect(tone({ ...finding, importFileId: 'foreign' })).toBeUndefined(); expect(tone({ ...finding, importFileId: undefined })).toBeUndefined(); expect(tone({ ...finding, fields: finding.fields.map(field => field.label === 'Source Rule ID' ? { ...field, value: 'S-Unknown' } : field) })).toBeUndefined(); expect(tone(finding, { ...detail, analysis: { ...detail.analysis, headers: ['Unknown'] } })).toBeUndefined(); expect(tone(finding, inventory)).toBeUndefined(); }); it('marks only native-linked, non-ignored Purple Knight affected rows and retains neutral ambiguous/foreign evidence', () => { const findings = makeSheet('Synthetic', [['Name', 'Status', 'Source Rule ID', 'Result', 'Source: File'], ['Synthetic spooler', 'IOE Found', 'SI000077', 'SI000077', 'synthetic-pk.html']], '', 'synthetic-pk.html'); const finding = findings.analysis.records[0]; const evidence = makeSheet('Purple Knight · SI000077', [['HostName', 'Ignored'], ['DUMMY-DC', 'False'], ['DUMMY-IGNORED', 'True']], '', 'synthetic-pk.html', { sourceTableIndex: 3 }); const tone = (sheet = evidence, row = evidence.analysis.records[0], outcome: 'pass' | 'fail' = 'fail') => resultSheetRowToneFor('purple-knight', row, outcome, { finding, sheet }); expect(tone()).toBe('violation'); expect(tone(evidence, evidence.analysis.records[1])).toBeUndefined(); expect(tone(evidence, evidence.analysis.records[0], 'pass')).toBeUndefined(); expect(tone({ ...evidence, sourceFile: 'foreign.html' })).toBeUndefined(); expect(tone({ ...evidence, name: 'Similar spooler title', sourceSheet: 'Other' })).toBeUndefined(); expect(tone({ ...evidence, analysis: { ...evidence.analysis, headers: ['HostName', 'Unknown'] } })).toBeUndefined(); });