import { beforeEach, describe, expect, it, vi } from "vitest"; import { prisma } from "@/lib/db"; import { zellenSchluessel } from "@/lib/zuweisungsmatrix"; import { zuweisungsDaten, zuweisungsMatrix } from "@/server/queries/zuweisungen"; import { datenbankLeeren } from "./hilfen/db"; /** * Zuweisungsmatrix (#025) gegen die Testdatenbank: Query und CSV-Route. Die Sitzung wird fuer * die Route nachgebildet – geprueft wird, dass der Handler das Recht selbst verlangt. */ const recht = vi.hoisted(() => ({ erlaubt: true })); vi.mock("@/server/auth/sitzungspflicht", async (original) => { const echt = await original(); return { ...echt, rechtErforderlich: vi.fn(async () => { if (!recht.erlaubt) throw new echt.RechteFehler("Nicht angemeldet."); return { benutzer: { id: "b", rolle: "LESER" } }; }), }; }); let zaehler = 0; async function schluesselAnlegen(daten: { name: string; status?: "AKTIV" | "WIDERRUFEN"; mitPrivat?: boolean; }) { zaehler += 1; return prisma.schluessel.create({ data: { name: daten.name, slug: `test-${zaehler}`, typ: "ED25519", kategorie: "DIENST", status: daten.status ?? "AKTIV", publicKey: "ssh-ed25519 AAAA test", fingerprint: `SHA256:test${zaehler}`, kommentar: "test", ...(daten.mitPrivat ? { privatVerschluesselt: Buffer.from("x"), dekVerschluesselt: Buffer.from("y"), masterKeyVersion: 1 } : {}), }, }); } let ids: Record; beforeEach(async () => { await datenbankLeeren(); recht.erlaubt = true; zaehler = 0; const gruppe = await prisma.hostgruppe.create({ data: { name: "pve" } }); const [pve1, pve2, alt] = await Promise.all([ prisma.host.create({ data: { name: "srv-pve01", adresse: "10.0.0.1" } }), prisma.host.create({ data: { name: "srv-pve02", adresse: "10.0.0.2" } }), prisma.host.create({ data: { name: "srv-alt", adresse: "10.0.0.3", archiviertAm: new Date() } }), ]); await prisma.hostgruppeMitglied.createMany({ data: [pve1, pve2, alt].map((h) => ({ hostId: h.id, hostgruppeId: gruppe.id })), }); const backup = await schluesselAnlegen({ name: "Backup", mitPrivat: true }); const formel = await schluesselAnlegen({ name: "=1+1" }); const widerrufen = await schluesselAnlegen({ name: "Alt", status: "WIDERRUFEN" }); await prisma.publicVerwendung.createMany({ data: [ { schluesselId: backup.id, hostgruppeId: gruppe.id, zielbenutzer: "root", optionen: "restrict" }, { schluesselId: formel.id, hostId: pve1.id, zielbenutzer: "deploy", zustand: "AUSGEROLLT" }, { schluesselId: formel.id, hostId: pve2.id, zielbenutzer: "root", zustand: "ENTFERNT" }, { schluesselId: widerrufen.id, hostId: pve1.id, zielbenutzer: "root" }, ], }); await prisma.privateVerwendung.create({ data: { schluesselId: backup.id, hostId: pve2.id, zielbenutzer: "backup", pfad: "~/.ssh/id_backup" }, }); ids = { gruppe: gruppe.id, pve1: pve1.id, pve2: pve2.id, alt: alt.id, backup: backup.id, formel: formel.id, }; }); describe("zuweisungsDaten", () => { it("liest nur aktive Schluessel, nicht archivierte Hosts und keine entfernten Verwendungen", async () => { const daten = await zuweisungsDaten(); expect(daten.schluessel.map((s) => s.name)).toEqual(["=1+1", "Backup"]); expect(daten.hosts.map((h) => h.name)).toEqual(["srv-pve01", "srv-pve02"]); expect(daten.hosts[0]!.gruppenIds).toEqual([ids["gruppe"]]); expect(daten.oeffentlich).toHaveLength(2); expect(daten.privat).toHaveLength(1); }); it("liefert nie Chiffrat-Felder", async () => { const text = JSON.stringify(await zuweisungsDaten()); for (const feld of ["privatVerschluesselt", "dekVerschluesselt", "masterKeyVersion", "publicKey"]) { expect(text).not.toContain(feld); } }); }); describe("zuweisungsMatrix", () => { it("loest die Gruppe auf die nicht archivierten Mitglieder auf", async () => { const { matrix, gruppen } = await zuweisungsMatrix(); expect(gruppen.map((g) => g.name)).toEqual(["pve"]); expect(matrix.zellen.get(zellenSchluessel(ids["backup"]!, ids["pve1"]!))?.kennungen).toEqual(["P·G"]); expect(matrix.zellen.get(zellenSchluessel(ids["backup"]!, ids["pve2"]!))?.kennungen).toEqual([ "P·G", "K", ]); expect(matrix.zellen.has(zellenSchluessel(ids["backup"]!, ids["alt"]!))).toBe(false); expect(matrix.zaehler).toEqual({ schluessel: 2, hosts: 2, zuweisungen: 4, nichtSynchron: 3 }); }); }); describe("GET /api/zuweisungen.csv", () => { async function abrufen(query = "") { const { GET } = await import("@/app/api/zuweisungen.csv/route"); return GET(new Request(`http://localhost/api/zuweisungen.csv${query}`)); } it("verweigert ohne Recht", async () => { recht.erlaubt = false; const antwort = await abrufen(); expect(antwort.status).toBe(403); }); it("liefert die Matrix als Excel-sichere CSV", async () => { const antwort = await abrufen(); expect(antwort.status).toBe(200); expect(antwort.headers.get("content-type")).toContain("text/csv"); expect(antwort.headers.get("content-disposition")).toContain("sshkv-zuweisungen.csv"); const text = await antwort.text(); // Response.text() entfernt die BOM nicht immer – beides zulassen. const zeilen = text.replace(/^/, "").trimEnd().split("\r\n"); expect(zeilen).toHaveLength(1 + 4); expect(zeilen).toContain( '"Backup";"SHA256:test1";"srv-pve01";"root";"Public Key";"pve";"restrict";"noch nicht ausgerollt"', ); expect(zeilen).toContain(`"'=1+1";"SHA256:test2";"srv-pve01";"deploy";"Public Key";"";"";"synchron"`); }); it("wendet die Filter der Seite an", async () => { const text = await (await abrufen("?q=srv-pve02")).text(); expect(text).toContain("srv-pve02"); expect(text).not.toContain("srv-pve01"); }); });