Files
ProxLedger/tests/agent-skript.test.ts
T
florian.wildeandClaude Opus 5 d9455db311 feat(agent): Bash-Agent mit Installer, systemd-Timer und Spool (#005)
Produkterkennung fuer PVE, PBS und PMG, JSON-Erzeugung ohne jq, Retry mit
Backoff, Nachsenden aus dem Spool, Update und Uninstall. Das Dashboard liefert
install.sh und agent.sh mit eingesetzter Adresse aus. 38 Tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 20:44:38 +02:00

196 lines
7.2 KiB
TypeScript

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<string, never> | Record<string, unknown> {
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<string, unknown>;
}
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<string, unknown>;
host: Record<string, unknown>;
};
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<string, unknown>;
};
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<string, unknown>;
};
expect(report.produkt.typ).toBe("pbs");
expect(report.subscription).toMatchObject({
key: "pbsc-9d41be5507",
status: "active",
serverid: "C3D4E5F607182A3B",
sockets: 1,
nextduedate: "2027-03-02",
});
});
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");
});
});
});