from __future__ import annotations import asyncio import unittest from fastapi import HTTPException, Request from app.main import ( ReactFlowEdgePayload, ReactFlowNodePayload, ReactFlowProjectPayload, build_reactflow_system_xml, compile_system_xml_model, compile_reactflow_network, parse_system_xml, ) from app.system_xml import validate_system_xml_document from tests.test_system_xml_protocol import ( physical_connection_project, physical_port, ) def valid_xml() -> bytes: return build_reactflow_system_xml(physical_connection_project()) def issue_codes(xml: bytes | str) -> set[str]: return { issue.code for issue in validate_system_xml_document(xml).issues } def xml_request(body: bytes) -> Request: delivered = False async def receive(): nonlocal delivered if delivered: return {"type": "http.disconnect"} delivered = True return {"type": "http.request", "body": body, "more_body": False} return Request( { "type": "http", "method": "POST", "path": "/api/system-xml/compile-model", "headers": [(b"content-type", b"application/xml")], }, receive, ) class SystemXmlParserTests(unittest.TestCase): def test_valid_v2_xml_round_trips_and_compiles(self) -> None: report = validate_system_xml_document(valid_xml()) self.assertTrue(report.valid) self.assertEqual(report.issues, ()) assert report.document is not None project = ReactFlowProjectPayload(**report.document.as_project_data()) network = compile_reactflow_network(project) self.assertEqual(project.name, "protocol-test") self.assertEqual(project.simulation.method, "BDF") self.assertEqual(set(network.components), {"cylinder_1", "tank_1"}) self.assertEqual(len(network.connections), 1) self.assertEqual(network.connections[0].id, "edge-1") def test_compile_api_consumes_raw_xml_body(self) -> None: response = asyncio.run(compile_system_xml_model(xml_request(valid_xml()))) self.assertTrue(response["success"]) self.assertEqual(response["name"], "protocol-test") self.assertEqual(len(response["components"]), 2) self.assertEqual(len(response["connections"]), 1) self.assertTrue(response["validation"]["valid"]) def test_parse_api_returns_structured_422_diagnostics(self) -> None: with self.assertRaises(HTTPException) as caught: asyncio.run(parse_system_xml(xml_request(b""))) self.assertEqual(caught.exception.status_code, 422) detail = caught.exception.detail self.assertEqual(detail["message"], "System XML validation failed.") self.assertEqual(detail["issues"][0]["layer"], "xml") self.assertEqual(detail["issues"][0]["code"], "XML_SYNTAX_ERROR") def test_malformed_xml_reports_xml_layer_error(self) -> None: report = validate_system_xml_document("") self.assertFalse(report.valid) self.assertIsNone(report.document) self.assertEqual(report.issues[0].layer, "xml") self.assertEqual(report.issues[0].code, "XML_SYNTAX_ERROR") self.assertIsNotNone(report.issues[0].line) def test_dtd_is_rejected_before_schema_validation(self) -> None: xml = valid_xml().replace( b"]>\n None: xml = valid_xml().replace(b'schemaVersion="2"', b'schemaVersion="1"') report = validate_system_xml_document(xml) self.assertFalse(report.valid) self.assertIsNone(report.document) self.assertTrue(report.issues) self.assertTrue(all(issue.layer == "schema" for issue in report.issues)) self.assertIn("XSD_VALIDATION_ERROR", issue_codes(xml)) def test_parameter_range_is_checked_semantically(self) -> None: xml = valid_xml().replace( b' None: xml = valid_xml().replace( b' None: xml = valid_xml().replace( b'nominalRole="outlet"', b'nominalRole="bidirectional"', 1, ) self.assertIn("PORT_NOMINAL_ROLE_MISMATCH", issue_codes(xml)) def test_unknown_connection_endpoint_is_reported(self) -> None: xml = valid_xml().replace( b'component="tank_1" port="port_a"', b'component="missing_tank" port="port_a"', 1, ) self.assertIn("ENDPOINT_COMPONENT_UNKNOWN", issue_codes(xml)) def test_physical_port_cannot_be_used_by_two_connections(self) -> None: project = physical_connection_project() project.nodes.append( ReactFlowNodePayload(**{ "id": "tank_2", "type": "simulationComponent", "position": {"x": 420, "y": 180}, "data": { "label": "receiver-tank-2", "componentType": "tank", "modelType": "tank", "ports": [physical_port("port_a", "inlet", "left")], "parameters": {"volume": 0.1, "p0": 100000, "T0": 300}, }, }) ) project.edges.append( ReactFlowEdgePayload(**{ "id": "edge-2", "source": "cylinder_1", "sourceHandle": "port_b", "target": "tank_2", "targetHandle": "port_a", }) ) xml = build_reactflow_system_xml(project) self.assertIn("PHYSICAL_PORT_ALREADY_CONNECTED", issue_codes(xml)) def test_unconnected_registered_port_is_a_warning(self) -> None: project = physical_connection_project() project.edges.clear() report = validate_system_xml_document(build_reactflow_system_xml(project)) self.assertTrue(report.valid) self.assertEqual(report.as_dict()["warningCount"], 2) self.assertEqual( {issue.code for issue in report.issues}, {"PORT_UNCONNECTED"}, ) def test_missing_required_parameter_is_reported(self) -> None: xml = valid_xml().replace( b' \n', b"", 1, ) self.assertIn("PARAMETER_REQUIRED_MISSING", issue_codes(xml)) def test_unsupported_solver_method_is_reported(self) -> None: xml = valid_xml().replace(b'method="BDF"', b'method="unknown"') self.assertIn("SIMULATION_METHOD_UNSUPPORTED", issue_codes(xml)) def test_non_positive_simulation_steps_are_rejected_by_schema(self) -> None: xml = valid_xml().replace(b'step="0.2"', b'step="0"') xml = xml.replace(b'maxStep="0.01"', b'maxStep="-1"') report = validate_system_xml_document(xml) self.assertFalse(report.valid) self.assertEqual( [issue.code for issue in report.issues].count("XSD_VALIDATION_ERROR"), 2, ) if __name__ == "__main__": unittest.main()