test_semantic_governance_api.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. from __future__ import annotations
  2. import uuid
  3. import pytest
  4. class FakeSemanticService:
  5. def __init__(self):
  6. self.calls = []
  7. self.asset = {
  8. "uid": str(uuid.uuid4()),
  9. "asset_kind": "business_term",
  10. "code": "CUSTOMER_LEVEL",
  11. "name": "客户等级",
  12. "owner_uid": str(uuid.uuid4()),
  13. "business_domain_uid": str(uuid.uuid4()),
  14. "status": "draft",
  15. "current_version": 1,
  16. "definition": {"definition": "客户运营分层等级", "aliases": []},
  17. }
  18. def list(self, **filters):
  19. self.calls.append(("list", filters))
  20. return [self.asset]
  21. def create_draft(self, kind, payload, *, actor_uid):
  22. self.calls.append(("create", kind, payload, actor_uid))
  23. return self.asset
  24. def submit(self, uid, *, expected_version, actor_uid):
  25. self.calls.append(("submit", uid, expected_version, actor_uid))
  26. return {**self.asset, "status": "in_review"}
  27. def review(self, uid, **payload):
  28. self.calls.append(("review", uid, payload))
  29. return {**self.asset, "status": "approved"}
  30. def publish(self, uid, *, expected_version, actor_uid):
  31. self.calls.append(("publish", uid, expected_version, actor_uid))
  32. return {**self.asset, "status": "published"}
  33. def rollback(self, uid, **payload):
  34. self.calls.append(("rollback", uid, payload))
  35. return {
  36. **self.asset,
  37. "status": "published",
  38. "current_version": 2,
  39. }
  40. def list_field_mappings(self, **filters):
  41. self.calls.append(("mappings", filters))
  42. return []
  43. def map_physical_field(self, payload, *, actor_uid):
  44. self.calls.append(("map", payload, actor_uid))
  45. return {"uid": str(uuid.uuid4()), **payload, "status": "published"}
  46. @pytest.fixture()
  47. def client(monkeypatch):
  48. from flask import request
  49. from app import create_app
  50. from app.api.data_development import semantic_routes
  51. from app.core.system import permissions
  52. service = FakeSemanticService()
  53. def identity():
  54. role = request.headers.get("Authorization", "").removeprefix("Bearer ")
  55. if role not in {"viewer", "editor", "admin"}:
  56. return None
  57. return {"id": f"00000000-0000-7000-8000-0000000000{role[0]}", "roles": [role]}
  58. monkeypatch.setattr(permissions, "authenticate_request", identity)
  59. monkeypatch.setattr(
  60. semantic_routes,
  61. "get_semantic_governance_service",
  62. lambda: service,
  63. )
  64. app = create_app()
  65. app.config.update(TESTING=True)
  66. return app.test_client(), service
  67. def test_viewer_lists_but_cannot_create(client):
  68. http, service = client
  69. listed = http.get(
  70. "/api/development/v1/semantic-assets?asset_kind=business_term",
  71. headers={"Authorization": "Bearer viewer"},
  72. )
  73. denied = http.post(
  74. "/api/development/v1/semantic-assets",
  75. headers={"Authorization": "Bearer viewer"},
  76. json={"asset_kind": "business_term"},
  77. )
  78. assert listed.status_code == 200
  79. assert listed.get_json()["data"][0]["code"] == "CUSTOMER_LEVEL"
  80. assert service.calls[0] == (
  81. "list",
  82. {"asset_kind": "business_term", "status": None},
  83. )
  84. assert denied.status_code == 403
  85. def test_editor_creates_submits_and_maps_but_cannot_approve(client):
  86. http, service = client
  87. headers = {"Authorization": "Bearer editor"}
  88. created = http.post(
  89. "/api/development/v1/semantic-assets",
  90. headers=headers,
  91. json={"asset_kind": "business_term", "code": "CUSTOMER_LEVEL"},
  92. )
  93. submitted = http.post(
  94. f"/api/development/v1/semantic-assets/{service.asset['uid']}/submit",
  95. headers=headers,
  96. json={"expected_version": 1},
  97. )
  98. mapped = http.post(
  99. "/api/development/v1/semantic-field-mappings",
  100. headers=headers,
  101. json={
  102. "asset_uid": str(uuid.uuid4()),
  103. "field_name": "customer_level",
  104. "data_element_uid": str(uuid.uuid4()),
  105. "owner_uid": str(uuid.uuid4()),
  106. },
  107. )
  108. denied = http.post(
  109. f"/api/development/v1/semantic-assets/{service.asset['uid']}/review",
  110. headers=headers,
  111. json={
  112. "expected_version": 1,
  113. "decision": "approve",
  114. "reason": "approved",
  115. },
  116. )
  117. assert created.status_code == 201
  118. assert submitted.get_json()["data"]["status"] == "in_review"
  119. assert mapped.status_code == 201
  120. assert denied.status_code == 403
  121. def test_admin_approves_publishes_and_rolls_back(client):
  122. http, service = client
  123. headers = {"Authorization": "Bearer admin"}
  124. reviewed = http.post(
  125. f"/api/development/v1/semantic-assets/{service.asset['uid']}/review",
  126. headers=headers,
  127. json={
  128. "expected_version": 1,
  129. "decision": "approve",
  130. "reason": "企业术语审核通过",
  131. },
  132. )
  133. published = http.post(
  134. f"/api/development/v1/semantic-assets/{service.asset['uid']}/publish",
  135. headers=headers,
  136. json={"expected_version": 1},
  137. )
  138. rolled_back = http.post(
  139. f"/api/development/v1/semantic-assets/{service.asset['uid']}/rollback",
  140. headers=headers,
  141. json={
  142. "expected_version": 1,
  143. "target_version": 1,
  144. "reason": "恢复验收版本",
  145. },
  146. )
  147. assert reviewed.get_json()["data"]["status"] == "approved"
  148. assert published.get_json()["data"]["status"] == "published"
  149. assert rolled_back.get_json()["data"]["current_version"] == 2