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 & Pick, ): 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)"=/); }); });