157 lines
9.5 KiB
TypeScript
157 lines
9.5 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { act } from 'react';
|
|
import { createRoot } from 'react-dom/client';
|
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import { PolicyAnalysisPanel, PolicyAnalysisPanelView } from '../../src/features/project-import/PolicyAnalysisPanel';
|
|
import type { PolicyAnalysisOverview } from '../../src/domain/policy-analysis-overview';
|
|
import { AssessorApiClient } from '../../src/api/assessor-api-client';
|
|
import { POLICY_EVALUATION_ENGINE, type StoredPolicyEvaluation } from '../../src/domain/policy-evaluation';
|
|
import type { UnifiedResultProjection } from '../../src/domain/unified-result';
|
|
|
|
const overview: PolicyAnalysisOverview = {
|
|
version: 1,
|
|
projectId: 'synthetic-project',
|
|
generatedAt: '2026-08-04T15:00:00.000Z',
|
|
summary: {
|
|
identityAnalysisRuns: 1, aggregationRuns: 1, evaluationRuns: 1, findingRuns: 1,
|
|
configuredMatches: 0, confirmedDeviations: 1, notApplicable: 0, policyFindings: 0,
|
|
reviewItems: 2, conflicts: 1, unresolvedIdentities: 0, catalogGaps: 1, parserGaps: 0, partialRuns: 1
|
|
},
|
|
current: {
|
|
version: 1,
|
|
summary: {
|
|
identityAnalysisRuns: 1, aggregationRuns: 1, evaluationRuns: 1, findingRuns: 1,
|
|
configuredMatches: 0, confirmedDeviations: 1, notApplicable: 0, policyFindings: 0,
|
|
reviewItems: 2, conflicts: 1, unresolvedIdentities: 0, catalogGaps: 1, parserGaps: 0, partialRuns: 1
|
|
},
|
|
evaluationRunIds: ['evaluation-1'], findingRunIds: ['finding-run-1'], reviewItems: [{
|
|
version: 1, id: 'review-1', category: 'conflict', source: 'snapshot-aggregation', sourceRunId: 'aggregation-1',
|
|
identityId: 'technical-identity-1', codes: ['policy-snapshot-customer-conflict'], observationCount: 2, backupCount: 2
|
|
}, {
|
|
version: 1, id: 'review-2', category: 'catalog-gap', source: 'policy-evaluation', sourceRunId: 'evaluation-1',
|
|
codes: ['policy-evaluation-baseline-missing'], observationCount: 1, backupCount: 1
|
|
}]
|
|
},
|
|
actions: { version: 1, reprocess: [], aggregate: [], evaluate: [], createFindings: [] },
|
|
profiles: [{ version: 1, id: 'profile-1', name: 'Synthetic Windows', revision: 1, active: true, contextCount: 1 }],
|
|
evaluations: [{
|
|
version: 1, evaluationRunId: 'evaluation-1', environmentProfileId: 'profile-1', status: 'partial',
|
|
completedAt: '2026-08-04T15:00:00.000Z', matchCount: 0, deviationCount: 1,
|
|
notApplicableCount: 0, reviewCount: 2, findingCount: 0, rejectedFindingCount: 0
|
|
}],
|
|
reviewItems: [{
|
|
version: 1, id: 'review-1', category: 'conflict', source: 'snapshot-aggregation', sourceRunId: 'aggregation-1',
|
|
identityId: 'technical-identity-1', codes: ['policy-snapshot-customer-conflict'], observationCount: 2, backupCount: 2
|
|
}, {
|
|
version: 1, id: 'review-2', category: 'catalog-gap', source: 'policy-evaluation', sourceRunId: 'evaluation-1',
|
|
codes: ['policy-evaluation-baseline-missing'], observationCount: 1, backupCount: 1
|
|
}]
|
|
};
|
|
|
|
describe('PolicyAnalysisPanelView', () => {
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
it('shows absent settings separately without inflating misconfiguration or GPO impact counts', () => {
|
|
const html = renderToStaticMarkup(<PolicyAnalysisPanelView overview={overview}
|
|
counts={{ Fail: 2, Pass: 4, Conflicts: 1, Duplicates: 1, 'Not configured': 3000, 'Review required': 0,
|
|
'No baseline rule': 1687, 'Not comparable': 242 }}
|
|
impact={{ settings: 1, backups: 2 }}/>);
|
|
expect(html).toContain('<span>Misconfigured</span><strong>2</strong>');
|
|
expect(html).toContain('<span>Not configured</span><strong>3000</strong>');
|
|
expect(html).toContain('<span>Review required</span><strong>0</strong>');
|
|
// Configured customer settings without a baseline rule stay visible and score-neutral.
|
|
expect(html).toContain('<span>No baseline rule</span><strong>1687</strong>');
|
|
expect(html).toContain('<span>GPO backups with failing settings</span><strong>2</strong>');
|
|
expect(html).toContain('One verdict per technical policy setting');
|
|
expect(html).toContain('May overlap with each other and with policy verdicts. Score-neutral.');
|
|
// The retained parser gap stays reportable; the current engine can no longer produce a
|
|
// catalog gap, so its tile is gone while historical review items keep their own label.
|
|
expect(html).toContain('<span>Comparison not supported</span>');
|
|
expect(html).not.toContain('<span>Baseline mapping unavailable</span>');
|
|
expect(html).toContain('<strong>Baseline mapping unavailable</strong>');
|
|
});
|
|
|
|
it('shows only the backend-current evaluation when the same timeslot retains a historical aggregation', async () => {
|
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
|
const observation = {
|
|
sourceKind: 'policy-observation', sourceResultId: 'observation-current', resultProjectionId: 'policy-observation:current',
|
|
projectId: 'synthetic-project', assessmentId: 'synthetic-timeslot', title: 'Synthetic setting', importedAt: '2026-09-15T08:00:00Z',
|
|
policyScope: 'computer', policyDeliveryMechanism: 'registry-policy', policyState: 'configured',
|
|
policyIdentityStatus: 'resolved', policyIdentityKey: 'synthetic-setting', policyBackupInstanceId: 'synthetic-backup',
|
|
policyValues: [{ kind: 'registry-policy', key: 'Software\\Policies\\Synthetic', valueName: 'Mode', operation: 'set-value',
|
|
typeCode: 4, data: { kind: 'uint32', value: 1 } }]
|
|
} as unknown as UnifiedResultProjection;
|
|
const comparison = (id: string, status: 'current' | 'historical') => ({
|
|
id, observationIds: status === 'current' ? ['observation-current'] : [], baselineId: 'synthetic-baseline',
|
|
baselineLabel: 'Synthetic baseline', baselineState: 'explicit-recommendation',
|
|
classification: status === 'current' ? 'configured_match' : 'confirmed_configured_deviation',
|
|
customerState: status === 'current' ? 'configured' : 'missing',
|
|
comparator: { version: 1, comparator: { kind: 'equality' }, relation: status === 'current' ? 'match' : 'different',
|
|
actual: status === 'current' ? { kind: 'integer', value: '1' } : undefined,
|
|
baseline: { kind: 'integer', value: '1' }, diagnosticCodes: [] },
|
|
identity: { id: 'synthetic-setting', scope: 'computer', target: { kind: 'registry-value', hive: 'HKEY_LOCAL_MACHINE',
|
|
key: 'Software\\Policies\\Synthetic', valueName: 'Mode' } }, diagnosticCodes: []
|
|
});
|
|
const evaluation = (id: string, aggregationRunId: string, status: 'current' | 'historical') => ({
|
|
run: { id, aggregationRunId, completedAt: status === 'current' ? '2026-09-15T09:00:00Z' : '2026-09-14T09:00:00Z',
|
|
baselineAnalysisIds: ['synthetic-baseline-analysis'], evaluator: POLICY_EVALUATION_ENGINE },
|
|
results: [comparison(`${id}-result`, status)]
|
|
}) as unknown as StoredPolicyEvaluation;
|
|
const current = evaluation('evaluation-current', 'aggregation-current', 'current');
|
|
const historical = evaluation('evaluation-historical', 'aggregation-historical', 'historical');
|
|
const currentOverview = { ...overview, current: { ...overview.current, evaluationRunIds: ['evaluation-current'] } };
|
|
vi.spyOn(AssessorApiClient.prototype, 'getPolicyAnalysisOverview').mockResolvedValue(currentOverview);
|
|
vi.spyOn(AssessorApiClient.prototype, 'queryUnifiedResults').mockResolvedValue({ total: 1, items: [observation] });
|
|
vi.spyOn(AssessorApiClient.prototype, 'listPolicyEvaluations').mockResolvedValue({
|
|
version: 1, projectId: 'synthetic-project', records: [historical, current]
|
|
});
|
|
|
|
const container = document.createElement('div');
|
|
const root = createRoot(container);
|
|
try {
|
|
await act(async () => {
|
|
root.render(<PolicyAnalysisPanel projectId="synthetic-project"/>);
|
|
await Promise.resolve();
|
|
});
|
|
const values = Object.fromEntries([...container.querySelectorAll('.policy-analysis-summary article')].map(article => [
|
|
article.querySelector('span')?.textContent, article.querySelector('strong')?.textContent
|
|
]));
|
|
expect(values).toMatchObject({ Misconfigured: '0', Pass: '1', 'Not configured': '0', 'Review required': '0' });
|
|
} finally {
|
|
await act(async () => root.unmount());
|
|
}
|
|
});
|
|
|
|
it('separates unique verdicts, overlapping diagnostics and analysis gaps', () => {
|
|
const html = renderToStaticMarkup(<PolicyAnalysisPanelView overview={overview}/>);
|
|
|
|
expect(html).toContain('Effective application requires RSoP or GP Results');
|
|
expect(html).toContain('Analysis runs');
|
|
expect(html).toContain('Evaluation runs');
|
|
expect(html).toContain('Policy verdicts');
|
|
expect(html).toContain('Misconfigured');
|
|
expect(html).toContain('Not configured');
|
|
expect(html).toContain('Duplicates');
|
|
expect(html).toContain('GPO backups with failing settings');
|
|
expect(html).toContain('Diagnostic flags');
|
|
expect(html).toContain('Different customer values for one technical setting');
|
|
expect(html).toContain('Analysis gaps');
|
|
expect(html).toContain('Comparison not supported');
|
|
expect(html).toContain('Baseline mapping unavailable');
|
|
expect(html).not.toContain('Unsupported settings');
|
|
expect(html).not.toContain('GPO sources</span><strong>1');
|
|
expect(html).toContain('<details class="policy-analysis-technical"><summary>Technical details</summary>');
|
|
expect(html).toContain('Evaluation history');
|
|
expect(html).toContain('Current review queue');
|
|
expect(html).toContain('Conflict');
|
|
expect(html).toContain('Baseline mapping unavailable');
|
|
expect(html).toContain('policy-evaluation-baseline-missing');
|
|
expect(html).not.toContain('Confirmed policy findings');
|
|
expect(html).not.toContain('Severity');
|
|
});
|
|
});
|