import { describe, expect, it } from 'vitest'; import { parseIso27002Controls, validateIsoControlSets, type ParsedControl, type ParsedPdf, type PdfLine } from '../../server/compliance/iso-pdf-corpus'; const line = (text: string, page: number): PdfLine => ({ text, page, y: 500, spans: [{ text, page, x: 100, y: 500, width: text.length * 5, height: 10 }] }); function controls(): ParsedControl[] { const counts = [[5, 37], [6, 8], [7, 14], [8, 34]]; return counts.flatMap(([theme, count]) => Array.from({ length: count }, (_, index) => ({ controlId: `${theme}.${index + 1}`, title: `Synthetic ${theme}.${index + 1}`, theme: theme === 5 ? 'organizational' as const : theme === 6 ? 'people' as const : theme === 7 ? 'physical' as const : 'technological' as const, sections: [{ kind: 'control' as const, text: 'Synthetic statement.', pageStart: 1, pageEnd: 1 }] }))); } describe('ISO PDF corpus structure parser', () => { it('keeps sections inside one control, preserves umlauts, and spans multi-page guidance', () => { const pdf: ParsedPdf = { sha256: 'a'.repeat(64), pageCount: 2, pages: [ { page: 1, lines: [ line('5.1 Informationssicherheitsrichtlinien', 1), line('Maßnahmenart', 1), line('präventiv', 1), line('Maßnahme', 1), line('Synthetische Maßnahme.', 1), line('Zweck', 1), line('Synthetischer Zweck.', 1), line('Anleitung', 1), line('Mehrseitige Anleitung mit Umlauten äöü.', 1) ] }, { page: 2, lines: [line('DIN EN ISO/IEC 27002:2024-01', 2), line('Fortsetzung der Anleitung.', 2), line('Weitere Informationen', 2), line('Synthetische Referenz.', 2)] } ] }; const parsed = parseIso27002Controls(pdf); expect(parsed).toHaveLength(1); expect(parsed[0].sections.map(section => section.kind)).toEqual(['attributes', 'control', 'purpose', 'guidance', 'other-information']); expect(parsed[0].sections.find(section => section.kind === 'guidance')).toMatchObject({ pageStart: 1, pageEnd: 2 }); expect(parsed[0].sections.find(section => section.kind === 'guidance')?.text).toContain('äöü'); expect(parsed[0].sections.some(section => section.text.includes('DIN EN'))).toBe(false); }); it('rejects incomplete, duplicate, and mismatched 93-control catalogs', () => { const reference = controls(); const semantic = controls(); expect(() => validateIsoControlSets(reference, semantic)).not.toThrow(); expect(() => validateIsoControlSets(reference.slice(1), semantic)).toThrow(/exactly 93/); expect(() => validateIsoControlSets([...reference.slice(0, -1), reference[0]], semantic)).toThrow(/unique controls/); expect(() => validateIsoControlSets(reference, [...semantic.slice(0, -1), { ...semantic.at(-1)!, controlId: '8.35' }])).toThrow(/identifiers do not match/); }); });