Files
Laufzettel/tests/pdf-grenze.test.ts
T
Florian WildeandClaude Opus 5 f3950847f3
Prüfung / pruefung (push) Waiting to run
Prüfung / e2e (push) Blocked by required conditions
feat(laeufe): Uebergabeprotokoll als PDF (#041)
Neben "Drucken" steht am Lauf "Als PDF", im Archiv an jedem Treffer "Protokoll
als PDF" — dasselbe Blatt als fertige Datei, zum Verschicken oder Abheften.
Unten darauf jetzt zwei Unterschriftsfelder mit Zeile fuer Ort, Datum und Namen;
wo eine Vier-Augen-Freigabe erteilt wurde, steht sie mit im Kopf.

Erzeugt wird es, indem der Server seine eigene Druckansicht ueber 127.0.0.1
laedt und druckt — mit dem Sitzungscookie des Anfragenden. Damit gibt es genau
eine Vorlage fuer Papier und Datei, und fuer das PDF gilt dieselbe Rechte- und
Mandantenpruefung wie fuer die Seite. Ein zweites Layout waere billiger und
wuerde frueher oder spaeter abweichen, ohne dass es jemandem auffaellt.

Ein eigener Browser-Kontext je PDF, kein geteilter: er traegt die fremde
Sitzung, und ein geteilter gaebe sie an das naechste PDF weiter. Gespeichert
wird nichts — ein abgelegtes PDF waere eine zweite Wahrheit neben dem Lauf.
Ein erzeugtes Passwort steht auch hier nie darauf, geprueft am extrahierten
Text des fertigen PDF und nicht an den Bytes: der Inhalt ist komprimiert, dort
nichts zu finden bewiese nichts.

Chromium bleibt optional, und die gemessene Zahl ist der Grund: das
Laufzeitbild waechst von 370 MB auf 1,52 GB, Alpines chromium kostet allein
1,13 GB. Er kommt nur mit `--build-arg MIT_PDF=1` beziehungsweise
`deploy/install.sh --mit-pdf` mit. Fehlt er, erscheint der Knopf gar nicht
erst — ein Knopf, der zuverlaessig in eine Fehlermeldung fuehrt, ist schlimmer
als keiner. Ein direkter Aufruf bekommt 503 mit einer Meldung, die sagt, was
fehlt und wo es steht; ein leeres PDF gibt es in keinem Fall.

Meine Schaetzung lag um das Dreieinhalbfache daneben: "+328 MB" war das
.apk-Paket, nicht das Bild, und stand schon im Dockerfile-Kommentar, bevor ich
gebaut hatte. Jetzt steht die gemessene Zahl an drei Stellen.

1545 Vitest-Tests (vorher 1526): 15 in tests/pdf.test.ts, die den Handler echt
fahren, 3 in tests/pdf-grenze.test.ts fuer die Obergrenze mit gestelltem
Browser, einer in tests/archiv.test.ts. Dazu drei E2E-Ablaeufe; volle Suite
gruen mit 118 bestanden. Ein Blatt wurde wirklich erzeugt und angesehen.

Sechs Gegenproben, alle angeschlagen — darunter ein echtes Passwort auf dem
Blatt und die Freigabe des Zaehlers aus dem finally genommen, der Fall, in dem
ein Fehlschlag die PDF-Erzeugung bis zum Neustart leise stilllegt.

Closes #41

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-25 11:14:25 +01:00

127 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
/**
* Die Obergrenze gleichzeitiger PDF-Erzeugungen (#041).
*
* **Eine eigene Datei, weil hier `@/server/pdf` *nicht* gemockt werden darf.**
* `tests/pdf.test.ts` ersetzt `seiteAlsPdf`, um den Route Handler zu fahren;
* hier läuft genau diese Funktion echt, und nur der Browser darunter ist
* gestellt. Sonst prüfte der Test seine eigene Attrappe.
*
* Geprüft wird das Akzeptanzkriterium „zwei gleichzeitige Erzeugungen laufen,
* die dritte wird abgewiesen“ — und dass die Grenze sich wieder öffnet. Eine
* Grenze, die nach einem Fehler klemmt, legt die PDF-Erzeugung bis zum
* Neustart still, und zwar leise.
*/
/** Hält jede laufende Erzeugung an, bis der Test sie freigibt. */
let anhalten: Array<() => void> = [];
let laufendeSeiten = 0;
const seiteGestellt = {
emulateMedia: async () => undefined,
goto: async () => ({ status: () => 200 }),
evaluate: async () => undefined,
pdf: async () => {
laufendeSeiten += 1;
await new Promise<void>((weiter) => anhalten.push(weiter));
return new Uint8Array(Buffer.from("%PDF-1.4\n%%EOF\n", "utf8"));
},
};
const browserGestellt = {
isConnected: () => true,
newContext: async () => ({
newPage: async () => seiteGestellt,
close: async () => undefined,
}),
close: async () => undefined,
};
vi.mock("playwright-core", () => ({
chromium: { launch: async () => browserGestellt },
}));
const { seiteAlsPdf, PdfFehler, pdfVorratSchliessen } = await import("@/server/pdf");
const VORHER = {
pfad: process.env["CHROMIUM_PFAD"],
max: process.env["PDF_MAX_GLEICHZEITIG"],
};
beforeEach(() => {
// Irgendeine Datei, die es gibt – `chromiumPfad` prüft nur, ob dort etwas liegt.
process.env["CHROMIUM_PFAD"] = process.execPath;
anhalten = [];
laufendeSeiten = 0;
});
afterEach(async () => {
// Alles freigeben, sonst hängt ein Test am nächsten.
for (const weiter of anhalten) weiter();
await pdfVorratSchliessen();
for (const [name, wert] of [
["CHROMIUM_PFAD", VORHER.pfad],
["PDF_MAX_GLEICHZEITIG", VORHER.max],
] as const) {
if (wert === undefined) delete process.env[name];
else process.env[name] = wert;
}
});
describe("Die Obergrenze gleichzeitiger PDF-Erzeugungen", () => {
it("lässt so viele zu wie eingestellt und weist die nächste ab", async () => {
process.env["PDF_MAX_GLEICHZEITIG"] = "2";
const erste = seiteAlsPdf("/drucken/lauf/a", null);
const zweite = seiteAlsPdf("/drucken/lauf/b", null);
// Beide müssen wirklich angefangen haben, bevor die dritte kommt.
await vi.waitFor(() => expect(laufendeSeiten).toBe(2));
await expect(seiteAlsPdf("/drucken/lauf/c", null)).rejects.toThrow(PdfFehler);
await expect(seiteAlsPdf("/drucken/lauf/c", null)).rejects.toThrow(/noch einmal versuchen/u);
// Die dritte hat keinen Browser angefasst.
expect(laufendeSeiten).toBe(2);
for (const weiter of anhalten) weiter();
await expect(erste).resolves.toBeInstanceOf(Uint8Array);
await expect(zweite).resolves.toBeInstanceOf(Uint8Array);
});
it("gibt den Platz wieder frei, sobald eine fertig ist", async () => {
process.env["PDF_MAX_GLEICHZEITIG"] = "1";
const erste = seiteAlsPdf("/drucken/lauf/a", null);
await vi.waitFor(() => expect(laufendeSeiten).toBe(1));
await expect(seiteAlsPdf("/drucken/lauf/b", null)).rejects.toThrow(PdfFehler);
anhalten.shift()?.();
await erste;
// Und jetzt geht es wieder – die Grenze zählt herunter, nicht nur herauf.
const zweite = seiteAlsPdf("/drucken/lauf/b", null);
await vi.waitFor(() => expect(laufendeSeiten).toBe(2));
anhalten.pop()?.();
await expect(zweite).resolves.toBeInstanceOf(Uint8Array);
});
it("gibt den Platz auch frei, wenn die Erzeugung scheitert", async () => {
/*
* **Der Fall, der eine Grenze still klemmen lässt.** Steht die Freigabe
* nicht im `finally`, zählt ein Fehlschlag den Zähler nie zurück: nach
* `PDF_MAX_GLEICHZEITIG` Fehlern entsteht bis zum Neustart kein PDF mehr,
* und die Meldung spricht von Gleichzeitigkeit, obwohl niemand etwas tut.
*/
process.env["PDF_MAX_GLEICHZEITIG"] = "1";
const kaputt = vi.spyOn(seiteGestellt, "goto").mockResolvedValueOnce({ status: () => 500 });
await expect(seiteAlsPdf("/drucken/lauf/a", null)).rejects.toThrow(PdfFehler);
kaputt.mockRestore();
const danach = seiteAlsPdf("/drucken/lauf/b", null);
await vi.waitFor(() => expect(laufendeSeiten).toBe(1));
anhalten.pop()?.();
await expect(danach).resolves.toBeInstanceOf(Uint8Array);
});
});