test_data_research_docker_api.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. """Opt-in end-to-end acceptance for the containerized data-research API."""
  2. from __future__ import annotations
  3. import os
  4. import uuid
  5. import pytest
  6. import requests
  7. from minio import Minio
  8. from sqlalchemy import create_engine, text
  9. pytestmark = pytest.mark.skipif(
  10. os.getenv("RUN_DATA_RESEARCH_DOCKER_API") != "1",
  11. reason="set RUN_DATA_RESEARCH_DOCKER_API=1 for Docker API acceptance",
  12. )
  13. def _data(response, expected_status):
  14. assert response.status_code == expected_status, response.text
  15. payload = response.json()
  16. assert payload["code"] == 200, payload
  17. return payload["data"]
  18. def test_file_ingestion_and_dynamic_ontology_publish_through_live_api():
  19. base_url = os.getenv("TEST_BACKEND_URL", "http://127.0.0.1:15500/api")
  20. database_url = os.environ["TEST_DATABASE_URL"]
  21. engine = create_engine(database_url, pool_pre_ping=True)
  22. minio = Minio(
  23. os.getenv("TEST_MINIO_HOST", "127.0.0.1:19000"),
  24. access_key="dataops-test",
  25. secret_key="dataops-test-password",
  26. secure=False,
  27. )
  28. source_uid = str(uuid.uuid4())
  29. owner_domain_uid = str(uuid.uuid4())
  30. contributor_domain_uid = str(uuid.uuid4())
  31. ontology_uid = None
  32. imported_ontology_uid = None
  33. artifact_uid = None
  34. artifact_object_key = None
  35. login = requests.post(
  36. f"{base_url}/system/auth/login",
  37. json={"username": "admin", "password": os.environ["TEST_ADMIN_PASSWORD"]},
  38. timeout=20,
  39. )
  40. token = _data(login, 200)["token"]
  41. headers = {"Authorization": f"Bearer {token}"}
  42. try:
  43. with engine.begin() as connection:
  44. connection.execute(
  45. text(
  46. "INSERT INTO public.ingestion_sources "
  47. "(uid, source_type, name, created_by) "
  48. "VALUES (CAST(:uid AS uuid), 'file', :name, 'docker-api-test')"
  49. ),
  50. {"uid": source_uid, "name": f"docker-api-{source_uid}"},
  51. )
  52. csv_content = b"code,name\nCUSTOMER_ID,Customer identifier\n"
  53. uploaded = requests.post(
  54. f"{base_url}/development/v1/sources/files",
  55. headers=headers,
  56. data={"source_uid": source_uid, "parser_version": "csv-v1"},
  57. files={"file": ("elements.csv", csv_content, "text/csv")},
  58. timeout=20,
  59. )
  60. artifact = _data(uploaded, 201)
  61. artifact_uid = artifact["uid"]
  62. duplicate = requests.post(
  63. f"{base_url}/development/v1/sources/files",
  64. headers=headers,
  65. data={"source_uid": source_uid, "parser_version": "csv-v1"},
  66. files={"file": ("elements.csv", csv_content, "text/csv")},
  67. timeout=20,
  68. )
  69. assert _data(duplicate, 200)["uid"] == artifact_uid
  70. job_payload = {
  71. "source_uid": source_uid,
  72. "artifact_uid": artifact_uid,
  73. "job_type": "file_extract",
  74. "parser_version": "csv-v1",
  75. "parameters": {"delimiter": ","},
  76. }
  77. created_job = _data(
  78. requests.post(
  79. f"{base_url}/development/v1/ingestion-jobs",
  80. headers=headers,
  81. json=job_payload,
  82. timeout=20,
  83. ),
  84. 201,
  85. )
  86. duplicate_job = _data(
  87. requests.post(
  88. f"{base_url}/development/v1/ingestion-jobs",
  89. headers=headers,
  90. json=job_payload,
  91. timeout=20,
  92. ),
  93. 200,
  94. )
  95. assert duplicate_job["uid"] == created_job["uid"]
  96. cancelled = _data(
  97. requests.post(
  98. f"{base_url}/development/v1/ingestion-jobs/"
  99. f"{created_job['uid']}/cancel",
  100. headers=headers,
  101. timeout=20,
  102. ),
  103. 200,
  104. )
  105. assert cancelled["status"] == "cancelled"
  106. ontology = _data(
  107. requests.post(
  108. f"{base_url}/development/v1/ontologies",
  109. headers=headers,
  110. json={
  111. "code": f"DOCKER_{uuid.uuid4().hex[:12].upper()}",
  112. "name": "Docker API customer ontology",
  113. "domain_links": [
  114. {"domain_uid": owner_domain_uid, "role": "owner"},
  115. {
  116. "domain_uid": contributor_domain_uid,
  117. "role": "contributor",
  118. },
  119. ],
  120. },
  121. timeout=20,
  122. ),
  123. 201,
  124. )
  125. ontology_uid = ontology["uid"]
  126. graph = {
  127. "classes": [{"uid": "customer", "name": "Customer"}],
  128. "properties": [
  129. {
  130. "uid": "customer-id",
  131. "name": "customerId",
  132. "class_uid": "customer",
  133. "required": True,
  134. "cardinality": "1",
  135. "data_element_uid": "customer-id-element",
  136. }
  137. ],
  138. "relations": [],
  139. "constraints": [],
  140. "domain_links": [
  141. {"domain_uid": owner_domain_uid, "role": "owner"},
  142. {
  143. "domain_uid": contributor_domain_uid,
  144. "role": "contributor",
  145. },
  146. ],
  147. "element_mappings": [
  148. {
  149. "property_uid": "customer-id",
  150. "data_element_uid": "customer-id-element",
  151. }
  152. ],
  153. }
  154. draft = _data(
  155. requests.patch(
  156. f"{base_url}/development/v1/ontologies/{ontology_uid}/graph",
  157. headers={**headers, "If-Match": '"0"'},
  158. json=graph,
  159. timeout=20,
  160. ),
  161. 200,
  162. )
  163. assert draft["version"] == 1
  164. detail = _data(
  165. requests.get(
  166. f"{base_url}/development/v1/ontologies/{ontology_uid}",
  167. headers=headers,
  168. timeout=20,
  169. ),
  170. 200,
  171. )
  172. assert detail["uid"] == ontology_uid
  173. assert detail["draft_revision"] == 1
  174. graph_response = requests.get(
  175. f"{base_url}/development/v1/ontologies/{ontology_uid}/graph",
  176. headers=headers,
  177. timeout=20,
  178. )
  179. graph_snapshot = _data(graph_response, 200)
  180. assert graph_response.headers["ETag"] == '"1"'
  181. assert graph_snapshot["graph_document"]["classes"][0]["uid"] == "customer"
  182. versions = _data(
  183. requests.get(
  184. f"{base_url}/development/v1/ontologies/{ontology_uid}/versions",
  185. headers=headers,
  186. timeout=20,
  187. ),
  188. 200,
  189. )
  190. assert versions[0]["version"] == 1
  191. assert _data(
  192. requests.post(
  193. f"{base_url}/development/v1/ontologies/{ontology_uid}/validate",
  194. headers=headers,
  195. timeout=20,
  196. ),
  197. 200,
  198. ) == []
  199. published = _data(
  200. requests.post(
  201. f"{base_url}/development/v1/ontologies/{ontology_uid}/publish",
  202. headers={**headers, "Idempotency-Key": f"docker-{ontology_uid}"},
  203. timeout=20,
  204. ),
  205. 200,
  206. )
  207. assert published["status"] == "published"
  208. exported = requests.get(
  209. f"{base_url}/development/v1/ontologies/{ontology_uid}/export?format=json",
  210. headers=headers,
  211. timeout=20,
  212. )
  213. assert exported.status_code == 200
  214. assert exported.json()["graph_document"]["classes"][0]["uid"] == "customer"
  215. imported = _data(
  216. requests.post(
  217. f"{base_url}/development/v1/ontologies/import?format=json",
  218. headers=headers,
  219. files={
  220. "file": (
  221. "ontology.json",
  222. exported.content,
  223. "application/json",
  224. )
  225. },
  226. timeout=20,
  227. ),
  228. 201,
  229. )
  230. imported_ontology_uid = imported["ontology_uid"]
  231. imported_graph = _data(
  232. requests.get(
  233. f"{base_url}/development/v1/ontologies/"
  234. f"{imported_ontology_uid}/graph",
  235. headers=headers,
  236. timeout=20,
  237. ),
  238. 200,
  239. )
  240. assert imported_graph["graph_document"] == graph
  241. with engine.connect() as connection:
  242. artifact_object_key = connection.execute(
  243. text(
  244. "SELECT storage_ref FROM public.source_artifacts "
  245. "WHERE uid = CAST(:uid AS uuid)"
  246. ),
  247. {"uid": artifact_uid},
  248. ).scalar_one().removeprefix("minio://dataops-bucket/")
  249. outbox_payload = connection.execute(
  250. text(
  251. "SELECT payload FROM public.outbox_events "
  252. "WHERE aggregate_id = :uid "
  253. "AND event_type = 'ontology.version_published'"
  254. ),
  255. {"uid": ontology_uid},
  256. ).scalar_one()
  257. assert outbox_payload["ontology_uid"] == ontology_uid
  258. finally:
  259. with engine.begin() as connection:
  260. for cleanup_ontology_uid in (
  261. imported_ontology_uid,
  262. ontology_uid,
  263. ):
  264. if not cleanup_ontology_uid:
  265. continue
  266. connection.execute(
  267. text(
  268. "DELETE FROM public.outbox_consumptions WHERE event_id IN "
  269. "(SELECT event_id FROM public.outbox_events "
  270. "WHERE aggregate_id = :uid)"
  271. ),
  272. {"uid": cleanup_ontology_uid},
  273. )
  274. connection.execute(
  275. text("DELETE FROM public.outbox_events WHERE aggregate_id = :uid"),
  276. {"uid": cleanup_ontology_uid},
  277. )
  278. connection.execute(
  279. text(
  280. "UPDATE public.ontologies SET active_version_uid = NULL "
  281. "WHERE uid = CAST(:uid AS uuid)"
  282. ),
  283. {"uid": cleanup_ontology_uid},
  284. )
  285. for table in (
  286. "ontology_publish_runs",
  287. "ontology_change_sets",
  288. "ontology_domain_links",
  289. "ontology_versions",
  290. "ontologies",
  291. ):
  292. connection.execute(
  293. text(
  294. f"DELETE FROM public.{table} "
  295. "WHERE ontology_uid = CAST(:uid AS uuid)"
  296. if table != "ontologies"
  297. else "DELETE FROM public.ontologies "
  298. "WHERE uid = CAST(:uid AS uuid)"
  299. ),
  300. {"uid": cleanup_ontology_uid},
  301. )
  302. connection.execute(
  303. text(
  304. "DELETE FROM public.ingestion_jobs "
  305. "WHERE source_uid = CAST(:uid AS uuid)"
  306. ),
  307. {"uid": source_uid},
  308. )
  309. connection.execute(
  310. text(
  311. "DELETE FROM public.source_artifacts "
  312. "WHERE source_uid = CAST(:uid AS uuid)"
  313. ),
  314. {"uid": source_uid},
  315. )
  316. connection.execute(
  317. text(
  318. "DELETE FROM public.ingestion_sources "
  319. "WHERE uid = CAST(:uid AS uuid)"
  320. ),
  321. {"uid": source_uid},
  322. )
  323. if artifact_object_key:
  324. minio.remove_object("dataops-bucket", artifact_object_key)
  325. engine.dispose()