#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>
190 lines
7.7 KiB
TypeScript
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();
|
|
}
|
|
}
|