Assessor_2.0/tests/features/client-data-fact-results.te...

119 lines
8.2 KiB
TypeScript

// @vitest-environment jsdom
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, DashboardTopFact } 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('opens an unknown fact without inventing a zero or source rows', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
const host = document.createElement('div'); document.body.append(host); const root = createRoot(host);
try {
await act(async () => root.render(<DashboardTopFact label="User Accounts" glyph="user" evidenceGroup="user-accounts" />));
const trigger = host.querySelector<HTMLButtonElement>('button')!;
expect(trigger.querySelector('strong')!.textContent).toBe('—');
await act(async () => trigger.click());
const popup = document.querySelector('.floating-data-window')!;
expect(popup.querySelector('dd')!.textContent).toBe('—');
expect(popup.textContent).toContain('No retained Client Information available');
expect(popup.querySelector('table')).toBeNull();
} finally { act(() => root.unmount()); }
});
it.each(['dark', 'light'])('opens the six actual Data facts with their own retained results and preserves Work in %s', async theme => {
document.documentElement.dataset.theme = theme;
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
vi.stubGlobal('ResizeObserver', class { observe() {} disconnect() {} });
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => { callback(0); return 1; });
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 gpoProse = 'Synthetic scanner explanation. '.repeat(40) + 'Command: auditpol.exe /get /category:*. Preserve the original https://example.invalid/reference and script.js.';
const facts: Array<[string, string | number]> = [
['domain.count', 1], ['domain.fqdn', 'synthetic.example'], ['entra.tenant_name', 'Synthetic tenant'],
['domain.controller_count', 3], ['gpo.count', 277], ['computer.count', 341], ['user.count', 590]
];
const snapshot: EnvironmentSnapshot = { version: 1, projectId: 'synthetic-client', scanRunIds: ['run'],
generatedAt: '2026-10-02T12:00:00Z', coverage: { observed: 5, 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: [
['sectionUserInformation', 'User source', 'Nb user accounts', 590, 'Synthetic users'],
['sectionComputerInformation', 'Computer source', 'Nb computer accounts', 341, 'Synthetic computers'],
['sectionDomainControllers', 'Controller source', 'DC count', 3, 'Synthetic controllers'],
['sectionGPO', 'GPO source', 'GPO count', 277, 'Synthetic policies']
].map(([key, label, header, count, marker]) => ({ version: 1, id: String(key), scanRunId: 'run', importFileId: 'file', tool,
sectionKey: String(key), label: String(label), ordinal: 1, state: 'observed',
prose: key === 'sectionGPO' ? gpoProse : undefined,
tables: [{ name: String(label), presentation: 'inline', headers: [String(header), 'Original evidence'], rows: [[count, marker]] }],
source: { rowIndex: 0 }, extractorVersion: '1', attributes: {} }))
};
const original = JSON.stringify(snapshot);
const request = vi.spyOn(AssessorApiClient.prototype, 'getEnvironmentSnapshot').mockResolvedValue(snapshot);
vi.spyOn(AssessorApiClient.prototype, 'listProjectGpoBackupStatus').mockResolvedValue({ records: [
{ artifactId: 'backup-a', fileName: 'Synthetic backup.zip', processingStatus: 'complete', gpoCount: 277, updatedAt: '2026-10-02T12:00:00Z' }
] } as never);
const host = document.createElement('div'); document.body.append(host); const root = createRoot(host);
const onOpenWork = vi.fn();
try {
await act(async () => root.render(<DashboardPanel onOpenWork={onOpenWork} />));
expect(host.querySelectorAll('button.client-data-fact')).toHaveLength(12);
for (const [label, count, evidence] of [
['AD Domains', '1', 'synthetic.example'], ['Entra Tenant', '1', 'Synthetic tenant'],
['User Accounts', '590', 'Synthetic users'], ['Computer Objects', '341', 'Synthetic computers'],
['Domain Controllers', '3', 'Synthetic controllers'], ['GPOs', '277', 'Synthetic backup.zip']
]) {
const triggers = [...host.querySelectorAll<HTMLButtonElement>(`[aria-label="Open ${label} results"]`)];
expect(triggers).toHaveLength(2);
for (const trigger of triggers) {
expect(trigger.querySelector('strong')!.textContent).toBe(count);
expect(trigger.getAttribute('aria-haspopup')).toBe('dialog');
await act(async () => trigger.click());
const popup = document.querySelector<HTMLElement>('.floating-data-window')!;
expect(popup.querySelector('h2')!.textContent).toBe(label);
expect(popup.textContent).toContain(evidence);
if (label === 'Computer Objects' || label === 'User Accounts' || label === 'GPOs') {
expect(popup.textContent).not.toContain('Original value');
if (label !== 'GPOs') expect(popup.querySelector('table.client-data-fact-evidence')).toBeNull();
expect(popup.querySelector('.client-env-evidence-table table')).not.toBeNull();
expect(popup.textContent).not.toContain('Scan run');
}
if (label === 'User Accounts') expect(popup.textContent).not.toContain('Synthetic computers');
if (label === 'GPOs') expect(popup.textContent).toContain('All processed client backups');
if (label === 'GPOs') {
const paragraphs = [...popup.querySelectorAll('.client-env-readable-prose p')];
expect(paragraphs.length).toBeGreaterThan(1);
expect(paragraphs.map(p => p.textContent).join(' ').replace(/\s+/g, ' ').trim()).toBe(gpoProse.replace(/\s+/g, ' ').trim());
expect(popup.querySelector('.client-env-evidence-table table')!.textContent).toContain('Synthetic policies');
}
expect(trigger.getAttribute('aria-expanded')).toBe('true');
await act(async () => window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })));
expect(document.querySelector('.floating-data-window')).toBeNull();
expect(trigger.getAttribute('aria-expanded')).toBe('false');
expect(document.activeElement).toBe(trigger);
}
}
expect(request).toHaveBeenCalledOnce();
expect(onOpenWork).not.toHaveBeenCalled();
expect(useApp().resetFilters).not.toHaveBeenCalled();
expect(useApp().setFacetFilter).not.toHaveBeenCalled();
expect(JSON.stringify(snapshot)).toBe(original);
} finally { act(() => root.unmount()); }
});