133 lines
4.9 KiB
Python
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}"
|