L10a Qualität & Abnahmetests: Lane-Bericht
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,103 @@
|
|||||||
|
# Lane L10a – Qualität & Abnahmetests (`lane/qualitaet`)
|
||||||
|
|
||||||
|
Stand: 2026-09-14 · Basis `a7d4b02` (`feature/craftvia-mvp`, L1–L9 integriert) · Spec §27, §34, §37–§40, §42, §43
|
||||||
|
|
||||||
|
## 1. Umfang / erfüllte Spec-Punkte
|
||||||
|
|
||||||
|
| Liefergegenstand | Umsetzung |
|
||||||
|
|---|---|
|
||||||
|
| 1 Demo-Seed | `prisma/seed.ts` ruft `scripts/lib/demo-seed.ts` (nur `SEED_DEMO=true` oder außerhalb Production, `SEED_DEMO=false` schaltet ab). Alles über die echten Fachservices (Statushistorie, Audit, Berichts-Snapshots konsistent), idempotent (Kundennummern `K-D…`, externe Nummern `DEMO-…`, feste Client-IDs beim Notdienst). Mandant `demo`: Team Nord (Tina Teamleiter, Max Monteur, Nora Nordmann), Team Süd (Sven Südfeld, Paul Petersen), 7 Kunden + 1 vorläufiger (Notdienst), 12 Objekte mit Zugangshinweisen (+ vorläufiges Notdienst-Objekt), Auftragsarten + 2 Checklisten-Vorlagen, 20 Aufträge über alle Statusgruppen (Neu, geplant/kommend, heute, unterwegs, in Arbeit/pausiert/Material fehlt, überfällig, mehrtägig mit Tagesbericht, Unterschrift ausstehend, zur Prüfung, technisch abgeschlossen, zur Abrechnung, abgerechnet, Notdienst), Materialvorgaben, Zeiten/Notizen/Material/Fotos, 2 freigegebene Abschlussberichte mit Unterschrift in der Historie von „Wohnanlage Elbblick – Haus A“, 1 Import in Prüfung (Musterdokument + Fake-Extraktion). Mandant `demo2`: 1 Kunde, 1 Objekt, 1 Auftrag. Neue Seed-Nutzer: `teamleiter2@`, `monteur2@`, `monteur3@demo.example`. Alle Personen/Adressen/Telefonnummern erfunden. |
|
||||||
|
| 2 E2E-Prozesstests §43.3 | 7 Skripte `scripts/test-e2e-*.ts` (Service-Ebene, eigene zz-Mandanten, Fixture `scripts/lib/e2e-fixture.ts`) – siehe §4 |
|
||||||
|
| 3 Sicherheitstests §43.4 | 4 Skripte `scripts/test-security-*.ts` inkl. HTTP-Test gegen den echten Server; RLS-Lauf dokumentiert (§5) |
|
||||||
|
| 4 Authentifizierter Durchstich | `scripts/smoke-auth.ts` auf alle Kernseiten je Rolle (Admin, Backoffice, Teamleiter, Monteur Nord/Süd, zweiter Mandant) erweitert: IDs aus den Demo-Daten, Status + erwarteter Text + keine Fehlerseite + keine Fremddaten; `PERF=1` misst die Render-Zeit |
|
||||||
|
| 5 Performance §34.1 | `scripts/seed-load.ts` (5 000 Aufträge, idempotent, `--reset`), Messung dev + Production-Build, Query-Pläne geprüft – keine Index-Migration nötig (§6) |
|
||||||
|
| Sicherheitsbefund behoben | Migration `20260914200000_qualitaet_audit_append_only`: `REVOKE UPDATE, DELETE, TRUNCATE ON audit_logs FROM craftvia_app` (§3) |
|
||||||
|
|
||||||
|
## 2. Dateien
|
||||||
|
|
||||||
|
- `prisma/seed.ts` (Demo-Nutzer + Aufruf Demo-Daten, expliziter Prozess-Exit wegen offener Queue-Handles)
|
||||||
|
- `scripts/lib/demo-seed.ts`, `scripts/lib/e2e-fixture.ts` (neu)
|
||||||
|
- `scripts/seed-load.ts` (neu), `scripts/smoke-auth.ts` (erweitert)
|
||||||
|
- `scripts/test-e2e-{regular-order,multiday-order,emergency,offline-sync,guards,import-duplicates,tenant-isolation}.ts` (neu)
|
||||||
|
- `scripts/test-security-{roles,uploads,auth,http}.ts` (neu)
|
||||||
|
- `prisma/migrations/20260914200000_qualitaet_audit_append_only/migration.sql` (neu)
|
||||||
|
- `docs/craftvia/lanes/qualitaet.md`
|
||||||
|
|
||||||
|
**Keine Änderungen an Fachcode, Fundament, `src/server/api/**`, Docker/CI.** Keine Fremd-Einzeiler, keine Stubs, keine neuen npm-Abhängigkeiten, keine Schemaänderung.
|
||||||
|
|
||||||
|
## 3. Migration (begründet)
|
||||||
|
|
||||||
|
`20260914200000_qualitaet_audit_append_only`: Die App schreibt Audit-Einträge nur per INSERT (`writeAuditLog`, Owner-Client) und liest sie im Viewer; kein Service/keine Action/keine Route ändert oder löscht sie (statisch geprüft in `test-security-auth.ts`). Bisher hatte die RLS-App-Rolle `craftvia_app` über `enable_tenant_rls` volle DML-Rechte auf `audit_logs` – mit `RLS_ENFORCED=true` hätte ein kompromittierter Fachpfad eigene Audit-Einträge manipulieren können (§43.4 „Audit-Log-Manipulation“). Backup/Restore und DSGVO-Löschung laufen über die Owner-Rolle und sind nicht betroffen. **Hinweis:** Ein späteres erneutes `SELECT enable_tenant_rls('audit_logs')` würde die Rechte wieder vergeben – dann die REVOKE-Zeile wiederholen. Keine TENANT_MODELS-/Topologie-Änderung.
|
||||||
|
|
||||||
|
## 4. Tests
|
||||||
|
|
||||||
|
`npm run gate` **grün**: prisma generate, tsc, lint (0 Fehler, 2 bestehende Warnungen außerhalb L10a), build inkl. Modul-Guard-Check, **60/60 Testskripte** (davon 11 neu). Prüfungen der neuen Skripte (✓-Zeilen im Gate-Lauf):
|
||||||
|
|
||||||
|
| Skript | ✓ | Inhalt |
|
||||||
|
|---|---|---|
|
||||||
|
| `test-e2e-regular-order` | 57 | §38 komplett: Import (Fake-Provider) → Dubletten-/Objektkandidat → Bestätigung → Zuweisung (+ Benachrichtigung) → Bundle (Zugangshinweis, internes Dokument verborgen) → Annehmen/Losfahren/Arbeit/Pause → Material Soll/Ist + Zusatz (ohne Grund abgelehnt) → Fotos (Pflichtfoto) → Checkliste → Blocker bei laufender Zeit → Zeiten berechnet → Abschlussbericht (Vorbelegung, Pflichtangabe) → Unterschrift → Teamleiter-/Backoffice-Freigabe (Bericht danach unveränderlich) → PDF (skip ohne Chromium; lokal erzeugt, `%PDF`, Checksumme) → Abrechnung → abgerechnet → vollständige Statushistorie + Audit → Objekt-Historie (Backoffice, Teamkollege). Mandant B und Monteur ohne Zuweisung an jedem Schritt abgewiesen |
|
||||||
|
| `test-e2e-multiday-order` | 22 | 2 Einsatztage, 2 Monteure, Tagesbericht je Tag nur mit Daten seines Tages (Zeiten 480/420 min, Fotos, Notizen, Material), zweiter Tagesbericht desselben Tages → conflict, daily_report_created → in_progress am Folgetag, Abschlussbericht über beide Tage (900 min), Freigaben, Abrechnung, Berichtsliste, Historie (3 freigegebene Berichte) |
|
||||||
|
| `test-e2e-emergency` | 31 | §39: Monteur legt Notdienst mit vorläufigem Kunden an (N-Nummer, laufende Session, Event), Dokumentation, Abschluss „Kunde abwesend“ (Grund Pflicht) → Zur Prüfung, `emergency.completed` + `signature_missing` + Pflichtmail; Rollen (Monteur/Teamleiter forbidden) + Mandant B; Backoffice: Dublettenkandidat, Abrechnung blockiert (Stammdaten/Bericht), Kunde zuordnen (vorläufiger → merged, Version +1), Objekt bestätigen, Auftrag ergänzen, Freigabe 5/5, abgerechnet, Historie |
|
||||||
|
| `test-e2e-offline-sync` | 40 | 12 Ops (2 Tage alt) in einem Batch → applied inkl. idMap und Gerätezeitstempeln, Wiederholung → duplicate ohne Doppelanlage, fremde clientOpId → rejected, Pflichtfoto fehlt → rejected blocked, Konflikt (Büroänderung) → conflict ohne Überschreiben + unabhängige Notiz applied + `sync.failed` an Monteur/Backoffice + Konfliktliste, Übernehmen (als Gerätenutzer) / Verwerfen, ungültige und nicht registrierte Ops, Scope und Mandant B |
|
||||||
|
| `test-e2e-guards` | 31 | Pflichtfoto + Checklistenpunkt „mit Foto“ blockieren Abschluss, Bericht und Sync-Op; Blocker verschwinden erst mit passenden Fotos; Nicht-Bild/fremdes Foto abgewiesen. Unterschrift: ohne → signature_pending (kein Weg zur Prüfung/Abrechnung), Nachreichen → in_review, erfasste Unterschrift unveränderlich, Verweigerung/Abwesenheit nur mit Grund (Snapshot, Hinweis ans Backoffice), Bild fehlt/fremd → invalid, „später“, „nicht erforderlich“ nur ohne Pflicht |
|
||||||
|
| `test-e2e-import-duplicates` | 24 | Kandidaten über Kundennummer, Firmenname, Adresse (Str./Straße), Telefon (anderes Format), E-Mail (Groß-/Kleinschreibung); nie automatische Zuordnung/Zusammenführung; Entscheidung „neu“ protokolliert, vergebene Nummer → conflict; manuelle Anlage „Mögliche Dublette“; Zusammenführen nur mit Bestätigung + Recht; Mandant B / Monteur |
|
||||||
|
| `test-e2e-tenant-isolation` | 159 | **Systematisch:** alle 35 Modelle aus `TENANT_MODELS` (Test scheitert bei neuem Modell ohne Fixture) – aus Mandant B findMany/findFirst/findUnique/count/update/updateMany/delete/deleteMany wirkungslos, Zeile unverändert, create mit fremder tenantId landet in B; **Postgres-RLS** je Tabelle als `craftvia_app` (Kontext B sieht/ändert/löscht nichts, Kontext A sieht, ohne Kontext 0 Zeilen); **67 Fachservice-Pfade** (Kunden, Objekte, Teams, Aufträge, Einsatz, Dokumente, Berichte, Import, Notdienst, Sync, Benachrichtigungen, Audit, Lotse) → not_found bzw. invalid bei Referenzen; 15 Listen/Suche/Bundle/Dashboard ohne A-Daten; A danach unverändert |
|
||||||
|
| `test-security-roles` | 113 | Rollen-Matrix aus `rbac.ts`: 26 Permission-Proben × 4 Rollen, Erwartung unabhängig aus `ROLE_DEFS` (fehlt → forbidden bzw. not_found bei Sichtbarkeitsrechten, vorhanden → nie forbidden); Abdeckungsprüfung aller Permissions (begründete Ausnahmen: Nutzer-/Rollenverwaltung → `test-tenant-users-authz.ts`, `document:read` → HTTP-Test); Sichtbarkeit read_team/Teamleiter-Dokumente |
|
||||||
|
| `test-security-uploads` | 41 | EXE/ELF/ZIP/HTML/SVG mit harmloser Endung → type_mismatch/unsupported_type, nichts gespeichert; Polyglot nur mit erkanntem Bildtyp; Übergröße je Art (15/25 MB), leer, Import; Dateinamen (Traversal, Backslash, CR/LF, reservierte Zeichen, Länge) → normalisiert, Key im Mandantenpräfix; Einsatz-Uploads; Sichtbarkeits-Eskalation; Malware-Befund und nicht erreichbarer ClamAV → fail closed; Audit nur für gespeicherte Dateien |
|
||||||
|
| `test-security-auth` | 31 | Login-Sperre nach 5 Fehlversuchen (auch richtiges Passwort abgewiesen, auditiert, Entsperren), kein Konto-Orakel, letzter Admin nicht aussperrbar; Rate-Limit Reset je IP/Konto; Session-Kill-Switch; Audit-Log: statischer Scan (keine Mutation in app/actions/services/api/jobs/lib), keine /api-Audit-Route, Viewer rein lesend, DB-Rechte `craftvia_app` (SELECT/INSERT ja, UPDATE/DELETE nein), Tenant-Guard, als `craftvia_app` UPDATE/DELETE → permission denied |
|
||||||
|
| `test-security-http` | 108 | Gegen `next start` (Produktions-Build aus dem Gate, freier Port) oder `SECURITY_BASE`: ohne Session 14 /api/v1-Routen → 401, Datei-Routen/Seiten → Login ohne Bytes; **manipulierte IDs: alle /api/v1-Routen mit ID (work-orders inkl. assign/transition/materials/documents/daily-/completion-report, reports approve/pdf/files, customers, sites history, imports + confirm, field documents, uploads) sowie `/files/<id>` und `/imports/<id>/file` → 404** ohne Daten im Body, Sync → rejected not_found, Listen/Bundle ohne A-Daten, Monteur ohne Zuweisung → 404, unsinnige IDs → 404 statt 500; Rollenaktionen → 403 + Audit „denied“; Uploads über `/documents/upload`, `/api/v1/uploads`, `/api/v1/work-orders/import` (EXE, HTML, SVG, Polyglot mit Download `image/jpeg` + `attachment` + `nosniff` + CSP, Traversal, CR/LF ohne Header-Injection, > 25 MB, Server bleibt erreichbar); CSRF (fremder Origin → 403); Sessions (manipuliert, fremder Schlüssel, abgelaufen, Kill-Switch, deaktiviert); PATCH/PUT/DELETE auf Audit-Pfade ohne Wirkung; Login-Sperre über `/api/auth/callback/credentials` (Positivkontrolle); Sicherheits-Header |
|
||||||
|
|
||||||
|
Statuscodes: Die HTTP-Tests akzeptieren für `invalid` 400 **oder** 422 und prüfen sonst 401/403/404 exakt – damit gültig vor und nach der Vereinheitlichung durch L10b. Die Offline-Prüfung „nicht registrierte Op“ ermittelt den Op-Typ dynamisch aus `EXTERNAL_OPS` (L10b registriert `report.submit`/`report.save_draft`). Schreibende Requests senden den eigenen Origin (L10b-Same-Origin-Prüfung). Je Nutzer weit unter 300 Requests/min (L10b-Rate-Limit).
|
||||||
|
|
||||||
|
## 5. RLS-Lauf (`RLS_ENFORCED=true`, `RLS_DATABASE_URL` auf die Lane-DB)
|
||||||
|
|
||||||
|
- **Alle neuen E2E- und Sicherheitsskripte: 11/11 grün** (inkl. Migration append-only).
|
||||||
|
- **Gesamte Testsuite: 58/60 grün.** Die zwei Abweichungen liegen in Fundament-Tests und sind keine Isolationslücken:
|
||||||
|
- `test-garage-storage.ts`: Abbruch „RLS_ENFORCED=true, aber RLS_DATABASE_URL fehlt“ – das Skript lädt die Umgebung offenbar erst nach dem Import von `db.ts` (fail-secure greift). Testumgebungsthema.
|
||||||
|
- `test-tenant-isolation.ts` (F-02): „findUnique über Compound-Key (fremd) → Throw“ erwartet den Owner-Pfad; mit scharfer RLS ist die fremde Zeile unsichtbar und das Ergebnis `null` – ebenfalls fail-closed, kein Datenabfluss. Test sollte beide Ausprägungen akzeptieren.
|
||||||
|
|
||||||
|
## 6. Performance (§34.1) – 5 020 Aufträge im Mandanten `demo`
|
||||||
|
|
||||||
|
`scripts/seed-load.ts` (3,1 s für 5 000 Aufträge inkl. Zuweisungen, Materialvorgaben, Historie, Berichte) → `PERF=1 scripts/smoke-auth.ts` (zweiter, warmer Request):
|
||||||
|
|
||||||
|
| Seite | Dev-Server | Production-Build (`next start`) |
|
||||||
|
|---|---|---|
|
||||||
|
| `/dashboard` (Backoffice) | 233 ms | 30 ms |
|
||||||
|
| `/work-orders` (Karten / Tabelle / überfällig / Gruppe / Suche / Seite 3) | 252–319 ms | 33–40 ms |
|
||||||
|
| `/reports` | 467 ms | 53 ms |
|
||||||
|
| `/search?q=` | 206 ms | 30 ms |
|
||||||
|
| `/m` / `/m/orders` (Teamleiter, Monteur mit ~2 500 Aufträgen im Team) | 724 / 1 536 ms | 63–67 / 104–111 ms |
|
||||||
|
|
||||||
|
Alle Seiten deutlich < 2 s. Query-Pläne (EXPLAIN ANALYZE) der Liste mit Monteur-Scope, Dashboard-Zählungen (überfällig, Berichte zur Prüfung), Paginierung und Berichtsliste: < 2 ms, Index-Scans auf den bestehenden Indizes (`work_orders(tenant_id, planned_start|status)` u. a.). Kein N+1: Liste und Dashboard laden mit festen Abfragen (Dashboard 11 parallele Counts + 2). **Keine Index-Migration nötig.** Last-Daten danach mit `--reset` wieder entfernt.
|
||||||
|
|
||||||
|
## 7. Authentifizierter Durchstich (Dev-Server :3110, Demo-Seed)
|
||||||
|
|
||||||
|
`BASE=http://localhost:3110 npx tsx scripts/smoke-auth.ts` → **OK, 94/94 Prüfungen**:
|
||||||
|
- **Admin:** Dashboard, Einstellungen (Nutzer, Audit, E-Mail, Lotse + Protokoll, Auftragsarten, Checklisten, Nummernkreise), Konto, Teams.
|
||||||
|
- **Backoffice:** Startseite → `/dashboard`, Aufträge (Karten, Tabelle, überfällig, Statusgruppe, Suche, Seite 3), Auftragsdetail mit allen 9 Tabs, Konflikte, Notdienst-Prüfung (Liste + Detail), Importe + **Prüfmaske**, Kunden (+ vorläufig, Detail), Objekte (+ Detail, **Historie** mit freigegebenem Auftrag), Berichte (+ freigegeben, zur Prüfung), Dokumente, Benachrichtigungen, Suche, Teams.
|
||||||
|
- **Teamleiter:** `/` → `/m`, Heute, Aufträge, mehrtägiger Auftrag, Auftrag zur Prüfung, Berichte; Auftrag von Team Süd → 404.
|
||||||
|
- **Monteur (Nord):** `/` → `/m`, Heute, Aufträge (Tab laufend), Auftragsdetail (Zugangshinweis), Fotos, Material, Notizen, Checkliste, Zeiten, Tagesbericht, Abschlussbericht, **Unterschrift**, Notdienst-Auftrag, Notdienst-Erfassung, Sync, Offline, Profil, eigenes Foto; fremder Auftrag/Kunde → 404, `/dashboard` → `/m`.
|
||||||
|
- **Monteur (Süd):** Heute, Unterschrift ausstehend; Auftrag von Team Nord → 404.
|
||||||
|
- **Zweiter Mandant (admin2@):** Dashboard/Aufträge/Kunden/Suche ohne Demo-Daten; Auftrag, Kunde, Objekt, Bericht, Notdienst, Import, Datei aus `demo` → 404.
|
||||||
|
|
||||||
|
Gefundene Fehler im Durchstich: **keine** (kein Fachcode geändert). Visuelle Prüfung im Browser (375/768/1024 px) nicht durchgeführt: der Browser-Login würde eine Passworteingabe bzw. das Einschleusen eines Session-Tokens durch den Agenten erfordern; die mobilen Seiten wurden in L4/L7 visuell geprüft.
|
||||||
|
|
||||||
|
## 8. Stubs / Abhängigkeiten
|
||||||
|
|
||||||
|
Keine Stubs. Genutzt werden ausschließlich die integrierten Services von L1–L9. Kompatibilität mit L10b siehe §4 (Statuscodes, Same-Origin, Rate-Limit, registrierte Sync-Ops).
|
||||||
|
|
||||||
|
## 9. Befunde, bekannte Lücken, Hinweise an den Architekten
|
||||||
|
|
||||||
|
1. **Login ohne IP-basiertes Limit** (nur Sperre je Identität nach 5 Fehlversuchen; Reset/Einladung haben IP+Konto-Limits). Password-Spraying über viele Konten wird nicht gebremst → Kandidat für das L10b-Rate-Limit (auf `/api/auth/callback/credentials`).
|
||||||
|
2. **Polyglot-Dateien** (gültige Signatur + angehängter Fremdinhalt) passieren den Magic-Byte-Scanner. Mitigiert durch Speicherung mit erkanntem MIME-Typ, Auslieferung als `attachment` + `nosniff` + CSP (HTTP-Test); echter Inhaltsscan nur mit `CLAMAV_HOST`.
|
||||||
|
3. **Doppelte Audit-Einträge bei Einsatz-Uploads:** `storeFieldUpload` (L4) schreibt nach `storeFile` einen zweiten `document/create`-Eintrag. Harmlos, aber redundant.
|
||||||
|
4. **`confirmImport` validiert das Formular vor der Mandanten-/Existenzprüfung** → fremde ID mit ungültigem Formular ergibt `invalid` statt `not_found` (kein Datenabfluss; mit gültigem Formular 404).
|
||||||
|
5. **Mobile Listen ohne Paginierung** (`/m/orders` max. 200, „Heute“ max. 100 Aufträge je Abruf; §34.1 „Auftragslisten müssen paginiert werden“). Performance unkritisch; bei sehr großen Teams fallen Einträge aus der Liste.
|
||||||
|
6. **RLS-Fundament-Tests** (§5): `test-garage-storage.ts` (Env-Ladereihenfolge) und `test-tenant-isolation.ts` (Compound-Key erwartet Throw statt `null`) für `RLS_ENFORCED=true` anpassen.
|
||||||
|
7. **Demo-Termine sind relativ zum Seed-Zeitpunkt** („heute“, „überfällig“); der Seed ist idempotent und verschiebt bestehende Demo-Aufträge nicht. Für frische Demo-Daten Mandant neu aufsetzen.
|
||||||
|
8. **Seed in Production:** `prisma/seed.ts` lädt `scripts/lib/demo-seed.ts` nur bei aktivierten Demo-Daten dynamisch; ein Production-Image ohne `scripts/` braucht `SEED_DEMO` ungesetzt (Default in Production: aus).
|
||||||
|
9. Der HTTP-Sicherheitstest startet im Gate den Build per `next start` („does not work with output: standalone“ ist nur eine Warnung). Ohne Build und ohne `SECURITY_BASE` wird er übersprungen (Exit 0).
|
||||||
|
|
||||||
|
## 10. Screens / Routen
|
||||||
|
|
||||||
|
Keine neuen Routen. Geprüft (Smoke/HTTP): siehe §7 und §4 (`test-security-http`).
|
||||||
Reference in New Issue
Block a user