refactor: form-core wiring, JWT sessions, slowapi, packaging, frontend rewrite, test declutter

This commit is contained in:
Jonas Miederer 2026-09-28 18:03:07 +02:00
parent 06ab25fb47
commit 03af41e837
74 changed files with 6089 additions and 5183 deletions

View file

@ -1,17 +1,20 @@
"""Step 3 tests: flows CRUD, versions, deploy, sync/stream runs, handoff E2E."""
import json
from pathlib import Path
from uuid import UUID, uuid4
import pytest
from app.flows.sync import lfx_id_for
FIXTURES = Path(__file__).parent / "fixtures" / "flows"
from .conftest import load_flow_fixture
CALLBACK_HEADERS = {"X-Callback-Token": "test-callback-token"}
def load_fixture(name: str) -> dict:
return json.loads((FIXTURES / name).read_text())
"""Alias of the shared conftest helper (kept for call-site stability)."""
return load_flow_fixture(name)
async def _create_flow(client, name="regeltest"):
@ -55,59 +58,57 @@ async def test_flow_crud(client, login):
assert response.status_code == 404
async def test_management_endpoints_require_admin(client, login):
"""Anonymous -> 401, member -> 403, admin -> through (A)."""
MANAGEMENT_PROBES = [
("POST", "/api/flows", {"name": "x"}),
("POST", "/api/flows/sync", None),
("POST", "/api/flows/{flow_id}/versions", {"langflow_json": {}}),
("GET", "/api/flows/{flow_id}/versions", None),
("POST", "/api/flows/{flow_id}/versions/1/deploy", None),
("POST", "/api/flows/import", {"langflow_flow_id": "x"}),
("GET", "/api/flows/langflow/available", None),
]
@pytest.mark.parametrize(("method", "path", "body"), MANAGEMENT_PROBES)
async def test_management_endpoints_require_admin(client, login, method, path, body):
"""Anonymous -> 401, member -> 403 (parametrized per endpoint)."""
flow_id = uuid4()
probes = [
("POST", "/api/flows", {"name": "x"}),
("POST", "/api/flows/sync", None),
("POST", f"/api/flows/{flow_id}/versions", {"langflow_json": {}}),
("GET", f"/api/flows/{flow_id}/versions", None),
("POST", f"/api/flows/{flow_id}/versions/1/deploy", None),
("POST", "/api/flows/import", {"langflow_flow_id": "x"}),
("GET", "/api/flows/langflow/available", None),
]
for method, path, body in probes:
if method == "POST":
response = await client.post(path, json=body or {})
else:
response = await client.get(path)
assert response.status_code == 401, (method, path, response.text)
target = path.format(flow_id=flow_id)
if method == "POST":
response = await client.post(target, json=body or {})
else:
response = await client.get(target)
assert response.status_code == 401, (method, target, response.text)
await login(email="member@example.com", role="member")
for method, path, body in probes:
if method == "POST":
response = await client.post(path, json=body or {})
else:
response = await client.get(path)
assert response.status_code == 403, (method, path, response.text)
if method == "POST":
response = await client.post(target, json=body or {})
else:
response = await client.get(target)
assert response.status_code == 403, (method, target, response.text)
async def test_runs_and_reads_require_login(client, login):
"""Run, run reads and approval reads are login-gated (A)."""
LOGIN_GATED_PATHS = [
("POST", "/api/flows/{flow_id}/run", {"input": {}}),
("GET", "/api/flows/{flow_id}/runs", None),
("GET", "/api/flows/runs/{run_id}", None),
("GET", "/api/approvals", None),
("POST", "/api/approvals/{task_id}/decision", {"decision": "approve"}),
]
@pytest.mark.parametrize(("method", "path", "body"), LOGIN_GATED_PATHS)
async def test_runs_and_reads_require_login(client, login, anon_client, method, path, body):
"""Run, run reads and approval reads are login-gated (parametrized)."""
await login()
flow = await _create_flow(client, "gated")
run_id = uuid4()
authed_paths = [
("POST", f"/api/flows/{flow['id']}/run", {"input": {}}),
("GET", f"/api/flows/{flow['id']}/runs", None),
("GET", f"/api/flows/runs/{run_id}", None),
("GET", "/api/approvals", None),
("POST", f"/api/approvals/{uuid4()}/decision", {"decision": "approve"}),
]
# Fresh client without a session cookie.
from httpx import ASGITransport, AsyncClient
from app.main import app
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as anon:
for method, path, body in authed_paths:
if method == "POST":
response = await anon.post(path, json=body or {})
else:
response = await anon.get(path)
assert response.status_code == 401, (method, path, response.text)
target = path.format(flow_id=flow["id"], run_id=uuid4(), task_id=uuid4())
async for anon in anon_client():
if method == "POST":
response = await anon.post(target, json=body or {})
else:
response = await anon.get(target)
assert response.status_code == 401, (method, target, response.text)
async def test_version_validation_and_numbering(client, login):
@ -211,12 +212,13 @@ async def test_run_stream_events(client, lfx_server, login):
current_event = "message"
assert events, "expected SSE events"
assert events[0][0] == "run"
first_payload = json.loads(events[0][1])
assert "run_id" in first_payload
assert events[-1][0] == "run_finished"
last_payload = json.loads(events[-1][1])
assert last_payload["status"] == "completed"
by_type = {}
for event, data in events:
by_type.setdefault(event, []).append(json.loads(data))
assert "run_id" in by_type["run"][0]
finished = [p for p in by_type.get("run_finished", []) if "status" in p]
assert finished and finished[0]["status"] == "completed"
first_payload = by_type["run"][0]
response = await client.get(f"/api/flows/runs/{first_payload['run_id']}")
assert response.status_code == 200