test_dataflow_create_reconciliation.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. from __future__ import annotations
  2. import os
  3. import pytest
  4. from neo4j import GraphDatabase
  5. from sqlalchemy import create_engine, text
  6. from sqlalchemy.orm import Session
  7. from app.core.common.identifiers import new_governance_uid
  8. from app.core.data_flow.create_reconciliation import DataFlowCreateReconciler
  9. from app.core.data_flow.dataflows import DataFlowService
  10. from app.core.data_rules.repository import DataRuleRepository
  11. def test_real_postgres_neo4j_orphan_is_reconciled_from_stored_intent(
  12. monkeypatch,
  13. ):
  14. pg_url = os.environ.get("DATA_RULE_POSTGRES_ACCEPTANCE_URL")
  15. neo4j_uri = os.environ.get("DATA_RULE_NEO4J_ACCEPTANCE_URI")
  16. password = os.environ.get("DATA_RULE_NEO4J_ACCEPTANCE_PASSWORD")
  17. if not pg_url or not neo4j_uri or not password:
  18. pytest.skip("real PostgreSQL and Neo4j acceptance are not configured")
  19. user = os.environ.get("DATA_RULE_NEO4J_ACCEPTANCE_USER", "neo4j")
  20. monkeypatch.setenv("DATABASE_URL", pg_url)
  21. from app import create_app
  22. app = create_app()
  23. app.config.update(
  24. TESTING=True,
  25. NEO4J_URI=neo4j_uri,
  26. NEO4J_USER=user,
  27. NEO4J_PASSWORD=password,
  28. NEO4J_ENCRYPTED=False,
  29. )
  30. pg = create_engine(pg_url)
  31. graph = GraphDatabase.driver(
  32. neo4j_uri, auth=(user, password), encrypted=False
  33. )
  34. actor = new_governance_uid()
  35. reservation_id = None
  36. dataflow_uid = None
  37. tag_uid = new_governance_uid()
  38. tag_node_id = None
  39. try:
  40. with graph.session() as graph_session:
  41. tag_node_id = graph_session.run(
  42. "CREATE (t:DataLabel {acceptance_uid: $uid}) "
  43. "RETURN id(t) AS node_id",
  44. {"uid": tag_uid},
  45. ).single()["node_id"]
  46. with app.app_context(), Session(pg) as session:
  47. session.execute(
  48. text(
  49. "INSERT INTO public.users "
  50. "(id, username, display_name, password_hash, status) "
  51. "VALUES (CAST(:id AS uuid), :username, "
  52. "'Reconcile Acceptance', 'not-a-login-hash', 'active')"
  53. ),
  54. {"id": actor, "username": f"reconcile-{actor}"},
  55. )
  56. session.commit()
  57. repository = DataRuleRepository(session)
  58. receipt = repository.reserve_dataflow_draft(actor_uid=actor)
  59. reservation_id = receipt["reservation_id"]
  60. dataflow_uid = receipt["dataflow_uid"]
  61. session.commit()
  62. node = {
  63. "uid": dataflow_uid,
  64. "name_zh": f"孤儿生产线-{dataflow_uid}",
  65. "name_en": f"orphan-{dataflow_uid}",
  66. "category": "应用类",
  67. "organization": "acceptance",
  68. "leader": "system",
  69. "frequency": "月",
  70. "describe": "reconciliation acceptance",
  71. "status": "active",
  72. "update_mode": "append",
  73. "script_type": "governed",
  74. "script_requirement": "{}",
  75. "script_path": "",
  76. }
  77. intent = {
  78. "dataflow_uid": dataflow_uid,
  79. "node": node,
  80. "tags": [{"id": tag_node_id}],
  81. }
  82. create_request = {
  83. "payload": {"name_zh": node["name_zh"]},
  84. "intent": intent,
  85. }
  86. closed = {
  87. key: receipt[key]
  88. for key in ("reservation_id", "dataflow_uid", "nonce")
  89. }
  90. claim = repository.begin_dataflow_create(
  91. closed,
  92. actor_uid=actor,
  93. create_request=create_request,
  94. create_intent=intent,
  95. lease_seconds=10,
  96. )
  97. session.commit()
  98. DataFlowService._merge_governed_dataflow(node)
  99. session.execute(
  100. text(
  101. "UPDATE public.dataflow_draft_reservations "
  102. "SET lease_expires_at = CURRENT_TIMESTAMP - INTERVAL '1 second' "
  103. "WHERE id = CAST(:id AS uuid)"
  104. ),
  105. {"id": reservation_id},
  106. )
  107. session.commit()
  108. monkeypatch.setattr(
  109. DataFlowService,
  110. "validate_governed_create_intent",
  111. lambda value, *, repository: value,
  112. )
  113. reconciler = DataFlowCreateReconciler(repository)
  114. before_preview = session.execute(
  115. text(
  116. "SELECT state, attempt_count "
  117. "FROM public.dataflow_draft_reservations "
  118. "WHERE id = CAST(:id AS uuid)"
  119. ),
  120. {"id": reservation_id},
  121. ).one()
  122. dry_run = reconciler.run(dry_run=True)
  123. assert dry_run["candidate_count"] == 1
  124. after_preview = session.execute(
  125. text(
  126. "SELECT state, attempt_count "
  127. "FROM public.dataflow_draft_reservations "
  128. "WHERE id = CAST(:id AS uuid)"
  129. ),
  130. {"id": reservation_id},
  131. ).one()
  132. assert after_preview == before_preview
  133. report = reconciler.run(dry_run=False)
  134. assert report["reconciled_count"] == 1
  135. assert report["items"][0]["attempt"] == claim["attempt"] + 1
  136. assert reconciler.run(dry_run=False)["candidate_count"] == 0
  137. state = session.execute(
  138. text(
  139. "SELECT state, request_digest, result_digest "
  140. "FROM public.dataflow_draft_reservations "
  141. "WHERE id = CAST(:id AS uuid)"
  142. ),
  143. {"id": reservation_id},
  144. ).mappings().one()
  145. assert state["state"] == "completed"
  146. assert state["request_digest"]
  147. assert state["result_digest"]
  148. DataFlowService._handle_tag_relationships(
  149. report["items"][0]["dataflow_node_id"],
  150. [{"id": tag_node_id}],
  151. strict=True,
  152. )
  153. with graph.session() as graph_session:
  154. record = graph_session.run(
  155. "MATCH (n:DataFlow {uid: $uid}) "
  156. "OPTIONAL MATCH (n)-[r:LABEL]->"
  157. "(:DataLabel {acceptance_uid: $tag_uid}) "
  158. "RETURN count(DISTINCT n) AS node_count, "
  159. "count(r) AS relationship_count",
  160. {"uid": dataflow_uid, "tag_uid": tag_uid},
  161. ).single()
  162. assert record["node_count"] == 1
  163. assert record["relationship_count"] == 1
  164. finally:
  165. if dataflow_uid or tag_uid:
  166. with graph.session() as graph_session:
  167. graph_session.run(
  168. "MATCH (n) WHERE "
  169. "(n:DataFlow AND n.uid = $uid) OR "
  170. "(n:DataLabel AND n.acceptance_uid = $tag_uid) "
  171. "DETACH DELETE n",
  172. {"uid": dataflow_uid, "tag_uid": tag_uid},
  173. )
  174. graph.close()
  175. with Session(pg) as session:
  176. if reservation_id:
  177. session.execute(
  178. text(
  179. "DELETE FROM public.dataflow_draft_reservations "
  180. "WHERE id = CAST(:id AS uuid)"
  181. ),
  182. {"id": reservation_id},
  183. )
  184. session.execute(
  185. text(
  186. "DELETE FROM public.users WHERE id = CAST(:id AS uuid)"
  187. ),
  188. {"id": actor},
  189. )
  190. session.commit()
  191. pg.dispose()