// @vitest-environment jsdom import { buildUnifiedResultCsv } from '../../src/exports/unified-result-csv'; import { act } from 'react'; import { createRoot } from 'react-dom/client'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { AssessorApiClient } from '../../src/api/assessor-api-client'; import { deriveUnifiedResultFacets, queryUnifiedResultItems, type UnifiedResultProjection } from '../../src/domain/unified-result'; import { ResultRow, ResultsBrowser } from '../../src/features/results-browser/ResultsBrowser'; import { discloseResultDbProjectionWithTrace } from '../../src/domain/result-db-disclosure'; import { ResultDbIdentityGuard } from '../../src/domain/result-db-identity-guard'; import * as browserDownloads from '../../src/platform/browser/browser-download-adapter'; import { read, utils } from 'xlsx'; import { buildUnifiedResultExcel } from '../../src/exports/unified-result-excel'; import { buildUnifiedResultJsonl } from '../../src/exports/unified-result-jsonl'; import { browserDownloadAdapter } from '../../src/platform/browser/browser-download-adapter'; function reviewedFinding(id: string): UnifiedResultProjection { return { resultProjectionId: `scanner-finding:${id}`, sourceResultId: id, sourceKind: 'scanner-finding', sourceTool: 'purple-knight', sourceRuleId: 'SI000055', projectId: 'synthetic', assessmentId: 'slot', collectionRunId: 'collection', scanRunId: 'scan', title: `Synthetic delegation finding ${id}`, severityOriginal: 'high', severityNormalized: 'high', outcomeOriginal: 'fail', outcomeNormalized: 'fail', technicalExecutionState: 'completed', resultType: 'exposure_finding', isIoe: true, isDeviation: true, lifecycleState: 'unknown', mappingConfidence: 1, reviewStatus: 'open', reviewDisposition: 'accepted', reviewPriority: 'critical', reviewOwner: 'Synthetic Owner', reviewDueDate: '2026-10-01', importedAt: '2026-09-28T00:00:00Z', createdAt: '2026-09-28T00:00:00Z', frameworkReferences: [], categories: [] }; } afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); }); describe('Result DB export disclosure', () => { it('shows no review column or review filter and exports JSONL without review state or environment fingerprints', async () => { const items = [reviewedFinding('a'), { ...reviewedFinding('b'), sourceTool: 'pingcastle', sourceRuleId: 'A-NotEnoughDC', title: 'The number of DCs is too small to provide redundancy: 1 DC', description: 'Last backup 55 days ago; 15 accounts never expire.' }]; vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); vi.spyOn(AssessorApiClient.prototype, 'queryUnifiedResultFacets') .mockImplementation(async query => deriveUnifiedResultFacets(items, query)); vi.spyOn(AssessorApiClient.prototype, 'queryUnifiedResults') .mockImplementation(async query => queryUnifiedResultItems(items, query)); const download = vi.spyOn(browserDownloadAdapter, 'download').mockImplementation(() => undefined); const container = document.createElement('div'); const root = createRoot(container); try { await act(async () => root.render()); expect(container.querySelectorAll('.results-browser-title')).toHaveLength(2); const headings = [...container.querySelectorAll('.results-browser-head span')].map(item => item.textContent); expect(headings).not.toContain('Review'); expect(container.querySelector('[data-label="Review"]')).toBeNull(); const filterHeadings = [...container.querySelectorAll('section.results-filter-group h3')].map(item => item.textContent); expect(filterHeadings.some(heading => /review/i.test(heading ?? ''))).toBe(false); const exportButton = [...container.querySelectorAll('button')] .find(button => button.textContent === 'Export JSONL')!; await act(async () => exportButton.click()); expect(download).toHaveBeenCalledTimes(1); const text = download.mock.calls[0][0].text; const [manifest, ...results] = text.trimEnd().split('\n').map(line => JSON.parse(line)); expect(manifest.handling.classification).toBe('confidential-security-posture'); expect(results).toHaveLength(2); expect(results.map(line => line.result.sourceRuleId).sort()).toEqual(['A-NotEnoughDC', 'SI000055']); expect(text).not.toMatch(/reviewStatus|reviewDisposition|reviewPriority|reviewOwner|reviewDueDate|Synthetic Owner|accepted/); const dc = results.find(line => line.result.sourceRuleId === 'A-NotEnoughDC')!.result; expect(dc.title).toBe('The number of DCs is too small to provide redundancy: [count] DC'); expect(dc.description).toBe('Last backup [duration] ago; [count] accounts never expire.'); expect(manifest).not.toHaveProperty('generatedAt'); expect(download.mock.calls[0][0].fileName).toMatch(/^assessor-result-db-filtered-\d{4}-\d{2}-\d{2}\.jsonl$/); } finally { await act(async () => root.unmount()); } }); it('never reveals that a stored row had client review state, neither in the row detail nor in the JSONL export', async () => { // The backend serves materialized rows that carry their trace, so this is the real export input. const reviewed = discloseResultDbProjectionWithTrace({ ...reviewedFinding('first'), score: 27 }); const unreviewed = discloseResultDbProjectionWithTrace({ ...reviewedFinding('second'), reviewStatus: undefined, reviewDisposition: undefined, reviewPriority: undefined, reviewOwner: undefined, reviewDueDate: undefined, score: 96 }); const items = [reviewed, unreviewed]; expect(reviewed.disclosureTrace?.withheldFields).toEqual(unreviewed.disclosureTrace?.withheldFields); vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); vi.spyOn(AssessorApiClient.prototype, 'queryUnifiedResultFacets') .mockImplementation(async query => deriveUnifiedResultFacets(items, query)); vi.spyOn(AssessorApiClient.prototype, 'queryUnifiedResults') .mockImplementation(async query => queryUnifiedResultItems(items, query)); const download = vi.spyOn(browserDownloadAdapter, 'download').mockImplementation(() => undefined); const container = document.createElement('div'); const root = createRoot(container); try { await act(async () => root.render( undefined} workRecordIds={[]} onOpenInWork={() => undefined}/>)); expect(container.textContent).not.toMatch(/review|Synthetic Owner/i); await act(async () => root.render()); const exportButton = [...container.querySelectorAll('button')] .find(button => button.textContent === 'Export JSONL')!; await act(async () => exportButton.click()); const text = download.mock.calls[0][0].text; const results = text.trimEnd().split('\n').slice(1).map(line => JSON.parse(line).result); expect(results).toHaveLength(2); expect(results.every(result => result.disclosureTrace)).toBe(true); expect(text).not.toMatch(/review|Synthetic Owner/i); expect(results.some(result => 'score' in result)).toBe(false); } finally { await act(async () => root.unmount()); } }); it('shows the stored trace of a row and opens its original in Work without showing it in the Result DB', async () => { const stored = discloseResultDbProjectionWithTrace({ ...reviewedFinding('traced'), affectedAsset: 'SYNTHETIC-HOST-9', description: 'Host SRVDC01 answers for 3 hosts.' }, new ResultDbIdentityGuard(['SRVDC01'])); const opened = vi.fn(); vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const container = document.createElement('div'); const root = createRoot(container); try { await act(async () => root.render( undefined} workRecordIds={['work-record-1']} onOpenInWork={opened}/>)); const detail = container.querySelector('.results-disclosure-trace')?.textContent ?? ''; expect(detail).toContain('Checked content'); expect(detail).toContain('Redacted in Description: count × 1, identity-guard × 1'); expect(detail).toContain('Withheld fields:'); expect(container.textContent).not.toContain('SRVDC01'); const button = [...container.querySelectorAll('button')].find(item => item.textContent === 'Open original in Work')!; await act(async () => button.click()); expect(opened).toHaveBeenCalledWith(['work-record-1']); await act(async () => root.render( undefined} workRecordIds={[]} onOpenInWork={opened}/>)); expect(container.textContent).toContain('The original is not part of the loaded Work scope.'); } finally { await act(async () => root.unmount()); } }); }); it('exports every filtered result to Excel from the button, with the JSONL disclosure rules', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const items = Array.from({length: 1001}, (_, i) => reviewedFinding(String(i))); items.push({...reviewedFinding('passed'), isDeviation:false, outcomeNormalized:'pass'}); vi.spyOn(AssessorApiClient.prototype, 'queryUnifiedResultFacets').mockImplementation(async q => deriveUnifiedResultFacets(items, q)); const query = vi.spyOn(AssessorApiClient.prototype, 'queryUnifiedResults').mockImplementation(async q => queryUnifiedResultItems(items, q)); vi.spyOn(AssessorApiClient.prototype, 'getResultDbStatus').mockRejectedValue(new Error('Synthetic unavailable status')); const download = vi.spyOn(browserDownloads, 'downloadBrowserBlob').mockImplementation(() => {}); const container = document.createElement('div'); const root = createRoot(container); try { await act(async () => root.render()); expect(container.querySelectorAll('.results-browser-title')).toHaveLength(50); await act(async () => [...container.querySelectorAll('button')].find(b => b.textContent === 'Export Excel')!.click()); expect(download).toHaveBeenCalledOnce(); const [blob, name] = download.mock.calls[0]; expect(name).toMatch(/\.xlsx$/); const bytes = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload=()=>resolve(reader.result as ArrayBuffer); reader.onerror=()=>reject(reader.error); reader.readAsArrayBuffer(blob); }); const workbook = read(bytes, {type:'array'}); const rows = utils.sheet_to_json(workbook.Sheets.Results); expect(rows).toHaveLength(1001); expect(JSON.stringify(rows)).not.toMatch(/reviewOwner|Synthetic Owner|projectId|assessmentId/); expect(query.mock.calls.some(([q]) => q?.offset === 1000 && q.deviation === true && q.projectId === 'synthetic')).toBe(true); expect(workbook.SheetNames).toContain('Export info'); } finally { await act(async () => root.unmount()); } }); it('keeps full long values, nested evidence and formula-like text in literal Excel cells', () => { const input = {...reviewedFinding('long'), sourceTool:'pingcastle', sourceRuleId:'A-NotEnoughDC', description:'A'.repeat(70000), recommendation:'=HYPERLINK("https://microsoft.com", "Documentation")'}; const query = {projectId:'synthetic',search:'private search'}; const disclosed = JSON.parse(buildUnifiedResultJsonl(query,[input],'2026-10-01T12:00:00Z').trim().split('\n')[1]).result; const wb = read(buildUnifiedResultExcel(query,[input],'2026-10-01T12:00:00Z'), {type:'array'}); const row = utils.sheet_to_json>(wb.Sheets.Results)[0]; expect(row.description).toContain('Complete value in Long text'); const parts = utils.sheet_to_json<{Text:string}>(wb.Sheets['Long text']); expect(parts.map(p=>p.Text).join('')).toBe(disclosed.description); expect(row.recommendation).toBe(disclosed.recommendation); for (const [address, cell] of Object.entries(wb.Sheets.Results)) { if (!address.startsWith('!')) expect(cell.f).toBeUndefined(); } expect(JSON.stringify(utils.sheet_to_json(wb.Sheets['Export info']))).not.toContain('private search'); }); it('exports the complete filtered CSV from the Result DB button', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const items = Array.from({length:1001},(_,i)=>reviewedFinding(String(i))); items.push({...reviewedFinding('passed'),isDeviation:false,outcomeNormalized:'pass'}); vi.spyOn(AssessorApiClient.prototype,'queryUnifiedResultFacets').mockImplementation(async q=>deriveUnifiedResultFacets(items,q)); vi.spyOn(AssessorApiClient.prototype,'queryUnifiedResults').mockImplementation(async q=>queryUnifiedResultItems(items,q)); vi.spyOn(AssessorApiClient.prototype,'getResultDbStatus').mockRejectedValue(new Error('Synthetic status')); const download=vi.spyOn(browserDownloadAdapter,'download').mockImplementation(()=>{}); const container=document.createElement('div');const root=createRoot(container); try { await act(async()=>root.render()); await act(async()=>[...container.querySelectorAll('button')].find(b=>b.textContent==='Export CSV')!.click()); expect(download).toHaveBeenCalledOnce(); const artifact=download.mock.calls[0][0]; expect(artifact.fileName).toMatch(/\.csv$/);expect(artifact.mediaType).toBe('text/csv;charset=utf-8'); expect(artifact.text.startsWith('\uFEFF')).toBe(true); const wb=read(artifact.text,{type:'string',FS:';',raw:true}); const rows=utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]); expect(rows).toHaveLength(1001); expect(artifact.text).not.toMatch(/Synthetic Owner|reviewOwner|projectId|assessmentId/); expect(artifact.text).toContain('confidential-security-posture'); } finally {await act(async()=>root.unmount());} }); it('round-trips CSV separators, quotes, line breaks, Unicode and long disclosed values safely',()=>{ const input={...reviewedFinding('csv'),sourceTool:'pingcastle',sourceRuleId:'A-NotEnoughDC', description:'A'.repeat(70000)+'; "Übersicht"\nNext line',recommendation:'\n=HYPERLINK("https://microsoft.com", "Documentation")'}; const expected=JSON.parse(buildUnifiedResultJsonl({},[input],'2026-10-01').trim().split('\n')[1]).result; const csv=buildUnifiedResultCsv({},[input],'2026-10-01'); const wb=read(csv,{type:'string',FS:';',raw:true}); const row=utils.sheet_to_json>(wb.Sheets[wb.SheetNames[0]])[0]; expect(row.description).toBe(expected.description); expect(row.recommendation).toBe("'"+expected.recommendation); expect(row.categories).toBe(JSON.stringify(expected.categories)); });