test_ontology_api.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. from __future__ import annotations
  2. from dataclasses import replace
  3. import pytest
  4. from app.core.data_research.ontology.models import DomainLink, GraphDocument, Ontology, OntologyVersion
  5. from app.core.data_research.ontology.validation import ValidationIssue
  6. class FakeOntologyService:
  7. def __init__(self):
  8. self.ontology = Ontology(
  9. uid="ontology-1",
  10. code="CUSTOMER",
  11. name="客户本体",
  12. owner_uid="owner-1",
  13. domain_links=(DomainLink("domain-1", "owner"), DomainLink("domain-2", "contributor")),
  14. )
  15. self.version = OntologyVersion(
  16. uid="version-1",
  17. ontology_uid="ontology-1",
  18. version=1,
  19. graph_document=GraphDocument.from_dict({"domain_links": [{"domain_uid": "domain-1", "role": "owner"}]}),
  20. content_hash="a" * 64,
  21. )
  22. self.calls = []
  23. def list(self):
  24. return [self.ontology]
  25. def get(self, uid):
  26. assert uid == self.ontology.uid
  27. return self.ontology
  28. def list_versions(self, uid):
  29. assert uid == self.ontology.uid
  30. return [self.version]
  31. def latest_version(self, uid):
  32. assert uid == self.ontology.uid
  33. return self.version
  34. def create(self, payload, actor_uid):
  35. self.calls.append(("create", payload, actor_uid))
  36. return self.ontology
  37. def save_draft(self, uid, graph, expected_revision, actor_uid):
  38. self.calls.append(("draft", uid, expected_revision, actor_uid))
  39. return self.version
  40. def validate(self, uid):
  41. self.calls.append(("validate", uid))
  42. return [ValidationIssue("ONTOLOGY_OWNER_REQUIRED", "owner required", "domain_links")]
  43. def publish(self, uid, idempotency_key, actor_uid):
  44. self.calls.append(("publish", uid, idempotency_key, actor_uid))
  45. return replace(self.version, status="published")
  46. def diff(self, uid, left, right):
  47. return {"classes": {"added": [], "removed": [], "changed": []}}
  48. def rollback(self, uid, target_version_uid, expected_revision, actor_uid):
  49. self.calls.append(("rollback", uid, target_version_uid, expected_revision, actor_uid))
  50. return replace(self.version, uid="version-2", version=2, parent_version_uid="version-1")
  51. @pytest.fixture()
  52. def client(monkeypatch):
  53. from flask import request
  54. from app import create_app
  55. from app.api.data_development import routes
  56. from app.core.system import permissions
  57. service = FakeOntologyService()
  58. def identity():
  59. token = request.headers.get("Authorization", "")
  60. role = token.removeprefix("Bearer ")
  61. return {"id": f"{role}-1", "roles": [role]} if role in {"viewer", "editor", "admin"} else None
  62. monkeypatch.setattr(permissions, "authenticate_request", identity)
  63. monkeypatch.setattr(routes, "get_ontology_service", lambda: service)
  64. app = create_app()
  65. app.config.update(TESTING=True)
  66. return app.test_client(), service
  67. def test_viewer_is_read_only_and_multi_domain_roles_are_returned(client):
  68. http, _service = client
  69. headers = {"Authorization": "Bearer viewer"}
  70. listed = http.get("/api/development/v1/ontologies", headers=headers)
  71. denied = http.post(
  72. "/api/development/v1/ontologies",
  73. headers=headers,
  74. json={"code": "CUSTOMER", "name": "客户本体"},
  75. )
  76. assert listed.status_code == 200
  77. assert listed.get_json()["data"][0]["domain_links"][1]["role"] == "contributor"
  78. assert denied.status_code == 403
  79. def test_viewer_loads_ontology_detail_versions_and_latest_graph(client):
  80. http, _service = client
  81. headers = {"Authorization": "Bearer viewer"}
  82. detail = http.get(
  83. "/api/development/v1/ontologies/ontology-1", headers=headers
  84. )
  85. versions = http.get(
  86. "/api/development/v1/ontologies/ontology-1/versions", headers=headers
  87. )
  88. graph = http.get(
  89. "/api/development/v1/ontologies/ontology-1/graph", headers=headers
  90. )
  91. assert detail.status_code == 200
  92. assert detail.get_json()["data"]["code"] == "CUSTOMER"
  93. assert versions.status_code == 200
  94. assert versions.get_json()["data"][0]["uid"] == "version-1"
  95. assert graph.status_code == 200
  96. assert graph.headers["ETag"] == '"0"'
  97. assert graph.get_json()["data"]["graph_document"]["domain_links"][0] == {
  98. "domain_uid": "domain-1",
  99. "role": "owner",
  100. }
  101. def test_editor_saves_etag_draft_but_cannot_publish(client):
  102. http, service = client
  103. headers = {"Authorization": "Bearer editor", "If-Match": '"0"'}
  104. saved = http.patch(
  105. "/api/development/v1/ontologies/ontology-1/graph",
  106. headers=headers,
  107. json={"classes": [], "domain_links": []},
  108. )
  109. denied = http.post(
  110. "/api/development/v1/ontologies/ontology-1/publish",
  111. headers={"Authorization": "Bearer editor", "Idempotency-Key": "publish-1"},
  112. )
  113. assert saved.status_code == 200
  114. assert service.calls[-1][2] == 0
  115. assert denied.status_code == 403
  116. def test_admin_validates_publishes_diffs_and_rolls_back(client):
  117. http, service = client
  118. headers = {"Authorization": "Bearer admin"}
  119. validation = http.post("/api/development/v1/ontologies/ontology-1/validate", headers=headers)
  120. published = http.post(
  121. "/api/development/v1/ontologies/ontology-1/publish",
  122. headers={**headers, "Idempotency-Key": "publish-1"},
  123. )
  124. diffed = http.get(
  125. "/api/development/v1/ontologies/ontology-1/diff?left=version-0&right=version-1",
  126. headers=headers,
  127. )
  128. rolled_back = http.post(
  129. "/api/development/v1/ontologies/ontology-1/rollback",
  130. headers=headers,
  131. json={"target_version_uid": "version-1", "expected_revision": 1},
  132. )
  133. assert validation.status_code == 200
  134. assert validation.get_json()["data"][0]["code"] == "ONTOLOGY_OWNER_REQUIRED"
  135. assert published.get_json()["data"]["status"] == "published"
  136. assert diffed.status_code == 200
  137. assert rolled_back.get_json()["data"]["parent_version_uid"] == "version-1"