128 lines
8.8 KiB
TypeScript
128 lines
8.8 KiB
TypeScript
// @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 { AssessorApiClient } from '../../src/api/assessor-api-client';
|
|
import type { UnifiedResultProjection } from '../../src/domain/unified-result';
|
|
import { DashboardPanel } from '../../src/features/dashboard/DashboardPanel';
|
|
import { buildClientReportProjection } from '../../src/features/dashboard/client-report-projection';
|
|
import { makeSheet } from '../../src/importers/sheet-factory';
|
|
import { browserDownloadAdapter } from '../../src/platform/browser/browser-download-adapter';
|
|
import { genericProfile } from '../../src/profiles/generic/generic-profile';
|
|
import { validAiOverviewResult } from '../fixtures/ai-overview';
|
|
|
|
vi.mock('../../src/app/AppProvider', () => ({ useApp: vi.fn() }));
|
|
vi.mock('../../src/features/search-filter/WorkTimeslotControl', () => ({ WorkTimeslotControl: () => <span>Dummy timeslot</span> }));
|
|
afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); });
|
|
|
|
describe('Client Info report buttons', () => {
|
|
it('exports separate HTML reports from every result in the selected Dummy timeslot, ignoring the Source filter', async () => {
|
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
|
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => { callback(0); return 1; });
|
|
const sheet = makeSheet('Dummy', [
|
|
['Name', 'Status', 'Severity', 'Source: Scan'],
|
|
['Visible filtered row', 'Failed', 'High', 'Scanner A'],
|
|
['Other source row', 'Failed', 'Medium', 'Scanner B']
|
|
], '', 'dummy.csv', { isProjectSheet: true });
|
|
const dataset = { fileName: 'Dummy', sheets: [sheet], scanTool: 'Generic' } as never;
|
|
const app = {
|
|
dataset, activeSheet: sheet, activeProfile: genericProfile, reviews: {},
|
|
targetProjectId: 'dummy-project', activeScanRunIds: ['dummy-run'], activeAssessmentIds: ['dummy-assessment'],
|
|
facetFilters: { 'provenance:source-scan': { values: ['Scanner A'] } },
|
|
setStatusFilter: vi.fn(), setFacetFilter: vi.fn(), setPathFindingIds: vi.fn(),
|
|
resetFilters: vi.fn(), setView: vi.fn(), setActionOnly: vi.fn(), setActiveSheet: vi.fn()
|
|
} as unknown as ReturnType<typeof useApp>;
|
|
vi.mocked(useApp).mockReturnValue(app);
|
|
const project = { id: 'dummy-project', name: 'Dummy Customer', createdAt: '', updatedAt: '', attributes: {} };
|
|
const scopes: Array<{ projectId: string; scanRunIds?: string[]; assessmentIds?: string[] }> = [];
|
|
const result = {
|
|
resultProjectionId: 'dummy-result', sourceKind: 'scanner-finding', sourceResultId: 'dummy-source',
|
|
projectId: 'dummy-project', assessmentId: 'dummy-assessment', collectionRunId: 'dummy-run', scanRunId: 'dummy-run',
|
|
importArtifactId: 'dummy-artifact-secret', sourceTool: 'Scanner B', sourceRuleId: 'DUMMY-RULE-123',
|
|
title: 'Dummy technical deviation', description: 'A synthetic control failed.', recommendation: 'Apply the fix.',
|
|
severityNormalized: 'high', outcomeNormalized: 'fail', isDeviation: true,
|
|
categories: [], frameworkReferences: [], policyActualValue: 0, policyBaselineValue: 1
|
|
} as unknown as UnifiedResultProjection;
|
|
const osRow = (name: string, count: number, row: number) => [['operating_system.name', name], ['operating_system.count', count]].map(([property, raw], column) => ({
|
|
version: 1, id: `dummy-os:${row}:${column}`, scanRunId: 'dummy-run', importFileId: 'dummy-file', tool: { id: 'pingcastle', name: 'PingCastle' },
|
|
subject: { type: 'computer-information.operating-systems', identity: name }, property, label: column ? 'Nb OS' : 'Operating system',
|
|
originalValue: raw, normalizedValue: raw, valueType: 'string', state: 'observed', sectionKey: 'sectionComputerInformation',
|
|
source: { rowIndex: row, columnIndex: column }, attributes: { tableName: 'Operating systems', sourceTableIndex: 0 }, mappingVersion: '1'
|
|
}));
|
|
const environment = {
|
|
version: 1, projectId: 'dummy-project', scanRunIds: ['dummy-run'], generatedAt: '2026-09-23T00:00:00Z',
|
|
coverage: { observed: 4, explicitlyAbsent: 0, empty: 0, restricted: 0, incomplete: 0 },
|
|
observations: [...osRow('Windows Server 2022', 7, 1), ...osRow('Windows 11 24H2', 1234, 2)],
|
|
sections: [
|
|
{ sectionKey: 'sectionUserInformation', tables: [{ name: 'Account analysis', headers: ['Nb User Accounts', 'Nb Enabled', 'Nb Disabled'], rows: [['321', '300', '21']] }] },
|
|
{ sectionKey: 'sectionComputerInformation', tables: [{ name: 'Computer analysis', headers: ['Nb Computer Accounts'], rows: [['1241']] }] }
|
|
]
|
|
} as never;
|
|
vi.spyOn(AssessorApiClient.prototype, 'getReportProjection').mockImplementation(async (projectId, scanRunIds, assessmentIds) => {
|
|
scopes.push({ projectId, scanRunIds, assessmentIds });
|
|
return buildClientReportProjection({ project, dataset, results: [result], environment,
|
|
assessmentIds, generatedAt: '2026-09-23T10:00:00Z' });
|
|
});
|
|
vi.spyOn(AssessorApiClient.prototype, 'getEnvironmentSnapshot').mockResolvedValue({
|
|
version: 1, projectId: 'dummy-project', scanRunIds: ['dummy-run'], generatedAt: '2026-09-23T00:00:00Z',
|
|
coverage: { observed: 0, explicitlyAbsent: 0, empty: 0, restricted: 0, incomplete: 0 },
|
|
observations: [], sections: []
|
|
});
|
|
const aiResult = validAiOverviewResult();
|
|
aiResult.managementBrief!.priorities[0].title = 'Dummy Managementmaßnahme';
|
|
const aiScopes: Array<{ assessmentIds: string[]; scanRunIds: string[] }> = [];
|
|
vi.spyOn(AssessorApiClient.prototype, 'getAiOverview').mockImplementation(async (projectId, assessmentIds = [], scanRunIds = []) => {
|
|
aiScopes.push({ assessmentIds, scanRunIds });
|
|
return {
|
|
version: 1, projectId, scope: { assessmentIds, scanRunIds }, generationEnabled: true, state: 'succeeded', stale: false,
|
|
run: { status: 'succeeded', completedAt: '2026-09-23T09:00:00Z', result: aiResult }
|
|
} as never;
|
|
});
|
|
vi.spyOn(AssessorApiClient.prototype, 'listProjectGpoBackupStatus').mockResolvedValue({ records: [] } as never);
|
|
const downloads = vi.spyOn(browserDownloadAdapter, 'download').mockImplementation(() => undefined);
|
|
const host = document.createElement('div'); document.body.append(host);
|
|
const root = createRoot(host);
|
|
await act(async () => { root.render(<DashboardPanel onOpenWork={vi.fn()} />); });
|
|
const click = async (label: string) => {
|
|
const button = [...host.querySelectorAll('button')].find(item => item.textContent?.trim() === label);
|
|
expect(button).toBeTruthy();
|
|
await act(async () => { button!.click(); await new Promise(resolve => setTimeout(resolve, 0)); });
|
|
};
|
|
await click('Management Report');
|
|
await click('Technical Report');
|
|
expect(scopes).toHaveLength(2);
|
|
expect(scopes.every(scope => scope.projectId === 'dummy-project'
|
|
&& scope.assessmentIds?.[0] === 'dummy-assessment'
|
|
&& scope.scanRunIds?.[0] === 'dummy-run')).toBe(true);
|
|
expect(downloads).toHaveBeenCalledTimes(2);
|
|
const management = downloads.mock.calls[0][0];
|
|
const technical = downloads.mock.calls[1][0];
|
|
expect(management.fileName).toMatch(/management-report-.*\.html$/);
|
|
expect(technical.fileName).toMatch(/technical-report-.*\.html$/);
|
|
expect(management.mediaType).toBe('text/html;charset=utf-8');
|
|
// A brief standing for decision makers: counts and the current German AI brief, no single findings.
|
|
expect(management.text).not.toContain('Dummy technical deviation');
|
|
expect(management.text).not.toContain('Critical / High');
|
|
expect(management.text).not.toContain('Severity-Verteilung');
|
|
expect(management.text).toContain('Dummy Managementmaßnahme');
|
|
expect(aiScopes.at(-1)).toEqual({ assessmentIds: ['dummy-assessment'], scanRunIds: ['dummy-run'] });
|
|
expect(technical.text).not.toContain('Dummy Managementmaßnahme');
|
|
expect(technical.text).toContain('Dummy technical deviation');
|
|
expect(management.text).not.toContain('DUMMY-RULE-123');
|
|
expect(management.text).not.toContain('dummy-artifact-secret');
|
|
expect(technical.text).toContain('DUMMY-RULE-123');
|
|
expect(technical.text).toContain('dummy-artifact-secret');
|
|
// Environment counts from the Client Info inventory helpers; the removed sections stay gone.
|
|
expect(technical.text).toContain('<dt>Computer objects</dt><dd>1.241</dd>');
|
|
expect(technical.text).toContain('<dt>User accounts</dt><dd>321</dd>');
|
|
expect(technical.text).toContain('<dt>Windows Server 2022</dt><dd>7</dd>');
|
|
expect(technical.text).toContain('<dt>Windows 11 24H2</dt><dd>1.234</dd>');
|
|
for (const removed of ['Result-Inventar', 'Konvergenz-Nachweis', 'Scope-Grenzen', 'Vollständiger Result-Index', 'id="appendix"']) {
|
|
expect(technical.text).not.toContain(removed);
|
|
}
|
|
await act(async () => { root.unmount(); });
|
|
host.remove();
|
|
});
|
|
});
|