""" BAN YARO — pytest Fixtures Wichtig: - ENV-Variablen werden VOR dem Import von `backend.main` gesetzt, weil `database.DB_PATH = os.getenv("DB_PATH", ...)` beim Modul-Import gebunden wird. - Wir mocken APScheduler weg, damit beim lifespan-Startup keine Hintergrund-Jobs starten. - Pro Test-Session legen wir eine frische SQLite-Datei in tmp_path an. """ from __future__ import annotations import os import sys import secrets import tempfile import shutil from pathlib import Path import pytest ROOT = Path(__file__).resolve().parent.parent BACKEND_DIR = ROOT / "backend" # Backend-Pfad in sys.path, damit `from main import app` funktioniert sys.path.insert(0, str(BACKEND_DIR)) # ------------------------------------------------------------------ # Session-weite App-Instanz # ------------------------------------------------------------------ @pytest.fixture(scope="session") def _tmp_db_dir(): """Temp-Verzeichnis für DB + Media während der Test-Session.""" d = tempfile.mkdtemp(prefix="banyaro-tests-") yield Path(d) shutil.rmtree(d, ignore_errors=True) @pytest.fixture(scope="session") def app(_tmp_db_dir): """ Initialisiert die FastAPI-App einmal pro Test-Session mit: - Test-DB in tmp_path - Stubbed Scheduler (keine Background-Jobs) - KI_MODE=off (keine Netzwerk-Aufrufe) - SMTP-Versand wird gemockt """ db_file = _tmp_db_dir / "test.db" media_dir = _tmp_db_dir / "media" breeder_dir = _tmp_db_dir / "breeder_docs" media_dir.mkdir(exist_ok=True) breeder_dir.mkdir(exist_ok=True) os.environ["DB_PATH"] = str(db_file) os.environ["MEDIA_DIR"] = str(media_dir) os.environ["BREEDER_DOCS_DIR"] = str(breeder_dir) os.environ["KI_MODE"] = "off" os.environ["ENV"] = "test" os.environ["JWT_SECRET"] = "test-secret-" + secrets.token_hex(8) os.environ["STAGING"] = "false" os.environ["ADMIN_EMAIL"] = "test-admin@example.com" # SMTP nicht konfigurieren → _SMTP_READY=False, Mails werden geskippt os.environ.pop("SMTP_SUPPORT_USER", None) os.environ.pop("SMTP_SUPPORT_PASS", None) # Scheduler-Modul vor main-Import stubben — sonst startet APScheduler beim Lifespan import scheduler as _sched _sched.start = lambda: None # type: ignore[assignment] _sched.stop = lambda: None # type: ignore[assignment] # Mailer abklemmen — keine echten E-Mails import mailer as _mailer async def _noop_send(*args, **kwargs): return True _mailer.send_email = _noop_send # type: ignore[assignment] # Rate-Limit + Login-Lockout fuer Tests deaktivieren — sonst # blockiert /api/auth/register nach 5 Aufrufen die ganze Test-Session. import ratelimit as _rl _noop_rl = lambda *a, **kw: None _rl.check = _noop_rl # type: ignore[assignment] _rl.is_account_locked = lambda *a, **kw: False # type: ignore[assignment] _rl.record_login_failure = lambda *a, **kw: 0 # type: ignore[assignment] # App importieren (initialisiert DB beim lifespan) from main import app as fastapi_app # Module-level Aliase ersetzen (Imports der Form `from ratelimit import check as rl_check`) import routes.auth as _routes_auth _routes_auth.rl_check = _noop_rl # type: ignore[assignment] if hasattr(_routes_auth, "_db_is_account_locked"): _routes_auth._db_is_account_locked = lambda *a, **kw: None # type: ignore[assignment] if hasattr(_routes_auth, "_db_record_login_failure"): _routes_auth._db_record_login_failure = lambda *a, **kw: 0 # type: ignore[assignment] return fastapi_app @pytest.fixture(scope="session") def client(app): """FastAPI TestClient — triggert lifespan (init_db, seed_movies, …).""" from fastapi.testclient import TestClient with TestClient(app) as c: yield c # ------------------------------------------------------------------ # Test-User # ------------------------------------------------------------------ def _register_and_verify(client, email: str, password: str, name: str) -> dict: """Helper: Register User + setzt email_verified=1 direkt in der DB.""" r = client.post("/api/auth/register", json={ "email": email, "password": password, "name": name }) assert r.status_code == 200, f"Register failed: {r.status_code} {r.text}" # email_verified=1 setzen (umgeht E-Mail-Verifikation für Tests) from database import db with db() as conn: conn.execute( "UPDATE users SET email_verified=1 WHERE email=?", (email,) ) return {"email": email, "password": password, "name": name} def _login(client, email: str, password: str) -> str: """Helper: Login → JWT-Token zurueck.""" r = client.post("/api/auth/login", json={"email": email, "password": password}) assert r.status_code == 200, f"Login failed: {r.status_code} {r.text}" return r.json()["token"] @pytest.fixture def user(client): """Frisch registrierter und verifizierter Test-User mit JWT-Token.""" email = f"user-{secrets.token_hex(4)}@example.com" pw = "TestPass123!" name = f"tester{secrets.token_hex(3)}" info = _register_and_verify(client, email, pw, name) token = _login(client, email, pw) info["token"] = token info["headers"] = {"Authorization": f"Bearer {token}"} return info @pytest.fixture def admin(client): """Test-Admin: registriert, verifiziert, rolle='admin' direkt in DB gesetzt.""" email = f"admin-{secrets.token_hex(4)}@example.com" pw = "AdminPass123!" # "admin"/"administrator" sind in der Blocklist → Bezeichner "ops..." verwenden name = f"ops{secrets.token_hex(3)}" info = _register_and_verify(client, email, pw, name) from database import db with db() as conn: conn.execute("UPDATE users SET rolle='admin' WHERE email=?", (email,)) token = _login(client, email, pw) info["token"] = token info["headers"] = {"Authorization": f"Bearer {token}"} return info @pytest.fixture def dog(client, user): """Erstellt einen Hund fuer den Test-User und gibt das Hund-Objekt zurueck.""" r = client.post( "/api/dogs", headers=user["headers"], json={"name": "Buddy", "rasse": "Labrador", "is_public": False}, ) assert r.status_code in (200, 201), f"create_dog failed: {r.status_code} {r.text}" return r.json()