Files
craftvia/scripts/test-framework-toggle.ts
msolarczekandClaude Opus 5 c8e6f30a27
CI / build-and-check (push) Canceled after 0s
CI / audit (push) Canceled after 0s
CI / sbom (push) Canceled after 0s
Basis: Certvia dev@a48c5fb als Fundament für Craftvia
Unveränderter Stand von certvia/dev (a48c5fb) plus Craftvia-Spezifikation
und Brandbook unter docs/craftvia/. ISMS-Module werden im Folgecommit entfernt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 11:05:39 +02:00

151 lines
8.0 KiB
TypeScript

// Test der nachträglichen Framework-Umschaltung (Admin: setTenantFrameworks).
//
// Spiegelt die Datenoperationen der Server-Action gegen die lokale DB und stellt den
// Ausgangszustand danach wieder her. Der Auth-Guard (`requirePlatformFullAdmin`) wird
// dabei NICHT durchlaufen — er ist identisch zu den sechs übrigen Admin-Aktionen.
//
// Geprüft wird:
// 1. Aktivieren — Framework-Zeile entsteht, 120 ISO-Anforderungen werden angelegt,
// die 321 TISAX-Anforderungen bleiben unangetastet, Flags stehen richtig.
// 2. Deaktivieren — Zugehörigkeit fällt weg, ISO-Anforderungen werden STILLGELEGT
// (archivedAt), nicht gelöscht; SoA-Einträge bleiben vollständig erhalten.
// 3. Wiederherstellung — der Mandant steht am Ende exakt wie vorher.
//
// Lauf: npx tsx scripts/test-framework-toggle.ts
// Nutzt die lokale Postgres-DB; .env liegt im Worktree.
import "dotenv/config";
import { join } from "node:path";
import type { Framework } from "@prisma/client";
import { prisma, dbForTenant } from "../src/server/db";
import { reconcilePackage, stampPackageVersion } from "../prisma/import-policies";
import { resolvePackageForTenant, getTenantFrameworks } from "../prisma/template-store";
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 state(tenantId: string) {
const db = dbForTenant(tenantId);
const [fw, tisax, tisaxArch, iso, isoArch, soa, flags] = await Promise.all([
getTenantFrameworks(prisma, tenantId),
db.policyRequirement.count({ where: { framework: "TISAX", archivedAt: null } }),
db.policyRequirement.count({ where: { framework: "TISAX", archivedAt: { not: null } } }),
db.policyRequirement.count({ where: { framework: "ISO_27001", archivedAt: null } }),
db.policyRequirement.count({ where: { framework: "ISO_27001", archivedAt: { not: null } } }),
db.soaEntry.count(),
db.policyVariable.findMany({
where: { key: { in: ["FLAG_FW_TISAX", "FLAG_FW_ISO27001"] } },
select: { key: true, value: true }, orderBy: { key: "asc" },
}),
]);
return { fw, tisax, tisaxArch, iso, isoArch, soa, flags: flags.map((f) => `${f.key}=${f.value}`).join(" ") };
}
/** Datenoperationen aus `setTenantFrameworks` (ohne Auth-Guard und revalidatePath). */
async function apply(tenantId: string, wanted: Framework[]) {
const current = await getTenantFrameworks(prisma, tenantId);
const added = wanted.filter((f) => !current.includes(f));
const removed = current.filter((f) => !wanted.includes(f));
const db = dbForTenant(tenantId);
for (const [i, framework] of wanted.entries()) {
await prisma.tenantFramework.upsert({
where: { tenantId_framework: { tenantId, framework } },
update: { isPrimary: i === 0 },
create: { tenantId, framework, isPrimary: i === 0 },
});
}
for (const [i, framework] of added.entries()) {
const { pkg } = await resolvePackageForTenant(prisma, tenantId, SEED, framework);
await reconcilePackage(prisma, tenantId, pkg, { framework, reconcileShared: i === 0 });
await stampPackageVersion(prisma, tenantId, pkg.version, framework);
}
if (removed.length > 0) {
await prisma.tenantFramework.deleteMany({ where: { tenantId, framework: { in: removed } } });
await db.policyRequirement.updateMany({
where: { framework: { in: removed }, archivedAt: null },
data: { archivedAt: new Date() },
});
}
await db.policyVariable.updateMany({ where: { key: "FLAG_FW_TISAX" }, data: { value: String(wanted.includes("TISAX")) } });
await db.policyVariable.updateMany({ where: { key: "FLAG_FW_ISO27001" }, data: { value: String(wanted.includes("ISO_27001")) } });
}
async function main() {
const tenant = await prisma.tenant.findFirst({ where: { slug: "demo" }, select: { id: true, name: true } });
if (!tenant) { console.log("Mandant demo nicht gefunden — Test uebersprungen."); return; }
const before = await state(tenant.id);
console.log(`Mandant: ${tenant.name}`);
console.log(` vorher: ${before.fw.join("+")} · TISAX=${before.tisax}/${before.tisaxArch} · ISO=${before.iso}/${before.isoArch} · SoA=${before.soa} · ${before.flags}`);
try {
console.log("\n1. ISO aktivieren");
await apply(tenant.id, [...before.fw, "ISO_27001"] as Framework[]);
const on = await state(tenant.id);
console.log(` ${on.fw.join("+")} · TISAX=${on.tisax}/${on.tisaxArch} · ISO=${on.iso}/${on.isoArch} · ${on.flags}`);
check(on.fw.includes("ISO_27001"), "Framework-Zugehörigkeit angelegt");
check(on.iso === 120, "120 ISO-Anforderungen aktiv", `${on.iso}`);
check(on.tisax === before.tisax && on.tisaxArch === before.tisaxArch,
"TISAX-Anforderungen unangetastet", `${on.tisax} aktiv / ${on.tisaxArch} archiviert`);
check(on.flags.includes("FLAG_FW_ISO27001=true") && on.flags.includes("FLAG_FW_TISAX=true"),
"Sichtbarkeits-Flags gesetzt", on.flags);
// Gepflegte SoA-Inhalte anlegen — sonst liefe die Erhaltungsprüfung unten über 0 → 0.
const db0 = dbForTenant(tenant.id);
for (const control of ["A.5.15", "A.8.5", "A.7.7"]) {
await db0.soaEntry.upsert({
where: { tenantId_framework_control: { tenantId: tenant.id, framework: "ISO_27001", control } },
update: { justification: "Testbegruendung", implementationStatus: "umgesetzt" },
create: {
tenantId: tenant.id, framework: "ISO_27001", control,
justification: "Testbegruendung", implementationStatus: "umgesetzt", applicable: true,
},
});
}
const gepflegt = await db0.soaEntry.count({ where: { justification: "Testbegruendung" } });
check(gepflegt === 3, "SoA-Einträge zum Test gepflegt", `${gepflegt}`);
console.log("\n2. ISO wieder deaktivieren");
await apply(tenant.id, before.fw as Framework[]);
const off = await state(tenant.id);
console.log(` ${off.fw.join("+")} · TISAX=${off.tisax}/${off.tisaxArch} · ISO=${off.iso}/${off.isoArch} · SoA=${off.soa} · ${off.flags}`);
check(!off.fw.includes("ISO_27001"), "Zugehörigkeit entfernt");
check(off.iso === 0 && off.isoArch === 120,
"ISO-Anforderungen stillgelegt statt gelöscht", `aktiv=${off.iso} archiviert=${off.isoArch}`);
check(off.tisax === before.tisax, "TISAX weiterhin unangetastet", `${off.tisax}`);
const ueberlebt = await dbForTenant(tenant.id).soaEntry.count({ where: { justification: "Testbegruendung" } });
check(ueberlebt === 3, "gepflegte SoA-Begründungen überleben das Deaktivieren unverändert", `${ueberlebt} von 3`);
check(off.flags.includes("FLAG_FW_ISO27001=false"), "Flag zurückgesetzt", off.flags);
console.log("\n3. Erneut aktivieren — stillgelegte Anforderungen reaktivieren");
await apply(tenant.id, [...before.fw, "ISO_27001"] as Framework[]);
const again = await state(tenant.id);
check(again.iso === 120 && again.isoArch === 0,
"Re-Import reaktiviert statt zu duplizieren", `aktiv=${again.iso} archiviert=${again.isoArch}`);
} finally {
console.log("\n4. Ausgangszustand wiederherstellen");
await apply(tenant.id, before.fw as Framework[]);
const db = dbForTenant(tenant.id);
await db.policyRequirement.deleteMany({ where: { framework: "ISO_27001" } });
await db.soaEntry.deleteMany({ where: { framework: "ISO_27001" } });
const end = await state(tenant.id);
console.log(` ${end.fw.join("+")} · TISAX=${end.tisax}/${end.tisaxArch} · ISO=${end.iso}/${end.isoArch} · ${end.flags}`);
check(JSON.stringify(end) === JSON.stringify(before), "Mandant steht wie vorher",
JSON.stringify(end) === JSON.stringify(before) ? "" : `${JSON.stringify(before)} → ${JSON.stringify(end)}`);
}
}
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);
});