Files
sshkeyvault/tests/vorgaenge.test.ts
T
florian.wildeandClaude Opus 5.5 4a23930b54 feat(worker): Worker mit pg-boss, Vorgaenge und Docker-Betrieb (#007)
Die Oberflaeche legt einen Vorgang an und schickt nur dessen ID in die
Warteschlange; der Worker fuehrt ihn aus und haelt Status, Ergebnis
oder Fehler fest. So bleiben lange SSH-Vorgaenge aus dem Webprozess
heraus, und der Fortschritt ist ohne Job-Interna sichtbar.

Kein automatisches Wiederholen: ein halb ausgefuehrter Schreibvorgang
auf einem Host gehoert vor einen Menschen. Fehlertexte aus fremden
Bibliotheken erreichen die Oberflaeche nie.

Dockerfile und Compose fuer den Betrieb: Master-Key als Secret nur im
Worker, Datenbank in einem internen Netz.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 09:21:41 +02:00

91 lines
3.3 KiB
TypeScript

import { afterAll, beforeEach, describe, expect, it } from "vitest";
import { prisma } from "@/lib/db";
import { vorgangEinreihen, vorgangLesen } from "@/server/vorgaenge/vorgaenge";
import { WARTESCHLANGE, warteschlangeHolen, type VorgangsJob } from "@/server/vorgaenge/warteschlange";
import { VorgangsFehler, vorgangAusfuehren, type HandlerTabelle } from "@/worker/ausfuehren";
import { datenbankLeeren } from "./hilfen/db";
/** Vorgaenge zwischen Web und Worker (#007). */
beforeEach(async () => {
await datenbankLeeren();
});
afterAll(async () => {
const boss = await warteschlangeHolen();
await boss.stop({ graceful: false });
});
async function anlegen() {
return prisma.vorgang.create({ data: { art: "ERREICHBARKEIT" } });
}
describe("Vorgang ausfuehren", () => {
it("haelt Ergebnis und Status fest", async () => {
const vorgang = await anlegen();
const handler: HandlerTabelle = { ERREICHBARKEIT: async () => ({ ergebnis: { erreichbar: true } }) };
await vorgangAusfuehren(vorgang.id, handler);
const fertig = await vorgangLesen(vorgang.id);
expect(fertig?.status).toBe("ERLEDIGT");
expect(fertig?.ergebnis).toEqual({ erreichbar: true });
expect(fertig?.beendetAm).not.toBeNull();
});
it("zeigt nur Meldungen von VorgangsFehlern, alles andere allgemein", async () => {
const a = await anlegen();
await vorgangAusfuehren(a.id, {
ERREICHBARKEIT: async () => {
throw new VorgangsFehler("Host antwortet nicht.");
},
});
expect((await vorgangLesen(a.id))?.fehler).toBe("Host antwortet nicht.");
const b = await anlegen();
await vorgangAusfuehren(b.id, {
ERREICHBARKEIT: async () => {
throw new Error("-----BEGIN OPENSSH PRIVATE KEY----- geheim");
},
});
const fehler = (await vorgangLesen(b.id))?.fehler ?? "";
expect(fehler).not.toContain("PRIVATE KEY");
expect(fehler).toMatch(/Interner Fehler/);
});
it("startet einen Vorgang nur einmal", async () => {
const vorgang = await anlegen();
let aufrufe = 0;
const handler: HandlerTabelle = {
ERREICHBARKEIT: async () => {
aufrufe += 1;
return {};
},
};
await Promise.all([vorgangAusfuehren(vorgang.id, handler), vorgangAusfuehren(vorgang.id, handler)]);
expect(aufrufe).toBe(1);
});
it("meldet fehlende Handler verstaendlich", async () => {
const vorgang = await anlegen();
await vorgangAusfuehren(vorgang.id, {});
expect((await vorgangLesen(vorgang.id))?.fehler).toMatch(/noch keine Ausführung/);
});
});
describe("Warteschlange", () => {
it("bringt einen eingereihten Vorgang bis in den Worker", async () => {
const boss = await warteschlangeHolen();
const id = await vorgangEinreihen({
art: "ERREICHBARKEIT",
angefordertVonId: null,
eingabe: { probe: 1 },
});
// Die Warteschlange ueberlebt das Leeren der Tabellen; deshalb gezielt den eigenen Job suchen.
const jobs = await boss.fetch<VorgangsJob>(WARTESCHLANGE, { batchSize: 100 });
const job = jobs.find((kandidat) => kandidat.data.vorgangId === id);
expect(job?.data).toEqual({ vorgangId: id });
await vorgangAusfuehren(job!.data.vorgangId, { ERREICHBARKEIT: async () => ({ ergebnis: "ok" }) });
await boss.complete(WARTESCHLANGE, job!.id);
expect((await vorgangLesen(id))?.status).toBe("ERLEDIGT");
});
});