Files
FireAdministration/tests/e2e/anmeldung.spec.ts
T
Florian WildeandClaude Opus 5 845942f36c Phase 0: Fundament der Feuerwehr-Verwaltungsplattform
Legt den kompletten Unterbau an, auf dem die Fachmodule aufsetzen.

Betrieb:
- docker-compose.yml mit Traefik, App, Worker, PostgreSQL 17 + PostGIS,
  Redis, MinIO und Gotenberg; Entwicklungs-Override ohne Traefik, dafuer
  mit Mailpit und Adminer
- Mehrstufiges Dockerfile: eigener Benutzer, tini als PID 1, Healthcheck
- .env.example mit einem Kommentar zu jeder Variable

Mandantentrennung:
- Row Level Security auf allen mandantenpflichtigen Tabellen
- Zwei getrennte Datenbankverbindungen: florian_app mit aktiver RLS fuer
  die Anwendung, Eigentuemer-Zugang nur fuer Migration, Seed und Anmeldung
- fuerOrganisation() als einziger Weg zu Fachdaten

Nachvollziehbarkeit:
- AuditLog append-only per Trigger; UPDATE ist dauerhaft gesperrt, DELETE
  nur im gekennzeichneten Aufbewahrungslauf
- Geheimnisse werden vor dem Schreiben entfernt

Anmeldung:
- OIDC gegen ein extern betriebenes authentik, Rollen aus Gruppen nur als
  Vorbelegung beim ersten Login
- Lokaler E-Mail/Passwort-Login als Rueckfallweg
- Terminal per PIN und RFID mit zwei Schranken (Geraet, dann Person)

Weiteres:
- lib/authz.ts mit 9 Rollen und 38 Einzelrechten
- lib/storage.ts (MinIO), lib/audit.ts, lib/warteschlange.ts (BullMQ)
- Integrationen fuer Mail, Telegram und LLM hinter abschaltbaren Interfaces
- tRPC v11, Basis-Layout mit Seitenleiste und dunkler Darstellung
- Seed mit einer Beispielwehr: 16 Personen, 3 Fahrzeuge, 40 Geraete
- 32 Vitest-Tests (davon 8 gegen eine echte Datenbank), 10 Playwright-Tests
- README, Anleitung mit Screenshots, Roadmap, Aufwandsschaetzung, Devlog,
  Versionierungsregeln und Changelog

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 00:13:53 +02:00

97 lines
3.8 KiB
TypeScript

import { expect, test } from "@playwright/test";
/**
* 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
*/
const ADMIN_EMAIL = process.env.SEED_ADMIN_EMAIL ?? "admin@beispielwehr.de";
const ADMIN_PASSWORT = process.env.SEED_ADMIN_PASSWORT ?? "Feuerwehr2026";
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");
await page.getByLabel("E-Mail-Adresse").fill(ADMIN_EMAIL);
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 aus dem Seed.
await expect(
page.getByRole("heading", { name: "Freiwillige Feuerwehr Musterhausen" }),
).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 und markiert künftige Module als noch nicht verfügbar", 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();
// Atemschutz kommt erst in Phase 3 — der Eintrag ist sichtbar, aber kein Link.
await expect(navigation.getByText("Atemschutz")).toBeVisible();
await expect(navigation.getByRole("link", { name: "Atemschutz" })).toHaveCount(0);
});
});
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);
});
});