113 lines
8.2 KiB
TypeScript
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(); }
|
|
});
|
|
});
|