chore: enforce ruff lint and format standards across backend

This commit is contained in:
Jonas Miederer 2026-09-14 08:10:14 +02:00
parent a8e312a4f8
commit a6cc585dd0
39 changed files with 459 additions and 559 deletions

View file

@ -107,9 +107,7 @@ def lfx_server(tmp_path_factory):
sock.close()
flow_dir = tmp_path_factory.mktemp("lfx-flows")
seed = (
Path(__file__).parent / "fixtures" / "flows" / "simple_rule.json"
).read_text()
seed = (Path(__file__).parent / "fixtures" / "flows" / "simple_rule.json").read_text()
(flow_dir / "seed.json").write_text(seed)
env = {

View file

@ -40,9 +40,7 @@ async def _handoff_task(client, live_server, continuation_flow_id=None):
}
if continuation_flow_id is not None:
body["continuation_flow_id"] = continuation_flow_id
run = (
await client.post(f"/api/flows/{flow['id']}/run", json=body)
).json()
run = (await client.post(f"/api/flows/{flow['id']}/run", json=body)).json()
assert run["status"] == "pending_approval"
tasks = (await client.get("/api/approvals")).json()
task = next(t for t in tasks if t["run_id"] == run["id"])
@ -67,9 +65,7 @@ async def test_approve_with_continuation_runs_facts(client, lfx_server, live_ser
cont = await _create_flow(client, f"cont-{uuid4().hex[:8]}")
await _version_and_deploy(client, cont["id"], fixture="simple_rule.json")
_, task = await _handoff_task(
client, live_server, continuation_flow_id=cont["id"]
)
_, task = await _handoff_task(client, live_server, continuation_flow_id=cont["id"])
assert task["continuation_flow_id"] == cont["id"]
response = await client.post(
@ -109,9 +105,7 @@ async def test_decision_guards(client, lfx_server, live_server):
response = await client.post(url, json={"decision": "reject"})
assert response.status_code == 409
response = await client.post(
f"/api/approvals/{uuid4()}/decision", json={"decision": "approve"}
)
response = await client.post(f"/api/approvals/{uuid4()}/decision", json={"decision": "approve"})
assert response.status_code == 404
_, task2 = await _handoff_task(client, live_server)
@ -143,17 +137,13 @@ async def test_decision_bad_continuation_reference(client, session):
flow_store.get_version(session, UUID(version["id"])),
input={},
)
task = flow_store.create_approval_task(
session, ApprovalCreate(run_id=run.id, title="t")
)
task = flow_store.create_approval_task(session, ApprovalCreate(run_id=run.id, title="t"))
# plant unknown continuation directly (store allows it pre-validation)
task.continuation_flow_id = uuid4()
session.add(task)
session.commit()
response = await client.post(
f"/api/approvals/{task.id}/decision", json={"decision": "approve"}
)
response = await client.post(f"/api/approvals/{task.id}/decision", json={"decision": "approve"})
assert response.status_code == 422
@ -168,9 +158,7 @@ async def test_tenant_isolation(client, lfx_server):
)
assert response.status_code == 200
response = await client.get(
f"/api/flows/{flow['id']}/runs", params={"tenant_id": "pfarramt-a"}
)
response = await client.get(f"/api/flows/{flow['id']}/runs", params={"tenant_id": "pfarramt-a"})
runs_a = response.json()
assert len(runs_a) == 1
assert runs_a[0]["tenant_id"] == "pfarramt-a"
@ -206,9 +194,7 @@ async def test_lfx_down_maps_to_503(client, lfx_server, monkeypatch):
deployed = await client.post(f"/api/flows/{flow['id']}/versions/1/deploy")
assert deployed.status_code == 200
monkeypatch.setattr(settings, "lfx_url", "http://127.0.0.1:9")
response = await client.post(
f"/api/flows/{flow['id']}/run", json={"input": {"betrag": 1}}
)
response = await client.post(f"/api/flows/{flow['id']}/run", json={"input": {"betrag": 1}})
assert response.status_code == 503
@ -221,9 +207,7 @@ async def test_lfx_drift_maps_to_502(client, lfx_server):
await LfxClient(settings.lfx_url, api_key=settings.lfx_api_key or None).delete_flow(
deployed["lfx_flow_id"]
)
response = await client.post(
f"/api/flows/{flow['id']}/run", json={"input": {"betrag": 1}}
)
response = await client.post(f"/api/flows/{flow['id']}/run", json={"input": {"betrag": 1}})
assert response.status_code == 502
@ -247,9 +231,7 @@ async def test_corrupt_flow_json_deploy_maps_to_422(client, lfx_server):
"edges": [],
},
}
response = await client.post(
f"/api/flows/{flow['id']}/versions", json={"langflow_json": bogus}
)
response = await client.post(f"/api/flows/{flow['id']}/versions", json={"langflow_json": bogus})
assert response.status_code == 201, response.text
response = await client.post(f"/api/flows/{flow['id']}/versions/1/deploy")
assert response.status_code == 422

View file

@ -50,7 +50,9 @@ async def _login(client, session, email, role="member"):
async def test_request_approve_consume_me_logout(client, session):
assert (await client.post("/api/auth/admin-bootstrap", json={"email": ADMIN})).status_code == 201
assert (
await client.post("/api/auth/admin-bootstrap", json={"email": ADMIN})
).status_code == 201
admin = await _login(client, session, ADMIN, role="admin")
assert admin["role"] == "admin"
@ -103,9 +105,7 @@ async def test_token_expiry_boundary(client, session):
user = _get_or_make_user(session, MEMBER)
token = service.issue_magic_token(session, user)
record = session.exec(
select(MagicLinkToken).where(
MagicLinkToken.token_hash == service.hash_token(token)
)
select(MagicLinkToken).where(MagicLinkToken.token_hash == service.hash_token(token))
).one()
record.expires_at = service.utcnow() - timedelta(seconds=1)
session.add(record)
@ -145,9 +145,7 @@ async def test_admin_only_endpoints(client, session):
async with AsyncClient(transport=transport, base_url="http://test") as anon:
assert (await anon.get("/api/access/requests")).status_code == 401
assert (
await anon.post(
f"/api/access/requests/{created.json()['id']}/approve"
)
await anon.post(f"/api/access/requests/{created.json()['id']}/approve")
).status_code == 401
assert (await anon.get("/api/auth/me")).status_code == 401
@ -156,9 +154,7 @@ async def test_approve_unknown_and_double_decide(client, session):
await client.post("/api/auth/admin-bootstrap", json={"email": ADMIN})
await _login(client, session, ADMIN, role="admin")
assert (
await client.post(f"/api/access/requests/{uuid4()}/approve")
).status_code == 404
assert (await client.post(f"/api/access/requests/{uuid4()}/approve")).status_code == 404
created = await client.post(
"/api/access/requests",
@ -246,9 +242,7 @@ def test_magic_link_delivery_failure_returns_false(monkeypatch):
monkeypatch.setattr(settings, "smtp_host", "dummy")
_broken_smtp(monkeypatch)
assert mail.send_magic_link("a@example.com", "http://x/login?token=t") is False
assert (
mail.send_access_notification("req-1", "A", "a@example.com") is False
)
assert mail.send_access_notification("req-1", "A", "a@example.com") is False
async def test_approve_survives_delivery_failure(client, session, monkeypatch):
@ -261,9 +255,7 @@ async def test_approve_survives_delivery_failure(client, session, monkeypatch):
json={"name": "M", "email": "m@example.com", "reason": ""},
)
assert created.status_code == 201, created.text
approved = await client.post(
f"/api/access/requests/{created.json()['id']}/approve"
)
approved = await client.post(f"/api/access/requests/{created.json()['id']}/approve")
assert approved.status_code == 200, approved.text
body = approved.json()
assert body["request"]["status"] == "approved"
@ -272,9 +264,7 @@ async def test_approve_survives_delivery_failure(client, session, monkeypatch):
async def test_magic_request_for_known_user(client, session):
_get_or_make_user(session, MEMBER)
response = await client.post(
"/api/auth/magic/request", json={"email": MEMBER}
)
response = await client.post("/api/auth/magic/request", json={"email": MEMBER})
assert response.status_code == 200, response.text
body = response.json()
assert body["message"] == "login link sent"
@ -282,24 +272,19 @@ async def test_magic_request_for_known_user(client, session):
assert "token" not in response.text
tokens = session.exec(
select(MagicLinkToken).where(
MagicLinkToken.user_id
== service.get_user_by_email(session, MEMBER).id
MagicLinkToken.user_id == service.get_user_by_email(session, MEMBER).id
)
).all()
assert len(tokens) == 1
async def test_magic_request_unknown_email_403(client):
response = await client.post(
"/api/auth/magic/request", json={"email": "stranger@example.com"}
)
response = await client.post("/api/auth/magic/request", json={"email": "stranger@example.com"})
assert response.status_code == 403
async def test_magic_request_admin_email_creates_admin(client, session):
response = await client.post(
"/api/auth/magic/request", json={"email": ADMIN}
)
response = await client.post("/api/auth/magic/request", json={"email": ADMIN})
assert response.status_code == 200, response.text
user = service.get_user_by_email(session, ADMIN)
assert user is not None and user.role == "admin"
@ -308,13 +293,9 @@ async def test_magic_request_admin_email_creates_admin(client, session):
async def test_magic_request_rate_limit(client, session):
_get_or_make_user(session, MEMBER)
for _ in range(service.MAGIC_LINKS_PER_EMAIL_PER_HOUR):
response = await client.post(
"/api/auth/magic/request", json={"email": MEMBER}
)
response = await client.post("/api/auth/magic/request", json={"email": MEMBER})
assert response.status_code == 200, response.text
limited = await client.post(
"/api/auth/magic/request", json={"email": MEMBER}
)
limited = await client.post("/api/auth/magic/request", json={"email": MEMBER})
assert limited.status_code == 429
@ -331,9 +312,7 @@ def test_magic_link_mail_has_clickable_branded_html(monkeypatch):
button_url="http://localhost:3000/login?token=GEHEIM",
note="Gültig 15 Minuten.",
)
html_part = next(
p for p in message.walk() if p.get_content_type() == "text/html"
)
html_part = next(p for p in message.walk() if p.get_content_type() == "text/html")
html = html_part.get_content()
# Button = anchor with href around the label; token never in text-only form.
assert '<a href="http://localhost:3000/login?token=GEHEIM"' in html
@ -343,7 +322,5 @@ def test_magic_link_mail_has_clickable_branded_html(monkeypatch):
# Multipart: plain-text fallback keeps the raw link.
parts = [p.get_content_type() for p in message.walk()]
assert "text/plain" in parts and "text/html" in parts
plain = next(
p.get_content() for p in message.walk() if p.get_content_type() == "text/plain"
)
plain = next(p.get_content() for p in message.walk() if p.get_content_type() == "text/plain")
assert "http://localhost:3000/login?token=GEHEIM" in plain

View file

@ -24,9 +24,7 @@ def _export_doc(name="importiert"):
}
async def test_import_creates_definition_version_and_deploys(
client, lfx_server, override_langflow
):
async def test_import_creates_definition_version_and_deploys(client, lfx_server, override_langflow):
override_langflow(_export_doc())
response = await client.post(
"/api/flows/import",
@ -37,31 +35,21 @@ async def test_import_creates_definition_version_and_deploys(
assert body["definition"]["name"] == "importiert"
assert body["version"]["version_no"] == 1
assert body["version"]["author"] == "admin"
assert body["version"]["lfx_flow_id"] == lfx_id_for(
UUID(body["definition"]["id"]), 1
)
assert body["version"]["lfx_flow_id"] == lfx_id_for(UUID(body["definition"]["id"]), 1)
assert body["version"]["deployed_at"] is not None
assert body["version"]["langflow_json"]["data"]["nodes"]
# Imported flow actually runs on LFX.
definition_id = body["definition"]["id"]
response = await client.post(
f"/api/flows/{definition_id}/run", json={"input": {"betrag": 10}}
)
response = await client.post(f"/api/flows/{definition_id}/run", json={"input": {"betrag": 10}})
assert response.status_code == 200, response.text
assert response.json()["status"] == "completed"
async def test_import_same_content_is_idempotent(
client, lfx_server, override_langflow
):
async def test_import_same_content_is_idempotent(client, lfx_server, override_langflow):
override_langflow(_export_doc())
first = await client.post(
"/api/flows/import", json={"langflow_flow_id": LANGFLOW_ID}
)
second = await client.post(
"/api/flows/import", json={"langflow_flow_id": LANGFLOW_ID}
)
first = await client.post("/api/flows/import", json={"langflow_flow_id": LANGFLOW_ID})
second = await client.post("/api/flows/import", json={"langflow_flow_id": LANGFLOW_ID})
assert first.status_code == 201
assert second.status_code == 201
assert first.json()["definition"]["id"] == second.json()["definition"]["id"]
@ -69,20 +57,14 @@ async def test_import_same_content_is_idempotent(
assert second.json()["version"]["version_no"] == 1
async def test_import_changed_content_bumps_version(
client, lfx_server, override_langflow
):
async def test_import_changed_content_bumps_version(client, lfx_server, override_langflow):
override_langflow(_export_doc())
first = await client.post(
"/api/flows/import", json={"langflow_flow_id": LANGFLOW_ID}
)
first = await client.post("/api/flows/import", json={"langflow_flow_id": LANGFLOW_ID})
assert first.status_code == 201
changed = _export_doc()
changed["description"] = "neue Beschreibung"
override_langflow(changed)
second = await client.post(
"/api/flows/import", json={"langflow_flow_id": LANGFLOW_ID}
)
second = await client.post("/api/flows/import", json={"langflow_flow_id": LANGFLOW_ID})
assert second.status_code == 201
assert second.json()["version"]["version_no"] == 2
@ -94,9 +76,7 @@ async def test_import_writes_canonical_file(
monkeypatch.setattr(settings, "flows_dir", str(tmp_path))
override_langflow(_export_doc())
response = await client.post(
"/api/flows/import", json={"langflow_flow_id": LANGFLOW_ID}
)
response = await client.post("/api/flows/import", json={"langflow_flow_id": LANGFLOW_ID})
assert response.status_code == 201, response.text
written = tmp_path / "importiert.json"
assert written.is_file()
@ -127,17 +107,13 @@ async def test_import_without_deploy(client, override_langflow):
async def test_import_langflow_not_found(client, override_langflow):
override_langflow(status=404)
response = await client.post(
"/api/flows/import", json={"langflow_flow_id": LANGFLOW_ID}
)
response = await client.post("/api/flows/import", json={"langflow_flow_id": LANGFLOW_ID})
assert response.status_code == 404
async def test_import_langflow_unreachable(client, override_langflow):
override_langflow(status=0)
response = await client.post(
"/api/flows/import", json={"langflow_flow_id": str(uuid4())}
)
response = await client.post("/api/flows/import", json={"langflow_flow_id": str(uuid4())})
assert response.status_code == 503

View file

@ -14,9 +14,7 @@ def load_fixture(name: str) -> dict:
async def _create_flow(client, name="regeltest"):
response = await client.post(
"/api/flows", json={"name": name, "description": "test flow"}
)
response = await client.post("/api/flows", json={"name": name, "description": "test flow"})
assert response.status_code == 201, response.text
return response.json()
@ -113,9 +111,7 @@ async def test_deploy_and_run_standard_and_sonderfall(client, lfx_server):
async def test_run_undeployed_version_fails(client):
flow = await _create_flow(client, "undeployed")
await _create_version(client, flow["id"])
response = await client.post(
f"/api/flows/{flow['id']}/run", json={"input": {"betrag": 1}}
)
response = await client.post(f"/api/flows/{flow['id']}/run", json={"input": {"betrag": 1}})
assert response.status_code == 422

View file

@ -9,7 +9,7 @@ from lfx.custom import Component
REPO_ROOT = Path(__file__).resolve().parent.parent.parent
sys.path.insert(0, str(REPO_ROOT / "backend" / "components"))
from kirche.form_inputs import CustomInputForm # noqa: E402
from kirche.form_inputs import CustomInputForm
FIXTURES = Path(__file__).parent / "fixtures" / "flows"

View file

@ -117,9 +117,7 @@ def test_required_advanced_hidden_code():
def test_display_name_match_and_fallback():
doc = _doc(_field("a", "StrInput"), node_type="Custom", display="CustomInput X")
assert set(
parser.extract_form_schema(doc)["json_schema"]["properties"]
) == {"a"}
assert set(parser.extract_form_schema(doc)["json_schema"]["properties"]) == {"a"}
plain = {
"data": {
"nodes": [{"data": {"type": "ChatInput", "node": {"template": {}}}}],
@ -158,9 +156,7 @@ def _lfx() -> LfxClient:
async def test_sync_caches_schema_per_version(session, lfx_server, tmp_path):
(tmp_path / "intake_form.json").write_text(
(FIXTURES / "intake_form.json").read_text()
)
(tmp_path / "intake_form.json").write_text((FIXTURES / "intake_form.json").read_text())
report = await sync.sync_from_directory(session, _lfx(), tmp_path)
assert not report.errors, report.errors
from app.flows import store
@ -180,13 +176,9 @@ async def test_input_schema_endpoint(client, lfx_server):
doc = json.loads((FIXTURES / "intake_form.json").read_text())
created = await client.post("/api/flows", json={"name": "schema-e2e"})
flow_id = created.json()["id"]
version = await client.post(
f"/api/flows/{flow_id}/versions", json={"langflow_json": doc}
)
version = await client.post(f"/api/flows/{flow_id}/versions", json={"langflow_json": doc})
assert version.status_code == 201, version.text
assert version.json()["input_schema"]["json_schema"]["properties"]["age"][
"type"
] == "integer"
assert version.json()["input_schema"]["json_schema"]["properties"]["age"]["type"] == "integer"
# Created but never deployed → 404.
assert (await client.get(f"/api/flows/{flow_id}/input-schema")).status_code == 404

View file

@ -20,9 +20,7 @@ def _client():
@respx.mock
async def test_export_flow_sends_api_key_and_returns_doc():
doc = {"id": "abc", "name": "n", "data": {"nodes": [], "edges": []}}
route = respx.get(f"{BASE}/api/v1/flows/abc").mock(
return_value=httpx.Response(200, json=doc)
)
route = respx.get(f"{BASE}/api/v1/flows/abc").mock(return_value=httpx.Response(200, json=doc))
result = await _client().export_flow("abc")
assert result == doc
assert route.called

View file

@ -28,7 +28,6 @@ def test_canonical_flows_pass_lfx_validate():
capture_output=True,
text=True,
timeout=300,
check=False,
)
assert process.returncode == 0, (
f"lfx validate failed:\n{process.stdout}\n{process.stderr}"
)
assert process.returncode == 0, f"lfx validate failed:\n{process.stdout}\n{process.stderr}"

View file

@ -5,7 +5,6 @@ import json
import shutil
import time
from pathlib import Path
from uuid import UUID
import pytest