refactor: remove XML export, JSON-only for .rfflow files
- Remove XML builder, format query param, and XML tests - Simplify ExportFlowModal (no format picker) - Simplify rfflowParser (JSON-only) - Remove format field from schemas and types Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,4 @@
|
||||
"""Tests for flow export/import (.rfflow) endpoints."""
|
||||
import json
|
||||
import xml.etree.ElementTree as ET
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
@@ -41,7 +39,7 @@ async def create_tree_with_tags(client: AsyncClient, headers: dict, data: dict |
|
||||
async def test_export_json_format(client, auth_headers, test_tree):
|
||||
"""Export should return valid .rfflow JSON with correct structure."""
|
||||
resp = await client.get(
|
||||
f"/api/v1/trees/{test_tree['id']}/export?format=json",
|
||||
f"/api/v1/trees/{test_tree['id']}/export",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
@@ -51,7 +49,6 @@ async def test_export_json_format(client, auth_headers, test_tree):
|
||||
data = resp.json()
|
||||
assert data["rfflow_version"] == "1.0"
|
||||
assert data["source_app"] == "ResolutionFlow"
|
||||
assert data["format"] == "json"
|
||||
assert data["exported_at"] is not None
|
||||
|
||||
flow = data["flow"]
|
||||
@@ -65,37 +62,12 @@ async def test_export_json_format(client, auth_headers, test_tree):
|
||||
assert "account_id" not in flow
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_export_xml_format(client, auth_headers, test_tree):
|
||||
"""Export as XML should be parseable and contain all required fields."""
|
||||
resp = await client.get(
|
||||
f"/api/v1/trees/{test_tree['id']}/export?format=xml",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert ".rfflow" in resp.headers.get("content-disposition", "")
|
||||
|
||||
# Parse XML
|
||||
root = ET.fromstring(resp.text)
|
||||
assert root.tag == "rfflow"
|
||||
assert root.get("version") == "1.0"
|
||||
|
||||
flow_el = root.find("flow")
|
||||
assert flow_el is not None
|
||||
assert flow_el.find("name").text == test_tree["name"]
|
||||
|
||||
# tree_structure should be valid JSON
|
||||
ts_text = flow_el.find("tree_structure").text
|
||||
ts = json.loads(ts_text)
|
||||
assert ts["id"] == "root"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_export_with_category_and_tags(client, auth_headers):
|
||||
"""Export should include category and tag data."""
|
||||
tree = await create_tree_with_tags(client, auth_headers)
|
||||
resp = await client.get(
|
||||
f"/api/v1/trees/{tree['id']}/export?format=json",
|
||||
f"/api/v1/trees/{tree['id']}/export",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
@@ -121,9 +93,8 @@ async def test_export_access_control(client, auth_headers, test_admin, admin_aut
|
||||
})
|
||||
other_headers = {"Authorization": f"Bearer {login_resp.json()['access_token']}"}
|
||||
|
||||
# The other user is in a different account, so can't see the tree
|
||||
resp = await client.get(
|
||||
f"/api/v1/trees/{test_tree['id']}/export?format=json",
|
||||
f"/api/v1/trees/{test_tree['id']}/export",
|
||||
headers=other_headers,
|
||||
)
|
||||
assert resp.status_code == 403
|
||||
@@ -134,7 +105,7 @@ async def test_export_nonexistent_tree(client, auth_headers):
|
||||
"""Export of non-existent tree returns 404."""
|
||||
import uuid
|
||||
resp = await client.get(
|
||||
f"/api/v1/trees/{uuid.uuid4()}/export?format=json",
|
||||
f"/api/v1/trees/{uuid.uuid4()}/export",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 404
|
||||
@@ -147,7 +118,7 @@ async def test_import_happy_path(client, auth_headers, test_tree):
|
||||
"""Import should create a draft tree owned by the importing user."""
|
||||
# First export
|
||||
export_resp = await client.get(
|
||||
f"/api/v1/trees/{test_tree['id']}/export?format=json",
|
||||
f"/api/v1/trees/{test_tree['id']}/export",
|
||||
headers=auth_headers,
|
||||
)
|
||||
rfflow_data = export_resp.json()
|
||||
@@ -180,7 +151,7 @@ async def test_import_happy_path(client, auth_headers, test_tree):
|
||||
async def test_import_with_name_override(client, auth_headers, test_tree):
|
||||
"""Import with name_override should use the override name."""
|
||||
export_resp = await client.get(
|
||||
f"/api/v1/trees/{test_tree['id']}/export?format=json",
|
||||
f"/api/v1/trees/{test_tree['id']}/export",
|
||||
headers=auth_headers,
|
||||
)
|
||||
rfflow_data = export_resp.json()
|
||||
@@ -201,7 +172,6 @@ async def test_import_with_new_tags(client, auth_headers):
|
||||
"rfflow_version": "1.0",
|
||||
"exported_at": "2026-03-05T14:30:00+00:00",
|
||||
"source_app": "ResolutionFlow",
|
||||
"format": "json",
|
||||
"flow": {
|
||||
"name": "Test Import Tags",
|
||||
"description": "Testing tag creation",
|
||||
@@ -235,7 +205,6 @@ async def test_import_with_category_creation(client, auth_headers):
|
||||
"rfflow_version": "1.0",
|
||||
"exported_at": "2026-03-05T14:30:00+00:00",
|
||||
"source_app": "ResolutionFlow",
|
||||
"format": "json",
|
||||
"flow": {
|
||||
"name": "Import Category Test",
|
||||
"description": None,
|
||||
@@ -267,7 +236,6 @@ async def test_import_invalid_version(client, auth_headers):
|
||||
"rfflow_version": "99.0",
|
||||
"exported_at": "2026-03-05T14:30:00+00:00",
|
||||
"source_app": "ResolutionFlow",
|
||||
"format": "json",
|
||||
"flow": {
|
||||
"name": "Bad Version",
|
||||
"tree_type": "troubleshooting",
|
||||
@@ -288,7 +256,7 @@ async def test_import_round_trip(client, auth_headers):
|
||||
|
||||
# Export
|
||||
export_resp = await client.get(
|
||||
f"/api/v1/trees/{original['id']}/export?format=json",
|
||||
f"/api/v1/trees/{original['id']}/export",
|
||||
headers=auth_headers,
|
||||
)
|
||||
rfflow = export_resp.json()
|
||||
@@ -312,53 +280,3 @@ async def test_import_round_trip(client, auth_headers):
|
||||
assert imported_tree["tree_structure"]["id"] == original["tree_structure"]["id"]
|
||||
assert imported_tree["tree_type"] == original["tree_type"]
|
||||
assert imported_tree["status"] == "draft" # Always draft on import
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_import_xml_round_trip(client, auth_headers):
|
||||
"""Export as XML then re-import the parsed structure should work."""
|
||||
original = await create_tree_with_tags(client, auth_headers)
|
||||
|
||||
# Export as XML
|
||||
export_resp = await client.get(
|
||||
f"/api/v1/trees/{original['id']}/export?format=xml",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert export_resp.status_code == 200
|
||||
|
||||
# Parse XML to build import request (simulating frontend parser)
|
||||
root = ET.fromstring(export_resp.text)
|
||||
flow_el = root.find("flow")
|
||||
tags = [t.text for t in flow_el.find("tags").findall("tag")]
|
||||
ts = json.loads(flow_el.find("tree_structure").text)
|
||||
|
||||
cat_el = flow_el.find("category")
|
||||
category = None
|
||||
if cat_el is not None:
|
||||
cat_name = cat_el.find("name")
|
||||
cat_slug = cat_el.find("slug")
|
||||
if cat_name is not None and cat_slug is not None:
|
||||
category = {"name": cat_name.text, "slug": cat_slug.text}
|
||||
|
||||
rfflow = {
|
||||
"rfflow_version": root.get("version"),
|
||||
"exported_at": root.find("exported_at").text,
|
||||
"source_app": root.find("source_app").text,
|
||||
"format": "xml",
|
||||
"flow": {
|
||||
"name": flow_el.find("name").text,
|
||||
"description": flow_el.find("description").text or None,
|
||||
"tree_type": flow_el.find("tree_type").text,
|
||||
"version": int(flow_el.find("version").text),
|
||||
"author_name": flow_el.find("author_name").text or None,
|
||||
"category": category,
|
||||
"tags": tags,
|
||||
"tree_structure": ts,
|
||||
"intake_form": None,
|
||||
},
|
||||
}
|
||||
|
||||
import_resp = await client.post("/api/v1/trees/import", json=rfflow, headers=auth_headers)
|
||||
assert import_resp.status_code == 201
|
||||
result = import_resp.json()
|
||||
assert result["name"] == original["name"]
|
||||
|
||||
Reference in New Issue
Block a user