Files
sshkeyvault/tests/zuweisungen.test.ts
T
florian.wildeandClaude Opus 5.5 a47cae4082 feat(zuweisungen): Matrix Schluessel x Host mit CSV-Export (#025)
Bisher liess sich nur je Schluessel oder je Host nachsehen, wo ein Key
verwaltet ist. Die Matrix zeigt das auf einen Blick, loest
Hostgruppen-Zuweisungen je Mitglied auf (P vs. P·G) und macht nicht
synchrone Zustaende ohne Farbe erkennbar.

Die Matrix plant nur: Zuweisen und Entfernen laufen ueber die
vorhandenen Aktionen, ausgerollt wird weiter per Dry-Run auf der
Schluesselseite. Der Aufbau ist eine reine Funktion in src/lib, damit
Seite und CSV-Export garantiert dasselbe zeigen und die
Gruppenaufloesung ohne Datenbank testbar ist. Der CSV-Export entschaerft
Formelbeginne, weil Schluesselnamen frei waehlbar sind und Excel sie
sonst ausfuehren wuerde.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:05:12 +02:00

151 lines
5.8 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<typeof import("@/server/auth/sitzungspflicht")>();
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<string, string>;
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");
});
});