Assessor_2.0/tests/features/environment-evidence-result...

113 lines
8.2 KiB
TypeScript

// @vitest-environment jsdom
import { act } from 'react';
import { createRoot } from 'react-dom/client';
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { EnvironmentObservation, EnvironmentSnapshot, ScannerReportSection } from '../../src/domain/environment-context';
import { EnvironmentEvidenceTables } from '../../src/features/dashboard/EnvironmentEvidenceTables';
import { environmentEvidenceForGroup, environmentSectionTitle, environmentTableSubsection } from '../../src/features/dashboard/environment-result-evidence';
const tool = { id: 'dummy', name: 'Dummy scanner' };
const section = (key: string, name: string, rows: (string | number)[][]): ScannerReportSection => ({
version: 1, id: key, scanRunId: 'dummy-run', importFileId: 'dummy-file', tool,
sectionKey: key, label: name, ordinal: 1, state: 'observed',
tables: [{ name, headers: ['Technical setting', 'Configured value', 'Extra evidence'], rows }],
prose: `${name} source prose`, source: { rowIndex: 4 }, extractorVersion: 'dummy-1', attributes: { source: 'dummy' }
});
const observation = (property: string, originalValue: string | number | boolean, normalizedValue?: string | number | boolean): EnvironmentObservation => ({
version: 1, id: property, scanRunId: 'dummy-run', importFileId: 'dummy-file', tool,
subject: { type: 'domain', label: 'DUMMY' }, property, label: property,
originalValue, normalizedValue, valueType: 'string', state: 'observed',
sectionKey: 'report-overview', source: { objectPath: '$.dummy.value' }, mappingVersion: 'dummy-2',
attributes: { role: 'test' }
});
const snapshot: EnvironmentSnapshot = {
version: 1, projectId: 'dummy-project', scanRunIds: ['dummy-run'], generatedAt: '2026-01-01T00:00:00Z',
coverage: { observed: 1, explicitlyAbsent: 0, empty: 0, restricted: 0, incomplete: 0 },
sections: [
section('sectionPasswordPolicies', 'Password policies', [['Minimum length', 14, 'GPO A'], ['History', 24, 'GPO B']]),
section('sectionPKI', 'Certificate templates', [['Template A', 'Enabled', 'Enrollment enabled']]),
section('sectionComputerInformation', 'Operating systems', [['Windows Server', 7, 'Unsupported count']]),
section('unknown-section', 'Unmapped report evidence', [['Unknown fact', 'retained', 'source-only']])
],
observations: [
observation('domain.count', 2, 3), observation('password_policy.minimum_length', 14),
observation('pki.template_count', 1), observation('operating_system.name', 'Windows Server'),
observation('domain.controller_count', 2), observation('entra.tenant_name', 'dummy tenant'),
observation('unmapped.custom_property', 'keep me')
]
};
afterEach(() => { vi.unstubAllGlobals(); document.body.replaceChildren(); });
describe('Environment Results source evidence', () => {
it('uses only the matching retained subsection heading and fails closed on conflicting evidence', () => {
const userSection = { ...section('sectionUserInformation', 'Honey Pot', []), tables: [
{ name: 'Account list', sourceTableIndex: 38, headers: ['Name'], rows: [['Dummy']] },
{ name: 'Tabelle 40', sourceTableIndex: 39, headers: ['Nb User Accounts'], rows: [[590]] }
] };
const honeyPot = { ...observation('pingcastle.honeypot.name', 'Dummy'), sectionKey: userSection.sectionKey,
attributes: { sourceTableIndex: 38, subsectionLabel: 'Honey Pot' } };
const accountAnalysis = { ...observation('pingcastle.account.count', 590), sectionKey: userSection.sectionKey,
attributes: { sourceTableIndex: 39, subsectionLabel: 'Account analysis' } };
const matchingSnapshot = { ...snapshot, sections: [userSection], observations: [honeyPot, accountAnalysis] };
expect(environmentSectionTitle(matchingSnapshot, userSection)).toBe('User Information');
expect(environmentTableSubsection(matchingSnapshot, userSection, userSection.tables[0])).toBe('Honey Pot');
expect(environmentTableSubsection(matchingSnapshot, userSection, userSection.tables[1])).toBe('Account analysis');
expect(environmentTableSubsection({ ...matchingSnapshot, observations: [honeyPot,
{ ...accountAnalysis, importFileId: 'other-file' }] }, userSection, userSection.tables[1])).toBeUndefined();
expect(environmentTableSubsection({ ...matchingSnapshot, observations: [honeyPot, accountAnalysis,
{ ...accountAnalysis, id: 'conflict', attributes: { sourceTableIndex: 39, subsectionLabel: 'Different heading' } }] },
userSection, userSection.tables[1])).toBeUndefined();
});
it('keeps category evidence separate while Source Overview retains every original source section and observation', () => {
expect(environmentEvidenceForGroup(snapshot, 'password-policy').sections.map(item => item.sectionKey)).toEqual(['sectionPasswordPolicies']);
expect(environmentEvidenceForGroup(snapshot, 'password-policy').observations.map(item => item.property)).toEqual(['password_policy.minimum_length']);
expect(environmentEvidenceForGroup(snapshot, 'certificates').sections.map(item => item.sectionKey)).toEqual(['sectionPKI']);
expect(environmentEvidenceForGroup(snapshot, 'inventory').observations.map(item => item.property)).toContain('domain.controller_count');
expect(environmentEvidenceForGroup(snapshot, 'source-metrics').observations.map(item => item.property)).toContain('entra.tenant_name');
expect(environmentEvidenceForGroup(snapshot, 'operating-systems').sections[0].tables[0].rows).toEqual([['Windows Server', 7, 'Unsupported count']]);
expect(environmentEvidenceForGroup(snapshot, 'all').sections).toHaveLength(snapshot.sections.length);
expect(environmentEvidenceForGroup(snapshot, 'all').observations).toHaveLength(snapshot.observations.length);
});
it('shows original table rows, source metadata and distinct original/normalized values instead of the tile summary', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
const container = document.createElement('div'); document.body.append(container);
const root = createRoot(container);
try {
await act(async () => root.render(<EnvironmentEvidenceTables snapshot={snapshot} group="password-policy"/>));
expect(container.textContent).toContain('Minimum length');
expect(container.textContent).toContain('History');
expect(container.textContent).toContain('GPO B');
expect(container.textContent).toContain('Password policies source prose');
expect(container.textContent).toContain('dummy-run');
expect(container.textContent).toContain('dummy-1');
expect([...container.querySelectorAll('.client-env-evidence-meta tr')].every(row => row.children.length === 2)).toBe(true);
expect(container.textContent).not.toContain('Enrollment enabled');
await act(async () => root.render(<EnvironmentEvidenceTables snapshot={snapshot} group="all"/>));
expect(container.textContent).toContain('Unmapped report evidence');
expect(container.textContent).toContain('keep me');
const domainRow = [...container.querySelectorAll('tr')].find(row => row.textContent?.includes('domain.count'));
expect(domainRow?.textContent).toContain('2');
expect(domainRow?.textContent).toContain('3');
expect(domainRow?.textContent).toContain('$.dummy.value');
} finally { await act(async () => root.unmount()); container.remove(); }
});
it('provides all native rows through the Show more action without truncating the source table', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
const longSnapshot = { ...snapshot, sections: [section('sectionPasswordPolicies', 'Long policies',
Array.from({ length: 101 }, (_, index) => [`Rule ${index + 1}`, index, `Evidence ${index + 1}`]))], observations: [] };
const container = document.createElement('div'); document.body.append(container);
const root = createRoot(container);
try {
await act(async () => root.render(<EnvironmentEvidenceTables snapshot={longSnapshot} group="password-policy"/>));
expect(container.textContent).toContain('Rule 100');
expect(container.textContent).not.toContain('Rule 101');
await act(async () => container.querySelector<HTMLButtonElement>('button')?.click());
expect(container.textContent).toContain('Rule 101');
} finally { await act(async () => root.unmount()); container.remove(); }
});
});