// @vitest-environment jsdom import { describe, expect, it } from 'vitest'; import { profileForSheet } from '../../src/app/AppProvider'; import cisCatFixture from '../../fixtures/cis-cat/assessment.synthetic.html?raw'; import maesterFixture from '../../fixtures/maester/test-results.synthetic.csv?raw'; import pingCastleFixture from '../../fixtures/pingcastle/healthcheck.synthetic.html?raw'; import { importFile } from '../../src/importers/importer'; import { confirmEntryMappings, previewEntryFromDataset } from '../../src/features/import-workflow/import-workflow'; import { genericProfile } from '../../src/profiles/generic/generic-profile'; import { importProfiles } from '../../src/profiles/profile-catalog'; import { toolProfileDefinitions } from '../../src/profiles/profile-definition-catalog'; const options = { profiles: importProfiles }; describe('declarative tool profiles', () => { it('provides a unique, versioned definition for every runtime profile', () => { expect(new Set(toolProfileDefinitions.map(definition => definition.id)).size).toBe(toolProfileDefinitions.length); expect(importProfiles.map(profile => profile.id)).toEqual(toolProfileDefinitions.map(definition => definition.id)); for (const profile of importProfiles) { expect(profile.version).toMatch(/^\d+\.\d+\.\d+$/); expect(profile.definition).toMatchObject({ schemaVersion: 1, id: profile.id, name: profile.name, version: profile.version }); expect(profile.definition.fieldRules.length).toBeGreaterThan(0); expect(profile.definition.comparisonKeys.length).toBeGreaterThan(0); expect(profile.definition.comparisonKeys.every(key => key.includes('rule-id'))).toBe(true); expect(profile.defaults(undefined).statusFilter).toBe('deviation'); expect(profile.presentation.statusFilters.map(filter => filter.value)).toContain('deviation'); } expect(genericProfile.definition.fallback).toBe(true); }); it.each([ { name: 'PingCastle', fileName: 'ad_hc_synthetic.example.html', type: 'text/html', content: pingCastleFixture, profileId: 'pingcastle', firstSheet: 'Risk Scores' }, { name: 'CIS-CAT', fileName: 'CIS-CAT-SYNTHETIC.html', type: 'text/html', content: cisCatFixture, profileId: 'cis-cat', firstSheet: 'assessmentResultTable' }, { name: 'Maester', fileName: 'TestResults-synthetic.csv', type: 'text/csv', content: maesterFixture, profileId: 'maester', firstSheet: 'CSV' } ])('selects and applies the $name profile', async ({ fileName, type, content, profileId, firstSheet }) => { const imported = await importFile(new File([content], fileName, { type }), options); expect(imported.profileId).toBe(profileId); // Gegen den Katalog pruefen statt gegen eine feste Zahl: die Zusage ist, // dass die tatsaechliche Profilversion im Importergebnis landet - ein // fachlich begruendeter Versionssprung darf diesen Test nicht brechen. expect(imported.profileVersion).toBe(importProfiles.find(profile => profile.id === profileId)?.version); expect(imported.sheets[0].name).toBe(firstSheet); expect(imported.scanTool).toBe(importProfiles.find(profile => profile.id === profileId)?.name); }); it('uses the sheet tool profile inside a mixed project workspace', async () => { const imported = await importFile(new File([cisCatFixture], 'CIS-CAT-SYNTHETIC.html', { type: 'text/html' }), options); const sheet = { ...imported.sheets[0], toolId: imported.profileId }; const mixedDataset = { ...imported, sheets: [sheet], profileId: 'generic', profileVersion: 'gemischt' }; expect(sheet.toolId).toBe('cis-cat'); expect(profileForSheet(mixedDataset, sheet).id).toBe('cis-cat'); expect(profileForSheet(mixedDataset, sheet).defaults(sheet).titleColumn).toBe('Benchmark Item'); }); it('combines the status filters of every tool in a mixed timeslot aggregate', async () => { const imported = await importFile(new File([cisCatFixture], 'CIS-CAT-SYNTHETIC.html', { type: 'text/html' }), options); const aggregate = { ...imported.sheets[0], id: 'aggregate', toolId: undefined, isProjectSheet: true }; const maesterSheet = { ...imported.sheets[0], id: 'maester-sheet', toolId: 'maester' }; const purpleKnightSheet = { ...imported.sheets[0], id: 'purple-sheet', toolId: 'purple-knight' }; const mixedDataset = { ...imported, sheets: [aggregate, maesterSheet, purpleKnightSheet], profileId: 'generic', profileVersion: 'gemischt' }; const aggregateProfile = profileForSheet(mixedDataset, aggregate); const filterValues = aggregateProfile.presentation.statusFilters.map(filter => filter.value); expect(aggregateProfile.id).toBe('multiple'); expect(filterValues).toContain('investigate'); expect(filterValues).toContain('ioes-found'); expect(profileForSheet(mixedDataset, maesterSheet).id).toBe('maester'); expect(profileForSheet(mixedDataset, purpleKnightSheet).id).toBe('purple-knight'); }); it('selects Purple Knight through the same profile factory and keeps status filters', async () => { const text = [ 'Name;Short Name;SI Version;Severity;Weight;Category;Result', 'Synthetic indicator;SYN-001;1;High;10;Identity;IOEs found' ].join('\n'); const imported = await importFile(new File([text], 'PurpleKnight-results.csv', { type: 'text/csv' }), options); const profile = importProfiles.find(candidate => candidate.id === imported.profileId); expect(imported.profileId).toBe('purple-knight'); // 1.1.0 seit die Importpipeline das deklarierte Status-Mapping anwendet // (siehe purple-knight-definition.ts und src/importers/status-normalization.ts). expect(imported.profileVersion).toBe('1.1.0'); expect(profile?.presentation.statusFilters.map(filter => filter.value)).toContain('ioes-found'); expect(profile?.matchesStatus(imported.sheets[0].analysis.records[0], 'ioes-found')).toBe(true); expect(profile?.matchesStatus(imported.sheets[0].analysis.records[0], 'deviation')).toBe(true); }); it('requires confirmation and keeps the generic profile when only the file name identifies a tool', async () => { const imported = await importFile(new File(['Alpha;Beta\none;two'], 'CIS-CAT-SYNTHETIC.csv', { type: 'text/csv' }), options); expect(imported.recognition?.source).toMatchObject({ selected: { toolId: 'cis-cat' }, requiresConfirmation: true }); expect(imported.profileId).toBe('generic'); expect(imported.mapping?.mappingProfile.id).toBe('generic'); const preview = previewEntryFromDataset(imported, { sha256: 'c'.repeat(64), fileName: imported.fileName, sizeBytes: imported.fileSize, lastModified: imported.lastModified }); const confirmed = confirmEntryMappings(preview, importProfiles); expect(confirmed.dataset).toMatchObject({ profileId: 'cis-cat', scanTool: 'CIS-CAT Pro' }); expect(confirmed.dataset?.mapping?.mappingProfile).toMatchObject({ kind: 'mapping-profile', id: 'cis-cat', version: importProfiles.find(profile => profile.id === 'cis-cat')?.version }); expect(confirmed.dataset?.recognition?.source.decisionSource).toBe('user-confirmed'); }); it('keeps an unknown table on the generic fallback profile', async () => { const imported = await importFile(new File(['Alpha;Beta\none;two'], 'unknown.csv', { type: 'text/csv' }), options); expect(imported.profileId).toBe('generic'); expect(imported.profileVersion).toBe('1.0.0'); expect(imported.sheets[0].rows[1]).toEqual(['one', 'two']); }); });