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>
211 lines
7.0 KiB
TypeScript
211 lines
7.0 KiB
TypeScript
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)"=/);
|
||
});
|
||
});
|