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>
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
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)"=/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user