Spaces:
Sleeping
Sleeping
File size: 5,445 Bytes
9de795a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | /** Weck-Verhalten des API-Clients (s. backend.ts): fetch ist gemockt, die Zeit
* simuliert – ein «3-Minuten-Aufwachen» läuft in Millisekunden. */
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { MAX_WECKDAUER_MS, type BackendStatus } from "./backend";
const json = (body: unknown, status = 200) =>
new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
const html = (status = 200) =>
new Response("<html>Space startet…</html>", {
status,
headers: { "content-type": "text/html" },
});
type Antwort = Response | Error;
/** fetch-Mock: je URL eine Schlange von Antworten; die letzte wiederholt sich. */
function mockFetch(schlangen: Record<string, (() => Antwort)[]>) {
const aufrufe: string[] = [];
const fn = vi.fn((url: string) => {
aufrufe.push(url);
const schlange = schlangen[url];
if (!schlange || schlange.length === 0) throw new Error(`kein Mock für ${url}`);
const naechste = (schlange.length > 1 ? schlange.shift() : schlange[0]) as () => Antwort;
const a = naechste();
return a instanceof Error ? Promise.reject(a) : Promise.resolve(a);
});
vi.stubGlobal("fetch", fn);
return aufrufe;
}
async function ladeApi() {
// Frisches Modul je Test: Weck-Zustand und Beobachter sind Modul-Zustand.
vi.resetModules();
const mod = await import("./api");
const ereignisse: BackendStatus[] = [];
mod.beobachteBackend((s) => ereignisse.push(s));
return { ...mod, ereignisse };
}
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
vi.unstubAllGlobals();
});
describe("api – Backend wecken", () => {
it("normale JSON-Antwort: kein Wecken, genau ein Aufruf", async () => {
const aufrufe = mockFetch({ "/api/samples/rooms": [() => json([{ id: "r1" }])] });
const { api, ereignisse } = await ladeApi();
await expect(api.rooms()).resolves.toEqual([{ id: "r1" }]);
expect(aufrufe).toEqual(["/api/samples/rooms"]);
expect(ereignisse).toEqual([]);
});
it("HTML-Ladeseite (Status 200): weckt, wiederholt EINMAL und liefert das Ergebnis", async () => {
const aufrufe = mockFetch({
"/api/samples/rooms": [() => html(), () => json([{ id: "r1" }])],
"/api/health": [() => html(), () => json({ status: "ok" })],
});
const { api, ereignisse } = await ladeApi();
const antwort = api.rooms();
await vi.advanceTimersByTimeAsync(10_000);
await expect(antwort).resolves.toEqual([{ id: "r1" }]);
expect(ereignisse).toEqual(["wacht", "bereit"]);
expect(aufrufe.filter((u) => u === "/api/samples/rooms")).toHaveLength(2);
});
it("JSON-Fehler-Envelope ist eine echte Antwort: Fehler durchreichen, nicht wecken", async () => {
const aufrufe = mockFetch({
"/api/samples/rooms": [() => json({ code: "NO_FEASIBLE_PLACEMENT", message: "zu eng" }, 422)],
});
const { api, ereignisse } = await ladeApi();
await expect(api.rooms()).rejects.toMatchObject({
code: "NO_FEASIBLE_PLACEMENT",
message: "zu eng",
});
expect(aufrufe).toHaveLength(1);
expect(ereignisse).toEqual([]);
});
it("Server bleibt weg: verständlicher Fehler statt SyntaxError", async () => {
mockFetch({ "/api/samples/rooms": [() => html()], "/api/health": [() => html()] });
const { api, ereignisse, NICHT_ERREICHBAR } = await ladeApi();
const antwort = api.rooms();
const erwartung = expect(antwort).rejects.toMatchObject({ code: NICHT_ERREICHBAR });
await vi.advanceTimersByTimeAsync(MAX_WECKDAUER_MS + 10_000);
await erwartung;
expect(ereignisse).toEqual(["wacht", "nicht-erreichbar"]);
});
it("gleichzeitige Aufrufe teilen EINEN Weckvorgang", async () => {
const aufrufe = mockFetch({
"/api/samples/rooms": [() => html(), () => json([])],
"/api/taxonomy": [() => html(), () => json({ achsen: [] })],
"/api/health": [() => html(), () => json({ status: "ok" })],
});
const { api, ereignisse } = await ladeApi();
const beide = Promise.all([api.rooms(), api.taxonomy()]);
await vi.advanceTimersByTimeAsync(10_000);
await expect(beide).resolves.toEqual([[], { achsen: [] }]);
// Ein Weckvorgang = zwei Pings (html, dann ok) – nicht vier.
expect(aufrufe.filter((u) => u === "/api/health")).toHaveLength(2);
expect(ereignisse).toEqual(["wacht", "bereit"]);
});
it("Netzfehler wird wie ein schlafendes Backend behandelt", async () => {
mockFetch({
"/api/samples/rooms": [() => new TypeError("Failed to fetch"), () => json([{ id: "r1" }])],
"/api/health": [() => json({ status: "ok" })],
});
const { api } = await ladeApi();
const antwort = api.rooms();
await vi.advanceTimersByTimeAsync(1_000);
await expect(antwort).resolves.toEqual([{ id: "r1" }]);
});
it("wach, aber weiterhin keine JSON-Antwort: Fehler nennt den HTTP-Status", async () => {
mockFetch({
"/api/samples/rooms": [
() => new Response("zu gross", { status: 413, headers: { "content-type": "text/plain" } }),
],
"/api/health": [() => json({ status: "ok" })],
});
const { api } = await ladeApi();
const antwort = api.rooms();
const erwartung = expect(antwort).rejects.toMatchObject({
code: "UNERWARTETE_ANTWORT",
message: expect.stringContaining("HTTP 413"),
});
await vi.advanceTimersByTimeAsync(1_000);
await erwartung;
});
});
|