Files
sshkeyvault/tests/zuweisungsmatrix.test.ts
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

211 lines
7.0 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.
import { describe, expect, it } from "vitest";
import { csvEntschaerfen, csvZeile } from "@/lib/csv";
import {
matrixAufbauen,
matrixCsv,
matrixFilterAus,
matrixFilterQuery,
zellenSchluessel,
type MatrixDaten,
type PublicZeile,
} from "@/lib/zuweisungsmatrix";
/** Zuweisungsmatrix (#025): reine Funktion, Gruppenaufloesung, Filter, CSV. */
const backup = {
id: "s-backup",
name: "Backup pve",
fingerprint: "SHA256:backup",
kategorie: "DIENST" as const,
person: null,
};
const anna = {
id: "s-anna",
name: "Anna Laptop",
fingerprint: "SHA256:anna",
kategorie: "PERSON" as const,
person: { id: "p-anna", name: "Anna" },
};
const leer = {
id: "s-leer",
name: "Unbenutzt",
fingerprint: "SHA256:leer",
kategorie: "DIENST" as const,
person: null,
};
const public_ = (
abweichung: Partial<PublicZeile> & Pick<PublicZeile, "id" | "schluesselId">,
): PublicZeile => ({
hostId: null,
hostgruppeId: null,
zielbenutzer: "root",
optionen: null,
zustand: "AUSGEROLLT",
...abweichung,
});
const DATEN: MatrixDaten = {
schluessel: [anna, backup, leer],
hosts: [
{ id: "h-pve1", name: "srv-pve01", gruppenIds: ["g-pve"] },
{ id: "h-pve2", name: "srv-pve02", gruppenIds: ["g-pve"] },
{ id: "h-web", name: "srv-web01", gruppenIds: [] },
],
gruppen: [{ id: "g-pve", name: "pve" }],
oeffentlich: [
public_({ id: "v-gruppe", schluesselId: "s-backup", hostgruppeId: "g-pve", optionen: "restrict" }),
public_({
id: "v-direkt",
schluesselId: "s-anna",
hostId: "h-pve1",
zielbenutzer: "anna",
zustand: "GEPLANT",
}),
public_({
id: "v-direkt2",
schluesselId: "s-anna",
hostId: "h-pve1",
zielbenutzer: "root",
zustand: "ABWEICHUNG",
}),
public_({ id: "v-weg", schluesselId: "s-anna", hostId: "h-web", zustand: "ENTFERNT" }),
],
privat: [
{
id: "k-1",
schluesselId: "s-backup",
hostId: "h-web",
zielbenutzer: "backup",
pfad: "~/.ssh/id_backup",
zustand: "FEHLGESCHLAGEN",
},
],
};
describe("matrixAufbauen", () => {
it("loest Gruppen-Zuweisungen je Mitglied auf und kennzeichnet sie", () => {
const m = matrixAufbauen(DATEN);
for (const host of ["h-pve1", "h-pve2"]) {
const zelle = m.zellen.get(zellenSchluessel("s-backup", host))!;
expect(zelle.kennungen).toEqual(["P·G"]);
expect(zelle.eintraege[0]).toMatchObject({
verwendungId: "v-gruppe",
gruppe: { name: "pve" },
optionen: "restrict",
});
}
});
it("unterscheidet direkt, Gruppe und Private Key", () => {
const m = matrixAufbauen(DATEN);
expect(m.zellen.get(zellenSchluessel("s-anna", "h-pve1"))!.kennungen).toEqual(["P"]);
const privat = m.zellen.get(zellenSchluessel("s-backup", "h-web"))!;
expect(privat.kennungen).toEqual(["K"]);
expect(privat.eintraege[0]).toMatchObject({ art: "privat", pfad: "~/.ssh/id_backup" });
});
it("zeigt den dringendsten Zustand und alle Zielbenutzer", () => {
const zelle = matrixAufbauen(DATEN).zellen.get(zellenSchluessel("s-anna", "h-pve1"))!;
expect(zelle.zustand).toBe("ABWEICHUNG");
expect(zelle.zielbenutzer).toEqual(["anna", "root"]);
});
it("laesst entfernte Verwendungen weg", () => {
expect(matrixAufbauen(DATEN).zellen.has(zellenSchluessel("s-anna", "h-web"))).toBe(false);
});
it("zaehlt Zuweisungen je Host und die nicht synchronen", () => {
expect(matrixAufbauen(DATEN).zaehler).toEqual({
schluessel: 3,
hosts: 3,
zuweisungen: 5,
nichtSynchron: 3,
});
});
it("filtert Spalten nach Hostgruppe und Zeilen nach Kategorie und Person", () => {
expect(matrixAufbauen(DATEN, { gruppeId: "g-pve" }).spalten.map((h) => h.id)).toEqual([
"h-pve1",
"h-pve2",
]);
expect(matrixAufbauen(DATEN, { kategorie: "PERSON" }).zeilen.map((s) => s.id)).toEqual(["s-anna"]);
expect(matrixAufbauen(DATEN, { personId: "p-anna" }).zeilen.map((s) => s.id)).toEqual(["s-anna"]);
});
it("sucht in Schluessel- und Hostnamen", () => {
const nachSchluessel = matrixAufbauen(DATEN, { q: "backup" });
expect(nachSchluessel.zeilen.map((s) => s.id)).toEqual(["s-backup"]);
expect(nachSchluessel.spalten).toHaveLength(3);
const nachHost = matrixAufbauen(DATEN, { q: "WEB01" });
expect(nachHost.zeilen).toHaveLength(3);
expect(nachHost.spalten.map((h) => h.id)).toEqual(["h-web"]);
const nichts = matrixAufbauen(DATEN, { q: "gibtsnicht" });
expect(nichts.zeilen).toHaveLength(0);
expect(nichts.spalten).toHaveLength(0);
});
it("blendet mit „nur belegt“ leere Zeilen und Spalten aus", () => {
const m = matrixAufbauen(DATEN, { nurBelegt: true, gruppeId: "g-pve" });
expect(m.zeilen.map((s) => s.id)).toEqual(["s-anna", "s-backup"]);
expect(m.spalten.map((h) => h.id)).toEqual(["h-pve1", "h-pve2"]);
expect(matrixAufbauen(DATEN, { nurBelegt: true, q: "unbenutzt" }).zeilen).toHaveLength(0);
});
it("zaehlt nur, was in der gefilterten Ansicht steht", () => {
expect(matrixAufbauen(DATEN, { gruppeId: "g-pve" }).zaehler.zuweisungen).toBe(4);
});
});
describe("Filter aus GET-Parametern", () => {
it("liest bekannte Werte und verwirft unbekannte Kategorien", () => {
const filter = matrixFilterAus({
gruppe: "g1",
kategorie: "QUATSCH",
person: ["p1", "p2"],
q: " pve ",
belegt: "1",
});
expect(filter).toEqual({ gruppeId: "g1", kategorie: "", personId: "p1", q: "pve", nurBelegt: true });
expect(matrixFilterQuery(filter)).toBe("?gruppe=g1&person=p1&q=pve&belegt=1");
expect(matrixFilterQuery(matrixFilterAus({}))).toBe("");
});
});
describe("CSV", () => {
it("entschaerft Formelbeginne fuer Excel", () => {
for (const wert of ["=1+1", "+49", "-x", "@SUMME(A1)", "\tx"])
expect(csvEntschaerfen(wert)).toBe(`'${wert}`);
expect(csvEntschaerfen("root")).toBe("root");
expect(csvZeile(['restrict,from="10.0.0.1"', null])).toBe('"restrict,from=""10.0.0.1""";""');
});
it("schreibt eine Zeile je Zelleneintrag mit Kopf und BOM", () => {
const csv = matrixCsv(matrixAufbauen(DATEN));
expect(csv.startsWith("")).toBe(true);
const zeilen = csv.slice(1).trimEnd().split("\r\n");
expect(zeilen[0]).toBe(
'"Schlüssel";"Fingerprint";"Host";"Zielbenutzer";"Art";"Über Gruppe";"Optionen";"Zustand"',
);
expect(zeilen).toHaveLength(1 + 5);
expect(zeilen).toContain(
'"Backup pve";"SHA256:backup";"srv-pve02";"root";"Public Key";"pve";"restrict";"synchron"',
);
expect(zeilen).toContain(
'"Backup pve";"SHA256:backup";"srv-web01";"backup";"Private Key";"";"Pfad: ~/.ssh/id_backup";"fehlgeschlagen"',
);
});
it("entschaerft boesartige Schluesselnamen im Export", () => {
const daten: MatrixDaten = {
...DATEN,
schluessel: [{ ...backup, name: '=HYPERLINK("http://x","klick")' }],
};
const csv = matrixCsv(matrixAufbauen(daten));
expect(csv).toContain(`"'=HYPERLINK(""http://x"",""klick"")"`);
expect(csv).not.toMatch(/(^|;|\n)"=/);
});
});