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 { /* * **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(); } }