feat: flow engine data layer with tests
- SQLModel tables: flow_definitions/versions/runs, approval_tasks - Alembic setup with autogenerate (sqlmodel types fixed via script.py.mako) - Settings extended (shared root .env), db engine/session helpers - pytest suite: conftest with isolated test DB, 5 model tests green
This commit is contained in:
parent
2ba062c4c5
commit
e178ae406d
12 changed files with 686 additions and 3 deletions
69
backend/tests/conftest.py
Normal file
69
backend/tests/conftest.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
"""Shared pytest fixtures.
|
||||
|
||||
Requires TEST_DATABASE_URL (a dedicated Postgres database, e.g. the
|
||||
`kirche_einfach_test` database from the compose stack). The URL is mapped
|
||||
onto DATABASE_URL *before* any app module is imported, so settings and the
|
||||
alembic env both target the test database. Never point this at production.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
TEST_DB_URL = os.environ.get("TEST_DATABASE_URL")
|
||||
if not TEST_DB_URL:
|
||||
raise RuntimeError(
|
||||
"TEST_DATABASE_URL is not set. Refusing to run tests without "
|
||||
"an explicit test database (see .env.example)."
|
||||
)
|
||||
|
||||
# Must precede all `app.*` imports: Settings is instantiated at import time.
|
||||
os.environ["DATABASE_URL"] = TEST_DB_URL
|
||||
|
||||
BACKEND_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def db_engine():
|
||||
"""Engine on the test DB, migrated to head once per session."""
|
||||
from alembic import command as alembic_command
|
||||
from alembic.config import Config
|
||||
|
||||
from app import db as app_db
|
||||
|
||||
app_db.reset_engine()
|
||||
engine = app_db.get_engine()
|
||||
|
||||
cfg = Config()
|
||||
cfg.set_main_option("script_location", str(BACKEND_DIR / "migrations"))
|
||||
alembic_command.upgrade(cfg, "head")
|
||||
|
||||
yield engine
|
||||
|
||||
engine.dispose()
|
||||
app_db.reset_engine()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def session(db_engine):
|
||||
"""Function-scoped session; tables are truncated after each test."""
|
||||
from sqlmodel import Session, SQLModel
|
||||
|
||||
with Session(db_engine) as s:
|
||||
yield s
|
||||
with db_engine.begin() as conn:
|
||||
for table in reversed(SQLModel.metadata.sorted_tables):
|
||||
conn.execute(table.delete())
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
async def client():
|
||||
"""ASGI test client for the FastAPI app."""
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
|
||||
from app.main import app
|
||||
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as c:
|
||||
yield c
|
||||
82
backend/tests/test_models.py
Normal file
82
backend/tests/test_models.py
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
"""Model-level tests: defaults, constraints, JSON roundtrip, linkage."""
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlmodel import select
|
||||
|
||||
from app.models import ApprovalTask, FlowDefinition, FlowRun, FlowVersion
|
||||
|
||||
|
||||
def test_flow_definition_defaults(session):
|
||||
d = FlowDefinition(name="bauvorhaben")
|
||||
session.add(d)
|
||||
session.commit()
|
||||
session.refresh(d)
|
||||
assert d.id is not None
|
||||
assert d.status == "active"
|
||||
assert d.continuation_flow_id is None
|
||||
assert d.created_at is not None
|
||||
assert d.created_at.tzinfo is not None
|
||||
|
||||
|
||||
def test_flow_definition_name_unique(session):
|
||||
session.add(FlowDefinition(name="doppelt"))
|
||||
session.commit()
|
||||
session.add(FlowDefinition(name="doppelt"))
|
||||
with pytest.raises(IntegrityError):
|
||||
session.commit()
|
||||
session.rollback()
|
||||
|
||||
|
||||
def test_flow_version_unique_per_flow(session):
|
||||
d = FlowDefinition(name="v-flow")
|
||||
session.add(d)
|
||||
session.commit()
|
||||
session.add(FlowVersion(flow_id=d.id, version_no=1, author="a"))
|
||||
session.commit()
|
||||
session.add(FlowVersion(flow_id=d.id, version_no=1, author="b"))
|
||||
with pytest.raises(IntegrityError):
|
||||
session.commit()
|
||||
session.rollback()
|
||||
session.add(FlowVersion(flow_id=d.id, version_no=2, author="b"))
|
||||
session.commit()
|
||||
|
||||
|
||||
def test_flow_version_json_roundtrip(session):
|
||||
payload = {"data": {"nodes": [{"id": "n1"}], "edges": []}, "name": "x"}
|
||||
d = FlowDefinition(name="json-flow")
|
||||
session.add(d)
|
||||
session.commit()
|
||||
v = FlowVersion(flow_id=d.id, version_no=1, langflow_json=payload)
|
||||
session.add(v)
|
||||
session.commit()
|
||||
session.refresh(v)
|
||||
assert v.langflow_json == payload
|
||||
assert v.deployed_at is None
|
||||
assert v.lfx_flow_id is None
|
||||
|
||||
|
||||
def test_run_and_task_linkage_with_tenant_default(session):
|
||||
d = FlowDefinition(name="linked")
|
||||
session.add(d)
|
||||
session.commit()
|
||||
v = FlowVersion(flow_id=d.id, version_no=1)
|
||||
session.add(v)
|
||||
session.commit()
|
||||
run = FlowRun(version_id=v.id, input={"a": 1})
|
||||
session.add(run)
|
||||
session.commit()
|
||||
task = ApprovalTask(run_id=run.id, version_id=v.id, title="bitte pruefen")
|
||||
session.add(task)
|
||||
session.commit()
|
||||
|
||||
assert run.tenant_id == "default"
|
||||
assert run.status == "running"
|
||||
assert task.tenant_id == "default"
|
||||
assert task.status == "open"
|
||||
|
||||
runs = session.exec(select(FlowRun).where(FlowRun.version_id == v.id)).all()
|
||||
assert len(runs) == 1
|
||||
tasks = session.exec(select(ApprovalTask).where(ApprovalTask.run_id == run.id)).all()
|
||||
assert len(tasks) == 1
|
||||
assert tasks[0].payload == {}
|
||||
Loading…
Add table
Add a link
Reference in a new issue