// @vitest-environment jsdom import { act } from 'react'; import { createRoot, type Root } from 'react-dom/client'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { useApp } from '../../src/app/AppProvider'; import { AssessorApiClient } from '../../src/api/assessor-api-client'; import { DashboardPanel } from '../../src/features/dashboard/DashboardPanel'; import { ClientInformationBrowser } from '../../src/features/dashboard/ClientInformationBrowser'; import { CloudInformationTile } from '../../src/features/dashboard/CloudInformationTile'; import { clientInformationFixture } from '../client-information-fixture'; import { cloudInformationFixture } from '../cloud-information-fixture'; import { selectClientInformation, type ClientInformationResult } from '../../src/domain/client-information'; import { makeSheet } from '../../src/importers/sheet-factory'; import { genericProfile } from '../../src/profiles/generic/generic-profile'; import { resetEnginePreferencesForTests, setEngineEnabled } from '../../src/app/engine-preferences'; vi.mock('../../src/app/AppProvider', () => ({ useApp: vi.fn() })); vi.mock('../../src/features/search-filter/WorkTimeslotControl', () => ({ WorkTimeslotControl: () => Synthetic timeslot })); let root: Root | undefined; let host: HTMLDivElement; afterEach(async () => { if (root) await act(async () => root!.unmount()); root = undefined; host?.remove(); vi.restoreAllMocks(); vi.unstubAllGlobals(); }); function mount() { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => { callback(0); return 1; }); host = document.createElement('div'); document.body.append(host); root = createRoot(host); return root; } async function click(label: string) { const button = [...document.querySelectorAll('button')].find(item => item.textContent?.trim() === label || item.getAttribute('aria-label') === label); expect(button, label).toBeTruthy(); await act(async () => button!.click()); } async function input(label: string, value: string) { const element = document.querySelector(`input[aria-label="${label}"]`)!; await act(async () => { Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')!.set!.call(element, value); element.dispatchEvent(new Event('input', { bubbles: true })); }); } describe('Client Info: selective Client Information access', () => { it.each(['dark', 'light'])('shows concrete facts for all four cloud areas and their unchanged Results in %s', async theme => { document.documentElement.dataset.theme = theme; const snapshot = cloudInformationFixture(); const before = JSON.stringify(snapshot); vi.spyOn(AssessorApiClient.prototype, 'queryClientInformation').mockImplementation(async scope => selectClientInformation(snapshot, scope, 7)); await act(async () => mount().render()); expect(host.querySelectorAll('dt')).toHaveLength(4); for (const value of ['FIDO2: enabled', 'Permanent: 2 reported assignments', 'Admin requests: disabled', 'Guests: restricted role']) expect(host.textContent).toContain(value); expect(host.textContent).not.toContain('Source evidence available'); await click('Open Cloud Information results'); await click('Details: CISA.MS.AAD.7.4'); const details = document.querySelector('[aria-label="Client Information details"]')!; expect(details.textContent).toContain('Reported permanent assignments: 2 (reported subset)'); expect(details.textContent).toContain('Synthetic admin'); expect(JSON.stringify(snapshot)).toBe(before); }); it.each(['dark', 'light'])('opens on demand in %s, filters and resets without altering Work or optional-engine choices', async theme => { document.documentElement.dataset.theme = theme; localStorage.clear(); resetEnginePreferencesForTests(); setEngineEnabled('context', false); setEngineEnabled('report', false); const snapshot = clientInformationFixture(); const query = vi.spyOn(AssessorApiClient.prototype, 'queryClientInformation').mockImplementation(async scope => selectClientInformation(snapshot, scope, 7)); vi.spyOn(AssessorApiClient.prototype, 'getEnvironmentSnapshot').mockResolvedValue(snapshot); vi.spyOn(AssessorApiClient.prototype, 'listProjectGpoBackupStatus').mockResolvedValue({ records: [] } as never); const sheet = makeSheet('Synthetic', [['Name', 'Status', 'Severity'], ['Synthetic check', 'Failed', 'High']], '', 'synthetic.csv', { isProjectSheet: true }); const setFacetFilter = vi.fn(); const resetFilters = vi.fn(); vi.mocked(useApp).mockReturnValue({ dataset: { fileName: 'Synthetic', sheets: [sheet], scanTool: 'Generic' }, activeSheet: sheet, activeProfile: genericProfile, reviews: {}, targetProjectId: 'client-1', activeScanRunIds: ['run-1'], activeAssessmentIds: ['slot-1'], facetFilters: {}, setFacetFilter, resetFilters, setStatusFilter: vi.fn(), setPathFindingIds: vi.fn(), setView: vi.fn(), setActionOnly: vi.fn(), setActiveSheet: vi.fn() } as unknown as ReturnType); await act(async () => mount().render()); expect(query).not.toHaveBeenCalled(); const tile = host.querySelector('[aria-label="Cloud Information"]')!; expect(tile).not.toBeNull(); expect(tile.querySelectorAll('dt')).toHaveLength(4); expect(tile.textContent).toContain('FIDO2: enabled'); expect(tile.textContent).toContain('Not reported'); await click('Open Cloud Information results'); expect(query).toHaveBeenLastCalledWith(expect.objectContaining({ projectId: 'client-1', assessmentId: 'slot-1', scanRunIds: ['run-1'], areas: ['cloud-access', 'cloud-roles', 'cloud-apps', 'cloud-sharing'] })); expect(document.querySelector('.floating-data-window')!.textContent).toContain('1 matching observations'); await click('Details: MFA source'); expect(document.querySelector('.floating-data-window')!.textContent).toContain('FIDO2'); await click('Reset'); expect(document.querySelector('.floating-data-window')!.textContent).toContain('1 matching observations'); await click('Close Cloud Information window'); expect(document.querySelector('[aria-label="Open Client Information results"]')).toBeNull(); // The standalone data browser is retained; only its Dashboard trigger was removed. await act(async () => root!.render()); expect(query).toHaveBeenLastCalledWith(expect.objectContaining({ projectId: 'client-1', assessmentId: 'slot-1', scanRunIds: ['run-1'] })); expect(document.body.textContent).toContain('6 matching observations'); await click('Details: MFA source'); expect(document.body.textContent).toContain('Reported test evidence.'); expect(document.body.textContent).toContain('FIDO2'); await input('Client Information property', 'domain.fqdn'); await click('Apply'); expect(query).toHaveBeenLastCalledWith(expect.objectContaining({ properties: ['domain.fqdn'] })); expect(document.body.textContent).toContain('2 matching observations'); expect(document.querySelector('[aria-label="Client Information details"]')).toBeNull(); await input('Client Information property', 'missing.property'); await click('Apply'); expect(document.body.textContent).toContain('Missing information does not mean'); await click('Reset'); expect(document.body.textContent).toContain('6 matching observations'); expect(setFacetFilter).not.toHaveBeenCalled(); expect(resetFilters).not.toHaveBeenCalled(); }); it('always shows four cloud areas without evidence and distinguishes unassessed evidence from observed configuration', async () => { const snapshot = clientInformationFixture(); const scope = { projectId: 'client-1', scanRunIds: ['run-1'] }; const cloud = snapshot.observations.find(item => item.property.startsWith('cloud.'))!; cloud.state = 'not-assessed'; const original = JSON.stringify(snapshot); await act(async () => mount().render()); expect(host.textContent).not.toContain('FIDO2: enabled'); expect([...host.querySelectorAll('dd')].every(item => item.textContent === 'Not reported')).toBe(true); expect(JSON.stringify(snapshot)).toBe(original); await act(async () => root!.render( item !== cloud) }} scope={scope} />)); expect(host.querySelectorAll('dd')).toHaveLength(4); expect([...host.querySelectorAll('dd')].every(item => item.textContent === 'Not reported')).toBe(true); await act(async () => root!.render()); expect(host.querySelectorAll('dd')).toHaveLength(4); await act(async () => root!.render()); await click('Open Cloud Information results'); expect(document.querySelector('.floating-data-window')!.textContent).toContain('No retained Client Information'); }); it('paginates, retries failures and suppresses late responses from a previous scope', async () => { const snapshot = clientInformationFixture(); snapshot.observations = Array.from({ length: 55 }, (_, index) => ({ ...snapshot.observations[0], id: String(index).padStart(3, '0'), label: `Synthetic ${index}` })); const query = vi.spyOn(AssessorApiClient.prototype, 'queryClientInformation').mockImplementation(async scope => selectClientInformation(snapshot, scope, 8)); const scope = { projectId: 'client-1', scanRunIds: ['run-1'] }; await act(async () => mount().render()); await click('Next observations'); expect(query).toHaveBeenLastCalledWith(expect.objectContaining({ offset: 50 })); expect(document.querySelector('.client-information-table tbody')!.children).toHaveLength(5); expect(document.querySelector('[aria-label="Client Information pages"] button:last-child')!.disabled).toBe(true); query.mockRejectedValueOnce(new Error('synthetic failure')); await click('Previous observations'); expect(document.querySelector('[role="alert"]')!.textContent).toContain('could not be loaded'); await click('Retry'); expect(document.querySelector('.client-information-table tbody')!.children).toHaveLength(50); let oldResolve!: (result: ClientInformationResult) => void; query.mockImplementationOnce(() => new Promise(resolve => { oldResolve = resolve; })); await click('Reset'); await act(async () => root!.render()); await act(async () => oldResolve({ ...selectClientInformation(snapshot, scope, 1), totalObservations: 999 })); expect(document.body.textContent).not.toContain('999 matching'); expect(query).toHaveBeenLastCalledWith(expect.objectContaining({ projectId: 'new-client', scanRunIds: ['new-run'] })); }); });