"""Testumgebung: eine eigene PostgreSQL-Datenbank je Testlauf (docker compose up -d). SKIP LOCKED, LISTEN/NOTIFY und Teilindizes gibt es nur in PostgreSQL, deshalb kein SQLite. """ import os import subprocess import uuid from collections.abc import Iterator from pathlib import Path import psycopg import pytest from fastapi.testclient import TestClient from sqlalchemy import text import app.approvals.runner from app.ai.models import Model, Provider, ProviderKey from app.chat import runner from app.cli import migrate from app.core.config import Settings from app.core.crypto import Vault, generate_key from app.core.database import Database from app.identity.models import Team, TeamMembership, User from app.main import create_app from app.models import Base from helpers import PASSWORD, Browser, DemoTarget, FakeProvider SECRET_KEY = generate_key() ADMIN_URL = os.environ.get("AIW_TEST_ADMIN_URL", "postgresql://aiw:aiw@127.0.0.1:55433/postgres") @pytest.fixture(scope="session") def database_url() -> Iterator[str]: name = f"aiw_test_{uuid.uuid4().hex[:8]}" try: admin = psycopg.connect(ADMIN_URL, autocommit=True) except psycopg.OperationalError as e: pytest.exit(f"PostgreSQL nicht erreichbar ({ADMIN_URL}): erst `docker compose up -d`. {e}", returncode=2) admin.execute(f"CREATE DATABASE {name}") url = ADMIN_URL.rsplit("/", 1)[0].replace("postgresql://", "postgresql+psycopg://") + f"/{name}" migrate(Settings(database_url=url)) yield url admin.execute(f"DROP DATABASE {name} WITH (FORCE)") admin.close() @pytest.fixture def settings(database_url: str, tmp_path: Path) -> Settings: return Settings(database_url=database_url, demo_password=PASSWORD, secret_key=SECRET_KEY, git_cache_dir=str(tmp_path / "git-cache"), upload_dir=str(tmp_path / "uploads"), cookie_secure=False, git_allow_local=True, git_allowed_hosts="bitbucket.intern", run_lease_seconds=2, event_flush_ms=0, worker_poll_seconds=0.05, max_runs_per_team=3, run_max_attempts=3, worker_http_port=0) @pytest.fixture def db(settings: Settings) -> Iterator[Database]: database = Database(settings.database_url) yield database tables = ", ".join(t.name for t in Base.metadata.sorted_tables) with database.session() as session: session.execute(text(f"TRUNCATE {tables} RESTART IDENTITY CASCADE")) database.dispose() @pytest.fixture(autouse=True) def fake_provider(monkeypatch: pytest.MonkeyPatch) -> type[FakeProvider]: """Kein Test spricht die echte API an; das Testdouble merkt sich die Aufrufe.""" FakeProvider.reset() DemoTarget.reset() monkeypatch.setattr(runner, "provider_factory", FakeProvider) return FakeProvider @pytest.fixture def people(db: Database) -> dict[str, int]: """Team demo: anna, ben, clara. Team andere: dora.""" ids: dict[str, int] = {} with db.session() as session: demo, other = Team(slug="demo", name="Demo"), Team(slug="andere", name="Andere") session.add_all([demo, other]) session.flush() for username, team in (("anna", demo), ("ben", demo), ("clara", demo), ("dora", other)): user = User(username=username, display_name=username.title()) session.add(user) session.flush() session.add(TeamMembership(team_id=team.id, user_id=user.id)) ids[username] = user.id ids["team_demo"], ids["team_other"] = demo.id, other.id # Ein eingerichtetes Modell mit Key für alle Teams, wie in der Entwicklung mit OpenAI provider = Provider(name="OpenAI", base_url="https://api.openai.com/v1") session.add(provider) session.flush() session.add(Model(provider_id=provider.id, name="gpt-6-luna", label="GPT-6 Luna", is_default=True)) session.add(ProviderKey(provider_id=provider.id, secret=Vault(SECRET_KEY).encrypt("sk-test-alle-teams"), hint="eams", updated_by=ids["anna"])) ids["provider"] = provider.id return ids @pytest.fixture def app(settings: Settings, db: Database): return create_app(settings) @pytest.fixture def browser(app) -> Iterator[callable]: clients: list[TestClient] = [] def open_as(username: str) -> Browser: client = TestClient(app) client.__enter__() clients.append(client) return Browser(client).login(username) yield open_as for client in clients: client.__exit__(None, None, None) @pytest.fixture def git_repo(tmp_path: Path) -> str: """Lokales Repository mit einem Commit auf main.""" repo = tmp_path / "repo" repo.mkdir() run = lambda *args: subprocess.run(["git", "-C", str(repo), *args], check=True, capture_output=True) run("init", "-b", "main") (repo / "README.md").write_text("# Test\n") run("add", ".") run("-c", "user.name=Test", "-c", "user.email=test@example.org", "commit", "-m", "Erster Commit") return f"file://{repo}"