kirche-einfach/backend/tests/test_input_schema.py

193 lines
6.8 KiB
Python

"""Phase 3 tests: form-schema extraction, sync cache, input-schema endpoint."""
import json
from pathlib import Path
from uuid import uuid4
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
FIXTURES = Path(__file__).parent / "fixtures" / "flows"
def _field(name, itype, **kw):
base = {
"name": name,
"display_name": name.replace("_", " ").title(),
"info": "",
"value": None,
"required": False,
"advanced": False,
"show": True,
"_input_type": itype,
}
base.update(kw)
return base
def _doc(*fields, node_type="CustomInputForm", display="CustomInput Form"):
template = {f["name"]: f for f in fields}
node = {
"data": {
"type": node_type,
"node": {"display_name": display, "template": template},
}
}
return {"data": {"nodes": [node], "edges": []}}
def test_type_mapping_table():
doc = _doc(
_field("a", "StrInput", value="x"),
_field("b", "MessageTextInput"),
_field("c", "MultilineInput"),
_field("d", "SecretStrInput"),
_field("e", "IntInput", value=3),
_field("f", "FloatInput", value=1.5),
_field("g", "BoolInput", value=True),
_field(
"h",
"DropdownInput",
options=["Music", "Youth"],
value="Youth",
),
_field(
"i",
"MultiselectInput",
options=["Worship", "Courses"],
value=["Worship"],
),
_field(
"j",
"SliderInput",
value=3,
range_spec={"min": 1, "max": 5, "step": 1},
),
_field("k", "FileInput"),
_field("l", "FutureFancyInput"),
)
schema = parser.extract_form_schema(doc)
assert "fallback" not in schema
props = schema["json_schema"]["properties"]
assert props["a"] == {"title": "A", "type": "string", "default": "x"}
assert props["b"]["type"] == "string"
assert props["e"] == {"title": "E", "type": "integer", "default": 3}
assert props["f"]["type"] == "number"
assert props["g"] == {"title": "G", "type": "boolean", "default": True}
assert props["h"] == {
"title": "H",
"type": "string",
"enum": ["Music", "Youth"],
"default": "Youth",
}
assert props["i"]["type"] == "array"
assert props["i"]["items"] == {"type": "string", "enum": ["Worship", "Courses"]}
assert props["i"]["uniqueItems"] is True
assert props["j"]["minimum"] == 1
assert props["j"]["maximum"] == 5
assert props["j"]["multipleOf"] == 1
assert props["k"]["type"] == "string"
assert props["l"]["type"] == "string"
ui = schema["ui_schema"]
assert ui["c"] == {"ui:widget": "textarea"}
assert ui["d"] == {"ui:widget": "password"}
assert ui["j"] == {"ui:widget": "range"}
def test_dropdown_default_outside_enum_omitted():
doc = _doc(_field("h", "DropdownInput", options=["Music"], value=""))
props = parser.extract_form_schema(doc)["json_schema"]["properties"]
assert props["h"] == {"title": "H", "type": "string", "enum": ["Music"]}
def test_required_advanced_hidden_code():
doc = _doc(
_field("a", "StrInput", required=True),
_field("b", "StrInput", advanced=True),
_field("c", "StrInput", show=False),
)
doc["data"]["nodes"][0]["data"]["node"]["template"]["code"] = {"value": "x"}
schema = parser.extract_form_schema(doc)
assert schema["json_schema"]["required"] == ["a"]
assert set(schema["json_schema"]["properties"]) == {"a", "b"}
assert schema["ui_schema"]["ui:order"] == ["a", "b", "*"]
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"}
plain = {
"data": {
"nodes": [{"data": {"type": "ChatInput", "node": {"template": {}}}}],
"edges": [],
}
}
schema = parser.extract_form_schema(plain)
assert schema["fallback"] == "textarea"
assert schema["json_schema"]["properties"] == {}
def test_real_intake_fixture_parses():
doc = json.loads((FIXTURES / "intake_form.json").read_text())
schema = parser.extract_form_schema(doc)
props = schema["json_schema"]["properties"]
assert "fallback" not in schema
assert set(props) == {
"full_name",
"notes",
"password",
"age",
"amount",
"newsletter",
"department",
"topics",
"satisfaction",
"attachment",
}
assert "full_name" in schema["json_schema"]["required"]
assert props["department"]["enum"] == ["Music", "Youth", "Seniors", "Admin"]
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):
(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
defs = store.list_definitions(session)
definition = next(d for d in defs if d.name == "intake_form")
versions = store.list_versions(session, definition.id)
assert len(versions) == 1
cached = versions[0].input_schema
assert cached["json_schema"]["properties"]["full_name"]["type"] == "string"
assert "form_values" not in cached["json_schema"]["properties"]
async def test_input_schema_endpoint(client, lfx_server):
assert (await client.get(f"/api/flows/{uuid4()}/input-schema")).status_code == 404
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})
assert version.status_code == 201, version.text
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
deployed = await client.post(f"/api/flows/{flow_id}/versions/1/deploy")
assert deployed.status_code == 200, deployed.text
response = await client.get(f"/api/flows/{flow_id}/input-schema")
assert response.status_code == 200, response.text
body = response.json()
assert body["version_no"] == 1
assert body["fallback"] is None
assert set(body["json_schema"]["properties"]) >= {"full_name", "topics"}
assert body["ui_schema"]["password"] == {"ui:widget": "password"}