139 lines
5.3 KiB
TypeScript
139 lines
5.3 KiB
TypeScript
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 mitAbgleichSperre = 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),
|
||
mitAbgleichSperre: (...args: unknown[]) => mitAbgleichSperre(...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(mitAbgleichSperre).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);
|
||
});
|
||
});
|