// Abnahmetest der Mehr-Framework-Fähigkeit (ISO 27001 neben TISAX) — reiner Trockenlauf. // // Prüft die Zusagen aus docs/UEBERGABE-framework-iso27001.md, ohne etwas zu schreiben: // 1. Paketebene: beide Mappings lesen denselben Dokumentensatz; kein ISO-Umsetzungstext // ist leer; die Anforderungs-IDs beider Frameworks überschneiden sich nicht. // 2. Falle 1.1: Ein ISO-Import in einen Mandanten mit TISAX archiviert KEINE // TISAX-Anforderung (`report.requirements.archived === 0`). // 3. Geteilte Inhalte (Dokumente, Variablen, Baseline, Nachweisregister) werden bei einem // Mandanten, der das Paket bereits hat, nicht doppelt angelegt. // 4. Der Trockenlauf hinterlässt keine Spuren: Zählstände vor und nach dem Lauf identisch. // // Lauf: npx tsx scripts/test-framework-dryrun.ts // Nutzt die lokale Postgres-DB; .env liegt im Worktree. Ohne Mandanten läuft nur Teil 1. import "dotenv/config"; import { join } from "node:path"; import { prisma } from "../src/server/db"; import { parsePackageFiles, reconcilePackage } from "../prisma/import-policies"; const SEED = join(process.cwd(), "seed", "isms-vorlagenpaket-v2"); let failed = 0; function check(ok: boolean, label: string, detail = "") { if (!ok) failed++; console.log(` ${ok ? "✓" : "✗"} ${label}${detail ? " — " + detail : ""}`); } async function counts(tenantId: string) { return { tisax: await prisma.policyRequirement.count({ where: { tenantId, framework: "TISAX", archivedAt: null } }), tisaxArch: await prisma.policyRequirement.count({ where: { tenantId, framework: "TISAX", archivedAt: { not: null } } }), iso: await prisma.policyRequirement.count({ where: { tenantId, framework: "ISO_27001", archivedAt: null } }), docs: await prisma.policyDocument.count({ where: { tenantId, archivedAt: null } }), }; } async function main() { console.log("1. Paketebene"); const iso = parsePackageFiles(SEED, "mapping-iso.json"); const tis = parsePackageFiles(SEED, "mapping.json"); const isoCodes = iso.documents.map((d) => d.code).sort(); const tisCodes = tis.documents.map((d) => d.code).sort(); check(JSON.stringify(isoCodes) === JSON.stringify(tisCodes), "beide Mappings lesen denselben Dokumentensatz", `${isoCodes.length} Dokumente`); check(iso.requirements.length === 120, "ISO-Mapping enthält 120 Anforderungen", `${iso.requirements.length}`); check(iso.requirements.filter((r) => !r.implementation.trim()).length === 0, "kein ISO-Umsetzungstext ist leer"); check(iso.requirements.every((r) => !!r.control), "jede ISO-Anforderung trägt eine Control-Referenz"); const overlap = iso.requirements.map((r) => r.reqId).filter((id) => tis.requirements.some((t) => t.reqId === id)); check(overlap.length === 0, "Anforderungs-IDs beider Frameworks überschneiden sich nicht", overlap.length ? overlap.slice(0, 3).join(", ") : ""); // Englische Fassung: gleiche Struktur, übersetzte Texte. const SEED_EN = join(process.cwd(), "seed", "isms-vorlagenpaket-v2-en"); const isoEn = parsePackageFiles(SEED_EN, "mapping-iso.json"); check(isoEn.requirements.length === iso.requirements.length, "EN-Fassung enthält gleich viele ISO-Anforderungen", `${isoEn.requirements.length}`); check(JSON.stringify(isoEn.requirements.map((r) => r.reqId).sort()) === JSON.stringify(iso.requirements.map((r) => r.reqId).sort()), "EN- und DE-Fassung haben identische Anforderungs-IDs"); check(isoEn.requirements.filter((r) => !r.implementation.trim()).length === 0, "kein EN-Umsetzungstext ist leer"); check(isoEn.documents.length === iso.documents.length, "EN-Fassung hat denselben Dokumentenumfang", `${isoEn.documents.length}`); const tenants = await prisma.tenant.findMany({ select: { id: true, name: true, slug: true } }); if (!tenants.length) { console.log("\n(keine Mandanten in der DB — Teil 2 übersprungen)"); return; } for (const t of tenants) { console.log(`\n2. Trockenlauf ISO_27001 gegen „${t.name}" (${t.slug})`); const before = await counts(t.id); console.log(` vorher: TISAX=${before.tisax} (archiviert ${before.tisaxArch}) · ISO=${before.iso} · Dokumente=${before.docs}`); const { report } = await reconcilePackage(prisma, t.id, iso, { dryRun: true, framework: "ISO_27001" }); const r = report.requirements, d = report.documents; console.log(` Anforderungen: +${r.added} ~${r.updated} =${r.unchanged} archiviert=${r.archived}`); console.log(` Dokumente: +${d.added} ~${d.updated} =${d.unchanged} reaktiviert=${d.reactivated} archiviert=${d.archived}`); check(r.archived === 0, "Falle 1.1 — keine fremde Anforderung archiviert", `archived=${r.archived}`); check(r.added + r.unchanged + r.updated === 120, "alle 120 ISO-Anforderungen erfasst"); check(d.archived === 0, "kein geteiltes Dokument wird stillgelegt", `archived=${d.archived}`); check(report.variables.obsolete === 0 && report.baseline.obsolete === 0 && report.evidence.obsolete === 0, "geteilte Inhalte verlieren nichts (Variablen, Baseline, Nachweisregister)"); // Mandant hat das Paket bereits: jedes vorhandene Paket-Dokument muss wiedererkannt // werden; angelegt wird nur, was im Mandanten wirklich fehlt. const vorhanden = await prisma.policyDocument.findMany({ where: { tenantId: t.id, code: { in: iso.documents.map((x) => x.code) } }, select: { code: true }, }); // Ein vorhandenes Dokument zählt je nach Zustand als unchanged, updated ODER // reactivated (wenn es beim Mandanten stillgelegt war) — alle drei sind „wiedererkannt". const wiedererkannt = d.unchanged + d.updated + d.reactivated; check(wiedererkannt === vorhanden.length, "jedes vorhandene Paket-Dokument wird wiedererkannt", `vorhanden=${vorhanden.length} unchanged=${d.unchanged} updated=${d.updated} reaktiviert=${d.reactivated}`); check(d.added === iso.documents.length - vorhanden.length, "angelegt wird nur, was im Mandanten fehlt", `added=${d.added} erwartet=${iso.documents.length - vorhanden.length}`); const after = await counts(t.id); check(JSON.stringify(before) === JSON.stringify(after), "Trockenlauf hat nichts geschrieben", JSON.stringify(after)); } } main() .catch((e) => { console.error(e); failed++; }) .finally(async () => { await prisma.$disconnect(); console.log(`\n${failed === 0 ? "OK — alle Prüfungen bestanden" : `FEHLGESCHLAGEN — ${failed} Prüfung(en)`}`); process.exit(failed === 0 ? 0 : 1); });