Verlauf filterbar (URL, API, proxdeploy history); Wiederholen füllt den Assistenten aus einem alten Auftrag; Aufräumen löscht nach Fehlschlag oder Abbruch den halb angelegten Gast – nur, wenn dessen Beschreibung auf den Auftrag verweist. Deshalb setzt das Klonen die Beschreibung jetzt sofort. Auf pve-demo geprüft (CT 109). Closes #014 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
163 lines
5.5 KiB
TypeScript
163 lines
5.5 KiB
TypeScript
import { afterEach, describe, expect, it } from "vitest";
|
||
import { phasenAusfuehren, type EreignisSenke } from "@/engine/ablauf/ablauf";
|
||
import { aufraeumSchritte } from "@/engine/ablaeufe/aufraeumen";
|
||
import { ProxmoxClient } from "@/engine/proxmox/client";
|
||
import { MOCK_TOKEN_GEHEIMNIS, MOCK_TOKEN_ID, ProxmoxMock } from "../proxmox-mock/mock";
|
||
import { szenarioEinrichten } from "../proxmox-mock/szenario";
|
||
|
||
const UPID = (art: string) => `UPID:pve02:00001234:00ABCDEF:6710F00D:${art}:130:proxdeploy@pve!test:`;
|
||
|
||
const aufraeumen: Array<() => Promise<void>> = [];
|
||
afterEach(async () => {
|
||
while (aufraeumen.length) await aufraeumen.pop()!();
|
||
});
|
||
|
||
type Gast = {
|
||
type: "qemu" | "lxc";
|
||
node: string;
|
||
status: string;
|
||
template?: number;
|
||
config: Record<string, unknown>;
|
||
};
|
||
|
||
async function aufbauen(gast: Gast | null) {
|
||
const mock = await new ProxmoxMock().starten();
|
||
szenarioEinrichten(mock, { version: "9.2.1" });
|
||
mock
|
||
.route("GET", "/cluster/resources", () => ({
|
||
data: [
|
||
{ vmid: 100, type: "qemu", node: "pve01", status: "running", name: "anderer" },
|
||
...(gast
|
||
? [
|
||
{
|
||
vmid: 120,
|
||
type: gast.type,
|
||
node: gast.node,
|
||
status: gast.status,
|
||
name: "web01",
|
||
template: gast.template ?? 0,
|
||
},
|
||
]
|
||
: []),
|
||
],
|
||
}))
|
||
.route("GET", /^\/nodes\/pve02\/(qemu|lxc)\/120\/config$/, () => ({ data: gast?.config ?? {} }))
|
||
.route("POST", /^\/nodes\/pve02\/(qemu|lxc)\/120\/status\/stop$/, () => ({ data: UPID("qmstop") }))
|
||
.route("DELETE", /^\/nodes\/pve02\/(qemu|lxc)\/120$/, () => ({ data: UPID("qmdestroy") }))
|
||
.route("GET", /\/tasks\/[^/]+\/status$/, () => ({ data: { status: "stopped", exitstatus: "OK" } }));
|
||
const client = new ProxmoxClient({
|
||
url: mock.url,
|
||
tokenId: MOCK_TOKEN_ID,
|
||
tokenGeheimnis: MOCK_TOKEN_GEHEIMNIS,
|
||
tlsPruefen: false,
|
||
fingerabdruck: mock.fingerabdruck,
|
||
});
|
||
aufraeumen.push(async () => {
|
||
await client.schliessen();
|
||
await mock.stoppen();
|
||
});
|
||
const phasen: Record<string, { status: string; detail?: string; ursache?: string }> = {};
|
||
const senke: EreignisSenke = {
|
||
phasenAnlegen: async () => {},
|
||
phaseGestartet: async () => {},
|
||
phaseDetail: async () => {},
|
||
phaseBeendet: async (s, status, a) => {
|
||
phasen[s] = { status, detail: a.detail, ursache: a.fehler?.ursache };
|
||
},
|
||
log: async () => {},
|
||
};
|
||
const ausfuehren = (typ: "VM" | "LXC" = "VM") =>
|
||
phasenAusfuehren(
|
||
aufraeumSchritte({ client, typ, vmid: 120, knoten: "pve01", auftragKurzId: "d-abcde" }),
|
||
senke,
|
||
);
|
||
const schreibend = () =>
|
||
mock.anfragen.filter((a) => a.methode !== "GET").map((a) => `${a.methode} ${a.pfad}`);
|
||
return { mock, ausfuehren, phasen, schreibend };
|
||
}
|
||
|
||
const EIGEN = "Ausgerollt mit ProxDeploy (Auftrag d-abcde)\nDistribution: Debian 13";
|
||
|
||
describe("Aufräumen", () => {
|
||
it("stoppt und löscht den eigenen Gast – auch wenn er inzwischen auf einem anderen Node liegt", async () => {
|
||
const t = await aufbauen({
|
||
type: "qemu",
|
||
node: "pve02",
|
||
status: "running",
|
||
config: { name: "web01", description: EIGEN },
|
||
});
|
||
expect((await t.ausfuehren()).ok).toBe(true);
|
||
expect(t.schreibend()).toEqual([
|
||
"POST /nodes/pve02/qemu/120/status/stop",
|
||
"DELETE /nodes/pve02/qemu/120",
|
||
]);
|
||
const loeschen = t.mock.anfragen.find((a) => a.methode === "DELETE")!;
|
||
expect(loeschen.abfrage.get("destroy-unreferenced-disks")).toBe("1");
|
||
expect(t.phasen["loeschen"]!.detail).toBe("VM 120 mit Disks gelöscht");
|
||
});
|
||
|
||
it("Container, der nicht läuft: Stoppen übersprungen", async () => {
|
||
const t = await aufbauen({
|
||
type: "lxc",
|
||
node: "pve02",
|
||
status: "stopped",
|
||
config: { hostname: "ct01", description: EIGEN },
|
||
});
|
||
expect((await t.ausfuehren("LXC")).ok).toBe(true);
|
||
expect(t.phasen["stoppen"]!.status).toBe("UEBERSPRUNGEN");
|
||
expect(t.schreibend()).toEqual(["DELETE /nodes/pve02/lxc/120"]);
|
||
});
|
||
|
||
it("schon weg: Erfolg ohne Löschen", async () => {
|
||
const t = await aufbauen(null);
|
||
expect((await t.ausfuehren()).ok).toBe(true);
|
||
expect(t.phasen["pruefungen"]!.detail).toMatch(/gibt es nicht mehr/);
|
||
expect(t.schreibend()).toEqual([]);
|
||
});
|
||
|
||
it.each([
|
||
[
|
||
"fremde Beschreibung",
|
||
{ type: "qemu", node: "pve02", status: "running", config: { description: "Produktiv-DB" } },
|
||
/gehört nicht erkennbar/,
|
||
],
|
||
[
|
||
"anderer Auftrag",
|
||
{
|
||
type: "qemu",
|
||
node: "pve02",
|
||
status: "running",
|
||
config: { description: "Ausgerollt mit ProxDeploy (Auftrag d-zzzzz)" },
|
||
},
|
||
/gehört nicht erkennbar/,
|
||
],
|
||
[
|
||
"Template",
|
||
{
|
||
type: "qemu",
|
||
node: "pve02",
|
||
status: "stopped",
|
||
template: 1,
|
||
config: { description: EIGEN, template: 1 },
|
||
},
|
||
/ist ein Template/,
|
||
],
|
||
[
|
||
"gesperrt",
|
||
{ type: "qemu", node: "pve02", status: "stopped", config: { description: EIGEN, lock: "clone" } },
|
||
/gesperrt \(clone\)/,
|
||
],
|
||
[
|
||
"falscher Typ",
|
||
{ type: "lxc", node: "pve02", status: "stopped", config: { description: EIGEN } },
|
||
/kein VM/,
|
||
],
|
||
] as Array<[string, Gast, RegExp]>)("%s: bricht ab und fasst nichts an", async (_name, gast, meldung) => {
|
||
const t = await aufbauen(gast);
|
||
const ergebnis = await t.ausfuehren("VM");
|
||
expect(ergebnis.ok).toBe(false);
|
||
expect(t.phasen["pruefungen"]!.ursache).toMatch(meldung);
|
||
expect(t.schreibend()).toEqual([]);
|
||
});
|
||
});
|