90 lines
6.1 KiB
TypeScript
90 lines
6.1 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('Change: resolved.');
|
|
expect(section('scanner')?.querySelector('tbody tr')?.textContent).toBe('Verdict✕ Deviation✓ Passed');
|
|
// Internal evidence codes are never shown; only reasons with a readable text.
|
|
expect(section('gpo')?.textContent).not.toContain('same-setting-current-deviation');
|
|
const gpo = section('gpo')!;
|
|
expect(gpo.textContent).toContain('Change: value changed, verdict unchanged.');
|
|
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('[data-testid="scanner"] .finding-comparison')).toBeNull();
|
|
});
|