import { execFileSync } from "node:child_process"; import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { afterAll, afterEach, beforeEach, describe, expect, it } from "vitest"; import { POST as enrollPost } from "@/app/api/agent/v1/enroll/route"; import { POST as reportPost } from "@/app/api/agent/v1/report/route"; import { agentRateLimit, enrollRateLimit } from "@/lib/ratelimit"; import { erzeugeToken } from "@/lib/tokens"; import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db"; const AGENT = path.join(process.cwd(), "agent", "proxledger-agent.sh"); let stubVerzeichnis: string; /** Legt ein ausführbares Skript an, das einen Proxmox-Befehl nachahmt. */ function befehlErsetzen(name: string, ausgabe: string) { const datei = path.join(stubVerzeichnis, name); writeFileSync(datei, `#!/usr/bin/env bash\ncat <<'ENDE'\n${ausgabe}\nENDE\n`); chmodSync(datei, 0o755); } /** Führt den Agent mit den vorbereiteten Befehlen aus und liefert den Report. */ function reportErzeugen(): Record | Record { const ausgabe = execFileSync("bash", [AGENT, "--dry-run"], { env: { ...process.env, PATH: `${stubVerzeichnis}:/usr/bin:/bin`, PROXLEDGER_CONFIG: path.join(stubVerzeichnis, "gibt-es-nicht.conf"), }, encoding: "utf8", }); return JSON.parse(ausgabe) as Record; } const PVE_SUBSCRIPTION = `key: pve1c-a6bd21fcb6 status: active serverid: A1B2C3D4E5F60718 checktime: 1789610400 productname: Proxmox VE Community Subscription 1 CPU/year regdate: 2025-09-24 00:00:00 nextduedate: 2026-09-24 sockets: 1 level: c`; describe("Agent-Skript", () => { beforeEach(() => { stubVerzeichnis = mkdtempSync(path.join(tmpdir(), "proxledger-agent-test-")); mkdirSync(stubVerzeichnis, { recursive: true }); }); afterEach(() => { rmSync(stubVerzeichnis, { recursive: true, force: true }); }); it("erzeugt aus einer vollständigen PVE-Ausgabe einen gültigen Report", () => { befehlErsetzen("pvesubscription", PVE_SUBSCRIPTION); befehlErsetzen("pveversion", "pve-manager/8.3.2/dc2b2f4e"); befehlErsetzen("lscpu", "Socket(s): 2\nModel name: Intel(R) Xeon(R) E-2388G"); befehlErsetzen( "ip", "1: lo inet 127.0.0.1/8 scope host lo\n2: vmbr0 inet 192.168.10.11/24 scope global vmbr0", ); befehlErsetzen( "apt", "Auflistung...\nlibc6/stable 2.40 amd64 [upgradable from: 2.39]\nbash/stable 5.3 amd64 [upgradable from: 5.2]", ); const report = reportErzeugen() as { reportId: string; agentVersion: string; erhobenAm: string; produkt: { typ: string; version: string }; subscription: Record; host: Record; }; expect(report.agentVersion).toBe("0.2.0"); expect(report.reportId).toMatch(/.{8,}/); expect(report.erhobenAm).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/); expect(report.produkt).toEqual({ typ: "pve", version: "pve-manager/8.3.2/dc2b2f4e" }); expect(report.subscription).toMatchObject({ key: "pve1c-a6bd21fcb6", status: "active", level: "c", sockets: 1, serverid: "A1B2C3D4E5F60718", productname: "Proxmox VE Community Subscription 1 CPU/year", nextduedate: "2026-09-24", // Die Uhrzeit aus regdate wird abgeschnitten. regdate: "2025-09-24", checktime: "1789610400", }); expect(report.host.sockets).toBe(2); expect(report.host.cpuModell).toBe("Intel(R) Xeon(R) E-2388G"); expect(report.host.paketUpdates).toBe(2); expect(report.host.kernel).toBeTypeOf("string"); }); it("liefert auch ohne Proxmox-Befehle gültiges JSON mit null-Werten", () => { const report = reportErzeugen() as { produkt: { typ: string | null }; subscription: Record; }; expect(report.produkt.typ).toBeNull(); expect(report.subscription.key).toBeNull(); expect(report.subscription.status).toBeNull(); expect(report.subscription.sockets).toBeNull(); }); it("maskiert Sonderzeichen aus Proxmox-Ausgaben korrekt", () => { befehlErsetzen( "pvesubscription", `key: pve1c-test status: active productname: Proxmox "VE" \\ Subscription mit Tab`, ); const report = reportErzeugen() as { subscription: { productname: string } }; expect(report.subscription.productname).toBe('Proxmox "VE" \\ Subscription\tmit Tab'); }); it("verarbeitet eine PBS-Ausgabe im JSON-Format", () => { befehlErsetzen( "proxmox-backup-manager", `{"key":"pbsc-9d41be5507","status":"active","serverid":"C3D4E5F607182A3B","sockets":1,"nextduedate":"2027-03-02"}`, ); const report = reportErzeugen() as { produkt: { typ: string }; subscription: Record; }; expect(report.produkt.typ).toBe("pbs"); expect(report.subscription).toMatchObject({ key: "pbsc-9d41be5507", status: "active", serverid: "C3D4E5F607182A3B", sockets: 1, nextduedate: "2027-03-02", }); }); it("meldet die Cluster-Mitglieder", () => { befehlErsetzen("pvesubscription", PVE_SUBSCRIPTION); befehlErsetzen( "pvecm", `Membership information ---------------------- Nodeid Votes Name 1 1 hv-rz5-pve-01 (local) 2 1 hv-rz5-pve-02 3 1 hv-rz5-pve-03`, ); const report = reportErzeugen() as { host: { clusterMitglieder: string[] } }; expect(report.host.clusterMitglieder).toEqual(["hv-rz5-pve-01", "hv-rz5-pve-02", "hv-rz5-pve-03"]); }); it("meldet eine leere Liste, wenn der Host in keinem Cluster ist", () => { befehlErsetzen("pvesubscription", PVE_SUBSCRIPTION); const report = reportErzeugen() as { host: { clusterMitglieder: string[] } }; expect(report.host.clusterMitglieder).toEqual([]); }); it("gibt die Version aus", () => { const ausgabe = execFileSync("bash", [AGENT, "--version"], { encoding: "utf8" }).trim(); expect(ausgabe).toBe("0.2.0"); }); describe.runIf(datenbankVerfuegbar)("Zusammenspiel mit der Agent-API", () => { afterAll(async () => { await testPrisma.$disconnect(); }); it("wird vom Report-Endpunkt unverändert angenommen", async () => { await datenbankLeeren(); agentRateLimit.zuruecksetzen(); enrollRateLimit.zuruecksetzen(); befehlErsetzen("pvesubscription", PVE_SUBSCRIPTION); befehlErsetzen("pveversion", "pve-manager/8.3.2/dc2b2f4e"); befehlErsetzen("lscpu", "Socket(s): 1\nModel name: Intel(R) Xeon(R) E-2388G"); const enrollToken = erzeugeToken("enrollment"); await testPrisma.enrollmentToken.create({ data: { tokenHash: enrollToken.hash, praefix: enrollToken.praefix, bezeichnung: "Agent-Test" }, }); const enrollAntwort = await enrollPost( new Request("http://localhost:3000/api/agent/v1/enroll", { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${enrollToken.klartext}` }, body: JSON.stringify({ agentId: "machine-id-fuer-agent-test", hostname: "pve-agent-test", produkt: "pve", }), }), ); const { token, hostId } = (await enrollAntwort.json()) as { token: string; hostId: string }; const report = reportErzeugen(); const antwort = await reportPost( new Request("http://localhost:3000/api/agent/v1/report", { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${token}` }, body: JSON.stringify(report), }), ); expect(antwort.status).toBe(200); const host = await testPrisma.host.findUniqueOrThrow({ where: { id: hostId } }); expect(host.subKey).toBe("pve1c-a6bd21fcb6"); expect(host.subStatus).toBe("ACTIVE"); expect(host.subNextduedate?.toISOString().slice(0, 10)).toBe("2026-09-24"); expect(host.subChecktime?.toISOString()).toBe("2026-09-17T02:00:00.000Z"); }); }); });