// @vitest-environment jsdom import { renderToStaticMarkup } from 'react-dom/server'; import { describe, expect, it } from 'vitest'; import type { FindingRecord, PolicySourceEvidence } from '../../src/domain/finding'; import { FindingReader } from '../../src/features/finding-reader/FindingReader'; /** Synthetic Registry.pol contribution of one GPO backup. */ function source(backup: string, key: string): PolicySourceEvidence { return { id: backup, backupKey: backup, backupInstanceId: `gpo-backup-${backup}`, sequence: 3, sourceReference: `{${backup}}/DomainSysvol/GPO/Machine/registry.pol#records=3`, baselineRelation: 'conflicting', deviationFields: [['data']], values: [{ kind: 'registry-policy', operation: 'set-value', key, valueName: 'SyntheticEnable', data: { kind: 'uint32', value: 0 }, typeCode: 4 }] }; } function record(): FindingRecord { const fields = [['Title', 'SyntheticEnable'], ['Status', 'Fail'], ['Scope', 'computer'], ['Conflict', 'Conflicting customer states; see original GPO source instructions.'], ['Technical Identity', 'synthetic-identity'], ['Registry path', 'HKEY_LOCAL_MACHINE\\Software\\Policies\\Synthetic'], ['Remediation', 'repeated per package']] .map(([label, value], index) => ({ label, value, index })); return { id: 'synthetic', index: 0, title: 'SyntheticEnable', fields, filled: fields, meta: [], policySourceEvidence: [source('a', 'SOFTWARE\\Policies\\Synthetic'), source('b', 'Software\\Policies\\Synthetic'), source('c', 'Software\\Policies\\Synthetic')], policyBaselineComparisons: [1, 2].map(index => ({ baseline: `Synthetic baseline ${index}`, status: 'Fail', summary: '', context: 'Synthetic role context.', scopeNotice: 'Synthetic scope notice.', explanation: '', remediation: 'Computer policy: HKEY_LOCAL_MACHINE\\Software\\Policies\\Synthetic\\SyntheticEnable\nExpected: 1', recommendations: [{ template: `Synthetic template ${index}`, settingId: `setting-${index}`, comparison: 'Different', expected: '1', registryType: '4', additional: [], missing: [] }] })) }; } function render(item = record()) { const container = document.createElement('div'); container.innerHTML = renderToStaticMarkup( {}} onNext={() => {}} onReview={() => {}}/>); return container; } describe('GPO first-read problem summary', () => { it('states configured versus expected, the contributing GPOs, the flag and the next step before the evidence', () => { const container = render(); const summary = container.querySelector('.reader-summary')!; const problem = summary.querySelector('.policy-problem')!; // The problem is the first content block after the setting heading. expect(summary.querySelector('h3 + .policy-problem')).toBe(problem); expect(problem.getAttribute('data-outcome')).toBe('fail'); const values = Object.fromEntries([...problem.querySelectorAll('.policy-problem__values > div')] .map(row => [row.querySelector('dt')!.textContent, row.querySelector('dd')!.textContent])); expect(values).toEqual({ Configured: '0', Expected: '1, registry type 4 (REG_DWORD)' }); expect(problem.querySelector('.policy-problem__context')?.textContent) .toBe('3 GPO backup sources with the same value · differs from all 2 assessed baseline variants in 2 baseline packages'); expect([...problem.querySelectorAll('.policy-problem__flag')].map(item => item.textContent)).toEqual([ 'Conflict: Conflicting customer states; see original GPO source instructions.', 'The registry key letter case differs between sources; Windows treats registry key names case-insensitively.' ]); // Problem and next step are separate blocks with their own marker. const next = problem.nextElementSibling!; expect(next.matches('section.policy-next-step')).toBe(true); expect(next.querySelector('h4')?.textContent).toBe('Next step'); expect(problem.querySelector('.policy-problem__sources')).toBeNull(); expect(next.querySelector('.policy-problem__sources')?.textContent) .toBe('In: GPO backup gpo-backup-a, GPO backup gpo-backup-b, GPO backup gpo-backup-c'); // Identical instructions of several backups are stated above, so their complete sections are on demand. const evidence = next.nextElementSibling!; expect(evidence.classList.contains('policy-source-evidence')).toBe(true); const collapsed = evidence.querySelector('details.policy-source-collapse')!; expect(collapsed.hasAttribute('open')).toBe(false); expect(collapsed.querySelector('summary')?.textContent).toBe('Show the 3 identical source instructions'); expect(collapsed.querySelectorAll('.policy-source-group')).toHaveLength(3); expect(container.querySelectorAll('.policy-source-deviation-value')).toHaveLength(3); // Not repeated as separate report fields; technical provenance moves to Additional Information. const headings = [...summary.querySelectorAll(':scope > div > h4')].map(item => item.textContent); expect(headings).not.toContain('Conflict'); expect(headings).not.toContain('Remediation'); expect(headings).not.toContain('Technical Identity'); expect(headings).toContain('Scope'); expect(container.querySelector('.all-fields')?.textContent).toContain('synthetic-identity'); }); it('keeps work tracking available but collapsed below the finding', () => { const container = render(); const tracking = container.querySelector('details.reader-tracking')!; expect(tracking.hasAttribute('open')).toBe(false); expect(tracking.querySelector('summary')?.textContent).toBe('Work trackingStatus: Open · Priority: Medium'); expect(tracking.querySelectorAll('.dashboard-grid select, .review-grid textarea')).toHaveLength(7); expect(container.querySelector('.reader-summary')!.compareDocumentPosition(tracking) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); expect(container.querySelector('.finding-reader > .dashboard-grid')).toBeNull(); }); it('adds no problem block without current structured comparisons', () => { const item = record(); delete item.policyBaselineComparisons; expect(render(item).querySelector('.policy-problem')).toBeNull(); }); }); describe('GPO source sections with differing instructions', () => { it('summarizes differing values per backup and keeps their complete sections on demand', () => { const item = record(); const differing = item.policySourceEvidence![1]; differing.values = [{ ...(differing.values[0] as object), data: { kind: 'uint32', value: 1 } }] as never; const container = render(item); const collapsed = container.querySelector('details.policy-source-collapse')!; expect(collapsed.hasAttribute('open')).toBe(false); expect(collapsed.querySelector('summary')?.textContent).toBe('Show the 3 source instructions'); expect(collapsed.querySelectorAll('.policy-source-group')).toHaveLength(3); // The value-to-backup overview stays visible outside the disclosure. const overview = container.querySelector('.policy-source-overview')!; expect(overview.closest('details')).toBeNull(); expect(overview.querySelector('.policy-source-overview__summary')?.textContent).toBe('2 different instructions across 3 sources'); }); }); describe('GPO problem and next-step markers', () => { it('use the shared marker width, the fail color for the problem and the orange next-step role', async () => { const { readFileSync } = await import('node:fs'); const styles = readFileSync('src/styles/components.css', 'utf8'); const tokens = readFileSync('src/styles/tokens.css', 'utf8'); expect(styles).toMatch(/\.reader-summary > \.policy-problem, \.reader-summary > \.policy-next-step \{[^}]*border-left: var\(--marker-width\) solid/); expect(styles).toMatch(/\.reader-summary > \.policy-problem\[data-outcome="fail"\] \{ border-left-color: var\(--outcome-fail\); \}/); expect(styles).toMatch(/\.reader-summary > \.policy-next-step \{[^}]*border-left-color: var\(--next-step-accent\);/); expect(tokens).toContain('--next-step-accent: var(--sev-high);'); }); }); describe('GPO problem summary with differing package results', () => { it('explains the rolled-up verdict, names the templates that match and states the shared intro once', () => { const principal = (key: string) => ({ id: key, backupKey: key, backupInstanceId: `gpo-backup-${key}`, sourceReference: `{${key}}/GptTmpl.inf#lines=1`, baselineRelation: 'review' as const, values: [{ kind: 'principal-set', principals: ['*S-1-5-32-544'] }] }); const variant = (template: string, match: boolean) => ({ template, settingId: template, comparison: match ? 'Match' : 'Different', expected: match ? 'Values: *S-1-5-32-544' : 'Empty account list', additional: match ? [] : ['*S-1-5-32-544'], missing: [] }); const comparison = (baseline: string, status: string, remediation: string, recommendations: ReturnType[]) => ({ baseline, status, summary: '', context: 'Synthetic role context.', scopeNotice: 'Synthetic scope notice.', explanation: '', remediation, recommendations }); const mixed = 'Establish the applicable baseline template before changing this setting. Compare the individual recommendations below; do not combine their values into a new baseline.'; const item = record(); item.fields = [{ label: 'Title', value: 'SyntheticRight', index: 0 }, { label: 'Status', value: 'Fail', index: 1 }]; item.policySourceEvidence = [principal('a')]; item.policyBaselineComparisons = [ comparison('Synthetic client baseline', 'Fail', 'Computer policy: SyntheticRight\nExpected: Values: Not set', [variant('Synthetic client', false)]), comparison('Synthetic server baseline 1', 'Review required', mixed, [variant('Synthetic server 1 - Member Server', false), variant('Synthetic server 1 - Domain Controller', true)]), comparison('Synthetic server baseline 2', 'Review required', mixed, [variant('Synthetic server 2 - Domain Controller', true), variant('Synthetic server 2 - Member Server', false)]) ]; const problem = render(item).querySelector('.policy-problem')!; expect([...problem.querySelectorAll('.policy-problem__context')].map(line => line.textContent)).toEqual([ '1 GPO backup source', 'Verdict Fail: the highest result of the individual baseline packages. Fail in 1 of 3 packages, Review required in 2 of 3 packages; in 2 of them the templates of the same package disagree.' ]); const expected = [...problem.querySelectorAll('.policy-problem__values > div')].find(row => row.querySelector('dt')?.textContent === 'Expected')!; expect([...expected.querySelectorAll('.policy-problem__entry')].map(entry => entry.textContent)).toEqual([ 'Empty account list — Different · 3 baseline templates', 'Values: *S-1-5-32-544 (Administrators) — Match · Synthetic server 1 - Domain Controller; Synthetic server 2 - Domain Controller' ]); const next = problem.nextElementSibling!; expect(next.textContent!.split('Establish the applicable baseline template').length - 1).toBe(1); expect([...next.querySelectorAll('.policy-problem__step')].map(step => step.querySelector('.policy-problem__packages')?.textContent)) .toEqual(['Synthetic client baseline']); }); }); describe('GPO backups without a link', () => { it('states "No effect" in the problem summary and marks every unlinked source, without changing the verdict', () => { const item = record(); item.policySourceEvidence = item.policySourceEvidence!.map(source => ({ ...source, linkStatus: 'not-linked' as const })); const container = render(item); const problem = container.querySelector('.policy-problem')!; expect(container.querySelector('.outcome-mark')?.textContent).toBe('Fail'); expect([...problem.querySelectorAll('.policy-problem__flag')].map(flag => flag.textContent)).toContain( 'No effect: every contributing GPO is not linked in its backup report, so this value applies to no device.'); expect(container.querySelectorAll('.policy-source-group__badge.is-not-linked')).toHaveLength(3); expect(container.querySelectorAll('.policy-source-group__link-note')).toHaveLength(3); }); it('counts unlinked contributors when only some GPOs are unlinked and stays silent without retained link facts', () => { const partial = record(); partial.policySourceEvidence = partial.policySourceEvidence!.map((source, index) => index === 0 ? { ...source, linkStatus: 'not-linked' as const } : { ...source, linkStatus: 'linked' as const }); const flags = [...render(partial).querySelectorAll('.policy-problem__flag')].map(flag => flag.textContent); expect(flags).toContain('1 of 3 contributing GPOs are not linked in their backup report and apply to no device.'); const unknown = render(record()); expect(unknown.querySelector('.policy-source-group__badge.is-not-linked')).toBeNull(); expect(unknown.textContent).not.toContain('not linked'); }); });