Assessor_2.0/tests/compliance/client-iso-controls.test.tsx

109 lines
6.3 KiB
TypeScript

// @vitest-environment jsdom
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { act } 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 { requestActivity } from '../../src/api/request-activity';
import { resetEnginePreferencesForTests, setEngineEnabled } from '../../src/app/engine-preferences';
import type { RegulatoryControlOverviewResponse } from '../../src/compliance/regulatory-mapping';
import { ClientIsoControls } from '../../src/compliance/ui/ClientIsoControls';
import { markRegulatoryDecisionSaved } from '../../src/compliance/ui/decision-revision';
import { workRecordIdsForResult } from '../../src/features/results-browser/work-record-lookup';
import { makeSheet } from '../../src/importers/sheet-factory';
// Client Info `ISO controls`: one tile per accepted control with its number of findings, laid out like
// the Context heatmap; a tile opens exactly its findings in Work. Switched off, nothing renders or loads.
const overview: 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' }] },
{ controlId: '5.15', annexAControlId: 'A.5.15', theme: 'Organizational', findings: [
{ resultProjectionId: 'scanner-finding:dummy-c', sourceKind: 'scanner-finding', sourceResultId: 'dummy-c' }] }
]
};
let host: HTMLDivElement;
let root: Root;
const onOpenControl = vi.fn();
const render = () => act(async () => {
root.render(<ClientIsoControls projectId="dummy-project" assessmentIds={['dummy-assessment']} scanRunIds={[]}
backendStatus="connected" onOpenControl={onOpenControl}/>);
await new Promise(resolve => setTimeout(resolve, 0));
});
beforeEach(() => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
localStorage.clear();
resetEnginePreferencesForTests();
onOpenControl.mockReset();
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();
});
describe('Client Info ISO controls', () => {
it('shows each accepted control with its finding count and opens exactly its findings', async () => {
const request = vi.spyOn(AssessorApiClient.prototype, 'getRegulatoryControlOverview').mockResolvedValue(overview);
await render();
expect(request).toHaveBeenCalledWith('dummy-project', { assessmentIds: ['dummy-assessment'], scanRunIds: [] });
expect(host.querySelector('h2')?.textContent).toBe('ISO controls');
expect(host.querySelector('.iso-controls__total')?.textContent).toBe('Findings: 3');
const tiles = [...host.querySelectorAll<HTMLButtonElement>('.iso-controls__cell')];
expect(tiles.map(tile => [...tile.children].map(part => part.textContent))).toEqual([
['Technological', 'A.8.20 · Networks security', '2 findings'],
['Organizational', 'A.5.15', '1 finding']
]);
await act(async () => tiles[0].click());
expect(onOpenControl).toHaveBeenCalledWith('8.20');
});
it('reloads after a control decision and says so when nothing is accepted', async () => {
const request = vi.spyOn(AssessorApiClient.prototype, 'getRegulatoryControlOverview')
.mockResolvedValueOnce({ ...overview, findingCount: 0, controls: [] }).mockResolvedValue(overview);
await render();
expect(host.querySelector('.iso-controls__empty')?.textContent).toBe('No accepted ISO controls in this timeslot.');
await act(async () => { markRegulatoryDecisionSaved(); await new Promise(resolve => setTimeout(resolve, 0)); });
expect(request).toHaveBeenCalledTimes(2);
expect(host.querySelectorAll('.iso-controls__cell')).toHaveLength(2);
});
it('renders nothing and requests nothing while the Compliance Engine is off', async () => {
const request = vi.spyOn(AssessorApiClient.prototype, 'getRegulatoryControlOverview').mockResolvedValue(overview);
setEngineEnabled('compliance', false);
await render();
expect(host.innerHTML).toBe('');
expect(request).not.toHaveBeenCalled();
});
it('names the overview request in the activity log as Compliance Engine work', () => {
expect(requestActivity('/projects/p/regulatory-control-overview?assessmentIds=a')).toEqual({ area: 'Compliance Engine', label: 'Loading ISO control overview' });
expect(requestActivity('/projects/p/regulatory-control-assignments?assessmentIds=a')).toEqual({ area: 'Compliance Engine', label: 'Loading ISO control filters' });
});
it('places the section below the Context heatmap and opens Work through the ISO control filter', () => {
const source = readFileSync(join(process.cwd(), 'src/features/dashboard/DashboardPanel.tsx'), 'utf8');
expect(source.indexOf('<ClientIsoControls')).toBeGreaterThan(source.indexOf('<ClientContextHeatmap'));
expect(source.indexOf('<ClientIsoControls')).toBeLessThan(source.indexOf('client-environment-grid'));
const drilldown = source.slice(source.indexOf('<ClientIsoControls'), source.indexOf('client-environment-grid'));
expect(drilldown).toContain('setStatusFilter(\x27deviation\x27)');
expect(drilldown).toContain('setFacetFilter(ISO_CONTROL_FACET_ID, { values: [controlId] })');
expect(drilldown).not.toContain('setPathFindingIds');
// A scanner finding resolves to its Work record through the occurrence id.
const sheet = makeSheet('Dummy', [['Title', 'Status'], ['Dummy check', 'Fail']], '', 'dummy.csv', { isProjectSheet: true });
const record = { ...sheet.analysis.records[0], findingOccurrenceId: 'dummy-a' };
const dataset = { fileName: 'Dummy', sheets: [{ ...sheet, analysis: { ...sheet.analysis, records: [record] } }] } as never;
expect(workRecordIdsForResult(dataset, overview.controls[0].findings[0])).toEqual([record.id]);
});
});