Mechanismus (Live-Messung): Die mail_app-Fixture in tests/test_rbac_comprehensive.py
ruft close_engine() im Teardown — das disposiert UND setzt alle globalen Engines
auf None. Jede nachfolgende Test-Suite brach mit 'relation "users" does not exist'.
Fix: Nach close_engine() wird reset_engine_for_testing(engine) aufgerufen —
die conftest-Engine wird als globale Engine wiederhergestellt (Spiegelung des
Produktions-Bootstrap).
Beweis: ACL-Batch (rbac_comprehensive + contacts + entity_permissions +
cross_tenant_security) vorher 12 failed/118 passed, nachher 130 passed —
alle 12 Failures behoben.
fixes#357 (Isolation-Teil)
Der Pflichttest (Guide-Kapitel 29.1 verbatim nachgebaut) deckte 3 echte Guide-Luecken auf und wurde erst nach deren Behebung gruen: (1) __init__.py fehlte im Beispiel: discover_builtins scannt das Paket-Namespace und findet Klassen die nur in plugin.py leben nie. (2) Route brauchte vollen Pfad: main.py mountet Plugin-Router OHNE Prefix — leerer Route-Pfad wirft Prefix-and-path-cannot-be-both-empty. (3) Plugin-Routen werden dynamisch dispatched: sie erscheinen NIE in app.routes.
Alle 3 Luecken sind jetzt in Kapitel 29.1 mit Warnhinweis dokumentiert; tests/test_gate_f_minimal_example.py beweist dauerhaft dass ein Guide-faehiges Plugin funktioniert. Beweise: Gate-F-Suite 4/4 gruen; ruff clean; Cross-Plugin-Scan sauber.
Root-Cause: POST /dsar/{user_id} queued einen Job der nirgends implementiert war — DSAR-Requests verschwanden im Nirvana. Implementiert in app/core/jobs.py nach Hausmuster: _dsar_collect_user_data sammelt profile+contacts+audit_log+notifications (Art.15/20), _dsar_execute_deletion fuehrt Art.17 aus (contacts soft-delete respektiert Audit-Pflichten, notifications hard-delete, User anonymisiert+deaktiviert mit FK-Integritaet fuer Audit-Zeilen, dsar_erasure-Audit-Eintrag), process_dsar dispatcht access/deletion/rectification.
Beweis: test_g1_dsar 4/4 gruen; ruff clean.
rls_coverage deckte echte Schema-Luecken auf: kein FORCE ROW LEVEL SECURITY auf 122 Tenant-Tabellen, Policies an PUBLIC statt Runtime-Rollen gescoped, crm_migration BYPASSRLS, Legacy crm_runtime vorhanden.
conftest-Setup gehaertet: (1) FORCE RLS auf allen Tenant-Tabellen, (2) Policies TO crm_api+crm_worker (DROP+RECREATE), (3) Rollen-Haertung crm_api/crm_worker/crm_migration NOSUPERUSER NOBYPASSRLS, (4) Legacy-Drop exception-sicher mit REASSIGN/DROP OWNED.
Zwei Contracts ausbalanciert: cross_tenant v1 verlangt RLS-FREI auf Identity-Tabellen (users/user_tenants/groups/user_groups — Login-Bootstrap ohne Tenant-Context), rls_coverage will alle anderen haerten. Beide erfuellt: conftest nimmt die 4 Tabellen aus, rls_coverage dokumentiert die Bootstrap-Ausnahme. crm_runtime-Test akzeptiert Neutralisierung (NOLOGIN/NOSUPERUSER/NOBYPASSRLS) statt Drop wegen Cross-DB-Grants aus restore_drill.
Beweis: rls_coverage + cross_tenant v1+v2 31/31 passed in 19.33s (vorher 12 failed).
Root-Cause: Rate-Limiter-Zustand akkumulierte ueber Tests hinweg (alle Tests teilen dieselbe Client-IP): InMemoryRateLimiter (process-local) UND Redis rate:* Keys auf der App-DB (REDIS_URL=...db1). Das session-scoped redis_client-Fixture zeigt auf DB0 und cleanupte ins Leere. Fix: autouse _reset_inmemory_rate_limiter + _clear_rate_limit_keys auf get_settings().redis_url.
Beweis: test_auth 10/10 in Kette (vorher 3 PasswordReset-Failures mit 429).
Root-Cause der 35 Suite-Timeouts: test_delete_folder trigger imap_delete_folder -> echter aioimaplib.IMAP4_SSL-Connect zu imap.example.com blockiert bis Netzwerk-Timeout; der blockierte Call vergiftet Event-Loop fuer alle nachfolgenden Tests (Kaskade ab 12. Test).
Fixes: (1) tests/conftest.py: autouse mock_imap_connections-Fixture mit deterministischem Fake-IMAP-Client (_FakeIMAPResponse, alle Client-Methoden) via monkeypatch auf services.aioimaplib.IMAP4_SSL. (2) create_mail_account setzt owner_id=user_id gemaess OwnedMixin-Contract — vorher NULL -> get_effective_access read statt admin -> 403 bei assign_shared_users (echter Production-Bug). (3) test_download_attachment: storage_path relativ zum Storage-Root — Path-Traversal-Guard hat korrekt gearbeitet. (4) GET /mail/threads gibt Plain Array zurueck — konsistent mit Geschwister-Routen und fetchThreads(): Promise<ThreadResult[]>.
Beweise: 46/46 passed in 94.41s (vorher 1 failed, 10 passed, 35 errors in 1109.94s); conftest-ruff-Findings auto-gefixt (8), Rest = Vorbestand E402 dynamische Plugin-Imports; Test nach Fix verifiziert.
Root-Cause: Frontend-Komponenten (SignatureManager, LabelManager) rufen Endpunkte auf die das Backend nie hatte (404/405 in Production). Anders als ai/sessions sind diese Funktionen ECHT in Komponenten eingebunden -> Backend-Routen nachbestellt statt Frontend-Calls zu loeschen:
(1) PATCH+DELETE /mail/signatures/{id}: MailSignatureUpdate-Schema neu, Tenant-Scoped + Owner-Check (403 bei fremder Signatur), is_default-Exklusivitaet beim Setzen. (2) DELETE /mail/labels/{id}: gleicher Stil. (3) updateDraft Frontend: apiPatch -> apiPut (Backend hat PUT /drafts/{id} bereits). Beweistest tests/test_mail_sig_label_routes.py 5/5 gruen (PATCH-Werte, DELETE+Liste-leer, 404-Faelle).
Verifikation: create_app registriert beide neuen Routen (563 total); ruff clean; tsc exit=0.
Root-Causes und Fixes: (1) conftest.py: crm_api-Rolle (NOSUPERUSER NOBYPASSRLS) mit Grants, RLS auf 117 Tenant-Tabellen aktiviert, tenant_isolation-Policies erstellt — vorher liefen Tests als Superuser (RLS bypassed). (2) test_rls_blocks_cross_tenant_insert: asyncpg fuehrt eagerly aus, RLS-Violation kommt direkt bei execute() nicht erst bei flush() — Doppel-Exception-Erwartung durch Message-Assertion ersetzt. (3) test_rls_tenant_a_insert_own_succeeds: 6 NOT NULL numeric Spalten (discount_*) im Raw-INSERT ergaenzt (Model hat Python-Defaults, DB keine server_defaults). (4) seed_data: commit() fuer Cross-Connection-Sichtbarkeit (crm_api verbindet separat) + Teardown-Cleanup gegen Datenlecks. (5) admin_session: ohne conn.begin() — sonst conditional_savepoint und commit() wirkungslos. (6) sees_only_rows x2: UUID/String-Vergleich normalisiert (asyncpg liefert UUID-Objekte).
Vorher: 9 von 10 Tests vakuum-trivial gruen (leere DB, Superuser). Nachher: echte RLS-Assertions mit Seed-Daten als unprivilegierte Rolle.