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>
151 lines
5.8 KiB
TypeScript
151 lines
5.8 KiB
TypeScript
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");
|
||
});
|
||
});
|