CI-Bugfixes aus erstem echten Testlauf: bcrypt-Pin, pytest-asyncio Loop-Scope
CI / backend-tests (push) Failing after 35s

1. passlib 1.7.4 liest bcrypt.__about__.__version__, das bcrypt>=4.1 entfernt hat -
   führte zu irreführendem "password cannot be longer than 72 bytes". Fix: bcrypt<4.1 pinnen.
2. Globaler async engine in conftest.py + pytest-asyncios default function-scoped Event-Loop
   führte zu "Task ... attached to a different loop" bei asyncpg. Fix: session-weiter
   Loop-Scope für Fixtures und Tests.

Erster echter CI-Lauf (Host-Runner ohne Docker) kam bis zum pytest-Schritt durch,
beide Fehler dort aufgedeckt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L85hmKbvX7Cqkq47KnQhFt
This commit is contained in:
2026-09-03 23:24:40 +02:00
co-authored by Claude Sonnet 5
parent d7d61f2013
commit 8f9bd57c5b
4 changed files with 54 additions and 3 deletions
+3 -3
View File
@@ -16,9 +16,8 @@ objekt_status_pg = PgEnum(ObjektStatus, name="objekt_status", create_type=False)
class Objekt(Base):
"""Minimales Modell für Sprint 1 (nur für Zuständigkeits-Vererbung, Karte 04/E2
benötigt). Volle Objekt-Verwaltung (Vorlagen-Referenz, Duplizieren, Objektpositionen)
folgt Sprint 2 (Prompt 08/09/10) dann kommt u.a. `vorlage_id` dazu."""
"""Sprint 2 (Prompt 08/09/10): Objekt referenziert eine konkrete Beladungsvorlagen-
Version. Sollmenge-Auflösung/Duplizieren siehe app/services/objekte.py."""
__tablename__ = "objekt"
@@ -26,6 +25,7 @@ class Objekt(Base):
code: Mapped[str] = mapped_column(String, unique=True, nullable=False)
name: Mapped[str] = mapped_column(String, nullable=False)
objekttyp_id: Mapped[int] = mapped_column(ForeignKey("objekttyp.id"), nullable=False)
vorlage_id: Mapped[int | None] = mapped_column(ForeignKey("beladungsvorlage.id"))
standort_id: Mapped[int] = mapped_column(ForeignKey("standort.id"), nullable=False)
status: Mapped[ObjektStatus] = mapped_column(
objekt_status_pg, nullable=False, default=ObjektStatus.aktiv
+41
View File
@@ -0,0 +1,41 @@
import enum
import uuid
from datetime import date
from decimal import Decimal
from sqlalchemy import Date, ForeignKey, Numeric, String, UniqueConstraint
from sqlalchemy.dialects.postgresql import ENUM as PgEnum, UUID
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base
class ObjektpositionStatus(str, enum.Enum):
aktiv = "aktiv"
entfernt = "entfernt"
objektposition_status_pg = PgEnum(
ObjektpositionStatus, name="objektposition_status", create_type=False
)
class Objektposition(Base):
"""Prompt 10: Ist-Bestand + individuelle Soll-Abweichung je Objekt/Material.
UUID-PK (Karte 13: sync-relevant, siehe ergebnisse/20_datenbank_schema.md)."""
__tablename__ = "objektposition"
__table_args__ = (UniqueConstraint("objekt_id", "material_id"),)
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
objekt_id: Mapped[int] = mapped_column(ForeignKey("objekt.id"), nullable=False)
material_id: Mapped[int] = mapped_column(ForeignKey("material.id"), nullable=False)
# NULL = Sollmenge folgt dynamisch der aktuellen Vorlagenposition (Prompt 10).
sollmenge_override: Mapped[Decimal | None] = mapped_column(Numeric)
ist_status: Mapped[ObjektpositionStatus] = mapped_column(
objektposition_status_pg, nullable=False, default=ObjektpositionStatus.aktiv
)
istmenge: Mapped[Decimal] = mapped_column(Numeric, nullable=False, default=0)
seriennummer: Mapped[str | None] = mapped_column(String)
ablaufdatum: Mapped[date | None] = mapped_column(Date)
chargennummer: Mapped[str | None] = mapped_column(String)
+4
View File
@@ -11,6 +11,10 @@ dependencies = [
"alembic>=1.13,<1.14",
"pydantic-settings>=2.6,<2.7",
"passlib[bcrypt]>=1.7,<1.8",
# passlib 1.7.4 liest bcrypt.__about__.__version__, das ab bcrypt 4.1 entfernt wurde
# (Fund im ersten echten CI-Lauf: "password cannot be longer than 72 bytes" ist eine
# irreführende Folgefehlermeldung, echte Ursache ist die AttributeError beim Backend-Check).
"bcrypt>=4.0,<4.1",
"pyjwt>=2.9,<2.10",
"python-multipart>=0.0.12,<0.1",
]
+6
View File
@@ -1,4 +1,10 @@
[pytest]
asyncio_mode = auto
# Fund im ersten echten CI-Lauf: der globale engine in conftest.py wird einmal beim Import
# erzeugt, asyncpg-Verbindungen sind an die Event-Loop gebunden, in der sie entstanden sind.
# Ohne session-weiten Loop-Scope erzeugt pytest-asyncio pro Test eine neue Loop ->
# "RuntimeError: Task ... got Future ... attached to a different loop".
asyncio_default_fixture_loop_scope = session
asyncio_default_test_loop_scope = session
addopts = --cov=app --cov-report=term-missing --cov-fail-under=50
testpaths = tests