Assessor_2.0/tests/features/gpo-evaluation-visibility.t...

173 lines
11 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @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 { DashboardPanel } from '../../src/features/dashboard/DashboardPanel';
import { DatasetViewer } from '../../src/features/dataset-viewer/DatasetViewer';
import { SearchFilterBar } from '../../src/features/search-filter/SearchFilterBar';
import { appendPolicyObservationsToDataset } from '../../src/features/project-import/policy-observations-to-dataset';
import { genericProfile } from '../../src/profiles/generic/generic-profile';
import { makeSheet } from '../../src/importers/sheet-factory';
import { filterAndSortRecords } from '../../src/features/search-filter/filter-records';
import type { UnifiedResultProjection } from '../../src/domain/unified-result';
import type { StoredPolicyEvaluation } from '../../src/domain/policy-evaluation';
import type { ViewMode } from '../../src/app/app-state';
import type { DynamicFacetFilter } from '../../src/features/search-filter/dynamic-facets';
vi.mock('../../src/app/AppProvider', () => {
const useApp = vi.fn();
return { useApp, useOptionalApp: useApp };
});
vi.mock('../../src/features/search-filter/WorkTimeslotControl', () => ({ WorkTimeslotControl: () => <span>Synthetic timeslot</span> }));
afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); Reflect.deleteProperty(HTMLElement.prototype, 'scrollIntoView'); });
function fixture(version: '1.3.3' | '1.4.0' | '1.9.0') {
vi.stubGlobal('ResizeObserver', class { observe() {} unobserve() {} disconnect() {} });
Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', { value: vi.fn(), configurable: true });
const observation = { sourceKind: 'policy-observation', sourceResultId: 'observation', projectId: 'synthetic',
assessmentId: 'synthetic-slot', title: 'Synthetic setting', importedAt: '2026-09-01T00:00:00Z',
policyScope: 'computer', policyIdentityKey: 'identity', policyIdentityStatus: 'resolved',
policyBackupInstanceId: 'backup', policyDeliveryMechanism: 'registry-policy',
policyValues: [{ kind: 'registry-policy', key: 'Software\\Policies\\Synthetic', valueName: 'Mode',
operation: 'set-value', typeCode: 4, data: { kind: 'uint32', value: 0 } }]
} as unknown as UnifiedResultProjection;
const evaluation = { run: { evaluator: { id: 'policy-baseline-evaluation', version }, completedAt: '2026-09-02T00:00:00Z' },
results: [{ id: 'comparison', observationIds: ['observation'], baselineId: 'baseline', baselineLabel: 'Synthetic baseline',
baselineState: 'explicit-recommendation', classification: 'confirmed_configured_deviation', customerState: 'configured',
identity: { id: 'identity', scope: 'computer', target: { kind: 'registry-value', hive: 'HKEY_LOCAL_MACHINE',
key: 'Software\\Policies\\Synthetic', valueName: 'Mode' } }, diagnosticCodes: [],
comparator: { version: 1, comparator: { kind: 'equality' }, relation: 'different',
actual: { kind: 'integer', value: '0' }, baseline: { kind: 'integer', value: '1' }, diagnosticCodes: [] }
}]
} as unknown as StoredPolicyEvaluation;
const dataset = appendPolicyObservationsToDataset(null, 'Synthetic', [observation], [evaluation], new Map())!;
const state = { dataset, activeSheet: dataset.sheets[0], activeProfile: genericProfile, reviews: {},
facetFilters: {}, statusFilter: 'deviation', search: '', actionOnly: false, pathFindingIds: [],
sortColumn: '', sortDirection: 'asc', titleColumn: 'Title', selectedRecordId: '', view: 'table',
setActiveSheet: vi.fn(), setSelectedRecord: vi.fn(), updateReview: vi.fn(),
setView: vi.fn((value: ViewMode) => { state.view = value; }),
setStatusFilter: vi.fn((value: string) => { state.statusFilter = value; }),
setSearch: vi.fn((value: string) => { state.search = value; }),
setActionOnly: vi.fn((value: boolean) => { state.actionOnly = value; }),
setPathFindingIds: vi.fn((value: string[]) => { state.pathFindingIds = value; }),
resetFilters: vi.fn(() => { state.facetFilters = {}; state.statusFilter = 'deviation'; }),
setFacetFilter: vi.fn((id: string, value: DynamicFacetFilter) => {
state.facetFilters = { ...state.facetFilters, [id]: value };
})
} as unknown as ReturnType<typeof useApp>;
vi.mocked(useApp).mockImplementation(() => state);
return { state, evaluation };
}
describe('saved GPO evaluation visibility after an evaluator update', () => {
it('shows a current backup-scope absence when the user selects Not configured', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
const { state, evaluation } = fixture('1.9.0');
evaluation.results[0].customerState = 'missing';
evaluation.results[0].observationIds = [];
evaluation.results[0].comparator.actual = { kind: 'none' };
evaluation.results[0].baselineVariantAssessment = {
version: 1, scope: 'backup-configuration', decision: 'all-fail', actualValues: [], diagnosticCodes: [], variants: []
};
evaluation.results[0].diagnosticCodes = ['policy-evaluation-customer-absence-scope-limited'];
const dataset = appendPolicyObservationsToDataset(null, 'Synthetic', [], [evaluation], new Map())!;
state.dataset = dataset;
state.activeSheet = dataset.sheets[0];
state.statusFilter = 'all';
const container = document.createElement('div');
const root = createRoot(container);
try {
await act(async () => root.render(<><SearchFilterBar/><DatasetViewer/></>));
const status = container.querySelector<HTMLSelectElement>('select[aria-label="Result Status"]')!;
expect([...status.options].map(option => option.value)).toContain('not-configured');
await act(async () => {
status.value = 'not-configured';
status.dispatchEvent(new Event('change', { bubbles: true }));
});
await act(async () => root.render(<><SearchFilterBar/><DatasetViewer/></>));
expect(state.statusFilter).toBe('not-configured');
expect(filterAndSortRecords(state.activeSheet, state)).toHaveLength(1);
expect(container.querySelector('.work-filter-empty')).toBeNull();
expect(container.textContent).toContain('Not configured');
} finally { await act(async () => root.unmount()); }
});
it('distinguishes a current unresolved comparison from an outdated evaluation and missing evidence', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
const { state } = fixture('1.4.0');
const sheet = makeSheet('GPOs', [['Title', 'Category', 'Status', 'Source: Scan'],
['Synthetic mixed variants', 'GPOs', 'Review required', 'GPOs']], '', 'Synthetic', { toolId: 'group-policy' });
state.activeSheet = sheet;
state.dataset = { ...state.dataset!, sheets: [sheet] };
const container = document.createElement('div');
const root = createRoot(container);
try {
await act(async () => root.render(<DashboardPanel onOpenWork={vi.fn()}/>));
expect(container.querySelector('.client-overall-status')).toBeNull();
expect(container.querySelector('.client-assessment-gaps')).toBeNull();
expect(container.querySelector('.client-priority-findings')).not.toBeNull();
} finally { await act(async () => root.unmount()); }
});
it.each(['1.3.3', '1.4.0'] as const)('opens the saved %s comparison from Client Info instead of an empty Deviation view', async version => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
const { state, evaluation } = fixture(version);
const original = JSON.stringify(evaluation);
const container = document.createElement('div');
const root = createRoot(container);
const openWork = vi.fn();
try {
await act(async () => root.render(<DashboardPanel onOpenWork={openWork}/>));
expect(container.querySelector('.client-overall-status')).toBeNull();
expect(container.querySelector('.client-assessment-gaps')).toBeNull();
// Seit 2026-09-25 liegen die Quellbeitraege im Results-Fenster neben Overall Score.
const overallResults = container.querySelector<HTMLButtonElement>('.client-overall button[aria-haspopup="dialog"]')!;
await act(async () => overallResults.click());
const source = document.body.querySelector<HTMLButtonElement>('.floating-data-window .client-source-gpo')!;
expect(source.textContent).toContain('Re-evaluation required');
expect(source.textContent).not.toContain('0 baseline deviations');
await act(async () => source.click());
expect(openWork).toHaveBeenCalledOnce();
expect(state.statusFilter).toBe('all');
expect(state.facetFilters['provenance:source-scan']?.values).toHaveLength(1);
state.view = 'table';
await act(async () => root.render(<><SearchFilterBar/><DatasetViewer/></>));
expect(container.textContent).toContain('Review required');
expect(container.textContent).toContain(`Fail · Engine ${version}`);
const status = container.querySelector<HTMLSelectElement>('select[aria-label="Result Status"]')!;
expect([...status.options].map(option => option.value)).toEqual(['all', 'deviation', 'passed', 'not-configured', 'no-baseline-rule', 'not-comparable', 'conflicts', 'duplicates']);
expect(filterAndSortRecords(state.activeSheet, { ...state, statusFilter: 'passed' })).toHaveLength(0);
expect(filterAndSortRecords(state.activeSheet, { ...state, statusFilter: 'deviation' })).toHaveLength(0);
expect(JSON.stringify(evaluation)).toBe(original);
} finally { await act(async () => root.unmount()); }
});
it.each(['records', 'columns', 'table', 'raw'] as const)('searches all findings from the empty %s Work view and clears the GPO source filter', async view => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
const { state } = fixture('1.4.0');
state.view = view;
const container = document.createElement('div');
const root = createRoot(container);
try {
await act(async () => root.render(<><SearchFilterBar/><DatasetViewer/></>));
const source = container.querySelector<HTMLSelectElement>('select[aria-label="Source: Scan"]')!;
await act(async () => {
source.value = source.options[1].value;
source.dispatchEvent(new Event('change', { bubbles: true }));
});
await act(async () => root.render(<><SearchFilterBar/><DatasetViewer/></>));
expect(container.querySelector('[aria-label="ASSESSOR – No dataset open"]')).toBeNull();
expect(container.textContent).toContain('No results match the current filters');
await act(async () => [...container.querySelectorAll<HTMLButtonElement>('.work-filter-empty button')]
.find(button => button.textContent === 'Search all Findings')!.click());
await act(async () => root.render(<><SearchFilterBar/><DatasetViewer/></>));
expect(state.statusFilter).toBe('all');
expect(state.facetFilters['provenance:source-scan']).toBeUndefined();
expect(container.querySelector('.work-filter-empty')).toBeNull();
expect(filterAndSortRecords(state.activeSheet, state)).toHaveLength(1);
expect(container.textContent).toContain('Mode');
expect(state.view).toBe(view);
} finally { await act(async () => root.unmount()); }
});
});