import { afterEach, describe, expect, it, vi } from "vitest"; /** * Der Zeitplan des Verzeichnis-Abgleichs (#016). * * Geprueft wird der **Durchgang**, nicht die Uhr: dass `node-cron` jede Minute * ausloest, ist Sache der Bibliothek; dass ein Durchgang die faelligen Quellen * holt, jede unter der Sperre abgleicht und an einem Fehler nicht abbricht, ist * unsere. * * Die Dienste darunter werden ersetzt. Das ist hier richtig und nicht faul: was * der Abgleich **tut**, prueft `tests/abgleich.test.ts` gegen einen echten * LDAP-Container. Hier geht es allein um die Ablaufsteuerung — und die laesst * sich mit echten Diensten nur pruefen, indem man eine Minute wartet und ein * Verzeichnis danebenstellt. */ const abgleichAusfuehren = vi.fn(); const faelligeQuellen = vi.fn(); /** Nur da, um zu belegen, dass der Zeitplan sie **nicht** nimmt. */ const mitSperre = vi.fn(); /** Dieselbe Klasse wie im Dienst – `instanceof` entscheidet ueber das Verhalten. */ class AbgleichLaeuftBereitsFehler extends Error {} vi.mock("@/server/services/abgleich", () => ({ AbgleichLaeuftBereitsFehler, abgleichAusfuehren: (...args: unknown[]) => abgleichAusfuehren(...args), faelligeQuellen: (...args: unknown[]) => faelligeQuellen(...args), })); // Seit #034 liegt die Sperre in `services/sperre`. Gemockt wird sie weiter, um // zu belegen, dass der Zeitplan sie **nicht** selbst nimmt. vi.mock("@/server/services/sperre", () => ({ mitSperre: (...args: unknown[]) => mitSperre(...args), })); const { abgleichDurchgang, zeitplanAnhalten, zeitplanLaeuft, zeitplanStarten } = await import("@/server/zeitplan/abgleich-plan"); afterEach(() => { zeitplanAnhalten(); vi.clearAllMocks(); vi.restoreAllMocks(); }); describe("Ein Durchgang des Zeitplans", () => { it("gleicht jede fällige Quelle ab", async () => { faelligeQuellen.mockResolvedValue([ { id: "q1", name: "Firma AD" }, { id: "q2", name: "Zweigstelle" }, ]); abgleichAusfuehren.mockResolvedValue({ status: "ERFOLG" }); await abgleichDurchgang(); expect(abgleichAusfuehren).toHaveBeenCalledTimes(2); expect(abgleichAusfuehren.mock.calls.map((aufruf) => aufruf[0])).toEqual(["q1", "q2"]); /* * **Ohne eigene Sperre.** Der Dienst nimmt sie selbst, in der Datenbank und * damit ueber Instanzen hinweg. Eine zweite hier traege einen anderen * Schluessel, liefe an der ersten vorbei — und ein von Hand angestossener * Abgleich saehe fuer den Zeitplan wie ein unerwarteter Fehler aus. */ expect(mitSperre).not.toHaveBeenCalled(); /* * Der handelnde „Benutzer“ ist der Systemlauf: ADMIN, damit die Rechtepruefung * des Dienstes greift, und ohne Id, weil kein Mensch es war. Der Dienst macht * daraus im Protokoll `benutzerId: null`. */ const zweitesArgument = abgleichAusfuehren.mock.calls[0]?.[1]; expect(zweitesArgument).toEqual({ id: "", rolle: "ADMIN" }); }); it("überspringt still, was gerade schon abgeglichen wird", async () => { /* * Die belegte Sperre meldet der Dienst als eigene Ausnahme. Im Normalbetrieb * mit zwei Instanzen — oder waehrend jemand „Jetzt abgleichen“ drueckt — * waere das jede Minute eine Zeile im Protokoll, deshalb weder Fehler noch * Ausgabe. */ faelligeQuellen.mockResolvedValue([ { id: "q1", name: "Firma AD" }, { id: "q2", name: "Zweigstelle" }, ]); abgleichAusfuehren.mockImplementation(async (id: string) => { if (id === "q1") throw new AbgleichLaeuftBereitsFehler("läuft schon"); return { status: "ERFOLG" }; }); const fehlerAusgabe = vi.spyOn(console, "error").mockImplementation(() => {}); await abgleichDurchgang(); // Und die naechste Quelle kommt trotzdem dran. expect(abgleichAusfuehren).toHaveBeenCalledTimes(2); expect(fehlerAusgabe).not.toHaveBeenCalled(); }); it("macht nach einem unerwarteten Fehler mit der nächsten Quelle weiter", async () => { /* * Der Fall, der den Zeitplan sonst still stillegt: eine Quelle wirft etwas, * das der Dienst nicht abgefangen hat. Ohne diese Schleife stuende danach * der Abgleich **aller** Quellen — bis jemand den Prozess neu startet. */ faelligeQuellen.mockResolvedValue([ { id: "kaputt", name: "Antwortet nicht" }, { id: "q2", name: "Zweigstelle" }, ]); abgleichAusfuehren.mockImplementation(async (id: string) => { if (id === "kaputt") throw new Error("Netz weg"); return { status: "ERFOLG" }; }); const fehlerAusgabe = vi.spyOn(console, "error").mockImplementation(() => {}); await abgleichDurchgang(); expect(abgleichAusfuehren).toHaveBeenCalledTimes(2); expect(abgleichAusfuehren.mock.calls[1]?.[0]).toBe("q2"); expect(fehlerAusgabe).toHaveBeenCalledOnce(); // Der Name der Quelle gehört in die Meldung, sonst sucht jemand im Dunkeln. expect(String(fehlerAusgabe.mock.calls[0]?.[0])).toContain("Antwortet nicht"); }); it("tut nichts, wenn keine Quelle fällig ist", async () => { faelligeQuellen.mockResolvedValue([]); await abgleichDurchgang(); expect(abgleichAusfuehren).not.toHaveBeenCalled(); }); }); describe("Der Zeitplan selbst", () => { it("startet genau einmal", () => { // Next laedt Module im Entwicklungsmodus mehrfach; ein zweiter Zeitplan // liefe unbemerkt mit und verdoppelte jeden Durchgang. expect(zeitplanLaeuft()).toBe(false); zeitplanStarten(); zeitplanStarten(); expect(zeitplanLaeuft()).toBe(true); zeitplanAnhalten(); expect(zeitplanLaeuft()).toBe(false); }); });