import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; import { katalogLaden, yamlPruefen } from "@/engine/katalog/laden"; import { templateWaehlen, versionVergleichen } from "@/engine/katalog/lxc-template"; import { dateiAusUrl, pruefsummeFinden } from "@/engine/katalog/pruefsumme"; import { DistributionSchema, ProfilSchema } from "@/engine/katalog/schema"; describe("Mitgelieferter Katalog", () => { it("lädt ohne Fehler: fünf Distributionen, drei Profile", async () => { const katalog = await katalogLaden("catalog"); expect(katalog.fehler).toEqual([]); expect(katalog.distributionen.map((d) => d.eintrag.id)).toEqual([ "debian-12", "debian-13", "ubuntu-22.04", "ubuntu-24.04", "ubuntu-26.04", ]); expect(katalog.profile.map((p) => p.eintrag.id)).toEqual(["base", "docker", "zabbix-agent"]); }); it("jede Distribution kann VM und LXC und gehört zur Familie debian", async () => { const katalog = await katalogLaden("catalog"); for (const { eintrag } of katalog.distributionen) { expect(eintrag.family).toBe("debian"); expect(eintrag.vm?.guest_agent_preinstalled).toBe(false); expect(eintrag.vm?.bios).toBe("ovmf"); expect(dateiAusUrl(eintrag.vm!.image_url)).toMatch(/\.(qcow2|img)$/); expect(eintrag.lxc?.template_pattern).toContain(eintrag.os.version_id); } }); it("jedes Profil hat eine Umsetzung für debian", async () => { const katalog = await katalogLaden("catalog"); for (const { eintrag } of katalog.profile) expect(Object.keys(eintrag.families)).toContain("debian"); }); }); describe("Schema", () => { const debian = { id: "debian-13", name: "Debian 13", family: "debian", os: { id: "debian", version_id: "13" }, lxc: { template_pattern: "debian-13-standard_*_amd64.tar.zst", ostype: "debian" }, }; it("verlangt vm oder lxc", () => { expect(DistributionSchema.safeParse({ ...debian, lxc: undefined }).success).toBe(false); }); it("lehnt unbekannte Schlüssel und HTTP-Quellen ab", () => { expect(DistributionSchema.safeParse({ ...debian, imgae_url: "x" }).success).toBe(false); const http = yamlPruefen( `id: x\nname: X\nfamily: debian\nos: { id: debian, version_id: "1" }\nvm: { image_url: "http://a/b.qcow2", checksum_url: "https://a/S", checksum_type: sha256, format: qcow2, default_user: u }`, DistributionSchema, ); expect(http.ok).toBe(false); if (!http.ok) expect(http.fehler.join()).toContain("vm.image_url"); }); it("meldet kaputtes YAML mit Zeile", () => { const ergebnis = yamlPruefen("id: [kaputt", DistributionSchema); expect(ergebnis.ok).toBe(false); if (!ergebnis.ok) expect(ergebnis.fehler[0]).toMatch(/^YAML:/); }); it("Profil: genau eine Aktion je Schritt, Bedingungen, eindeutige Variablen", () => { const basis = { id: "p", name: "P", families: { debian: [{ packages: ["htop"] }] } }; expect(ProfilSchema.safeParse(basis).success).toBe(true); expect( ProfilSchema.safeParse({ ...basis, families: { debian: [{ packages: ["a"], update: true }] } }).success, ).toBe(false); expect( ProfilSchema.safeParse({ ...basis, families: { debian: [{ packages: ["a"], if: "x" }] } }).success, ).toBe(true); expect( ProfilSchema.safeParse({ ...basis, variables: [ { name: "a", label: "A" }, { name: "a", label: "B" }, ], }).success, ).toBe(false); expect( ProfilSchema.safeParse({ ...basis, variables: [{ name: "a", label: "A", type: "select" }] }).success, ).toBe(false); }); }); describe("Katalog laden: Fehler einzeln", () => { function katalog(dateien: Record): string { const wurzel = mkdtempSync(join(tmpdir(), "pd-katalog-")); mkdirSync(join(wurzel, "distros")); mkdirSync(join(wurzel, "profiles")); for (const [pfad, inhalt] of Object.entries(dateien)) writeFileSync(join(wurzel, pfad), inhalt); return wurzel; } it("überspringt fehlerhafte Dateien, lädt den Rest und nennt Dateiname und Pfad", async () => { const wurzel = katalog({ "profiles/gut.yaml": "id: gut\nname: Gut\nfamilies:\n debian:\n - update: true\n", "profiles/kaputt.yaml": "id: kaputt\nname: K\nfamilies:\n debian:\n - pakete: [a]\n", "profiles/anders.yaml": "id: falsch\nname: F\nfamilies:\n debian:\n - update: true\n", "profiles/haengt.yaml": "id: haengt\nname: H\nrequires: [gibtsnicht]\nfamilies:\n debian:\n - update: true\n", }); const ergebnis = await katalogLaden(wurzel); expect(ergebnis.profile.map((p) => p.eintrag.id)).toEqual(["gut"]); expect(ergebnis.fehler.map((f) => f.datei).sort()).toEqual(["anders.yaml", "haengt.yaml", "kaputt.yaml"]); expect(ergebnis.fehler.find((f) => f.datei === "anders.yaml")!.meldung).toMatch(/Dateinamen/); }); }); describe("Prüfsummen", () => { const sha512 = "a".repeat(128); const sha256 = "b".repeat(64); it("liest das Debian-Format (zwei Leerzeichen)", () => { expect( pruefsummeFinden( `${sha512} debian-13-genericcloud-amd64.qcow2\n${"c".repeat(128)} andere.raw`, "debian-13-genericcloud-amd64.qcow2", "sha512", ), ).toBe(sha512); }); it("liest das Ubuntu-Format (Binärmodus mit *)", () => { expect( pruefsummeFinden( `${sha256} *noble-server-cloudimg-amd64.img`, "noble-server-cloudimg-amd64.img", "sha256", ), ).toBe(sha256); }); it("liest das BSD-Format und ignoriert falsche Längen", () => { expect(pruefsummeFinden(`SHA256 (x.img) = ${sha256.toUpperCase()}`, "x.img", "sha256")).toBe(sha256); expect(pruefsummeFinden(`${sha256} x.img`, "x.img", "sha512")).toBeNull(); expect(pruefsummeFinden(`${sha256} y.img`, "x.img", "sha256")).toBeNull(); }); it("nimmt den Dateinamen aus der URL", () => { expect( dateiAusUrl("https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.qcow2"), ).toBe("debian-13-genericcloud-amd64.qcow2"); }); }); describe("LXC-Template wählen", () => { const verfuegbar = [ "debian-13-standard_13.1-2_amd64.tar.zst", "debian-13-standard_13.6-1_amd64.tar.zst", "debian-13-standard_13.6-1_arm64.tar.zst", "debian-12-standard_12.12-1_amd64.tar.zst", "debian-12-standard_12.2-1_amd64.tar.zst", ]; it("nimmt bei mehreren Treffern die höchste Version (wie sort -V)", () => { expect(templateWaehlen("debian-13-standard_*_amd64.tar.zst", verfuegbar)).toBe( "debian-13-standard_13.6-1_amd64.tar.zst", ); expect(templateWaehlen("debian-12-standard_*_amd64.tar.zst", verfuegbar)).toBe( "debian-12-standard_12.12-1_amd64.tar.zst", ); }); it("findet nichts Falsches", () => { expect(templateWaehlen("ubuntu-24.04-standard_*_amd64.tar.zst", verfuegbar)).toBeNull(); expect(versionVergleichen("12.2-1", "12.12-1")).toBeLessThan(0); }); });