// Reiner Test der Fristen-/Timer-Logik für Vorfälle (IM-B, Fachkonzept §6). // // Prüft: // 1. Fristen aus dem Kenntniszeitpunkt (detectedAt/reportedAt) korrekt berechnet. // 2. NIS2-Timer greifen NUR bei nis2Category ∈ {wichtig, wesentlich} UND nis2Relevant. // 3. DSGVO-Frist (72 h) greift bei Personenbezug (dsgvoRelevant). // 4. Interne SLA je Severity (Reaktions-/Behebungsfrist). // 5. Überfällig-Erkennung + offene/erledigte Meldestufen (reportStatus). // // Lauf: npx tsx scripts/test-incident-deadlines.ts (rein, keine DB). import { addHours, addMonths, computeDeadlines, computeSla, deadlineItems, isDueSoon, isOverdue, isReportable, knowledgeTime, nextReportStatus, nis2TimersApply, openReportDeadlines, remainingMs, slaForSeverity, stageDone, NIS2_ERSTMELDUNG_HOURS, NIS2_FOLGEMELDUNG_HOURS, } from "../src/lib/incident-deadlines"; let failures = 0; const ok = (cond: boolean, msg: string) => { console.log(`${cond ? "✓" : "✗ FEHLER"} ${msg}`); if (!cond) failures++; }; const HOUR = 3_600_000; const known = new Date("2026-03-10T08:00:00.000Z"); // ── 1. Kenntniszeitpunkt-Priorität ────────────────────────────────────────── { const detected = new Date("2026-03-10T08:00:00Z"); const reported = new Date("2026-03-11T08:00:00Z"); ok( knowledgeTime({ detectedAt: detected, reportedAt: reported })?.getTime() === detected.getTime(), "knowledgeTime: detectedAt hat Vorrang vor reportedAt", ); ok( knowledgeTime({ detectedAt: null, reportedAt: reported })?.getTime() === reported.getTime(), "knowledgeTime: ohne detectedAt greift reportedAt", ); ok( knowledgeTime({ detectedAt: null, reportedAt: null, occurredAt: null, createdAt: known })?.getTime() === known.getTime(), "knowledgeTime: Fallback auf createdAt", ); } // ── 1b. Fristen aus Kenntniszeitpunkt (NIS2 wesentlich) ───────────────────── { const d = computeDeadlines({ nis2Category: "wesentlich", nis2Relevant: true, dsgvoRelevant: false, knownAt: known, }); ok( d.erstmeldungDueAt?.getTime() === known.getTime() + NIS2_ERSTMELDUNG_HOURS * HOUR, "NIS2 Erstmeldung = Kenntnis + 24 h", ); ok( d.folgemeldungDueAt?.getTime() === known.getTime() + NIS2_FOLGEMELDUNG_HOURS * HOUR, "NIS2 Folgemeldung = Kenntnis + 72 h", ); ok(d.abschlussDueAt !== null, "NIS2 Abschlussbericht gesetzt"); ok( d.abschlussDueAt!.getMonth() === addMonths(known, 1).getMonth(), "NIS2 Abschlussbericht = Kenntnis + 1 Monat (Monat +1)", ); ok(d.dsgvoDueAt === null, "ohne Personenbezug keine DSGVO-Frist"); } // ── 2. NIS2-Timer nur bei passender Kategorie UND Relevanz ────────────────── { ok(nis2TimersApply("wichtig", true), "nis2TimersApply: wichtig + relevant → true"); ok(nis2TimersApply("wesentlich", true), "nis2TimersApply: wesentlich + relevant → true"); ok(!nis2TimersApply("keine", true), "nis2TimersApply: keine → false (auch wenn relevant)"); ok(!nis2TimersApply("wesentlich", false), "nis2TimersApply: nicht relevant → false"); const noCat = computeDeadlines({ nis2Category: "keine", nis2Relevant: true, dsgvoRelevant: false, knownAt: known, }); ok( noCat.erstmeldungDueAt === null && noCat.folgemeldungDueAt === null && noCat.abschlussDueAt === null, "computeDeadlines: nis2Category=keine → keine NIS2-Fristen", ); const notRelevant = computeDeadlines({ nis2Category: "wesentlich", nis2Relevant: false, dsgvoRelevant: false, knownAt: known, }); ok( notRelevant.erstmeldungDueAt === null, "computeDeadlines: nis2Relevant=false → keine NIS2-Fristen (trotz Kategorie)", ); } // ── 3. DSGVO bei Personenbezug ────────────────────────────────────────────── { const d = computeDeadlines({ nis2Category: "keine", nis2Relevant: false, dsgvoRelevant: true, knownAt: known, }); ok(d.dsgvoDueAt?.getTime() === known.getTime() + 72 * HOUR, "DSGVO-Frist = Kenntnis + 72 h"); ok(d.erstmeldungDueAt === null, "DSGVO ohne NIS2: keine NIS2-Fristen"); ok( isReportable({ nis2Category: "keine", nis2Relevant: false, dsgvoRelevant: true }), "isReportable: nur DSGVO → meldepflichtig", ); ok( !isReportable({ nis2Category: "keine", nis2Relevant: true, dsgvoRelevant: false }), "isReportable: NIS2-relevant aber Kategorie keine → nicht meldepflichtig", ); } // ── 4. Interne SLA je Severity ────────────────────────────────────────────── { ok(slaForSeverity("kritisch").reactionHours === 1, "SLA kritisch: Reaktion 1 h"); ok(slaForSeverity("kritisch").resolutionHours === 24, "SLA kritisch: Behebung 24 h"); ok( slaForSeverity("niedrig").reactionHours > slaForSeverity("hoch").reactionHours, "SLA: niedrigere Severity → längere Reaktionsfrist", ); const sla = computeSla("hoch", known); ok( sla.reactionDueAt?.getTime() === addHours(known, 4).getTime(), "computeSla hoch: Reaktionsfrist = Kenntnis + 4 h", ); ok(computeSla("mittel", null).reactionDueAt === null, "computeSla ohne Kenntnis → null"); } // ── 5. Überfällig-Erkennung + Meldestufen ─────────────────────────────────── { const now = new Date("2026-03-11T10:00:00.000Z"); // 26 h nach Kenntnis ok(isOverdue(addHours(known, 24), now), "isOverdue: 24-h-Frist ist nach 26 h überfällig"); ok(!isOverdue(addHours(known, 72), now), "isOverdue: 72-h-Frist noch nicht überfällig"); ok(remainingMs(addHours(known, 72), now)! > 0, "remainingMs: 72-h-Frist hat positive Restzeit"); ok(isDueSoon(addHours(known, 30), now), "isDueSoon: Frist in 4 h ist bald fällig"); ok(!isDueSoon(addHours(known, 240), now), "isDueSoon: Frist in >24 h ist nicht bald fällig"); ok(stageDone("erstmeldung", "erstmeldung"), "stageDone: reportStatus=erstmeldung → Erstmeldung erledigt"); ok(!stageDone("pruefung", "erstmeldung"), "stageDone: reportStatus=pruefung → Erstmeldung offen"); ok(stageDone("folgemeldung", "erstmeldung"), "stageDone: reportStatus=folgemeldung → Erstmeldung erledigt"); const inc = { severity: "hoch", reportStatus: "pruefung", detectedAt: known, reportedAt: null, occurredAt: null, createdAt: known, erstmeldungDueAt: addHours(known, 24), folgemeldungDueAt: addHours(known, 72), abschlussDueAt: addMonths(known, 1), dsgvoDueAt: null, }; const items = deadlineItems(inc, now); const erst = items.find((i) => i.kind === "erstmeldung")!; ok(erst.overdue, "deadlineItems: offene Erstmeldung nach 26 h ist überfällig"); ok(items.some((i) => i.kind === "reaction"), "deadlineItems: interne SLA-Reaktionsfrist enthalten"); const open = openReportDeadlines(inc, now); ok( open.every((i) => !i.done) && open.some((i) => i.kind === "erstmeldung"), "openReportDeadlines: nur offene Meldestufen, Erstmeldung enthalten", ); const submitted = openReportDeadlines({ ...inc, reportStatus: "folgemeldung" }, now); ok( !submitted.some((i) => i.kind === "erstmeldung" || i.kind === "folgemeldung"), "openReportDeadlines: eingereichte Stufen (reportStatus=folgemeldung) fallen raus", ); } // ── 6. Meldung-Track Vorwärts-Übergänge ───────────────────────────────────── { ok(nextReportStatus("pruefung") === "erstmeldung", "nextReportStatus: pruefung → erstmeldung"); ok(nextReportStatus("erstmeldung") === "folgemeldung", "nextReportStatus: erstmeldung → folgemeldung"); ok(nextReportStatus("folgemeldung") === "abschluss", "nextReportStatus: folgemeldung → abschluss"); ok(nextReportStatus("abschluss") === null, "nextReportStatus: abschluss ist Endzustand"); } if (failures) { console.error(`\n✗ ${failures} Prüfung(en) fehlgeschlagen.`); process.exit(1); } console.log("\n✓ Alle Fristen-/Timer-Prüfungen bestanden.");