Files
MABEA/backend/tests/conftest.py
T
patrickandClaude Sonnet 5 b9cf195a63
CI / backend-tests (push) Failing after 0s
Sprint 1: Stammdaten-Modelle/-Endpunkte, Admin-Benutzerverwaltung, Zuständigkeits-Vererbung (E2)
- ORM-Modelle: Bereich, Kategorie, Standort, Objekttyp, Material (Prompt 06/07),
  minimales Objekt-Modell (voll ausgebaut erst Sprint 2), Zustaendigkeit/
  Kontrollverantwortung
- Admin-API 7a: GET/POST Stammdaten-Endpunkte, PATCH Material, Benutzerverwaltung
  (anlegen/Rollen ändern), Zuständigkeits-/Kontrollverantwortungs-CRUD
- Vererbungslogik E2 als eigener Service (app/services/zustaendigkeit.py):
  Standort-Zuordnung vererbt sich auf alle Objekte, Objekt-Zeile ist Vereinigung
  statt Ersatz - GET /zustaendigkeiten/benutzer/{id}/objekte exponiert das
- Tests: Stammdaten-CRUD + Rollenrechte, Benutzerverwaltung (inkl. 409 bei
  Login-Duplikat), E2-Vererbungslogik (Standort-only, Standort+Objekt-Vereinigung,
  End-to-End über den Endpunkt)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L85hmKbvX7Cqkq47KnQhFt
2026-09-03 23:12:49 +02:00

144 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import pytest
import pytest_asyncio
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from app.core.app_settings import settings
from app.core.security import hash_password
from app.db.session import get_db
from app.main import app
from app.models.auth import Benutzer, BenutzerRolle, RolleTyp
from app.models.objekt import Objekt
from app.models.stammdaten import Bereich, Objekttyp, Standort
# Erwartet eine bereits per Alembic migrierte Test-Datenbank (CI: install -> migrate -> pytest,
# siehe testphasen.md Phase 0). Jeder Test läuft in einer Transaktion, die am Ende zurückgerollt
# wird, damit Tests sich nicht gegenseitig beeinflussen.
engine = create_async_engine(settings.database_url, pool_pre_ping=True)
TestSessionLocal = async_sessionmaker(engine, expire_on_commit=False)
@pytest_asyncio.fixture
async def db_session():
"""Jeder Test läuft in einer äußeren Transaktion, die am Ende zurückgerollt wird.
Die Session ist per `join_transaction_mode="create_savepoint"` an die äußere
Connection gebunden: ein `commit()` innerhalb des Tests (z. B. durch `get_db`,
das jetzt selbst committet) schließt nur eine SAVEPOINT ab, nicht die äußere
Transaktion das äußere `connection.rollback()` verwirft am Ende trotzdem alles.
(SQLAlchemy 2.0 "Joining a Session into an External Transaction" Review-Fund
fastapi-expert: ohne dieses Pattern hätte ein commit() in Endpunkten die
Testisolation gebrochen.)
"""
async with engine.connect() as connection:
await connection.begin()
session = AsyncSession(
bind=connection, expire_on_commit=False, join_transaction_mode="create_savepoint"
)
try:
yield session
finally:
await session.close()
await connection.rollback()
@pytest_asyncio.fixture
async def client(db_session):
async def _get_db_override():
yield db_session
app.dependency_overrides[get_db] = _get_db_override
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
yield ac
app.dependency_overrides.clear()
@pytest_asyncio.fixture
async def mitarbeiter_user(db_session):
benutzer = Benutzer(
name="Test Mitarbeiter",
login="mitarbeiter1",
passwort_hash=hash_password("test-passwort-123"),
aktiv=True,
)
db_session.add(benutzer)
await db_session.flush()
db_session.add(BenutzerRolle(benutzer_id=benutzer.id, rolle=RolleTyp.mitarbeiter))
await db_session.flush()
return benutzer
@pytest_asyncio.fixture
async def admin_user(db_session):
benutzer = Benutzer(
name="Test Administration",
login="admin1",
passwort_hash=hash_password("test-passwort-123"),
aktiv=True,
)
db_session.add(benutzer)
await db_session.flush()
db_session.add(BenutzerRolle(benutzer_id=benutzer.id, rolle=RolleTyp.administration))
await db_session.flush()
return benutzer
async def login(client, username: str, password: str = "test-passwort-123") -> str:
response = await client.post(
"/api/v1/auth/login", data={"username": username, "password": password}
)
assert response.status_code == 200, response.text
return response.json()["access_token"]
def auth_header(token: str) -> dict:
return {"Authorization": f"Bearer {token}"}
@pytest_asyncio.fixture
async def hauptserver_id(db_session):
# Seed-Migration 0002 legt genau einen Datensatz mit typ='haupt' an (Sprintplan E6).
from sqlalchemy import select
from app.models.auth import KnotenTyp, Systemknoten
result = await db_session.execute(
select(Systemknoten.id).where(Systemknoten.typ == KnotenTyp.haupt)
)
return result.scalar_one()
@pytest_asyncio.fixture
async def standort_factory(db_session):
async def _make(name: str) -> Standort:
standort = Standort(name=name)
db_session.add(standort)
await db_session.flush()
return standort
return _make
@pytest_asyncio.fixture
async def objekt_factory(db_session, hauptserver_id):
async def _make(*, name: str, code: str, standort: Standort) -> Objekt:
bereich = Bereich(name=f"Bereich-{code}")
db_session.add(bereich)
await db_session.flush()
objekttyp = Objekttyp(bereich_id=bereich.id, name=f"Typ-{code}")
db_session.add(objekttyp)
await db_session.flush()
objekt = Objekt(
code=code,
name=name,
objekttyp_id=objekttyp.id,
standort_id=standort.id,
zustaendiger_server_id=hauptserver_id,
)
db_session.add(objekt)
await db_session.flush()
return objekt
return _make