147 lines
8.9 KiB
TypeScript
147 lines
8.9 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 { resetEnginePreferencesForTests, setEngineEnabled } from '../../src/app/engine-preferences';
|
|
import type { BsiCatalogState, BsiSearchIndexStatus } from '../../src/compliance/bsi-catalog-management';
|
|
import { BsiCatalogLibrary } from '../../src/compliance/ui/BsiCatalogLibrary';
|
|
import { useRegulatoryDecisionRevision } from '../../src/compliance/ui/decision-revision';
|
|
|
|
// Data Hub BSI tile (user decision 2026-10-08): like the MITRE matrix tile, it names the active catalog and
|
|
// mapping and offers `Update from BSI`; `Use bundled catalog` appears only after an update. Synthetic states.
|
|
const bundled: BsiCatalogState = { version: 1, origin: 'bundled', catalogVersion: '2026-09-24T07:12:39.131983+00:00', crosswalkVersion: '1.0.1-qa',
|
|
requirementCount: 901, crosswalkEntryCount: 83, catalogSha256: 'a'.repeat(64), crosswalkSha256: 'b'.repeat(64), missingTargets: ['BER.6.8'] };
|
|
const updated: BsiCatalogState = { ...bundled, origin: 'bsi', updatedAt: '2026-10-08T12:00:00.000Z', catalogVersion: '2026-10-01T00:00:00+00:00',
|
|
crosswalkVersion: '1.0.2', missingTargets: ['BER.6.8', 'GEB.5.4'] };
|
|
|
|
let host: HTMLDivElement;
|
|
let root: Root;
|
|
let revisions: number[];
|
|
function Revision() { revisions.push(useRegulatoryDecisionRevision()); return null; }
|
|
const render = () => act(async () => {
|
|
root.render(<><BsiCatalogLibrary/><Revision/></>);
|
|
await new Promise(resolve => setTimeout(resolve, 0));
|
|
});
|
|
const badge = () => host.querySelector('[data-bsi-catalog] .datahub-rack-badge')?.textContent;
|
|
const button = (label: string) => [...host.querySelectorAll<HTMLButtonElement>('[data-bsi-catalog] button')].find(item => item.textContent === label);
|
|
|
|
beforeEach(() => {
|
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
|
localStorage.clear();
|
|
resetEnginePreferencesForTests();
|
|
revisions = [];
|
|
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('Data Hub BSI catalog tile', () => {
|
|
it('names the bundled catalog and mapping, updates from BSI and reloads the derived BSI views', async () => {
|
|
vi.spyOn(AssessorApiClient.prototype, 'getBsiCatalog').mockResolvedValue(bundled);
|
|
const refresh = vi.spyOn(AssessorApiClient.prototype, 'refreshBsiCatalog').mockResolvedValue({ ...updated, updated: true });
|
|
await render();
|
|
expect(host.querySelector('[data-bsi-catalog] .datahub-rack-header strong')?.textContent).toBe('BSI GRUNDSCHUTZ++ KERNEL');
|
|
expect(badge()).toBe('2026-09-24 · Mapping 1.0.1-qa · Bundled');
|
|
expect(button('Use bundled catalog')).toBeUndefined();
|
|
const before = revisions.at(-1)!;
|
|
await act(async () => button('Update from BSI')!.click());
|
|
expect(refresh).toHaveBeenCalledTimes(1);
|
|
expect(badge()).toBe('2026-10-01 · Mapping 1.0.2 · Updated');
|
|
expect(host.querySelector('[role="status"]')?.textContent).toBe('✓Updated to catalog 2026-10-01 and mapping 1.0.2. 2 mapping targets are not in the catalog.');
|
|
// Client Info and Work reload their derived BSI requirements.
|
|
expect(revisions.at(-1)).toBe(before + 1);
|
|
expect(button('Use bundled catalog')).toBeDefined();
|
|
});
|
|
|
|
it('says when the catalog is up to date without reloading other views', async () => {
|
|
vi.spyOn(AssessorApiClient.prototype, 'getBsiCatalog').mockResolvedValue(bundled);
|
|
vi.spyOn(AssessorApiClient.prototype, 'refreshBsiCatalog').mockResolvedValue({ ...bundled, updated: false });
|
|
await render();
|
|
const before = revisions.at(-1);
|
|
await act(async () => button('Update from BSI')!.click());
|
|
expect(host.querySelector('[role="status"]')?.textContent).toBe('✓The BSI catalog is up to date.');
|
|
expect(revisions.at(-1)).toBe(before);
|
|
});
|
|
|
|
it('returns to the bundled catalog and shows an update failure without changing the badge', async () => {
|
|
vi.spyOn(AssessorApiClient.prototype, 'getBsiCatalog').mockResolvedValue(updated);
|
|
const reset = vi.spyOn(AssessorApiClient.prototype, 'resetBsiCatalog').mockResolvedValue(bundled);
|
|
vi.spyOn(AssessorApiClient.prototype, 'refreshBsiCatalog').mockRejectedValue(new Error('BSI could not be reached. Please try again later.'));
|
|
await render();
|
|
await act(async () => button('Update from BSI')!.click());
|
|
expect(host.querySelector('[role="alert"]')?.textContent).toBe('BSI could not be reached. Please try again later.');
|
|
expect(badge()).toBe('2026-10-01 · Mapping 1.0.2 · Updated');
|
|
await act(async () => button('Use bundled catalog')!.click());
|
|
expect(reset).toHaveBeenCalledTimes(1);
|
|
expect(badge()).toBe('2026-09-24 · Mapping 1.0.1-qa · Bundled');
|
|
expect(host.querySelector('[role="status"]')?.textContent).toBe('✓The bundled BSI catalog is active again.');
|
|
expect(button('Use bundled catalog')).toBeUndefined();
|
|
});
|
|
|
|
it('shows the search index state and builds it from the Data Hub, following its progress', async () => {
|
|
vi.spyOn(AssessorApiClient.prototype, 'getBsiCatalog').mockResolvedValue(bundled);
|
|
const index = (state: BsiSearchIndexStatus['state'], extra: Partial<BsiSearchIndexStatus> = {}): BsiSearchIndexStatus => ({ version: 1, state,
|
|
catalogVersion: bundled.catalogVersion, corpusVersion: 'kernel-synthetic', requirementCount: 901, indexedChunks: 0, ...extra });
|
|
const status = vi.spyOn(AssessorApiClient.prototype, 'getBsiSearchIndex').mockResolvedValue(index('not-built'));
|
|
const build = vi.spyOn(AssessorApiClient.prototype, 'buildBsiSearchIndex').mockResolvedValue(index('building', { progress: { completed: 64, total: 1167 } }));
|
|
const indexLine = () => host.querySelector('[data-bsi-search-index]')?.textContent;
|
|
await render();
|
|
expect(indexLine()).toBe('Search index Not built · the first BSI mapping builds it otherwise');
|
|
vi.useFakeTimers();
|
|
try {
|
|
status.mockResolvedValue(index('built', { indexedChunks: 1167 }));
|
|
await act(async () => button('Build search index')!.click());
|
|
expect(build).toHaveBeenCalledWith(false);
|
|
expect(indexLine()).toBe('Search index Building · 64 of 1,167 chunks embedded');
|
|
expect(button('Building search index…')?.disabled).toBe(true);
|
|
expect(button('Update from BSI')?.disabled).toBe(true);
|
|
await act(async () => { await vi.advanceTimersByTimeAsync(2_000); });
|
|
expect(indexLine()).toBe('Search index Built · 1,167 chunks for 901 requirements');
|
|
} finally { vi.useRealTimers(); }
|
|
// A complete index is rebuilt on request.
|
|
await act(async () => button('Rebuild search index')!.click());
|
|
expect(build).toHaveBeenLastCalledWith(true);
|
|
});
|
|
|
|
it('names an outdated index and hides the build action while embedding is not configured', async () => {
|
|
vi.spyOn(AssessorApiClient.prototype, 'getBsiCatalog').mockResolvedValue(bundled);
|
|
const status = vi.spyOn(AssessorApiClient.prototype, 'getBsiSearchIndex').mockResolvedValue({ version: 1, state: 'outdated',
|
|
catalogVersion: bundled.catalogVersion, corpusVersion: 'kernel-synthetic', requirementCount: 901, indexedChunks: 0 });
|
|
await render();
|
|
expect(host.querySelector('[data-bsi-search-index]')?.textContent).toBe('Search index Outdated · built for another catalog state');
|
|
expect(button('Build search index')).toBeDefined();
|
|
status.mockResolvedValue({ version: 1, state: 'unavailable', catalogVersion: bundled.catalogVersion, corpusVersion: 'kernel-synthetic',
|
|
requirementCount: 901, indexedChunks: 0, disabledReason: 'Embedding or Qdrant is not configured.' });
|
|
await act(async () => root.unmount());
|
|
root = createRoot(host);
|
|
await render();
|
|
expect(button('Build search index')).toBeUndefined();
|
|
expect(host.querySelector('[data-bsi-catalog] .datahub-alert')?.textContent).toBe('Embedding or Qdrant is not configured.');
|
|
});
|
|
|
|
it('renders and requests nothing while the Compliance Engine is off', async () => {
|
|
const request = vi.spyOn(AssessorApiClient.prototype, 'getBsiCatalog').mockResolvedValue(bundled);
|
|
setEngineEnabled('compliance', false);
|
|
await render();
|
|
expect(host.querySelector('[data-bsi-catalog]')).toBeNull();
|
|
expect(request).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('sits in the Data Hub reference row directly after the ISO corpus tile', () => {
|
|
const source = readFileSync(join(process.cwd(), 'src/features/project-import/DataHub.tsx'), 'utf8');
|
|
expect(source).toContain(' <IsoCorpusLibrary/>\n <BsiCatalogLibrary/>\n');
|
|
const styles = readFileSync(join(process.cwd(), 'src/styles/datahub-redesign.css'), 'utf8');
|
|
expect(styles).toContain('.datahub-reference-row > .bsi-catalog-library { grid-area: 4 / 1 / 5 / -1; }');
|
|
});
|
|
});
|