Assessor_2.0/tests/comparison/finding-comparison.test.tsx

89 lines
5.9 KiB
TypeScript

// @vitest-environment jsdom
import { act } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
import { AssessorApiClient } from '../../src/api/assessor-api-client';
import type { Assessment, ImportFileRecord, ScanRun } from '../../src/domain/canonical-model';
import type { FindingRecord } from '../../src/domain/finding';
import type { TimeslotComparisonReport } from '../../src/comparison/timeslot-comparison-report';
import { ComparisonTab } from '../../src/features/comparison/ComparisonTab';
import { FindingComparison } from '../../src/features/comparison/FindingComparison';
import { resetTimeslotComparisonRequestsForTests } from '../../src/features/comparison/use-timeslot-comparison';
import { resetEnginePreferencesForTests, setEngineEnabled } from '../../src/app/engine-preferences';
// Finding Reader `Compare` section: present once the Compare tab loaded a comparison of the open timeslot B,
// with both states and the GPO values side by side; absent for an unrelated record or with the engine off.
vi.mock('../../src/app/AppProvider', () => ({
useOptionalApp: () => ({ targetProjectId: 'dummy-project', activeAssessmentIds: ['assessment-b'] })
}));
const at = { a: '2026-01-10T10:00:00.000Z', b: '2026-02-10T10:00:00.000Z' };
const assessment = (id: string, effectiveAt: string): Assessment => ({ id, projectId: 'dummy-project', name: id, createdAt: effectiveAt, updatedAt: effectiveAt, effectiveAt });
const run = (id: string, assessmentId: string, time: string): ScanRun => ({ id, assessmentId, sequence: 1, label: id, tool: { id: 'cis-cat', name: 'CIS-CAT Pro' }, importedAt: time });
const file = (runId: string, time: string) => ({ id: `file-${runId}`, scanRunId: runId, fileName: 'synthetic.html', format: 'HTML', sizeBytes: 1,
importedAt: time, lastModifiedAt: time, detectedTool: { id: 'cis-cat', name: 'CIS-CAT Pro' } }) as unknown as ImportFileRecord;
const gpoRecordId = (identity: string) => `gpo-work-setting:${encodeURIComponent(JSON.stringify(['dummy-project', 'aggregation-b', 'computer', identity]))}`;
const record = (id: string, findingOccurrenceId?: string) => ({ id, fields: [], ...(findingOccurrenceId ? { findingOccurrenceId } : {}) }) as unknown as FindingRecord;
const report = {
version: '1.0.0', projectId: 'dummy-project', baselineRunIds: ['run-a'], targetRunIds: ['run-b'], sources: [],
changes: [
{ sourceId: 'cis-cat', change: 'resolved', side: 'baseline', recordIds: ['a-1'], counterpartIds: ['b-1'], reasons: [] },
{ sourceId: 'group-policy', change: 'value-changed', side: 'baseline', recordIds: ['ga-1'], counterpartIds: ['gb-1'],
identityKey: JSON.stringify(['computer', 'MinimumPasswordLength']), reasons: ['same-setting-current-deviation'],
values: { baseline: ['{"kind":"number","value":7}'], target: ['{"kind":"number","value":10}'] } }
],
summary: { resolved: 1, unchanged: 0, 'value-changed': 1, unverified: 0, 'not-comparable': 0, 'not-applicable': 0, new: 0, regressed: 0, targetNotComparable: 0 },
score: { sources: [], excludedSources: [] }, scanner: {}
} as unknown as TimeslotComparisonReport;
let host: HTMLDivElement;
let root: Root;
beforeEach(() => {
localStorage.clear(); resetEnginePreferencesForTests(); resetTimeslotComparisonRequestsForTests();
vi.spyOn(AssessorApiClient.prototype, 'listProjectImportFiles').mockResolvedValue([file('run-a', at.a), file('run-b', at.b)]);
vi.spyOn(AssessorApiClient.prototype, 'listProjectAssessmentHistory').mockResolvedValue({
assessments: [assessment('assessment-a', at.a), assessment('assessment-b', at.b)],
scanRuns: [run('run-a', 'assessment-a', at.a), run('run-b', 'assessment-b', at.b)]
} as never);
vi.spyOn(AssessorApiClient.prototype, 'listProjectGpoBackupStatus').mockResolvedValue({ records: [] } as never);
vi.spyOn(AssessorApiClient.prototype, 'getTimeslotComparison').mockResolvedValue(report);
host = document.createElement('div'); document.body.append(host); root = createRoot(host);
});
afterEach(async () => {
await act(async () => root.unmount()); host.remove();
vi.restoreAllMocks(); resetEnginePreferencesForTests();
});
const settle = () => act(async () => { for (let index = 0; index < 5; index++) await Promise.resolve(); });
it('shows the comparison of a finding and of a GPO setting after the Compare tab loaded it', async () => {
await act(async () => root.render(<>
<ComparisonTab projectId="dummy-project" assessmentIds={['assessment-b']}/>
<div data-testid="scanner"><FindingComparison record={record('work-b-1', 'b-1')}/></div>
<div data-testid="gpo"><FindingComparison record={record(gpoRecordId('MinimumPasswordLength'))}/></div>
<div data-testid="other"><FindingComparison record={record('work-b-9', 'b-9')}/></div>
</>));
await settle();
const section = (id: string) => host.querySelector(`[data-testid="${id}"] .finding-comparison`);
expect(section('scanner')?.hasAttribute('open')).toBe(false);
expect(section('scanner')?.textContent).toContain('Resolved');
expect(section('scanner')?.querySelector('tbody tr')?.textContent).toBe('ResultDeviationPassed');
const gpo = section('gpo')!;
expect(gpo.textContent).toContain('Value changed');
expect(gpo.textContent).toContain('Still a deviation');
const valueRow = [...gpo.querySelectorAll('tbody tr')].find(row => row.textContent?.startsWith('Configured value'));
expect(valueRow?.querySelectorAll('td')[1].textContent).toContain('7');
expect(valueRow?.querySelectorAll('td')[2].textContent).toContain('10');
expect(section('other')).toBeNull();
});
it('renders nothing while the Comparison Engine is off', async () => {
setEngineEnabled('comparison', false);
await act(async () => root.render(<>
<ComparisonTab projectId="dummy-project" assessmentIds={['assessment-b']}/>
<div data-testid="scanner"><FindingComparison record={record('work-b-1', 'b-1')}/></div>
</>));
await settle();
expect(host.querySelector('.finding-comparison')).toBeNull();
});