Assessor_2.0/tests/features/client-work-packages-tab.te...

235 lines
20 KiB
TypeScript

// @vitest-environment jsdom
import { act } from 'react';
import { createRoot } from 'react-dom/client';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { useApp } from '../../src/app/AppProvider';
import { installTableRowMarking } from '../../src/app/table-row-marking';
import { resetEnginePreferencesForTests, setEngineEnabled } from '../../src/app/engine-preferences';
import { AssessorApiClient } from '../../src/api/assessor-api-client';
import type { WorkPackagesResponse } from '../../src/api/assessor-api-contract';
import { DashboardPanel } from '../../src/features/dashboard/DashboardPanel';
import { makeSheet } from '../../src/importers/sheet-factory';
import { buildWorkPackages, scannerFindingKey } from '../../src/remediation/remediation-engine';
import { workRecordKey } from '../../src/remediation/work-package-index';
import { resetWorkPackageRequestsForTests } from '../../src/remediation/use-work-packages';
import { clientFactsSnapshot, clientFactsState } from '../fixtures/client-info-facts';
vi.mock('../../src/app/AppProvider', () => { const useApp = vi.fn(); return { useApp, useOptionalApp: useApp }; });
vi.mock('../../src/features/search-filter/WorkTimeslotControl', () => ({ WorkTimeslotControl: () => <span>Synthetic assessment selector</span> }));
vi.mock('../../src/features/context-layer/use-context-cluster-index', () => ({ useContextClusterIndex: () => undefined }));
function fixture(projectId = 'synthetic-project', assessmentId = 'synthetic-assessment') {
const sheet = makeSheet('Synthetic', [['Name', 'Status', 'Severity', 'Source Rule ID', 'Source: Scan', 'Description', 'Remediation'],
['Synthetic admin A', 'Failed', 'High', 'MT.1014', 'Maester', 'Synthetic explanation A', 'Synthetic exact remediation A'],
['Synthetic admin B', 'Failed', 'High', 'CISA.MS.AAD.3.6', 'Maester', 'Synthetic explanation B', 'Synthetic exact remediation B'],
['Synthetic external control', 'Failed', 'Medium', 'MT.1056', 'Maester', 'Synthetic external explanation', 'Synthetic external remediation'],
['Synthetic unassigned control', 'Failed', 'Low', 'SYNTHETIC.UNASSIGNED', 'Maester', 'Synthetic unassigned explanation', 'Synthetic unassigned remediation']
], '', 'synthetic.csv', { isProjectSheet: true });
sheet.analysis.records.forEach((record, index) => { record.findingOccurrenceId = `${projectId}-occ-${index}`; });
const findings = sheet.analysis.records.map(record => ({ key: scannerFindingKey(record.findingOccurrenceId!), occurrenceId: record.findingOccurrenceId,
tool: 'maester', ruleId: record.fields[3].value, title: record.title, outcome: 'fail' }));
const result = buildWorkPackages({ projectId, assessmentIds: [assessmentId], scanRunIds: ['synthetic-run'], findings,
context: { engine: { version: '1' }, catalog: { version: '1' }, repositoryRevision: 1,
clusters: [{ clusterId: 'cloud-privileged-access-exposure', label: { en: 'Cloud Privileged Access Exposure' } },
{ clusterId: 'cloud-external-access-exposure', label: { en: 'Cloud External Access Exposure' } }],
findings: findings.slice(0, 3).map((finding, index) => ({ occurrenceId: finding.occurrenceId!,
clusterIds: [index < 2 ? 'cloud-privileged-access-exposure' : 'cloud-external-access-exposure'], conditionIds: [] })), settings: [] }
});
const app = { ...clientFactsState(), targetProjectId: projectId, activeAssessmentIds: [assessmentId], activeSheet: sheet,
dataset: { fileName: `Synthetic ${projectId}`, sheets: [sheet], scanTool: 'Generic' },
search: 'retained Work search', statusFilter: 'passed', facetFilters: { severity: { values: ['High'] } }, setFacetFilter: vi.fn() };
return { app, result };
}
function setup() {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); localStorage.clear(); resetEnginePreferencesForTests(); resetWorkPackageRequestsForTests();
setEngineEnabled('context', false);
vi.spyOn(AssessorApiClient.prototype, 'getEnvironmentSnapshot').mockResolvedValue(clientFactsSnapshot);
vi.spyOn(AssessorApiClient.prototype, 'listProjectGpoBackupStatus').mockResolvedValue({ records: [] } as never);
return vi.spyOn(AssessorApiClient.prototype, 'getAssessmentContext');
}
afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); document.body.replaceChildren(); localStorage.clear(); resetEnginePreferencesForTests(); resetWorkPackageRequestsForTests(); });
describe.each(['light', 'dark'])('Client Info Work Packages (%s)', theme => {
it('opens packages and original finding details without marking any Work Packages table row blue', async () => {
setup(); document.documentElement.dataset.theme = theme;
const { app, result } = fixture();
vi.mocked(useApp).mockReturnValue(app as unknown as ReturnType<typeof useApp>);
vi.spyOn(AssessorApiClient.prototype, 'getWorkPackages').mockResolvedValue({ status: 'ready', result });
const original = JSON.stringify(app.dataset); const openWork = vi.fn();
const host = document.createElement('div'); document.body.append(host); const root = createRoot(host);
const stopMarking = installTableRowMarking(document);
try {
await act(async () => root.render(<DashboardPanel onOpenWork={openWork}/>));
await act(async () => [...host.querySelectorAll<HTMLButtonElement>('[role="tab"]')].find(tab => tab.textContent === 'Work Packages')!.click());
const panel = host.querySelector<HTMLElement>('[data-group-scroll="client-info-work-packages"]')!;
const packageButton = panel.querySelector<HTMLButtonElement>('[data-work-package-select]')!;
await act(async () => packageButton.click());
expect(packageButton.getAttribute('aria-expanded')).toBe('true');
const finding = panel.querySelector<HTMLButtonElement>('.client-security-finding-toggle')!;
await act(async () => finding.click());
expect(finding.getAttribute('aria-expanded')).toBe('true');
const details = panel.querySelector<HTMLElement>('.client-security-finding-detail')!;
const record = app.activeSheet.analysis.records.find(record => record.title === finding.querySelector('strong')!.textContent)!;
expect(details.textContent).toContain(record.fields[5].value);
expect(details.textContent).toContain(record.fields[6].value);
await act(async () => details.click());
expect(panel.querySelector('[data-row-marked="true"]')).toBeNull();
await act(async () => panel.querySelector<HTMLButtonElement>('[data-work-package-unassigned-summary]')!.click());
expect(panel.querySelector('[data-work-package-unassigned-details]')?.textContent).toContain('Synthetic unassigned control');
expect(panel.querySelector('[data-row-marked="true"]')).toBeNull();
// Other tables retain the established reading marker.
const table = document.createElement('table'); table.innerHTML = '<tbody><tr><td>Other table</td></tr></tbody>'; host.append(table);
table.querySelector('td')!.click(); expect(table.querySelector('tr')!.getAttribute('data-row-marked')).toBe('true');
expect(openWork).not.toHaveBeenCalled(); expect(JSON.stringify(app.dataset)).toBe(original);
} finally { stopMarking(); await act(async () => root.unmount()); host.remove(); }
});
it('keeps Security and Work Packages unchanged after all Work/Graph filters and the selected source sheet change', async () => {
setup(); document.documentElement.dataset.theme = theme;
const { app, result } = fixture();
vi.mocked(useApp).mockReturnValue(app as unknown as ReturnType<typeof useApp>);
const packages = vi.spyOn(AssessorApiClient.prototype, 'getWorkPackages').mockResolvedValue({ status: 'ready', result });
const openWork = vi.fn(); const original = JSON.stringify(app.dataset);
const host = document.createElement('div'); document.body.append(host); const root = createRoot(host);
const render = () => root.render(<DashboardPanel onOpenWork={openWork}/>);
const tab = (name: string) => [...host.querySelectorAll<HTMLButtonElement>('[role="tab"]')].find(button => button.textContent === name)!;
try {
await act(async () => render()); await act(async () => tab('Security').click());
const security = host.querySelector<HTMLElement>('[data-group-scroll="client-info-security"]')!;
await act(async () => security.querySelector<HTMLButtonElement>('.client-security-group-toggle')!.click());
expect(security.querySelectorAll('.client-security-finding-toggle')).toHaveLength(4);
expect(security.querySelector('.client-security-remediation strong')?.textContent).toBe('4');
const securityContent = security.textContent;
await act(async () => tab('Work Packages').click());
const wp = host.querySelector<HTMLElement>('[data-group-scroll="client-info-work-packages"]')!;
await act(async () => wp.querySelector<HTMLButtonElement>('[data-work-package-select]')!.click());
const wpContent = wp.textContent;
const subset = makeSheet('Selected source subset', [['Name', 'Status'], ['Only a source subset', 'Passed']], '', 'subset.csv');
const changed = { ...app, activeSheet: subset, activeProfile: { ...app.activeProfile, matchesStatus: () => false },
search: 'no matching title', statusFilter: 'passed', actionOnly: true, pathFindingIds: ['absent graph member'],
facetFilters: { 'provenance:source-scan': { values: ['Absent scanner'] }, severity: { values: ['Critical'] },
'context:cluster': { values: ['absent cluster'] }, 'report:business-impact': { values: ['absent impact'] },
'mitre:technique': { values: ['T0000'] }, arbitrary: { values: ['absent'] } } };
vi.mocked(useApp).mockReturnValue(changed as unknown as ReturnType<typeof useApp>);
await act(async () => render());
expect(wp.textContent).toBe(wpContent); expect(packages).toHaveBeenCalledTimes(1);
await act(async () => tab('Security').click());
expect(security.textContent).toBe(securityContent);
expect(security.querySelectorAll('.client-security-finding-toggle')).toHaveLength(4);
expect(openWork).not.toHaveBeenCalled(); expect(app.setFacetFilter).not.toHaveBeenCalled();
expect(JSON.stringify(app.dataset)).toBe(original);
expect(changed.facetFilters['provenance:source-scan'].values).toEqual(['Absent scanner']);
} finally { await act(async () => root.unmount()); host.remove(); }
});
it('opens the third tab lazily, retains filters and expands exact package findings and their original details locally', async () => {
const context = setup(); document.documentElement.dataset.theme = theme;
const { app, result } = fixture(); vi.mocked(useApp).mockReturnValue(app as unknown as ReturnType<typeof useApp>);
const packages = vi.spyOn(AssessorApiClient.prototype, 'getWorkPackages').mockResolvedValue({ status: 'ready', result });
const original = JSON.stringify({ app, result }); const openWork = vi.fn();
const host = document.createElement('div'); document.body.append(host); const root = createRoot(host);
try {
await act(async () => root.render(<DashboardPanel onOpenWork={openWork}/>));
const tabs = [...host.querySelectorAll<HTMLButtonElement>('[role="tab"]')];
expect(tabs.map(tab => tab.textContent)).toEqual(['Overview', 'Security', 'Work Packages']);
expect(packages).not.toHaveBeenCalled();
await act(async () => tabs[1].dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true })));
const panel = host.querySelector<HTMLElement>('[data-group-scroll="client-info-work-packages"]')!;
expect(panel.hidden).toBe(false); expect(document.activeElement).toBe(tabs[2]);
expect(packages).toHaveBeenCalledWith('synthetic-project', ['synthetic-assessment'], ['synthetic-run']);
expect(context).not.toHaveBeenCalled();
expect(panel.querySelector('.floating-data-window')).toBeNull();
expect(panel.querySelectorAll('[data-work-package-row]')).toHaveLength(result.packages.length);
expect(panel.querySelector('[aria-label="Package totals"]')?.textContent).toContain('Findings4');
const filter = panel.querySelector<HTMLSelectElement>('[aria-label="Filter Package"]')!;
const selectedPackage = result.packages[0];
await act(async () => { filter.value = selectedPackage.id; filter.dispatchEvent(new Event('change', { bubbles: true })); });
expect(panel.querySelectorAll('[data-work-package-row]')).toHaveLength(1);
await act(async () => panel.querySelector<HTMLButtonElement>('[data-sort-column="findings"]')!.click());
await act(async () => tabs[0].click());
await act(async () => tabs[2].click());
expect(filter.value).toBe(selectedPackage.id); expect(packages).toHaveBeenCalledTimes(1);
expect(panel.querySelector('[aria-sort="descending"] button')?.textContent).toBe('Findings');
await act(async () => panel.querySelector<HTMLButtonElement>(`[data-work-package-select="${selectedPackage.id}"]`)!.click());
const packageButton = panel.querySelector<HTMLButtonElement>(`[data-work-package-select="${selectedPackage.id}"]`)!;
const detailRow = panel.querySelector<HTMLTableRowElement>(`[data-work-package-details="${selectedPackage.id}"]`)!;
expect(packageButton.getAttribute('aria-expanded')).toBe('true'); expect(detailRow.hidden).toBe(false);
expect(detailRow.previousElementSibling?.getAttribute('data-work-package-row')).toBe(selectedPackage.id);
const expected = app.activeSheet.analysis.records.filter(record => selectedPackage.findingKeys.includes(workRecordKey(record)));
expect([...detailRow.querySelectorAll('.client-security-finding-toggle strong')].map(title => title.textContent)).toEqual(expected.map(record => record.title));
const finding = detailRow.querySelector<HTMLButtonElement>('.client-security-finding-toggle')!;
await act(async () => finding.click());
expect(finding.getAttribute('aria-expanded')).toBe('true');
expect(detailRow.textContent).toContain(expected[0].fields[5].value);
expect(detailRow.textContent).toContain(expected[0].fields[6].value);
await act(async () => tabs[0].click()); await act(async () => tabs[2].click());
expect(packageButton.getAttribute('aria-expanded')).toBe('true'); expect(finding.getAttribute('aria-expanded')).toBe('true');
await act(async () => packageButton.click());
expect(detailRow.hidden).toBe(true); expect(detailRow.querySelector('.client-security-finding-toggle')).toBeNull();
await act(async () => panel.querySelector<HTMLButtonElement>('[data-work-package-unassigned-summary]')!.click());
expect(panel.querySelector('[data-work-package-unassigned-details]')?.textContent).toContain('Synthetic unassigned control');
expect(filter.value).toBe('');
expect(panel.querySelector('[title*="in Work"]')).toBeNull();
expect(app.setFacetFilter).not.toHaveBeenCalled(); expect(openWork).not.toHaveBeenCalled();
expect(JSON.stringify({ app, result })).toBe(original);
} finally { await act(async () => root.unmount()); host.remove(); }
});
it('clears the old scope while loading another client/assessment and suppresses late packages when Remediation is disabled', async () => {
const context = setup(); document.documentElement.dataset.theme = theme;
const first = fixture(), second = fixture('synthetic-other-client', 'synthetic-other-assessment');
vi.mocked(useApp).mockReturnValue(first.app as unknown as ReturnType<typeof useApp>);
let finish!: (response: WorkPackagesResponse) => void;
const packages = vi.spyOn(AssessorApiClient.prototype, 'getWorkPackages').mockResolvedValueOnce({ status: 'ready', result: first.result })
.mockImplementationOnce(() => new Promise(resolve => { finish = resolve; }));
const host = document.createElement('div'); document.body.append(host); const root = createRoot(host);
const render = () => root.render(<DashboardPanel onOpenWork={() => {}}/>);
try {
await act(async () => render());
await act(async () => [...host.querySelectorAll<HTMLButtonElement>('[role="tab"]')].find(tab => tab.textContent === 'Work Packages')!.click());
expect(host.querySelectorAll('[data-work-package-row]')).toHaveLength(first.result.packages.length);
vi.mocked(useApp).mockReturnValue(second.app as unknown as ReturnType<typeof useApp>);
await act(async () => render());
expect(packages).toHaveBeenLastCalledWith('synthetic-other-client', ['synthetic-other-assessment'], ['synthetic-run']);
expect(host.querySelector('[data-work-package-row]')).toBeNull();
expect(host.textContent).toContain('Loading work packages');
await act(async () => setEngineEnabled('remediation', false));
expect([...host.querySelectorAll('[role="tab"]')].map(tab => tab.textContent)).toEqual(['Overview', 'Security']);
expect(host.querySelector('[role="tabpanel"]:not([hidden])')?.getAttribute('data-group-scroll')).toBe('client-info-overview');
await act(async () => finish({ status: 'ready', result: second.result }));
expect(host.querySelector('[data-work-package-row]')).toBeNull();
expect(packages).toHaveBeenCalledTimes(2); expect(context).not.toHaveBeenCalled();
} finally { await act(async () => root.unmount()); host.remove(); }
});
it('reads all exact package and same-change members from the project Work sheet even when another source sheet is selected', async () => {
setup(); document.documentElement.dataset.theme = theme;
const { app, result } = fixture();
const grouped = result.packages.find(p => p.findingKeys.length === 2)!;
grouped.tasks = [{ id: 'synthetic-same-change', findingKeys: [...grouped.findingKeys], redundant: true }];
grouped.facts.tasks = 1; grouped.facts.redundantFindings = 2;
const sourceSheet = makeSheet('Synthetic source subset', [['Name', 'Status'], ['A different source row', 'Passed']], '', 'source.csv');
app.dataset.sheets.push(sourceSheet); app.activeSheet = sourceSheet;
vi.mocked(useApp).mockReturnValue(app as unknown as ReturnType<typeof useApp>);
vi.spyOn(AssessorApiClient.prototype, 'getWorkPackages').mockResolvedValue({ status: 'ready', result });
const original = JSON.stringify({ app, result }); const openWork = vi.fn();
const host = document.createElement('div'); document.body.append(host); const root = createRoot(host);
try {
await act(async () => root.render(<DashboardPanel onOpenWork={openWork}/>));
await act(async () => [...host.querySelectorAll<HTMLButtonElement>('[role="tab"]')].find(tab => tab.textContent === 'Work Packages')!.click());
await act(async () => host.querySelector<HTMLButtonElement>(`[data-work-package-select="${grouped.id}"]`)!.click());
const details = host.querySelector(`[data-work-package-details="${grouped.id}"]`)!;
expect([...details.querySelectorAll('.client-security-finding-toggle strong')].map(title => title.textContent).sort()).toEqual(['Synthetic admin A', 'Synthetic admin B']);
expect(details.textContent).not.toContain('A different source row');
await act(async () => host.querySelector<HTMLButtonElement>('[data-work-package-related-summary]')!.click());
const related = host.querySelector<HTMLElement>('[data-work-package-related-summary]')!;
const relatedDetails = document.getElementById(related.getAttribute('aria-controls')!)!;
expect(relatedDetails.hidden).toBe(false);
expect(relatedDetails.querySelectorAll('.client-security-finding-toggle')).toHaveLength(2);
await act(async () => related.click()); expect(relatedDetails.hidden).toBe(true);
expect(app.setFacetFilter).not.toHaveBeenCalled(); expect(openWork).not.toHaveBeenCalled();
expect(JSON.stringify({ app, result })).toBe(original);
} finally { await act(async () => root.unmount()); host.remove(); }
});
});