refactor: form-core wiring, JWT sessions, slowapi, packaging, frontend rewrite, test declutter
This commit is contained in:
parent
06ab25fb47
commit
03af41e837
74 changed files with 6089 additions and 5183 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue