Files
leocrm/docs/test-strategy.md
T

402 lines
20 KiB
Markdown
Raw Normal View History

# LeoCRM Test-Strategie
> **Wichtig:** Dieses Dokument muss nach jeder größeren Änderung am Codebase
> (neue Plugins, neue Module, Refactoring, Security-Änderungen) überarbeitet
> werden. Siehe `leocrm-test-strategy.promptinclude.md`.
---
## 1. Übersicht
LeoCRM verwendet eine mehrschichtige Test-Strategie um Funktionalität,
Sicherheit und Stabilität sicherzustellen.
### Test-Pyramide
```
┌──────────┐
│ E2E │ ← Browser-Tests (geplant, noch nicht implementiert)
├──────────┤
│Integration│ ← pytest mit echter PostgreSQL/Redis Test-DB
├──────────┤
│ Unit │ ← pytest mit Mocks (teilweise)
└──────────┘
```
### Aktuelle Abdeckung
| Ebene | Tool | Status | Abdeckung |
|-------|------|--------|-----------|
| Backend-Tests | pytest | ✅ aktiv | 97 Testdateien, 2174 Tests |
| Frontend-Tests | vitest | ⚠️ geplant | 0 Tests (54k Zeilen ungetestet) |
| E2E-Tests | Playwright/Cypress | ⚠️ geplant | 0 Tests |
| Security-Tests | bandit, pip-audit | ⚠️ geplant | nicht implementiert |
| CI-Pipeline | scripts/ci_pipeline.sh | ✅ aktiv | 15+ Checks |
---
## 2. Backend-Tests (pytest)
### Architektur
- **Test-DB:** PostgreSQL `leocrm_test` (localhost:5432)
- **Redis:** localhost:6379/0 (wird vor jedem Test geflushed)
- **Fixture-Strategie:** Function-scoped (jeder Test bekommt frische DB)
- **Schema-Erstellung:** `Base.metadata.create_all` (gleiche wie Produktion sync_plugin_schema.py — Alembic-Migrationen laufen nur in Produktion via prestart.sh)
- **Plugin-Aktivierung:** In-Memory-Registry muss pro Fixture gesetzt werden
### Bekannte Einschränkungen
1. **RLS (Row Level Security) nicht testbar:**
- Die Test-DB verwendet `create_all` statt Alembic-Migrationen
- RLS-Policies werden normalerweise durch Migrationen erstellt
- RLS-Tests wurden ausgebaut (bringt nichts wenn es sich nicht testen lässt)
- **Lösung:** Alembic-Migrationen in Test-DB ausführen (Roadmap)
2. **LLM-API-Tests blockieren:**
- Tests die externe LLM-APIs aufrufen (Ollama Cloud, OpenRouter) blockieren
- Die komplette Suite hängt bei ~46% wenn LLM-Calls nicht gemockt sind
- **Lösung:** LLM-Calls in Tests mocken (Roadmap)
3. **Fixture-Overhead (~2,5s pro Test):**
- `seed_tenant_and_users` (1,27s) + `create_app` (0,73s) + `login` (0,34s)
- Wird pro Test ausgeführt (Function-Scope)
- Session-Scope ist nicht möglich weil ~30 Testdateien `seed_tenant_and_users` direkt aufrufen
- **Lösung:** Seed-Daten session-scopen + clean_tables anpassen (Roadmap)
4. **Keine Parallelisierung möglich:**
- `pytest-xdist` funktioniert nicht weil alle Worker dieselbe Test-DB teilen
- **Lösung:** Pro-Worker Datenbank (Roadmap)
### Test-Kategorien
| Kategorie | Beschreibung | Beispiele |
|-----------|-------------|----------|
| **Funktionale Tests** | Testet ob Features funktionieren | test_calendar, test_tags, test_dms |
| **Permission-Tests** | Testet ABAC/Permission-System | test_permissions, test_entity_permissions |
| **Plugin-Tests** | Testet Plugin-Lifecycle und -Funktionen | test_plugins, test_entity_links |
| **Cross-Tenant-Tests** | Testet Tenant-Isolation | test_cross_tenant_security |
| **AI-Tests** | Testet AI-Proactive, Copilot, GraphRAG | test_ai_proactive, test_ai_copilot |
| **Integration-Tests** | Testet Modul-übergreifend | test_unified_search, test_outbox |
### Konventionen
1. **Test-Dateien:** `tests/test_<modul>.py`
2. **Fixtures:** In `tests/conftest.py` definiert
3. **Plugin-Aktivierung:** Jede Plugin-Test-Datei muss `init_permission_registry(active_plugin_names={...})` aufrufen
4. **Entity-Typen:** Verwende korrekte ENTITY_MODELS-Keys (z.B. `file` nicht `dms_file`, `mail_account` nicht `mailbox`)
5. **URLs:** Verwende korrekte API-Pfade (z.B. `/api/v1/entity-links/` nicht `/api/v1/dms/`)
6. **Dedup-Tests:** Verwende unterschiedlichen Dateiinhalt pro Upload um Dedup-Logik nicht zu triggern
7. **Keine zufälligen UUIDs:** Verwende echte Entity-IDs aus der DB, nicht `uuid.uuid4()`
---
## 3. Frontend-Tests (geplant)
### Aktuell
- **0 Tests** für 54.000 Zeilen TSX/TypeScript
- Frontend-Bugs werden nur manuell im Browser gefunden
### Roadmap
- **Unit-Tests:** vitest für React-Komponenten
- **Integration-Tests:** Testing Library für Komponenten-Interaktionen
- **E2E-Tests:** Playwright für kritische User-Flows (Login, Kontakt erstellen, Kalender)
---
## 4. Security-Testing (geplant)
### Aktuell
- Keine automatisierten Security-Tests
- Security-Bugs wurden durch manuelle Code-Review gefunden (siehe Bugfix-Session 2026-08-12)
### Bekannte Security-Lücken (behoben am 2026-08-12)
| Bug | Fix | Status |
|-----|-----|--------|
| `MAIL_ENCRYPTION_KEY` hatte Default-Wert | RuntimeError wenn nicht gesetzt | ✅ |
| `revoke_permission` ohne Owner-Check | `check_single_entity_access` hinzugefügt | ✅ |
| `is_active=True` hart codiert im DB-Fallback | User-Status aus DB laden | ✅ |
| Plugin-Gate allow bei fehlendem Tenant | TODO - bricht Tests, muss in Produktion anders gelöst werden | ⚠️ |
| Public Share URL falsch | URL korrigiert | ✅ |
| Logout nur in Redis | Auch PostgreSQL invalidieren | ✅ |
| Rate-Limit nur auf IP | Token-Hash für Bearer-Auth | ✅ |
| RLS-Commit statt flush | Alle Commits durch flush ersetzt | ✅ |
| Webhook ohne Tenant-Context | `set_tenant_context` hinzugefügt | ✅ |
| `npm ci \|\| npm install` Fallback | Nur `npm ci` | ✅ |
### Roadmap
- **bandit:** Python Security-Scanner in CI-Pipeline
- **pip-audit:** Dependency-Scanning
- **npm audit:** Frontend-Dependency-Scanning
- **OWASP ZAP:** Web-Application-Scanner gegen Test-Instanz
- **Security-Test-Suite:** Eigene pytest-Tests für Security-Szenarien
---
## 5. CI/CD Pipeline
### Aktuelle Checks (scripts/ci_pipeline.sh)
1. Python Compile Check
2. Cross-Plugin Import Check
3. Alembic Revision Graph
4. Alembic Migration Test (wenn DATABASE_URL gesetzt)
5. Migration Hash Check
6. TypeScript Type Check
7. Frontend Build
8. Test Collection
9. Backend Tests
10. Frontend Tests
11. SQL Injection Check
12. npm ci strict mode
### Bekannte CI-Lücken
- **Smoke-Test gegen Build:** Tests laufen gegen `leocrm_test` DB, nicht gegen den aktuellen Build
- **Frontend-Tests:** vitest ist konfiguriert aber hat 0 Tests
- **Security-Scanning:** bandit/pip-audit nicht in Pipeline
- **E2E-Tests:** Nicht in Pipeline
---
## 6. Was getestet wird und was nicht
### ✅ Wird getestet
- API-Endpunkte (CRUD, Validierung, Permissions)
- Plugin-Lifecycle (Install, Activate, Deactivate)
- ABAC/Permission-System
- Cross-Tenant-Isolation (ohne RLS)
- AI-Proactive/GraphRAG (mit Mocks)
- Outbox/Event-System
- Backup/Restore
- Auth/Login/Logout
- Tags, Calendar, DMS, Mail, Contacts, Tasks
### ❌ Wird NICHT getestet
- **Frontend** (54k Zeilen, 0 Tests)
- **RLS-Policies** (Test-DB hat keine RLS)
- **LLM-APIs** (blockieren Suite, nicht gemockt)
- **Race Conditions** (keine Last-Tests)
- **Security-Edge-Cases** (SQL Injection nur oberflächlich)
- **Dockerfile/Deployment** (nur Code, nicht Infrastruktur)
- **CI-Scripts selbst** (Shell-Scripts nicht getestet)
- **Produktions-Logs** (keine Log-Analyse)
---
## 7. Roadmap
| Priorität | Maßnahme | Aufwand | Nutzen |
|-----------|---------|--------|-------|
| 🔴 Hoch | Frontend Unit-Tests (vitest) | mittel | 54k Zeilen abgedeckt |
| 🔴 Hoch | LLM-API-Calls mocken | gering | Suite läuft komplett durch |
| 🟡 Mittel | Security-Test-Suite | mittel | Security-Bugs automatisch gefunden |
| 🟡 Mittel | E2E-Tests (Playwright) | hoch | Kritische User-Flows getestet |
| 🟡 Mittel | Alembic-Migrationen in Test-DB | mittel | RLS testbar |
| 🟡 Mittel | Fixture-Optimierung (Session-Scope) | hoch | Suite 3x schneller |
| 🟢 Niedrig | bandit/pip-audit in CI | gering | Automatisches Security-Scanning |
| 🟢 Niedrig | Pro-Worker Test-DB | mittel | Parallelisierung möglich |
| 🟢 Niedrig | Log-Analyse Pipeline | gering | Produktions-Fehler erkannt |
---
## 8. Wann muss dieses Dokument aktualisiert werden?
Dieses Dokument MUSS aktualisiert werden bei:
1. **Neue Plugins oder Module** → Test-Kategorien und Abdeckung aktualisieren
2. **Security-Änderungen** → Security-Lücken und Fixes dokumentieren
3. **Neue Test-Infrastruktur** (z. B. vitest, Playwright) → Abschnitt hinzufügen
4. **CI-Pipeline-Änderungen** → Checks und Lücken aktualisieren
5. **Größere Refactoring** → Konventionen und Einschränkungen überprüfen
6. **Nach jeder Bugfix-Session** → Bekannte Lücken und Fixes dokumentieren
**Verantwortlich:** Agent/Entwickler der die Änderung durchführt.
---
## 9. Historie
| Datum | Ereignis |
|-------|---------|
| 2026-08-12 | Test-Strategie erstellt nach Bugfix-Session (14 Security-Bugs, ~170 Testfehler behoben) |
| 2026-08-13 | Phase A Verifikation: 8-Check-Pipeline verbindlich, A-TEST Ergebnisse dokumentiert, RLS- und Test-Isolations-Probleme bestätigt |
---
## 10. Verbindliche Test-Pipeline (8 Checks)
Diese Pipeline ist verbindlich für Phase-Gate-Reviews und muss vor jedem Phasenabschluss grün sein.
| # | Check | Kommando | Wann |
|---|-------|----------|------|
| 1 | Backend Tests | `python -m pytest -v --tb=short` | Einzel-Task + Block + Phase-Gate |
| 2 | Frontend Tests | `cd frontend && npx vitest run --reporter=verbose` | Einzel-Task + Block + Phase-Gate |
| 3 | TypeScript Check | `cd frontend && npx tsc --noEmit` | Einzel-Task + Block + Phase-Gate |
| 4 | E2E Tests | `cd frontend && npx playwright test` (kritische Flows) | Block + Phase-Gate |
| 5 | Frontend Build | `cd frontend && npm run build` | Block + Phase-Gate |
| 6 | Health Check | `curl /api/v1/health` → 200 | Phase-Gate (deployed) |
| 7 | Login Check | Login → 200 | Phase-Gate (deployed) |
| 8 | Cross-Tenant Test | `python -m pytest tests/test_cross_tenant_security.py` | Block + Phase-Gate |
### Staffelung
- **Einzel-Task:** relevante Unit-/Integration-/Frontend-Tests + Typecheck/Build soweit betroffen
- **Größerer Block:** Checks 15 + 8
- **Phase-Gate:** alle 8 Checks inklusive Deploy/Health/Login
### Phase A Verifikationsergebnisse (2026-08-13)
| Check | Ergebnis | Hinweis |
|-------|----------|---------|
| 1. Backend Tests | ✅ 145/145 (Auth/Resilience/Hooks/Contacts/Companies/Plugins) | ⚠️ test_tenant.py: 15 Batch-Failures (DB-Lock-Konflikte, Einzeltests passen) |
| 2. Frontend Tests | ✅ 744/751 passed | 7 Worker-Crashes (Resource-Limits im Container, nicht Test-Failures) |
| 3. TypeScript Check | ✅ 0 errors | |
| 4. E2E Tests | ⏳ Nicht ausgeführt | Playwright nicht in dieser Umgebung verfügbar |
| 5. Frontend Build | ✅ 3.5s, 90 precache entries | |
| 6. Health Check | ✅ 200 (Production: 33-74ms avg ~45ms) | |
| 7. Login Check | ✅ 200 (Production: 22-63ms avg ~48ms) | |
| 8. Cross-Tenant Test | ✅ 7/8 passed | RLS-Policies in Produktion gefixt (Migration 0136: app.tenant_id → app.current_tenant_id) |
### Bekannte Test-Infrastruktur-Probleme (Phase A bestätigt)
1. **Schema-Drift behoben**`conftest.py` nutzt `Base.metadata.create_all` (gleiche wie Produktion `sync_plugin_schema.py`). Alembic-Migrationen laufen nur in Produktion via `prestart.sh`. Schema-Drifts wurden durch Migrationen 0134-0136 in Produktion gefixt.
2. **Test-Isolation**`test_tenant.py` hat 15 Failures im Batch (DB-Lock-Konflikte bei TRUNCATE). Einzeltests passen. **Lösung:** Pro-Worker Datenbank (T-PARALLEL) oder Serial-Only-Mode.
3. **Vitest Worker-Crashes** — 7 von 96 Test-Files crashen mit „Worker exited unexpectedly". Resource-Limits im Container. **Lösung:** `--pool=forks` oder Memory-Limit erhöhen.
4. **Vollständiger pytest-Lauf dauert >15min** — 1401 Tests mit DB-Setup. **Lösung:** T-PARALLEL (pytest-xdist mit pro-Worker DB).
---
## Phase D — Undo/Restore Test-Ergebnisse
### Neue Test-Datei: `tests/test_restore_registry.py` (26 Tests)
| Test-Gruppe | Tests | Status |
|-------------|-------|--------|
| RestoreRegistry (Singleton, Register, Get, List, Overwrite, Excluded Fields) | 6 | ✅ |
| RestoreFromHistory (Unsupported type, History not found) | 2 | ✅ |
| HistoryHooks (extract_entity_id, register, hook fires record_history) | 6 | ✅ |
| TrashList (Returns delete entries, Entity type filter) | 2 | ✅ |
| BulkRestore (All success, Partial failure, All fail) | 3 | ✅ |
| Retention (Returns count, Zero when none) | 2 | ✅ |
| SensitiveFieldsExclusion (Default, Contact, DMS, Mail, All registered) | 5 | ✅ |
### Verifizierte Aspekte
- ✅ RestoreRegistry: Nur explizit registrierte Entity-Typen können restored werden
- ✅ Excluded Fields: id, tenant_id, timestamps, search_tsv, embedding werden nie restored
- ✅ Entity-spezifische Exclusions: Contact (relationship IDs), DMS (storage_path, content_hash), Mail (message_id, raw_path)
- ✅ Mail Special Handler: IMAP-Semantik (Trash-Move, Folder-Verify, kein falscher lokaler Status)
- ✅ Bulk Restore: Partial-Failure-Semantik (`partial_success` Flag, per-item results)
- ✅ Retention: GDPR-Hard-Delete nach konfigurierbarer Aufbewahrungsfrist
- ✅ Hook-based History: `do_action('entity.after_create/update/delete')``record_history()`
- ✅ Dynamic Permission Checks: Restore-Permission aus RestoreConfig, nicht hardcoded
---
## Phase E — Unified Search Test-Konventionen
### Neue Test-Datei: `tests/test_unified_search_phase_e.py` (40 Tests)
| Test-Gruppe | Tests | Status |
|-------------|-------|--------|
| Provider-Capability-Flags (supports_fts/vector/rag/graph, get_providers_by_capability, get_capabilities) | 3 | ✅ |
| RRF Multi-Fusion (2/3/4 Listen, Multi-Listen-Scoring, Backward-Compat, Empty Inputs) | 5 | ✅ |
| Chunking (empty/short/long/exact multiple, Overlap, Hash deterministisch, Whitespace-Normalisierung) | 6 | ✅ |
| Lifecycle (remove_from_index setzt embedding+TSV NULL, rebuild_index, entity.deleted/restored) | 6 | ✅ |
| API-Filter (date_from/date_to, tags, sort, facets-Struktur) | 4 | ✅ |
| AI-Tool (Name/Description, Parameter, OpenAI-Schema, Handler kompakt, Fehlerfälle) | 5 | ✅ |
| Neue Provider (AgentMemory, AIChat, Workflow — Import + Flags) | 4 | ✅ |
| Sensitive-Fields-Exclusion (nicht in search_tsv, nicht in Embedding-Text, Redaction) | 4 | ✅ |
### Konventionen für Search-Tests
1. **Isolation & Determinismus:** Jeder Test nutzt eine eigene Tenant-ID und eigene Entity-IDs. Keine zufälligen UUIDs — echte IDs aus der DB verwenden.
2. **Schema-Anpassung idempotent:** Die Test-DB (`create_all`) definiert `search_tsv` als generierte Spalte, Produktion (Migration 0001) als Plain-Column mit Trigger. Der Lifecycle-Helper konvertiert die Spalte idempotent per `DO $$ ... DROP EXPRESSION` und droppt den `contacts_tsv_update`-Trigger, damit `remove_from_index` `search_tsv = NULL` setzen kann. Die Schema-Änderung persistiert über Testläufe (nur Tabellen werden getruncated).
3. **Patch-Targets:** Funktionen, die innerhalb einer Funktion importiert werden (z.B. `index_entity` in `lifecycle.py`, `get_session_factory` in `ai_tool.py`), müssen am Ursprungsmodul gepatcht werden (`app.plugins.builtins.unified_search.embedding.index_entity`, `app.core.db.get_session_factory`), nicht am importierenden Modul.
4. **Keine echten LLM/Embedding-Calls:** Alle KI-Aufrufe werden mit `AsyncMock` gemockt. Kein Test darf ein echtes Modell kontaktieren.
### Mock-Patterns für LLM/Embedding-Calls
```python
from unittest.mock import AsyncMock, patch
# LLM-Query-Verständnis (Normalisierung, Facets, Summary)
with patch("app.plugins.builtins.unified_search.llm.llm_complete", new_callable=AsyncMock) as mock_llm:
mock_llm.return_value = {"normalized_query": "max mustermann", "facets": {}, "summary": "1 Ergebnis"}
# ... Test
# Embedding-Generierung
with patch("app.plugins.builtins.unified_search.embedding.generate_embedding", new_callable=AsyncMock) as mock_emb:
mock_emb.return_value = [0.1, 0.2, 0.3]
# ... Test
# LLM-Embedding-Client
with patch("app.plugins.builtins.unified_search.embedding.llm_embed", new_callable=AsyncMock) as mock_llm_emb:
mock_llm_emb.return_value = [0.1, 0.2, 0.3]
# ... Test
```
**Regeln:**
- `llm_complete` liefert ein Dict mit `normalized_query`, `facets`, `summary` (und optional `suggestions`).
## Phase F — Agent-System Test-Konventionen
### Neue Test-Datei: `tests/test_phase_f_agents.py` (45 Tests)
| Test-Gruppe | Tests | Status |
|-------------|-------|--------|
| ReAct Loop (Multi-Step, max_steps, Timeout, Error-Recovery, Cost, Dry-Run, Audit) | 8 | ✅ |
| Agent-Permissions (Intersection, System-Admin, Visibility, Execute, Optimistic Lock) | 9 | ✅ |
| Approval Requests (Create, Approve/Reject/Expire, List-Filter) | 6 | ✅ |
| Skill Registry (Registration, get_by_names, keine Permission-Grants) | 3 | ✅ |
| Context Builder (System-Prompt, ReAct-Format, Sensitive-Fields, Tool-Descriptions) | 5 | ✅ |
| Data Policy (Sensitive-Fields, Provider-Compliance, Allowed-Categories) | 4 | ✅ |
| Transparency (AI-Generated-Marking, AI-Participant-Erkennung) | 2 | ✅ |
| Workstream (Message, Step, Result) | 3 | ✅ |
| Budget Limits (Run-Stopp bei Budget, Cost-Akkumulation) | 2 | ✅ |
### Konventionen für Agent-Tests
1. **Keine echte DB / kein echtes LLM / kein Redis:** Alle externen Abhängigkeiten werden mit `AsyncMock` / `MagicMock` gemockt. Die Tests überschreiben die `conftest`-Fixtures `db_setup` und `clean_tables` mit No-Op-Fixtures, damit kein PostgreSQL/Redis benötigt wird.
2. **Patch-Targets am Ursprungsmodul:** Funktionen, die innerhalb einer Funktion importiert werden, müssen am Ursprungsmodul gepatcht werden. Beispiel: `get_provider_compliance` wird in `enforce_data_policy` aus `app.ai.llm_client` importiert → Patch auf `app.ai.llm_client.get_provider_compliance`, nicht `app.ai.data_policy.get_provider_compliance`.
3. **Mock-LLM-Responses:** `llm_complete` wird mit `AsyncMock` gemockt und liefert Dicts mit `content`, `usage`, `cost_usd`, `model`, `raw_response` (mit `choices[0].message.content` und `message.tool_calls`).
4. **Tool-Calls:** Mock-Tool-Calls haben `id`, `function.name`, `function.arguments` (JSON-String). Tool-Handler werden als `AsyncMock` registriert.
5. **Keine zufälligen UUIDs in Assertions:** Echte Entity-IDs aus Mocks verwenden; UUIDs nur als generierte Test-IDs.
6. **SQLAlchemy-Modelle in Mocks:** Für `select(model.version)` in Optimistic-Lock-Tests `sqlalchemy.column()` verwenden, nicht Plain-Strings (sonst `ArgumentError`).
7. **Reservierte Attributnamen:** SQLAlchemy-Modelle dürfen kein `metadata`-Attribut haben (reserviert in der Declarative API). `ApprovalRequest` nutzt `request_metadata` mit DB-Spaltenname `metadata` via `mapped_column("metadata", ...)`.
### Mock-Patterns für Agent-Tests
```python
from unittest.mock import AsyncMock, MagicMock, patch
# LLM-Call
with patch("app.ai.agent_loop.llm_complete", new_callable=AsyncMock) as mock_llm:
mock_llm.return_value = {
"content": "Final answer",
"usage": {"total_tokens": 100},
"cost_usd": 0.001,
"model": "gpt-4o",
"raw_response": raw_response,
}
# ... Test
# Provider-Compliance (in data_policy importiert aus llm_client)
with patch("app.ai.llm_client.get_provider_compliance", new_callable=AsyncMock) as mock_compliance:
mock_compliance.return_value = {"allowed_data_classes": ["internal"]}
# ... Test
# Tool-Handler
handler = AsyncMock(return_value="result")
registry = MagicMock()
registry.get = lambda name: MagicMock(handler=handler) if name == "search" else None
```
- `generate_embedding` / `llm_embed` liefern eine Liste von Floats (Embedding-Vektor).
- Bei Fehlerpfaden: `mock_llm.side_effect = Exception("...")` oder `return_value = None` für Fallback-Verhalten testen.
- DB-Session-Factory in AI-Tool-Handler-Tests: `patch("app.core.db.get_session_factory", return_value=sf)` mit `async_sessionmaker(bind=db_session.bind, ...)`.