104 lines
8.6 KiB
TypeScript
104 lines
8.6 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { writeFileSync } from 'node:fs';
|
|
import { syntheticHeatmapContext } from '../assessment-context/context-heatmap-fixture';
|
|
import { resetAssessmentContextRequestsForTests } from '../../src/features/context-layer/use-assessment-context';
|
|
import { act } from 'react';
|
|
import { createRoot } from 'react-dom/client';
|
|
import { afterEach, expect, it, vi } from 'vitest';
|
|
import { useApp } from '../../src/app/AppProvider';
|
|
import { resetEnginePreferencesForTests, setEngineEnabled } from '../../src/app/engine-preferences';
|
|
import { AssessorApiClient } from '../../src/api/assessor-api-client';
|
|
import { DashboardPanel } from '../../src/features/dashboard/DashboardPanel';
|
|
import { makeSheet } from '../../src/importers/sheet-factory';
|
|
import { genericProfile } from '../../src/profiles/generic/generic-profile';
|
|
import type { EnvironmentSnapshot } from '../../src/domain/environment-context';
|
|
|
|
vi.mock('../../src/app/AppProvider', () => ({ useApp: vi.fn() }));
|
|
vi.mock('../../src/features/search-filter/WorkTimeslotControl', () => ({ WorkTimeslotControl: () => <span>Synthetic timeslot</span> }));
|
|
afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); document.body.replaceChildren(); });
|
|
|
|
it.each(['dark', 'light'])('restores the previous Client Info layout, results, Core counts and heatmap drilldown in %s', async theme => {
|
|
document.documentElement.dataset.theme = theme;
|
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
|
vi.stubGlobal('ResizeObserver', class { observe() {} disconnect() {} });
|
|
localStorage.clear(); resetEnginePreferencesForTests(); setEngineEnabled('context', false);
|
|
const sheet = makeSheet('Synthetic', [['Name', 'Status'], ['Control', 'Failed']], '', 'synthetic.csv', { isProjectSheet: true });
|
|
vi.mocked(useApp).mockReturnValue({ dataset: { fileName: 'Synthetic client', sheets: [sheet], scanTool: 'Generic' },
|
|
activeSheet: sheet, activeProfile: genericProfile, reviews: {}, targetProjectId: 'synthetic-client',
|
|
activeAssessmentIds: ['slot'], activeScanRunIds: ['run'], facetFilters: {},
|
|
setStatusFilter: vi.fn(), setFacetFilter: vi.fn(), setPathFindingIds: vi.fn(), resetFilters: vi.fn(),
|
|
setView: vi.fn(), setActionOnly: vi.fn(), setActiveSheet: vi.fn()
|
|
} as unknown as ReturnType<typeof useApp>);
|
|
const tool = { id: 'synthetic', name: 'Synthetic scanner' };
|
|
const facts: Array<[string, string | number]> = [
|
|
['domain.fqdn', 'synthetic.example'], ['domain.netbios_name', 'SYNTHETIC'], ['forest.name', 'synthetic.forest'],
|
|
['domain.count', 2], ['domain.controller_count', 3], ['domain.functional_level', 'Domain level'],
|
|
['forest.functional_level', 'Forest level'], ['forest.schema_version', 'Windows Server 2019'],
|
|
['domain.created_at', '2001-03-08'], ['entra.tenant_name', 'Synthetic tenant']
|
|
];
|
|
const snapshot: EnvironmentSnapshot = { version: 1, projectId: 'synthetic-client', scanRunIds: ['run'],
|
|
generatedAt: '2026-10-02T12:00:00Z', coverage: { observed: 10, explicitlyAbsent: 0, empty: 0, restricted: 0, incomplete: 0 },
|
|
observations: facts.map(([property, value]) => ({ version: 1, id: property, scanRunId: 'run', importFileId: 'file', tool,
|
|
subject: { type: 'domain' }, property, label: property, originalValue: value, normalizedValue: value,
|
|
valueType: typeof value === 'number' ? 'number' : 'string', state: 'observed', source: { rowIndex: 0 }, mappingVersion: '1', attributes: {} })),
|
|
sections: [{ version: 1, id: 'native-domain', scanRunId: 'run', importFileId: 'file', tool,
|
|
sectionKey: 'sectionDomainInformation', label: 'Domain source', ordinal: 1, state: 'observed',
|
|
tables: [{ name: 'Domain source', headers: ['Original evidence'], rows: [['Synthetic retained source']] }],
|
|
source: { rowIndex: 0 }, extractorVersion: '1', attributes: {} }]
|
|
};
|
|
vi.spyOn(AssessorApiClient.prototype, 'getEnvironmentSnapshot').mockResolvedValue(snapshot);
|
|
vi.spyOn(AssessorApiClient.prototype, 'listProjectGpoBackupStatus').mockResolvedValue({ records: [] } as never);
|
|
resetAssessmentContextRequestsForTests();
|
|
vi.spyOn(AssessorApiClient.prototype, 'getAssessmentContext').mockResolvedValue(syntheticHeatmapContext());
|
|
const onOpenWork = vi.fn();
|
|
const original = JSON.stringify(useApp().dataset);
|
|
const host = document.createElement('div'); document.body.append(host);
|
|
const root = createRoot(host);
|
|
try {
|
|
await act(async () => root.render(<DashboardPanel onOpenWork={onOpenWork} />));
|
|
const left = host.querySelector('.client-posture')!;
|
|
expect(host.querySelector('.client-analysis-column')).toBeNull();
|
|
expect(host.querySelector('.client-priority-summary')).toBeNull();
|
|
const right = host.querySelector('.client-priority')!;
|
|
expect(left.querySelector('.client-overall')).not.toBeNull();
|
|
expect(left.querySelectorAll('.client-domains li')).toHaveLength(6);
|
|
expect(right.querySelectorAll('.client-data-fact')).toHaveLength(6);
|
|
expect(host.querySelectorAll('.client-environment-grid > section')).toHaveLength(6);
|
|
const cloud = host.querySelector('[aria-label="Cloud Information"]')!;
|
|
expect([...cloud.querySelectorAll('dd')].map(item => item.textContent)).toEqual(Array(4).fill('Not reported'));
|
|
expect(host.querySelector('.client-environment-grid')?.parentElement).toBe(host.firstElementChild);
|
|
expect(host.querySelector('.client-risk-register')?.parentElement).toBe(host.firstElementChild);
|
|
const coreCounts = [...host.querySelectorAll('.client-domain-score, .client-domains b, .client-priority-context, .client-overall-ring, .client-overall-coverage')].map(el => el.textContent);
|
|
await act(async () => setEngineEnabled('context', true));
|
|
expect(host.querySelectorAll('.context-heatmap__cell')).toHaveLength(22);
|
|
expect(host.querySelector('.context-heatmap')?.parentElement).toBe(host.firstElementChild);
|
|
expect(left.querySelector('.context-heatmap')).toBeNull();
|
|
expect(right.querySelector('.context-heatmap')).toBeNull();
|
|
expect([...host.querySelectorAll('.client-domain-score, .client-domains b, .client-priority-context, .client-overall-ring, .client-overall-coverage')].map(el => el.textContent)).toEqual(coreCounts);
|
|
if (process.env.ASSESSOR_LAYOUT_PREVIEW && theme === 'dark') {
|
|
writeFileSync('tests/client-info-layout-preview.html', `<!doctype html><html data-theme="dark"><head><meta charset="UTF-8"><title>Synthetic restored Client Info</title></head><body><p>Synthetic restored layout preview · no client data</p><button class="btn" id="theme">Switch theme</button><main>${host.innerHTML}</main><script type="module">import '@fontsource-variable/inter/wght.css';import '/src/styles/layers.css';import '/src/styles/ui-tokens.css';import '/src/styles/tokens.css';import '/src/styles/base.css';import '/src/styles/components.css';import '/src/styles/datahub.css';import '/src/features/context-layer/context-heatmap.css';document.getElementById('theme').onclick=()=>{document.documentElement.dataset.theme=document.documentElement.dataset.theme==='dark'?'light':'dark';};</script></body></html>`);
|
|
}
|
|
const domain = host.querySelector('.client-domain-information')!;
|
|
for (const value of ['SYNTHETIC', 'Windows Server 2019', '2001-03-08', 'Synthetic tenant']) expect(domain.textContent).toContain(value);
|
|
expect([...host.querySelectorAll('h3')].some(heading => heading.textContent === 'Directory')).toBe(false);
|
|
const button = domain.querySelector<HTMLButtonElement>('[aria-label="Open Domain Information results"]')!;
|
|
expect(button).not.toBeNull();
|
|
await act(async () => button.click());
|
|
const popup = document.querySelector('[role="dialog"]')!;
|
|
expect(popup).not.toBeNull();
|
|
expect([...popup.querySelectorAll('.client-env-facts dt')].map(item => item.textContent)).toEqual([
|
|
'Domain', 'NetBIOS name', 'Forest', 'Domains', 'Domain controllers', 'Domain functional level',
|
|
'Forest functional level', 'DC OS', 'Schema version', 'Domain created', 'Entra ID'
|
|
]);
|
|
for (const [, value] of facts) expect(popup.textContent).toContain(String(value));
|
|
expect(popup.textContent).toContain('Synthetic retained source');
|
|
expect(useApp().setFacetFilter).not.toHaveBeenCalled();
|
|
await act(async () => popup.querySelector<HTMLButtonElement>('[aria-label="Close Domain Information window"]')?.click());
|
|
const cluster = host.querySelector<HTMLButtonElement>('[data-cluster="cloud-privileged-access-exposure"]')!;
|
|
await act(async () => cluster.click());
|
|
expect(useApp().setFacetFilter).toHaveBeenCalledWith('context:cluster', { values: ['cloud-privileged-access-exposure'] });
|
|
expect(onOpenWork).toHaveBeenCalledOnce();
|
|
expect(JSON.stringify(useApp().dataset)).toBe(original);
|
|
} finally { act(() => root.unmount()); }
|
|
});
|