Files
Florian WildeandClaude Opus 5 75c335df23
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
test: Zusagen pruefbar machen, die nur von Hand hielten (#038, #037)
#038: Die E2E-Ablaeufe laufen ab jetzt vor jedem Merge nach main, nicht vor
jedem Commit. Gemessen: Bau 14 s, Start 7 s, Ablaeufe 256 s — zu den vier
Minuten des bestehenden Tores. Neun Minuten vor jedem Commit ist ein Tor, das
umgangen wird. Vor jedem Merge waeren es in Phase 2 acht Laeufe gewesen, und
jeder der vier Befunde aus #027 waere am Tag seiner Entstehung aufgefallen.

Geprueft wurde jeder der 16 Ablaeufe gegen die Frage, ob es ein
Vitest-Gegenstueck gibt. Neun Zusagen haengen ausschliesslich dort, und keine
laesst sich holen: alle brauchen einen echten Browser. Die Liste steht in
CLAUDE.md.

Der eigentliche Fund dabei war keine Testluecke, sondern eine Konvention ohne
Pruefer: "jede Seite prueft selbst" steht seit #007 in CLAUDE.md, und geprueft
hat es bis hierher nur ein Mensch, der nachsieht. Der Review von Phase 2 hat
alle 27 Seiten durchgesehen — ein Ergebnis, das fuer genau einen Tag gilt.
tests/zugang.test.ts liest sie jetzt, samt jedem Route Handler; die Ausnahmen
(Auth, Healthcheck) stehen namentlich mit Begruendung, und ein Test prueft auch
die Gegenrichtung.

#037: Das Lesemodell der Anhaenge liegt in queries/ und engt selbst ein — vorher
hing das daran, dass beide Aufrufer den Lauf vorher pruefen. /meine-geraete wird
bei 390 px gemessen, mit einem echten MITARBEITER-Konto aus dem E2E-Vorlauf,
dazu ein Test, dass dort kein Link in die Verwaltung angeboten wird.

Dabei selbst einen Fehler gemacht, der die neue Regel begruendet: fuer das
zweite Konto brauchte `anmelden` eine Variante, die nicht auf / wartet, und ich
habe das Warten fuer alle gelockert. Das machte zwei unverwandte Ablaeufe
unzuverlaessig — sichtbar nur im vollen Durchgang, einzeln waren beide gruen.
Ein gemeinsamer Helfer in einer Testsuite ist geteilter Zustand. Ohne vollen
Durchgang haette ich zwei rote Ablaeufe gemergt.

1457 Vitest-Tests (vorher 1454), 115 E2E-Ablaeufe (vorher 113). Offen bleibt nur
noch #024, und das verlangt einen echten Proxmox-Host.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-11 06:05:29 +02:00

190 lines
7.7 KiB
TypeScript

import { chromium } from "@playwright/test";
import "dotenv/config";
import { prisma } from "@/lib/db";
import { ABTEILUNGEN } from "@/../prisma/seed-daten/stammdaten";
import { passwortHashen } from "@/server/auth/benutzer";
import {
anmelden,
E2E_MITARBEITER_MAIL,
E2E_MITARBEITER_NAME,
E2E_MITARBEITER_PASSWORT,
SEED_GERAET,
SEED_TAG,
SEED_VORLAGE,
} from "./helfer";
import { e2eAufraeumenMitBericht } from "./aufraeumen";
/**
* Was vor dem ersten Ablauf stimmen muss (#018).
*
* Bisher standen die Voraussetzungen als Satz im Kopfkommentar jeder Datei
* („Voraussetzung: ein gebauter Stand laeuft und `pnpm beispiel:vorlage` hat
* …“). Ein Kommentar prueft nichts. Stimmte etwas davon nicht, fiel das erst
* mitten im Lauf auf, und die Meldung sprach von einem Zeitablauf oder von null
* Treffern — nie von der Ursache.
*
* Hier wird es geprueft, einmal, bevor irgendein Ablauf startet. Jede Meldung
* sagt, **was** fehlt und **was zu tun ist**.
*/
const APP_URL = process.env["E2E_APP_URL"] ?? "http://127.0.0.1:3000";
function fehlen(was: string, abhilfe: string): never {
throw new Error(`Ende-zu-Ende-Tests: ${was}\n → ${abhilfe}`);
}
export default async function vorbereiten(): Promise<void> {
/*
* **Zuerst aufraeumen, dann pruefen** (#027). Was hier noch steht, ist der
* Rueckstand eines abgebrochenen Durchgangs: `globalTeardown` und die
* `afterAll`-Bloecke laufen bei einem Testfehler, aber nicht bei `Strg+C`.
* Ein Vorlauf laesst sich nicht ueberspringen — das ist der Unterschied.
*/
await e2eAufraeumenMitBericht("vor");
if (!process.env["SEED_ADMIN_EMAIL"] || !process.env["SEED_ADMIN_PASSWORD"]) {
fehlen(
"SEED_ADMIN_EMAIL oder SEED_ADMIN_PASSWORD fehlt.",
"Beide stehen in der .env und müssen zu dem Administrator passen, den der Seed angelegt hat.",
);
}
// 1. Laeuft ueberhaupt etwas an dieser Adresse?
const antwort = await fetch(`${APP_URL}/api/gesundheit`, { signal: AbortSignal.timeout(5000) }).catch(
() => undefined,
);
if (!antwort?.ok) {
fehlen(
`Unter ${APP_URL} antwortet keine Anwendung.`,
"Erst bauen und starten: pnpm build && PORT=3300 APP_URL=http://127.0.0.1:3300 pnpm start — " +
"und die Adresse über E2E_APP_URL mitgeben. Gegen `pnpm dev` laufen die Abläufe nicht " +
"(siehe CLAUDE.md, „Oberfläche prüfen“).",
);
}
// 2. Steht der Seed-Bestand, mit dem die Ablaeufe arbeiten?
const vorlage = await prisma.vorlage.findFirst({
where: { name: SEED_VORLAGE },
select: { id: true, versionen: { where: { status: "VEROEFFENTLICHT" }, select: { id: true } } },
});
if (!vorlage || vorlage.versionen.length === 0) {
fehlen(
`Die Vorlage „${SEED_VORLAGE}“ fehlt oder ist nicht veröffentlicht.`,
"pnpm db:seed laufen lassen.",
);
}
const geraet = await prisma.geraet.findUnique({ where: { tagCode: SEED_TAG }, select: { id: true } });
if (!geraet) {
fehlen(`Das Beispielgerät ${SEED_GERAET} (${SEED_TAG}) fehlt.`, "pnpm db:seed laufen lassen.");
}
/*
* 2c. Stehen die **Werte** des Seed-Bestands noch, nicht nur die Zeilen?
*
* Der Onboarding-Ablauf prueft den `New-ADUser`-Befehl **zeichengenau**, und
* darin steckt die Ziel-OU der Abteilung. In der Entwicklungsdatenbank stand
* an „Buchhaltung“ `DC=firma,DC=de` statt `DC=firma,DC=local` — von Hand
* geaendert oder aus einer aelteren Seed-Fassung. Der Seed setzt bestehende
* Abteilungen absichtlich **nicht** zurueck (gepflegte Daten sollen bleiben),
* also blieb die Abweichung stehen.
*
* Der Ablauf scheiterte daran mit „expected … received …“ — einer Meldung, die
* nach einem Fehler im Befehlsbau aussieht und keiner war. Hier faellt es
* vorher auf, und die Meldung sagt, was zu tun ist (Reviewbefund zu #029).
*/
for (const vorgabe of ABTEILUNGEN) {
const abteilung = await prisma.abteilung.findFirst({
where: { name: vorgabe.name },
select: { zielOu: true, standardGruppen: true },
});
if (!abteilung) continue;
if (abteilung.zielOu !== vorgabe.zielOu) {
fehlen(
`Die Abteilung „${vorgabe.name}“ hat eine abweichende Ziel-OU: ` +
`„${abteilung.zielOu}“ statt „${vorgabe.zielOu}“.`,
"Der Onboarding-Ablauf prüft den erzeugten Befehl zeichengenau. Entweder die Ziel-OU " +
"unter Admin → Abteilungen zurückstellen, oder gegen eine frische Datenbank fahren: " +
"pnpm db:seed:reset.",
);
}
}
/*
* 2b. In welchem Betriebsmodus laeuft die Installation?
*
* Ein Ablauf soll das nicht selbst aus der Datenbank holen muessen — und zwei
* Quellen fuer dieselbe Auskunft liefen frueher oder spaeter auseinander. Der
* Modus steht danach in `E2E_BETRIEBSMODUS`, und ein Ablauf, der nur in einem
* der beiden Sinn ergibt, ueberspringt sich selbst mit einem Grund.
*/
const einstellung = await prisma.einstellung.findUnique({
where: { id: "global" },
select: { modus: true },
});
process.env["E2E_BETRIEBSMODUS"] = einstellung?.modus ?? "INTERN";
/*
* 2d. Das Konto der Rolle `MITARBEITER` fuer `/meine-geraete` (#037).
*
* Es muss an einer `Mitarbeiter`-Zeile haengen — der CHECK
* `Benutzer_mitarbeiter_nur_bei_rolle` verlangt das in **beide** Richtungen
* (#035): ein MITARBEITER ohne Person waere ein Recht, das sich auf niemanden
* bezieht. Genommen wird die erste Person des Seed-Bestands, denn sie hat
* Geraete, und ohne Geraet zeigte die Seite nur ihren Leerzustand.
*/
const person = await prisma.mitarbeiter.findFirst({
where: { aktiv: true, geraete: { some: {} } },
select: { id: true, kundeId: true, anzeigename: true },
orderBy: { anzeigename: "asc" },
});
if (!person) {
fehlen(
"Es gibt keine Person mit einem Gerät.",
"pnpm db:seed laufen lassen — die Selbstauskunft braucht eine Person, der ein Gerät gehört.",
);
}
await prisma.benutzer.upsert({
where: { email: E2E_MITARBEITER_MAIL },
create: {
email: E2E_MITARBEITER_MAIL,
name: E2E_MITARBEITER_NAME,
rolle: "MITARBEITER",
passwortHash: await passwortHashen(E2E_MITARBEITER_PASSWORT),
mitarbeiterId: person.id,
aktiv: true,
},
// Ein Durchgang, der vorher abgebrochen ist, laesst das Konto stehen. Die
// Person kann dabei eine andere sein als beim letzten Mal.
update: { rolle: "MITARBEITER", mitarbeiterId: person.id, aktiv: true },
});
process.env["E2E_MITARBEITER_PERSON"] = person.anzeigename;
/*
* 3. Liest der Server **dieselbe** Datenbank?
*
* Der Testprozess schreibt über DATABASE_URL aus der .env, der Server bekommt
* seine eigene mit. Zeigen beide woandershin, legt ein Ablauf seine Daten in
* die eine und sucht sie in der anderen. Geprüft wird an einer Vorlage, die
* hier entsteht und gleich wieder verschwindet.
*/
const probe = `ZZ Datenbankprüfung ${Date.now()}`;
const angelegt = await prisma.vorlage.create({ data: { name: probe, kategorie: "ZZ Test" } });
const browser = await chromium.launch();
try {
const tab = await browser.newPage({ baseURL: APP_URL });
await anmelden(tab);
await tab.goto(`/vorlagen?suche=${encodeURIComponent(probe)}`);
if ((await tab.getByRole("link", { name: probe }).count()) === 0) {
fehlen(
"Der Server unter Test liest eine andere Datenbank als dieser Lauf.",
"DATABASE_URL des Testprozesses (aus der .env) und die des Servers müssen auf dieselbe " +
"Datenbank zeigen — sonst schreibt der Ablauf in die eine und sucht in der anderen.",
);
}
} finally {
await browser.close();
await prisma.vorlage.delete({ where: { id: angelegt.id } });
await prisma.$disconnect();
}
}