bifroest/tests/conftest.py
2026-10-27 08:34:17 +01:00

133 lines
4.9 KiB
Python

"""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}"