fix: typer exit codes, JWT 7d expiry, shared test fixtures, AdminTab query, mantine 9.6.3
This commit is contained in:
parent
03af41e837
commit
696db34c90
16 changed files with 210 additions and 251 deletions
|
|
@ -298,11 +298,16 @@ def flow_helpers(client, login):
|
|||
deployed = await _deploy(flow["id"], version["version_no"])
|
||||
return flow, deployed
|
||||
|
||||
async def _version_and_deploy(flow_id: str, fixture: str = "simple_rule.json") -> dict:
|
||||
await _create_version(flow_id, fixture)
|
||||
return await _deploy(flow_id)
|
||||
|
||||
return {
|
||||
"create_flow": _create_flow,
|
||||
"create_version": _create_version,
|
||||
"deploy": _deploy,
|
||||
"deployed_flow": _deployed_flow,
|
||||
"version_and_deploy": _version_and_deploy,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -7,35 +7,13 @@ from app.flows.schemas import ApprovalCreate
|
|||
from .conftest import load_flow_fixture
|
||||
|
||||
|
||||
def load_fixture(name: str) -> dict:
|
||||
"""Alias of the shared conftest helper (kept for call-site stability)."""
|
||||
return load_flow_fixture(name)
|
||||
|
||||
|
||||
async def _create_flow(client, name):
|
||||
response = await client.post("/api/flows", json={"name": name})
|
||||
assert response.status_code == 201, response.text
|
||||
return response.json()
|
||||
|
||||
|
||||
async def _version_and_deploy(client, flow_id, fixture="simple_rule.json"):
|
||||
response = await client.post(
|
||||
f"/api/flows/{flow_id}/versions",
|
||||
json={"langflow_json": load_fixture(fixture)},
|
||||
)
|
||||
assert response.status_code == 201, response.text
|
||||
response = await client.post(f"/api/flows/{flow_id}/versions/1/deploy")
|
||||
assert response.status_code == 200, response.text
|
||||
return response.json()
|
||||
|
||||
|
||||
async def _handoff_task(client, live_server, monkeypatch):
|
||||
async def _handoff_task(client, live_server, monkeypatch, flow_helpers):
|
||||
"""Run approval_handoff over threshold; returns (run, task)."""
|
||||
from app.settings import settings
|
||||
|
||||
monkeypatch.setattr(settings, "callback_base_url", live_server)
|
||||
flow = await _create_flow(client, f"handoff-{uuid4().hex[:8]}")
|
||||
await _version_and_deploy(client, flow["id"], fixture="approval_handoff.json")
|
||||
flow = await flow_helpers["create_flow"](f"handoff-{uuid4().hex[:8]}")
|
||||
await flow_helpers["version_and_deploy"](flow["id"], fixture="approval_handoff.json")
|
||||
body = {"input": {"betrag": 250000, "schwelle": 200000}}
|
||||
run = (await client.post(f"/api/flows/{flow['id']}/run", json=body)).json()
|
||||
assert run["status"] == "pending_approval"
|
||||
|
|
@ -44,9 +22,11 @@ async def _handoff_task(client, live_server, monkeypatch):
|
|||
return run, task
|
||||
|
||||
|
||||
async def test_approve_without_continuation(client, lfx_server, live_server, login, monkeypatch):
|
||||
async def test_approve_without_continuation(
|
||||
client, lfx_server, live_server, login, monkeypatch, flow_helpers
|
||||
):
|
||||
await login()
|
||||
_, task = await _handoff_task(client, live_server, monkeypatch)
|
||||
_, task = await _handoff_task(client, live_server, monkeypatch, flow_helpers)
|
||||
response = await client.post(
|
||||
f"/api/approvals/{task['id']}/decision",
|
||||
json={"decision": "approve", "note": "ok"},
|
||||
|
|
@ -60,11 +40,11 @@ async def test_approve_without_continuation(client, lfx_server, live_server, log
|
|||
|
||||
|
||||
async def test_approve_with_continuation_runs_facts(
|
||||
client, lfx_server, live_server, login, monkeypatch
|
||||
client, lfx_server, live_server, login, monkeypatch, flow_helpers
|
||||
):
|
||||
await login()
|
||||
cont = await _create_flow(client, f"cont-{uuid4().hex[:8]}")
|
||||
await _version_and_deploy(client, cont["id"], fixture="simple_rule.json")
|
||||
cont = await flow_helpers["create_flow"](f"cont-{uuid4().hex[:8]}")
|
||||
await flow_helpers["version_and_deploy"](cont["id"], fixture="simple_rule.json")
|
||||
|
||||
# Continuation is authoring-time wiring on the definition (admin-only);
|
||||
# run bodies cannot set it.
|
||||
|
|
@ -74,7 +54,7 @@ async def test_approve_with_continuation_runs_facts(
|
|||
)
|
||||
assert response.status_code == 201, response.text
|
||||
flow = response.json()
|
||||
await _version_and_deploy(client, flow["id"], fixture="approval_handoff.json")
|
||||
await flow_helpers["version_and_deploy"](flow["id"], fixture="approval_handoff.json")
|
||||
|
||||
from app.settings import settings
|
||||
|
||||
|
|
@ -105,16 +85,18 @@ async def test_approve_with_continuation_runs_facts(
|
|||
assert "Sonderfall" in cont_run["output"]["result"]
|
||||
|
||||
|
||||
async def test_client_continuation_is_ignored(client, lfx_server, live_server, login, monkeypatch):
|
||||
async def test_client_continuation_is_ignored(
|
||||
client, lfx_server, live_server, login, monkeypatch, flow_helpers
|
||||
):
|
||||
"""A run body cannot chain arbitrary flows (G)."""
|
||||
from app.settings import settings
|
||||
|
||||
await login()
|
||||
monkeypatch.setattr(settings, "callback_base_url", live_server)
|
||||
cont = await _create_flow(client, f"cont-{uuid4().hex[:8]}")
|
||||
await _version_and_deploy(client, cont["id"], fixture="simple_rule.json")
|
||||
flow = await _create_flow(client, f"handoff-{uuid4().hex[:8]}")
|
||||
await _version_and_deploy(client, flow["id"], fixture="approval_handoff.json")
|
||||
cont = await flow_helpers["create_flow"](f"cont-{uuid4().hex[:8]}")
|
||||
await flow_helpers["version_and_deploy"](cont["id"], fixture="simple_rule.json")
|
||||
flow = await flow_helpers["create_flow"](f"handoff-{uuid4().hex[:8]}")
|
||||
await flow_helpers["version_and_deploy"](flow["id"], fixture="approval_handoff.json")
|
||||
run = (
|
||||
await client.post(
|
||||
f"/api/flows/{flow['id']}/run",
|
||||
|
|
@ -130,9 +112,9 @@ async def test_client_continuation_is_ignored(client, lfx_server, live_server, l
|
|||
assert task["continuation_flow_id"] is None
|
||||
|
||||
|
||||
async def test_reject_task(client, lfx_server, live_server, login, monkeypatch):
|
||||
async def test_reject_task(client, lfx_server, live_server, login, monkeypatch, flow_helpers):
|
||||
await login()
|
||||
_, task = await _handoff_task(client, live_server, monkeypatch)
|
||||
_, task = await _handoff_task(client, live_server, monkeypatch, flow_helpers)
|
||||
# Any logged-in user (not just admins) can decide; identity is the session.
|
||||
await login(email="chef@example.com", role="member")
|
||||
response = await client.post(
|
||||
|
|
@ -147,9 +129,9 @@ async def test_reject_task(client, lfx_server, live_server, login, monkeypatch):
|
|||
assert body["continuation_run"] is None
|
||||
|
||||
|
||||
async def test_decision_guards(client, lfx_server, live_server, login, monkeypatch):
|
||||
async def test_decision_guards(client, lfx_server, live_server, login, monkeypatch, flow_helpers):
|
||||
await login()
|
||||
_, task = await _handoff_task(client, live_server, monkeypatch)
|
||||
_, task = await _handoff_task(client, live_server, monkeypatch, flow_helpers)
|
||||
url = f"/api/approvals/{task['id']}/decision"
|
||||
|
||||
response = await client.post(url, json={"decision": "approve"})
|
||||
|
|
@ -160,7 +142,7 @@ async def test_decision_guards(client, lfx_server, live_server, login, monkeypat
|
|||
response = await client.post(f"/api/approvals/{uuid4()}/decision", json={"decision": "approve"})
|
||||
assert response.status_code == 404
|
||||
|
||||
_, task2 = await _handoff_task(client, live_server, monkeypatch)
|
||||
_, task2 = await _handoff_task(client, live_server, monkeypatch, flow_helpers)
|
||||
response = await client.post(
|
||||
f"/api/approvals/{task2['id']}/decision", json={"decision": "maybe"}
|
||||
)
|
||||
|
|
@ -174,15 +156,15 @@ async def test_decision_guards(client, lfx_server, live_server, login, monkeypat
|
|||
assert response.status_code == 200
|
||||
|
||||
|
||||
async def test_decision_bad_continuation_reference(client, session, login):
|
||||
async def test_decision_bad_continuation_reference(client, session, login, flow_helpers):
|
||||
from app.flows import store as flow_store
|
||||
|
||||
await login()
|
||||
flow = await _create_flow(client, f"badcont-{uuid4().hex[:8]}")
|
||||
flow = await flow_helpers["create_flow"](f"badcont-{uuid4().hex[:8]}")
|
||||
version = (
|
||||
await client.post(
|
||||
f"/api/flows/{flow['id']}/versions",
|
||||
json={"langflow_json": load_fixture("simple_rule.json")},
|
||||
json={"langflow_json": load_flow_fixture("simple_rule.json")},
|
||||
)
|
||||
).json()
|
||||
run = flow_store.create_run(
|
||||
|
|
@ -200,11 +182,11 @@ async def test_decision_bad_continuation_reference(client, session, login):
|
|||
assert response.status_code == 422
|
||||
|
||||
|
||||
async def test_tenant_isolation(client, lfx_server, login):
|
||||
async def test_tenant_isolation(client, lfx_server, login, flow_helpers):
|
||||
"""Strict session scoping: explicit ?tenant_id= cannot widen reads (E)."""
|
||||
await login()
|
||||
flow = await _create_flow(client, f"tenant-{uuid4().hex[:8]}")
|
||||
await _version_and_deploy(client, flow["id"])
|
||||
flow = await flow_helpers["create_flow"](f"tenant-{uuid4().hex[:8]}")
|
||||
await flow_helpers["version_and_deploy"](flow["id"])
|
||||
|
||||
await login(email="a@example.com", role="member", tenant_id="pfarramt-a")
|
||||
response = await client.post(f"/api/flows/{flow['id']}/run", json={"input": {"betrag": 1}})
|
||||
|
|
@ -235,14 +217,14 @@ async def test_tenant_isolation(client, lfx_server, login):
|
|||
assert response.status_code == 200
|
||||
|
||||
|
||||
async def test_lfx_down_maps_to_503(client, lfx_server, monkeypatch, login):
|
||||
async def test_lfx_down_maps_to_503(client, lfx_server, monkeypatch, login, flow_helpers):
|
||||
from app.settings import settings
|
||||
|
||||
await login()
|
||||
flow = await _create_flow(client, f"down-{uuid4().hex[:8]}")
|
||||
flow = await flow_helpers["create_flow"](f"down-{uuid4().hex[:8]}")
|
||||
await client.post(
|
||||
f"/api/flows/{flow['id']}/versions",
|
||||
json={"langflow_json": load_fixture("simple_rule.json")},
|
||||
json={"langflow_json": load_flow_fixture("simple_rule.json")},
|
||||
)
|
||||
monkeypatch.setattr(settings, "lfx_url", "http://127.0.0.1:9")
|
||||
response = await client.post(f"/api/flows/{flow['id']}/versions/1/deploy")
|
||||
|
|
@ -257,13 +239,13 @@ async def test_lfx_down_maps_to_503(client, lfx_server, monkeypatch, login):
|
|||
assert response.status_code == 503
|
||||
|
||||
|
||||
async def test_lfx_drift_maps_to_502(client, lfx_server, login):
|
||||
async def test_lfx_drift_maps_to_502(client, lfx_server, login, flow_helpers):
|
||||
from app.flows.lfx_client import LfxClient
|
||||
from app.settings import settings
|
||||
|
||||
await login()
|
||||
flow = await _create_flow(client, f"drift-{uuid4().hex[:8]}")
|
||||
deployed = await _version_and_deploy(client, flow["id"])
|
||||
flow = await flow_helpers["create_flow"](f"drift-{uuid4().hex[:8]}")
|
||||
deployed = await flow_helpers["version_and_deploy"](flow["id"])
|
||||
await LfxClient(settings.lfx_url, api_key=settings.lfx_api_key or None).delete_flow(
|
||||
deployed["lfx_flow_id"]
|
||||
)
|
||||
|
|
@ -271,9 +253,9 @@ async def test_lfx_drift_maps_to_502(client, lfx_server, login):
|
|||
assert response.status_code == 502
|
||||
|
||||
|
||||
async def test_corrupt_flow_json_deploy_maps_to_422(client, lfx_server, login):
|
||||
async def test_corrupt_flow_json_deploy_maps_to_422(client, lfx_server, login, flow_helpers):
|
||||
await login()
|
||||
flow = await _create_flow(client, f"corrupt-{uuid4().hex[:8]}")
|
||||
flow = await flow_helpers["create_flow"](f"corrupt-{uuid4().hex[:8]}")
|
||||
bogus = {
|
||||
"name": "bogus",
|
||||
"data": {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import pytest
|
|||
from sqlmodel import select
|
||||
|
||||
from app.auth import service
|
||||
from app.auth.deps import SESSION_COOKIE, resolve_tenant
|
||||
from app.auth.deps import SESSION_COOKIE
|
||||
from app.models import MagicLinkToken, User
|
||||
from app.settings import settings
|
||||
|
||||
|
|
@ -200,6 +200,28 @@ async def test_no_token_or_session_in_logs(client, session, caplog):
|
|||
assert service.hash_token(token) not in caplog.text
|
||||
|
||||
|
||||
def test_session_jwt_short_expiry_and_claims(session):
|
||||
"""Stateless sessions expire in days (no revocation — bound the window)."""
|
||||
user = _get_or_make_user(session, MEMBER)
|
||||
claims = service.decode_session_token(service.mint_session_token(user))
|
||||
assert claims["sub"] == str(user.id)
|
||||
assert claims["tenant_id"] == user.tenant_id
|
||||
assert claims["exp"] - claims["iat"] == 7 * 24 * 3600
|
||||
|
||||
|
||||
async def test_deleted_user_token_fails_closed(client, session):
|
||||
"""Tokens of deleted users stop working (row reloaded per request)."""
|
||||
user = _get_or_make_user(session, MEMBER)
|
||||
token = service.issue_magic_token(session, user)
|
||||
response = await client.post("/api/auth/magic/consume", json={"token": token})
|
||||
assert response.status_code == 200, response.text
|
||||
jwt_cookie = response.cookies[SESSION_COOKIE]
|
||||
session.delete(user)
|
||||
session.commit()
|
||||
client.cookies.set(SESSION_COOKIE, jwt_cookie)
|
||||
assert (await client.get("/api/auth/me")).status_code == 401
|
||||
|
||||
|
||||
async def test_access_request_rate_limit(client):
|
||||
email = f"impatient-{uuid4()}@example.com"
|
||||
for _ in range(service.REQUESTS_PER_EMAIL_PER_HOUR):
|
||||
|
|
@ -214,14 +236,6 @@ async def test_access_request_rate_limit(client):
|
|||
assert limited.status_code == 429
|
||||
|
||||
|
||||
def test_resolve_tenant():
|
||||
assert resolve_tenant(None, None) == "default"
|
||||
assert resolve_tenant(None, "acme") == "acme"
|
||||
user = User(email="a@b.c", tenant_id="parish-7")
|
||||
assert resolve_tenant(user, None) == "parish-7"
|
||||
assert resolve_tenant(user, "other") == "parish-7"
|
||||
|
||||
|
||||
def _broken_smtp(monkeypatch):
|
||||
from app.auth import mail
|
||||
|
||||
|
|
|
|||
|
|
@ -12,35 +12,9 @@ from .conftest import load_flow_fixture
|
|||
CALLBACK_HEADERS = {"X-Callback-Token": "test-callback-token"}
|
||||
|
||||
|
||||
def load_fixture(name: str) -> dict:
|
||||
"""Alias of the shared conftest helper (kept for call-site stability)."""
|
||||
return load_flow_fixture(name)
|
||||
|
||||
|
||||
async def _create_flow(client, name="regeltest"):
|
||||
response = await client.post("/api/flows", json={"name": name, "description": "test flow"})
|
||||
assert response.status_code == 201, response.text
|
||||
return response.json()
|
||||
|
||||
|
||||
async def _create_version(client, flow_id, fixture="simple_rule.json"):
|
||||
response = await client.post(
|
||||
f"/api/flows/{flow_id}/versions",
|
||||
json={"langflow_json": load_fixture(fixture), "author": "test"},
|
||||
)
|
||||
assert response.status_code == 201, response.text
|
||||
return response.json()
|
||||
|
||||
|
||||
async def _deploy(client, flow_id, version_no=1):
|
||||
response = await client.post(f"/api/flows/{flow_id}/versions/{version_no}/deploy")
|
||||
assert response.status_code == 200, response.text
|
||||
return response.json()
|
||||
|
||||
|
||||
async def test_flow_crud(client, login):
|
||||
async def test_flow_crud(client, login, flow_helpers):
|
||||
await login()
|
||||
created = await _create_flow(client, "crud-flow")
|
||||
created = await flow_helpers["create_flow"]("crud-flow")
|
||||
assert created["status"] == "active"
|
||||
|
||||
response = await client.get(f"/api/flows/{created['id']}")
|
||||
|
|
@ -98,10 +72,12 @@ LOGIN_GATED_PATHS = [
|
|||
|
||||
|
||||
@pytest.mark.parametrize(("method", "path", "body"), LOGIN_GATED_PATHS)
|
||||
async def test_runs_and_reads_require_login(client, login, anon_client, method, path, body):
|
||||
async def test_runs_and_reads_require_login(
|
||||
client, login, anon_client, flow_helpers, method, path, body
|
||||
):
|
||||
"""Run, run reads and approval reads are login-gated (parametrized)."""
|
||||
await login()
|
||||
flow = await _create_flow(client, "gated")
|
||||
flow = await flow_helpers["create_flow"]("gated")
|
||||
target = path.format(flow_id=flow["id"], run_id=uuid4(), task_id=uuid4())
|
||||
async for anon in anon_client():
|
||||
if method == "POST":
|
||||
|
|
@ -111,13 +87,13 @@ async def test_runs_and_reads_require_login(client, login, anon_client, method,
|
|||
assert response.status_code == 401, (method, target, response.text)
|
||||
|
||||
|
||||
async def test_version_validation_and_numbering(client, login):
|
||||
async def test_version_validation_and_numbering(client, login, flow_helpers):
|
||||
await login()
|
||||
flow = await _create_flow(client, "versioned")
|
||||
flow = await flow_helpers["create_flow"]("versioned")
|
||||
|
||||
v1 = await _create_version(client, flow["id"])
|
||||
v1 = await flow_helpers["create_version"](flow["id"])
|
||||
assert v1["version_no"] == 1
|
||||
v2 = await _create_version(client, flow["id"])
|
||||
v2 = await flow_helpers["create_version"](flow["id"])
|
||||
assert v2["version_no"] == 2
|
||||
assert v1["deployed_at"] is None
|
||||
|
||||
|
|
@ -128,7 +104,7 @@ async def test_version_validation_and_numbering(client, login):
|
|||
|
||||
response = await client.post(
|
||||
f"/api/flows/{uuid4()}/versions",
|
||||
json={"langflow_json": load_fixture("simple_rule.json")},
|
||||
json={"langflow_json": load_flow_fixture("simple_rule.json")},
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
|
|
@ -136,11 +112,11 @@ async def test_version_validation_and_numbering(client, login):
|
|||
assert [v["version_no"] for v in response.json()] == [1, 2]
|
||||
|
||||
|
||||
async def test_deploy_and_run_standard_and_sonderfall(client, lfx_server, login):
|
||||
async def test_deploy_and_run_standard_and_sonderfall(client, lfx_server, login, flow_helpers):
|
||||
await login()
|
||||
flow = await _create_flow(client, "dachsanierung")
|
||||
await _create_version(client, flow["id"])
|
||||
deployed = await _deploy(client, flow["id"])
|
||||
flow = await flow_helpers["create_flow"]("dachsanierung")
|
||||
await flow_helpers["create_version"](flow["id"])
|
||||
deployed = await flow_helpers["deploy"](flow["id"])
|
||||
assert deployed["lfx_flow_id"] == lfx_id_for(UUID(flow["id"]), 1)
|
||||
assert deployed["deployed_at"] is not None
|
||||
|
||||
|
|
@ -168,20 +144,20 @@ async def test_deploy_and_run_standard_and_sonderfall(client, lfx_server, login)
|
|||
assert response.json()["id"] == run["id"]
|
||||
|
||||
|
||||
async def test_run_undeployed_version_fails(client, login):
|
||||
async def test_run_undeployed_version_fails(client, login, flow_helpers):
|
||||
await login()
|
||||
flow = await _create_flow(client, "undeployed")
|
||||
await _create_version(client, flow["id"])
|
||||
flow = await flow_helpers["create_flow"]("undeployed")
|
||||
await flow_helpers["create_version"](flow["id"])
|
||||
response = await client.post(f"/api/flows/{flow['id']}/run", json={"input": {"betrag": 1}})
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
async def test_run_explicit_version_no(client, lfx_server, login):
|
||||
async def test_run_explicit_version_no(client, lfx_server, login, flow_helpers):
|
||||
await login()
|
||||
flow = await _create_flow(client, "explicit-version")
|
||||
await _create_version(client, flow["id"])
|
||||
await _create_version(client, flow["id"])
|
||||
await _deploy(client, flow["id"], version_no=2)
|
||||
flow = await flow_helpers["create_flow"]("explicit-version")
|
||||
await flow_helpers["create_version"](flow["id"])
|
||||
await flow_helpers["create_version"](flow["id"])
|
||||
await flow_helpers["deploy"](flow["id"], version_no=2)
|
||||
response = await client.post(
|
||||
f"/api/flows/{flow['id']}/run",
|
||||
json={"input": {"betrag": 1}, "version_no": 2},
|
||||
|
|
@ -190,11 +166,11 @@ async def test_run_explicit_version_no(client, lfx_server, login):
|
|||
assert response.json()["status"] == "completed"
|
||||
|
||||
|
||||
async def test_run_stream_events(client, lfx_server, login):
|
||||
async def test_run_stream_events(client, lfx_server, login, flow_helpers):
|
||||
await login()
|
||||
flow = await _create_flow(client, "streamed")
|
||||
await _create_version(client, flow["id"])
|
||||
await _deploy(client, flow["id"])
|
||||
flow = await flow_helpers["create_flow"]("streamed")
|
||||
await flow_helpers["create_version"](flow["id"])
|
||||
await flow_helpers["deploy"](flow["id"])
|
||||
|
||||
events: list[tuple[str, str]] = []
|
||||
async with client.stream(
|
||||
|
|
@ -225,14 +201,16 @@ async def test_run_stream_events(client, lfx_server, login):
|
|||
assert response.json()["status"] == "completed"
|
||||
|
||||
|
||||
async def test_approval_handoff_end_to_end(client, lfx_server, live_server, login, monkeypatch):
|
||||
async def test_approval_handoff_end_to_end(
|
||||
client, lfx_server, live_server, login, monkeypatch, flow_helpers
|
||||
):
|
||||
from app.settings import settings
|
||||
|
||||
await login()
|
||||
monkeypatch.setattr(settings, "callback_base_url", live_server)
|
||||
flow = await _create_flow(client, "freigabe")
|
||||
await _create_version(client, flow["id"], fixture="approval_handoff.json")
|
||||
await _deploy(client, flow["id"])
|
||||
flow = await flow_helpers["create_flow"]("freigabe")
|
||||
await flow_helpers["create_version"](flow["id"], fixture="approval_handoff.json")
|
||||
await flow_helpers["deploy"](flow["id"])
|
||||
|
||||
response = await client.post(
|
||||
f"/api/flows/{flow['id']}/run",
|
||||
|
|
@ -254,15 +232,15 @@ async def test_approval_handoff_end_to_end(client, lfx_server, live_server, logi
|
|||
|
||||
|
||||
async def test_approval_handoff_auto_creates_no_task(
|
||||
client, lfx_server, live_server, login, monkeypatch
|
||||
client, lfx_server, live_server, login, monkeypatch, flow_helpers
|
||||
):
|
||||
from app.settings import settings
|
||||
|
||||
await login()
|
||||
monkeypatch.setattr(settings, "callback_base_url", live_server)
|
||||
flow = await _create_flow(client, "freigabe-auto")
|
||||
await _create_version(client, flow["id"], fixture="approval_handoff.json")
|
||||
await _deploy(client, flow["id"])
|
||||
flow = await flow_helpers["create_flow"]("freigabe-auto")
|
||||
await flow_helpers["create_version"](flow["id"], fixture="approval_handoff.json")
|
||||
await flow_helpers["deploy"](flow["id"])
|
||||
|
||||
response = await client.post(
|
||||
f"/api/flows/{flow['id']}/run",
|
||||
|
|
@ -276,14 +254,14 @@ async def test_approval_handoff_auto_creates_no_task(
|
|||
assert response.json() == []
|
||||
|
||||
|
||||
async def test_approval_callback_contract(client, session, login):
|
||||
async def test_approval_callback_contract(client, session, login, flow_helpers):
|
||||
from uuid import UUID
|
||||
|
||||
from app.flows import store as flow_store
|
||||
|
||||
await login()
|
||||
flow = await _create_flow(client, "callback-target")
|
||||
version = await _create_version(client, flow["id"])
|
||||
flow = await flow_helpers["create_flow"]("callback-target")
|
||||
version = await flow_helpers["create_version"](flow["id"])
|
||||
version_obj = flow_store.get_version(session, UUID(version["id"]))
|
||||
|
||||
run = flow_store.create_run(session, version_obj, input={}, tenant_id="default")
|
||||
|
|
@ -321,15 +299,15 @@ async def test_approval_callback_contract(client, session, login):
|
|||
assert response.status_code == 404
|
||||
|
||||
|
||||
async def test_approval_callback_token_required(client, session, login):
|
||||
async def test_approval_callback_token_required(client, session, login, flow_helpers):
|
||||
"""Forged callbacks without the shared token are rejected (C)."""
|
||||
from uuid import UUID
|
||||
|
||||
from app.flows import store as flow_store
|
||||
|
||||
await login()
|
||||
flow = await _create_flow(client, "callback-gated")
|
||||
version = await _create_version(client, flow["id"])
|
||||
flow = await flow_helpers["create_flow"]("callback-gated")
|
||||
version = await flow_helpers["create_version"](flow["id"])
|
||||
version_obj = flow_store.get_version(session, UUID(version["id"]))
|
||||
run = flow_store.create_run(session, version_obj, input={}, tenant_id="default")
|
||||
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ import pytest
|
|||
|
||||
from app.flows import input_schema as parser
|
||||
from app.flows import sync
|
||||
from app.flows.lfx_client import LfxClient
|
||||
from app.settings import settings
|
||||
|
||||
from .conftest import FIXTURES_DIR as FIXTURES
|
||||
|
||||
|
|
@ -195,13 +193,9 @@ def test_real_intake_fixture_parses():
|
|||
assert schema["ui_schema"]["notes"] == {"ui:widget": "textarea"}
|
||||
|
||||
|
||||
def _lfx() -> LfxClient:
|
||||
return LfxClient(settings.lfx_url, api_key=settings.lfx_api_key or None)
|
||||
|
||||
|
||||
async def test_sync_caches_schema_per_version(session, lfx_server, tmp_path):
|
||||
async def test_sync_caches_schema_per_version(session, lfx_server, lfx_client, tmp_path):
|
||||
(tmp_path / "intake_form.json").write_text((FIXTURES / "intake_form.json").read_text())
|
||||
report = await sync.sync_from_directory(session, _lfx(), tmp_path)
|
||||
report = await sync.sync_from_directory(session, lfx_client, tmp_path)
|
||||
assert not report.errors, report.errors
|
||||
from app.flows import store
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from pathlib import Path
|
|||
import pytest
|
||||
|
||||
from app.flows import store, sync
|
||||
from app.flows.lfx_client import LfxClient, LfxNotFound
|
||||
from app.flows.lfx_client import LfxNotFound
|
||||
from app.flows.schemas import SyncReport
|
||||
from app.flows.sync import lfx_id_for
|
||||
from app.settings import settings
|
||||
|
|
@ -22,19 +22,9 @@ def _seed_dir(tmp_path: Path, name: str = "simple_rule.json") -> Path:
|
|||
return tmp_path
|
||||
|
||||
|
||||
def load_fixture(name: str) -> dict:
|
||||
"""Alias of the shared conftest helper (kept for call-site stability)."""
|
||||
return load_flow_fixture(name)
|
||||
|
||||
|
||||
def _lfx() -> LfxClient:
|
||||
"""Alias of the shared lfx_client fixture logic (call-site stability)."""
|
||||
return LfxClient(settings.lfx_url, api_key=settings.lfx_api_key or None)
|
||||
|
||||
|
||||
async def test_sync_adds_new_flow(session, lfx_server, tmp_path):
|
||||
async def test_sync_adds_new_flow(session, lfx_server, lfx_client, tmp_path):
|
||||
_seed_dir(tmp_path)
|
||||
report = await sync.sync_from_directory(session, _lfx(), tmp_path)
|
||||
report = await sync.sync_from_directory(session, lfx_client, tmp_path)
|
||||
assert report.added == ["simple_rule"]
|
||||
assert report.errors == {}
|
||||
definition = store.get_definition_by_source_path(session, "simple_rule.json")
|
||||
|
|
@ -46,83 +36,83 @@ async def test_sync_adds_new_flow(session, lfx_server, tmp_path):
|
|||
assert versions[0].deployed_at is not None
|
||||
|
||||
|
||||
async def test_sync_skips_ignored_files(session, lfx_server, tmp_path):
|
||||
async def test_sync_skips_ignored_files(session, lfx_server, lfx_client, tmp_path):
|
||||
_seed_dir(tmp_path)
|
||||
(tmp_path / ".flowsignore").write_text("simple_rule\n", encoding="utf-8")
|
||||
report = await sync.sync_from_directory(session, _lfx(), tmp_path)
|
||||
report = await sync.sync_from_directory(session, lfx_client, tmp_path)
|
||||
assert report.added == [] and report.errors == {}
|
||||
assert store.get_definition_by_source_path(session, "simple_rule.json") is None
|
||||
|
||||
|
||||
async def test_sync_skips_unchanged(session, lfx_server, tmp_path):
|
||||
async def test_sync_skips_unchanged(session, lfx_server, lfx_client, tmp_path):
|
||||
_seed_dir(tmp_path)
|
||||
first = await sync.sync_from_directory(session, _lfx(), tmp_path)
|
||||
first = await sync.sync_from_directory(session, lfx_client, tmp_path)
|
||||
assert first.added == ["simple_rule"]
|
||||
second = await sync.sync_from_directory(session, _lfx(), tmp_path)
|
||||
second = await sync.sync_from_directory(session, lfx_client, tmp_path)
|
||||
assert second.skipped == ["simple_rule"]
|
||||
assert second.added == second.updated == []
|
||||
definition = store.get_definition_by_source_path(session, "simple_rule.json")
|
||||
assert len(store.list_versions(session, definition.id)) == 1
|
||||
|
||||
|
||||
async def test_sync_updates_on_change_and_replaces_lfx(session, lfx_server, tmp_path):
|
||||
async def test_sync_updates_on_change_and_replaces_lfx(session, lfx_server, lfx_client, tmp_path):
|
||||
_seed_dir(tmp_path)
|
||||
await sync.sync_from_directory(session, _lfx(), tmp_path)
|
||||
await sync.sync_from_directory(session, lfx_client, tmp_path)
|
||||
definition = store.get_definition_by_source_path(session, "simple_rule.json")
|
||||
old_lfx_id = lfx_id_for(definition.id, 1)
|
||||
|
||||
doc = load_fixture("simple_rule.json")
|
||||
doc = load_flow_fixture("simple_rule.json")
|
||||
doc["description"] = "geaendert"
|
||||
(tmp_path / "simple_rule.json").write_text(json.dumps(doc))
|
||||
|
||||
report = await sync.sync_from_directory(session, _lfx(), tmp_path)
|
||||
report = await sync.sync_from_directory(session, lfx_client, tmp_path)
|
||||
assert report.updated == ["simple_rule"]
|
||||
versions = store.list_versions(session, definition.id)
|
||||
assert [v.version_no for v in versions] == [1, 2]
|
||||
assert versions[1].lfx_flow_id == lfx_id_for(definition.id, 2)
|
||||
# Superseded LFX registration is gone.
|
||||
with pytest.raises(LfxNotFound):
|
||||
await _lfx().flow_info(old_lfx_id)
|
||||
await lfx_client.flow_info(old_lfx_id)
|
||||
|
||||
|
||||
async def test_sync_deletes_missing_file_with_cascade(session, lfx_server, tmp_path):
|
||||
async def test_sync_deletes_missing_file_with_cascade(session, lfx_server, lfx_client, tmp_path):
|
||||
_seed_dir(tmp_path)
|
||||
await sync.sync_from_directory(session, _lfx(), tmp_path)
|
||||
await sync.sync_from_directory(session, lfx_client, tmp_path)
|
||||
definition = store.get_definition_by_source_path(session, "simple_rule.json")
|
||||
version = store.list_versions(session, definition.id)[0]
|
||||
run = store.create_run(session, version, input={"a": 1}, tenant_id="default")
|
||||
lfx_id = lfx_id_for(definition.id, 1)
|
||||
|
||||
(tmp_path / "simple_rule.json").unlink()
|
||||
report = await sync.sync_from_directory(session, _lfx(), tmp_path)
|
||||
report = await sync.sync_from_directory(session, lfx_client, tmp_path)
|
||||
assert report.deleted == ["simple_rule"]
|
||||
assert store.get_definition_by_source_path(session, "simple_rule.json") is None
|
||||
with pytest.raises(store.NotFoundError):
|
||||
store.get_run(session, run.id)
|
||||
with pytest.raises(LfxNotFound):
|
||||
await _lfx().flow_info(lfx_id)
|
||||
await lfx_client.flow_info(lfx_id)
|
||||
|
||||
|
||||
async def test_sync_reports_invalid_json(session, lfx_server, tmp_path):
|
||||
async def test_sync_reports_invalid_json(session, lfx_server, lfx_client, tmp_path):
|
||||
(tmp_path / "kaputt.json").write_text("{kein json")
|
||||
report = await sync.sync_from_directory(session, _lfx(), tmp_path)
|
||||
report = await sync.sync_from_directory(session, lfx_client, tmp_path)
|
||||
assert "kaputt.json" in report.errors
|
||||
assert report.added == []
|
||||
assert store.list_definitions(session) == []
|
||||
|
||||
|
||||
async def test_sync_missing_dir_raises(session, lfx_server, tmp_path):
|
||||
async def test_sync_missing_dir_raises(session, lfx_server, lfx_client, tmp_path):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
await sync.sync_from_directory(session, _lfx(), tmp_path / "gibts-nicht")
|
||||
await sync.sync_from_directory(session, lfx_client, tmp_path / "gibts-nicht")
|
||||
|
||||
|
||||
async def test_sync_rename_updates_definition_name(session, lfx_server, tmp_path):
|
||||
async def test_sync_rename_updates_definition_name(session, lfx_server, lfx_client, tmp_path):
|
||||
_seed_dir(tmp_path)
|
||||
await sync.sync_from_directory(session, _lfx(), tmp_path)
|
||||
doc = load_fixture("simple_rule.json")
|
||||
await sync.sync_from_directory(session, lfx_client, tmp_path)
|
||||
doc = load_flow_fixture("simple_rule.json")
|
||||
doc["name"] = "umbenannt"
|
||||
(tmp_path / "simple_rule.json").write_text(json.dumps(doc))
|
||||
report = await sync.sync_from_directory(session, _lfx(), tmp_path)
|
||||
report = await sync.sync_from_directory(session, lfx_client, tmp_path)
|
||||
assert report.updated == ["simple_rule"]
|
||||
definition = store.get_definition_by_source_path(session, "simple_rule.json")
|
||||
assert definition.name == "umbenannt"
|
||||
|
|
@ -146,11 +136,6 @@ async def test_sync_endpoint_returns_report(
|
|||
assert response.json()["skipped"] == ["simple_rule"]
|
||||
|
||||
|
||||
def _langflow_doc(name: str, langflow_id: str) -> dict:
|
||||
"""Alias of the shared conftest helper (kept for call-site stability)."""
|
||||
return langflow_export_doc(name, langflow_id)
|
||||
|
||||
|
||||
async def test_sync_endpoint_pulls_from_langflow_then_syncs(
|
||||
client, lfx_server, tmp_path, monkeypatch, login, override_langflow
|
||||
):
|
||||
|
|
@ -159,7 +144,7 @@ async def test_sync_endpoint_pulls_from_langflow_then_syncs(
|
|||
langflow_id = "22222222-3333-4444-8555-666666666666"
|
||||
override_langflow(
|
||||
flows=[{"id": langflow_id, "name": "Gehaltserhöhung"}],
|
||||
docs={langflow_id: _langflow_doc("Gehaltserhöhung", langflow_id)},
|
||||
docs={langflow_id: langflow_export_doc("Gehaltserhöhung", langflow_id)},
|
||||
)
|
||||
response = await client.post("/api/flows/sync")
|
||||
assert response.status_code == 200, response.text
|
||||
|
|
@ -188,7 +173,7 @@ async def test_sync_endpoint_pull_reuses_same_named_file(
|
|||
langflow_id = "44444444-5555-4666-8777-888888888888"
|
||||
override_langflow(
|
||||
flows=[{"id": langflow_id, "name": "simple_rule"}],
|
||||
docs={langflow_id: _langflow_doc("simple_rule", langflow_id)},
|
||||
docs={langflow_id: langflow_export_doc("simple_rule", langflow_id)},
|
||||
)
|
||||
response = await client.post("/api/flows/sync")
|
||||
assert response.status_code == 200, response.text
|
||||
|
|
@ -210,7 +195,7 @@ async def test_sync_endpoint_pull_error_continues(
|
|||
good_id = "33333333-4444-4555-8666-777777777777"
|
||||
override_langflow(
|
||||
flows=[{"id": good_id, "name": "Gut"}, {"id": "kaputt-id", "name": "Kaputt"}],
|
||||
docs={good_id: _langflow_doc("Gut", good_id)},
|
||||
docs={good_id: langflow_export_doc("Gut", good_id)},
|
||||
)
|
||||
response = await client.post("/api/flows/sync")
|
||||
assert response.status_code == 200, response.text
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue