// @vitest-environment jsdom import { act, type ReactNode } from 'react'; import { createRoot, type Root } from 'react-dom/client'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { AssessorApiClient } from '../../src/api/assessor-api-client'; import { AppProvider, useApp } from '../../src/app/AppProvider'; import { resetEnginePreferencesForTests, setEngineEnabled } from '../../src/app/engine-preferences'; import type { RegulatoryControlOverviewResponse } from '../../src/compliance/regulatory-mapping'; import { complianceWorkFacetIndex, ISO_CONTROL_FACET_ID, ISO_THEME_FACET_ID, resetComplianceWorkFacetsForTests, useComplianceWorkFacets } from '../../src/compliance/ui/compliance-work-facets'; import { markRegulatoryDecisionSaved } from '../../src/compliance/ui/decision-revision'; import { filterAndSortRecords } from '../../src/features/search-filter/filter-records'; import { selectWorkFacets } from '../../src/features/search-filter/work-facets'; import { makeSheet } from '../../src/importers/sheet-factory'; import { genericProfile } from '../../src/profiles/generic/generic-profile'; // Work `ISO control` and `ISO theme` (Compliance Engine): a second reading of accepted controls that // filters Work like the Context Cluster facet, sits before the final `Source: Scan`, and is absent while // the engine is off. const gpoKey = `gpo-work-setting:${encodeURIComponent(JSON.stringify(['dummy', 'baseline', 'computer', 'registry:dummy:setting']))}`; const assignments: RegulatoryControlOverviewResponse = { version: 1, projectId: 'dummy-project', scope: { assessmentIds: ['dummy-assessment'], scanRunIds: [] }, findingCount: 3, controls: [ { controlId: '8.20', annexAControlId: 'A.8.20', title: 'Networks security', theme: 'Technological', findings: [ { resultProjectionId: 'scanner-finding:dummy-a', sourceKind: 'scanner-finding', sourceResultId: 'dummy-a' }, { resultProjectionId: 'scanner-finding:dummy-b', sourceKind: 'scanner-finding', sourceResultId: 'dummy-b' }, { resultProjectionId: 'policy-evaluation-comparison:dummy', sourceKind: 'policy-evaluation', sourceResultId: 'dummy-comparison', sourceRuleId: 'registry:dummy:setting', policyScope: 'computer' }] }, { controlId: '5.15', annexAControlId: 'A.5.15', theme: 'Organizational', findings: [ { resultProjectionId: 'scanner-finding:dummy-b', sourceKind: 'scanner-finding', sourceResultId: 'dummy-b' }] } ] }; function workSheet() { const sheet = makeSheet('Dummy', [ ['Title', 'Status', 'Source: Scan'], ['Dummy check A', 'Failed', 'Purple Knight'], ['Dummy check B', 'Passed', 'Purple Knight'], ['Dummy GPO setting', 'Failed', 'GPOs'], ['Dummy unmapped check', 'Failed', 'Purple Knight'] ], '', 'dummy.csv', { isProjectSheet: true }); const [a, b, gpo] = sheet.analysis.records; a.findingOccurrenceId = 'dummy-a'; b.findingOccurrenceId = 'dummy-b'; (gpo as { id: string }).id = gpoKey; return sheet; } const options = (statusFilter = 'all', facetFilters = {}) => ({ dataset: null, activeProfile: genericProfile, reviews: {}, search: '', actionOnly: false, statusFilter, facetFilters, complianceFacets: complianceWorkFacetIndex(assignments) }); describe('Work ISO control and ISO theme facets', () => { it('offers accepted controls and themes before the final Source: Scan control', () => { const facets = selectWorkFacets(workSheet(), options()); const ids = facets.map(facet => facet.id); expect(ids.at(-1)).toBe('provenance:source-scan'); expect(ids.slice(-3, -1)).toEqual([ISO_CONTROL_FACET_ID, ISO_THEME_FACET_ID]); const control = facets.find(facet => facet.id === ISO_CONTROL_FACET_ID)!; expect(control.label).toBe('ISO control'); expect(control.values.map(value => [value.label, value.count])).toEqual([['A.5.15', 1], ['A.8.20 ยท Networks security', 3]]); const theme = facets.find(facet => facet.id === ISO_THEME_FACET_ID)!; expect(theme.values.map(value => [value.value, value.label, value.count])).toEqual([['organizational', 'Organizational', 1], ['technological', 'Technological', 3]]); }); it('filters Work records by control and theme, matching scanner occurrences and GPO settings', () => { const sheet = workSheet(); const titles = (filters: object, status = 'all') => filterAndSortRecords(sheet, { ...options(status, filters), sortColumn: '', sortDirection: 'asc' }) .map(record => record.title); expect(titles({ [ISO_CONTROL_FACET_ID]: { values: ['5.15'] } })).toEqual(['Dummy check B']); expect(titles({ [ISO_THEME_FACET_ID]: { values: ['technological'] } })).toEqual(['Dummy check A', 'Dummy check B', 'Dummy GPO setting']); // Other filters still apply: the status filter narrows the ISO selection. expect(titles({ [ISO_CONTROL_FACET_ID]: { values: ['8.20'] } }, 'deviation')).toEqual(['Dummy check A', 'Dummy GPO setting']); }); it('counts each control against the other active filters', () => { const facets = selectWorkFacets(workSheet(), options('deviation')); expect(facets.find(facet => facet.id === ISO_CONTROL_FACET_ID)!.values.map(value => [value.value, value.count])).toEqual([['8.20', 2]]); }); it('adds no ISO facet without loaded assignments', () => { const facets = selectWorkFacets(workSheet(), { ...options(), complianceFacets: undefined }); expect(facets.some(facet => facet.id.startsWith('compliance:'))).toBe(false); }); }); vi.mock('../../src/features/project-import/load-canonical-project-workspace', () => ({ loadCanonicalProjectWorkspace: vi.fn() })); let host: HTMLDivElement; let root: Root; beforeEach(() => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); localStorage.clear(); resetEnginePreferencesForTests(); resetComplianceWorkFacetsForTests(); vi.spyOn(AssessorApiClient.prototype, 'health').mockResolvedValue({ status: 'ok' } as never); host = document.createElement('div'); document.body.append(host); root = createRoot(host); }); afterEach(async () => { await act(async () => { root.unmount(); }); host.remove(); vi.restoreAllMocks(); vi.unstubAllGlobals(); resetEnginePreferencesForTests(); }); function Consumer(): ReactNode { const index = useComplianceWorkFacets(); return {index ? index.facets.map(facet => facet.values.length).join(',') : 'none'}; } function Harness({ withScope }: { withScope: boolean }): ReactNode { const app = useApp(); return <> {JSON.stringify(app.facetFilters)} {withScope && <>} ; } describe('Work ISO facet loading and the engine switch', () => { it('shares one request for every Work consumer and reloads after a control decision', async () => { const { loadCanonicalProjectWorkspace } = await import('../../src/features/project-import/load-canonical-project-workspace'); vi.mocked(loadCanonicalProjectWorkspace).mockResolvedValue({ assessmentIds: ['dummy-assessment'], models: [], dataset: { fileName: 'Dummy', scanTool: 'Generic', sheets: [workSheet()] } } as never); vi.spyOn(AssessorApiClient.prototype, 'getAssessmentContext').mockResolvedValue({} as never); const request = vi.spyOn(AssessorApiClient.prototype, 'getRegulatoryControlAssignments').mockResolvedValue(assignments); await act(async () => root.render()); await act(async () => { host.querySelector('button')!.click(); await new Promise(resolve => setTimeout(resolve, 0)); }); await act(async () => { await new Promise(resolve => setTimeout(resolve, 0)); }); expect(request).toHaveBeenCalledTimes(1); expect([...host.querySelectorAll('[data-iso-facets]')].map(node => node.textContent)).toEqual(['2,2', '2,2']); await act(async () => { markRegulatoryDecisionSaved(); await new Promise(resolve => setTimeout(resolve, 0)); }); expect(request).toHaveBeenCalledTimes(2); }); it('loads nothing and clears an active ISO filter when the Compliance Engine is switched off', async () => { const request = vi.spyOn(AssessorApiClient.prototype, 'getRegulatoryControlAssignments').mockResolvedValue(assignments); await act(async () => root.render()); await act(async () => host.querySelectorAll('button')[1].click()); expect(JSON.parse(host.querySelector('[data-filters]')!.textContent!)).toEqual({ [ISO_CONTROL_FACET_ID]: { values: ['8.20'] } }); await act(async () => { setEngineEnabled('compliance', false); }); expect(JSON.parse(host.querySelector('[data-filters]')!.textContent!)).toEqual({}); await act(async () => root.render()); expect(host.querySelector('[data-iso-facets]')?.textContent).toBe('none'); expect(request).not.toHaveBeenCalled(); }); });