109 lines
6.3 KiB
TypeScript
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]);
|
|
});
|
|
});
|