chore: enforce ruff lint and format standards across backend
This commit is contained in:
parent
a8e312a4f8
commit
a6cc585dd0
39 changed files with 459 additions and 559 deletions
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import json
|
|||
import shutil
|
||||
import time
|
||||
from pathlib import Path
|
||||
from uuid import UUID
|
||||
|
||||
import pytest
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue