Ein Server, mehrere Wehren. Ein Konto, mehrere Zugehoerigkeiten. Eine Kreisebene, die zusieht. Datenmodell und RLS trugen seit Phase 0 mehrere Wehren. Was fehlte, war alles darueber: bis hierher war die Personenakte zugleich das Anmeldekonto, und die gehoert genau einer Wehr. Wer in zweien gefuehrt wurde, brauchte zwei Passwoerter, und die Anmeldung per E-Mail war mehrdeutig. Neu sind `Benutzer` (das Konto) und `Mitgliedschaft` (die Zugehoerigkeit). Das Konto traegt bewusst keine Mandantenkennung und kann keine tragen - es ist der Angelpunkt, an dem ein Mensch zu mehreren Wehren gehoert. Geschuetzt wird es deshalb ueber Rechte statt ueber RLS, wie `Berechtigung` seit Phase 0. Was es in einer Wehr darf, entscheidet seine Mitgliedschaft. Die Sitzung behaelt ihre Form und entsteht nur anders - aus der aktiven Mitgliedschaft statt aus der Person. Von 414 Fundstellen in 61 Dateien musste dadurch keine angefasst werden. Die eine Ausnahme ist Absicht: `personId` ist nullbar geworden, weil eine Aufsichtssitzung keine Akte hat, und der Typechecker hat daraus genau die fuenf Stellen gemacht, an denen die Kreisaufsicht nichts verloren hat. Ihre Rechte werden gefiltert statt aufgezaehlt: alles, was `.lesen` heisst, minus Kasse, Audit-Log, KI-Protokoll und Personenexport. So bekommt sie neue Leserechte automatisch und neue Schreibrechte nie. Die Migration ist datenerhaltend und bricht ab, statt einen halben Zustand zu hinterlassen. Nebenbei behoben: `db:test-reset` liess die Nachbarwehr stehen, der Fehlversuch-Test sperrte das gemeinsame Testkonto, ein Testaufbau war nicht wiederholbar, und die Wartelogik der Bildschirmfotos konnte ausloesen, bevor eine Seite ueberhaupt geladen war. 581 Unit-Tests und 266 E2E-Tests gruen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
138 lines
5.1 KiB
TypeScript
138 lines
5.1 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import { ADMIN_EMAIL, ADMIN_PASSWORT, WEHR_NAME } from "./wehr";
|
|
|
|
/**
|
|
* Beispiel-E2E-Test für Phase 0.
|
|
*
|
|
* Geprüft wird der Weg, ohne den nichts anderes funktioniert: jemand ruft die
|
|
* Anwendung auf, wird zur Anmeldung geschickt, meldet sich an und landet auf
|
|
* der Übersicht seiner Wehr.
|
|
*
|
|
* Voraussetzung: Stack läuft, Datenbank ist migriert und geseedet.
|
|
* docker compose up -d && npm run db:migrate && npm run db:seed
|
|
*/
|
|
|
|
test.describe("Anmeldung", () => {
|
|
test("schickt nicht angemeldete Aufrufe zur Anmeldeseite", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/");
|
|
await expect(page).toHaveURL(/\/anmelden/);
|
|
await expect(page.getByRole("heading", { name: "FLORIAN" })).toBeVisible();
|
|
});
|
|
|
|
test("weist eine falsche Kombination ab und nennt den Grund auf Deutsch", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/anmelden");
|
|
|
|
// Bewusst eine Adresse, die es nicht gibt, statt der des Administrators.
|
|
//
|
|
// Ein Fehlversuch zählt auf das Konto, und nach fünf davon ist es fünf
|
|
// Minuten gesperrt. Bei zwei Projekten und Wiederholungen kamen im vollen
|
|
// Lauf genug zusammen, um andere Testdateien mitzureißen — sie fielen
|
|
// dann mit „Anmeldung fehlgeschlagen“ aus, ohne dass es etwas mit dem zu
|
|
// tun hatte, was sie prüfen.
|
|
//
|
|
// Die Meldung muss ohnehin dieselbe sein: sie darf nicht verraten, ob es
|
|
// die Adresse gibt. Genau das prüft dieser Test mit.
|
|
await page.getByLabel("E-Mail-Adresse").fill("gibt-es-nicht@teststelle.de");
|
|
await page.getByLabel("Passwort").fill("definitiv-das-falsche-passwort");
|
|
await page.getByRole("button", { name: "Anmelden" }).click();
|
|
|
|
// Nicht ueber die Rolle "alert" suchen: Next.js haengt einen eigenen
|
|
// Routen-Ansager mit derselben Rolle in die Seite.
|
|
const meldung = page.getByTestId("anmeldefehler");
|
|
await expect(meldung).toBeVisible();
|
|
await expect(meldung).toContainText("stimmt nicht");
|
|
// Und niemand ist hineingekommen.
|
|
await expect(page).toHaveURL(/\/anmelden/);
|
|
});
|
|
|
|
test("lässt den Administrator hinein und zeigt die Übersicht seiner Wehr", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/anmelden");
|
|
|
|
await page.getByLabel("E-Mail-Adresse").fill(ADMIN_EMAIL);
|
|
await page.getByLabel("Passwort").fill(ADMIN_PASSWORT);
|
|
await page.getByRole("button", { name: "Anmelden" }).click();
|
|
|
|
await page.waitForURL("/");
|
|
|
|
// Die Wehr, gegen die getestet wird — nicht die Beispielwehr aus der
|
|
// Anleitung. Welche das ist, steht in wehr.ts.
|
|
await expect(page.getByRole("heading", { name: WEHR_NAME })).toBeVisible();
|
|
|
|
// Die Kennzahlen aus dem Seed: 15 Mitglieder, 3 Fahrzeuge, 40 Geräte.
|
|
await expect(page.getByText("Aktive Mitglieder")).toBeVisible();
|
|
await expect(
|
|
page.getByText("Fahrzeuge", { exact: true }).first(),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByText("Geräte", { exact: true }).first(),
|
|
).toBeVisible();
|
|
|
|
// Der Sicherheitshinweis muss stehen, solange nichts freigegeben ist.
|
|
await expect(page.getByRole("note")).toContainText("analoge Überwachung");
|
|
});
|
|
|
|
test("zeigt die Navigation mit allen umgesetzten Modulen", async ({ page }) => {
|
|
await page.goto("/anmelden");
|
|
await page.getByLabel("E-Mail-Adresse").fill(ADMIN_EMAIL);
|
|
await page.getByLabel("Passwort").fill(ADMIN_PASSWORT);
|
|
await page.getByRole("button", { name: "Anmelden" }).click();
|
|
await page.waitForURL("/");
|
|
|
|
const navigation = page.getByRole("navigation", {
|
|
name: "Hauptnavigation",
|
|
});
|
|
await expect(
|
|
navigation.getByRole("link", { name: "Übersicht" }),
|
|
).toBeVisible();
|
|
|
|
// Termine, Dienstausweis und Benachrichtigungen stehen seit Phase 7 zur
|
|
// Verfügung und sind Verweise.
|
|
await expect(
|
|
navigation.getByRole("link", { name: "Termine" }),
|
|
).toBeVisible();
|
|
await expect(
|
|
navigation.getByRole("link", { name: "Dienstausweis" }),
|
|
).toBeVisible();
|
|
await expect(
|
|
navigation.getByRole("link", { name: "Benachrichtigungen" }),
|
|
).toBeVisible();
|
|
|
|
// Die Kasse ist seit Phase 9 ein Verweis.
|
|
//
|
|
// Bis dahin stand hier die Gegenprobe: der Eintrag sichtbar, aber
|
|
// ausgegraut, weil das Modul noch fehlte. Mit Phase 9 gibt es kein
|
|
// künftiges Modul mehr — eine Zusicherung über ein Modul, das es nicht
|
|
// gibt, wäre keine. Kommt später eines dazu, gehört sie hierher zurück.
|
|
await expect(navigation.getByRole("link", { name: "Kasse" })).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test.describe("Erreichbarkeit", () => {
|
|
test("meldet den Zustand der Randsysteme unter /api/health", async ({
|
|
request,
|
|
}) => {
|
|
const antwort = await request.get("/api/health");
|
|
expect(antwort.status()).toBe(200);
|
|
|
|
const daten = (await antwort.json()) as {
|
|
status: string;
|
|
dienste: {
|
|
datenbank: boolean;
|
|
speicher: boolean;
|
|
warteschlange: boolean;
|
|
};
|
|
};
|
|
|
|
expect(daten.status).toBe("ok");
|
|
expect(daten.dienste.datenbank).toBe(true);
|
|
expect(daten.dienste.speicher).toBe(true);
|
|
expect(daten.dienste.warteschlange).toBe(true);
|
|
});
|
|
});
|