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>
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user