51 lines
2.8 KiB
TypeScript
51 lines
2.8 KiB
TypeScript
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/);
|
|
});
|
|
});
|