test_unified_responsibility_api.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. from __future__ import annotations
  2. USER_A = "01900000-0000-7000-8000-000000006101"
  3. POLICY_UID = "01900000-0000-7000-8000-000000006102"
  4. DELEGATION_UID = "01900000-0000-7000-8000-000000006103"
  5. class FakeUnifiedService:
  6. def __init__(self):
  7. self.calls = []
  8. def resolve(self, resource_type, resource_uid, at=None):
  9. self.calls.append(("resolve", resource_type, resource_uid, at))
  10. return {
  11. "resource_type": resource_type,
  12. "resource_uid": resource_uid,
  13. "status": "resolved",
  14. "final_owners": [{"effective_user_id": USER_A}],
  15. "grants_data_access": False,
  16. }
  17. def set_parent(self, payload, expected_revision, actor_uid):
  18. self.calls.append(("set_parent", payload, expected_revision, actor_uid))
  19. return {**payload, "revision": expected_revision + 1}
  20. def create_delegation(self, payload, actor_uid):
  21. self.calls.append(("create_delegation", payload, actor_uid))
  22. return {"uid": DELEGATION_UID, **payload, "current_version": 1}
  23. def list_delegations(self, status=None):
  24. self.calls.append(("list_delegations", status))
  25. return []
  26. def revoke_delegation(self, uid, expected_version, actor_uid):
  27. self.calls.append(("revoke", uid, expected_version, actor_uid))
  28. return {"uid": uid, "status": "revoked", "current_version": 2}
  29. def expire_delegations(self, at=None, actor_uid=None):
  30. self.calls.append(("expire", at, actor_uid))
  31. return []
  32. def transfer_departing_user(self, payload, actor_uid):
  33. self.calls.append(("transfer", payload, actor_uid))
  34. return {"uid": DELEGATION_UID, "delegation_type": "departure_transfer"}
  35. def create_policy(self, payload, actor_uid):
  36. self.calls.append(("create_policy", payload, actor_uid))
  37. return {"uid": POLICY_UID, **payload, "current_version": 1}
  38. def list_policies(self):
  39. self.calls.append(("list_policies",))
  40. return []
  41. def revise_policy(self, uid, definition, expected_version, actor_uid):
  42. self.calls.append(("revise_policy", uid, definition, expected_version, actor_uid))
  43. return {"uid": uid, "current_version": expected_version + 1}
  44. def publish_policy(self, uid, expected_version, actor_uid):
  45. self.calls.append(("publish_policy", uid, expected_version, actor_uid))
  46. return {"uid": uid, "status": "published", "current_version": expected_version}
  47. def evaluate_joint_review(self, resource_type, resource_uid, decisions):
  48. self.calls.append(("evaluate", resource_type, resource_uid, decisions))
  49. return {"status": "approved", "deterministic": True}
  50. def operations(self, owner_uid):
  51. self.calls.append(("operations", owner_uid))
  52. return {"owner_uid": owner_uid, "tasks": [], "metrics": [], "grants_data_access": False}
  53. def _headers(role: str, **extra):
  54. return {"Authorization": f"Bearer {role}", **extra}
  55. def _client(monkeypatch):
  56. from app import create_app
  57. from app.api.system import responsibilities
  58. service = FakeUnifiedService()
  59. monkeypatch.setattr(responsibilities, "_unified_service", lambda: service)
  60. monkeypatch.setattr(
  61. "app.core.system.auth.load_identity_from_token",
  62. lambda token, secret: (
  63. {"id": USER_A, "username": token, "roles": [token]}
  64. if token in {"viewer", "editor", "admin"}
  65. else None
  66. ),
  67. )
  68. app = create_app()
  69. app.config.update(TESTING=True)
  70. return app.test_client(), service
  71. def test_effective_owner_is_readable_without_granting_access(monkeypatch):
  72. client, service = _client(monkeypatch)
  73. response = client.get(
  74. "/api/system/responsibilities/data_asset/asset-1/resolved",
  75. headers=_headers("viewer"),
  76. )
  77. assert response.status_code == 200
  78. assert response.get_json()["data"]["status"] == "resolved"
  79. assert response.get_json()["data"]["grants_data_access"] is False
  80. assert service.calls[-1][:3] == ("resolve", "data_asset", "asset-1")
  81. def test_hierarchy_delegation_and_policy_mutations_are_admin_only_and_versioned(monkeypatch):
  82. client, service = _client(monkeypatch)
  83. hierarchy = {"parent_type": "business_domain", "parent_uid": "domain-1"}
  84. forbidden = client.put(
  85. "/api/system/responsibilities/hierarchy/data_asset/asset-1",
  86. json=hierarchy,
  87. headers=_headers("editor", **{"If-Match": '"0"'}),
  88. )
  89. assert forbidden.status_code == 403
  90. missing = client.put(
  91. "/api/system/responsibilities/hierarchy/data_asset/asset-1",
  92. json=hierarchy,
  93. headers=_headers("admin"),
  94. )
  95. assert missing.status_code == 428
  96. updated = client.put(
  97. "/api/system/responsibilities/hierarchy/data_asset/asset-1",
  98. json=hierarchy,
  99. headers=_headers("admin", **{"If-Match": '"0"'}),
  100. )
  101. assert updated.status_code == 200
  102. assert updated.headers["ETag"] == '"1"'
  103. created = client.post(
  104. "/api/system/responsibilities/delegations",
  105. json={"delegation_type": "temporary"},
  106. headers=_headers("admin"),
  107. )
  108. assert created.status_code == 201
  109. revised = client.post(
  110. f"/api/system/responsibilities/policies/{POLICY_UID}/revisions",
  111. json={"definition": {"require_all_domains": True}},
  112. headers=_headers("admin", **{"If-Match": '"1"'}),
  113. )
  114. assert revised.status_code == 200
  115. assert revised.headers["ETag"] == '"2"'
  116. assert service.calls[-1][3] == 1
  117. def test_read_models_cover_delegations_policies_operations_and_joint_review(monkeypatch):
  118. client, service = _client(monkeypatch)
  119. assert client.get(
  120. "/api/system/responsibilities/delegations?status=active",
  121. headers=_headers("viewer"),
  122. ).status_code == 200
  123. assert client.get(
  124. "/api/system/responsibilities/policies",
  125. headers=_headers("viewer"),
  126. ).status_code == 200
  127. operations = client.get(
  128. f"/api/system/responsibilities/operations?owner_uid={USER_A}",
  129. headers=_headers("viewer"),
  130. )
  131. assert operations.status_code == 200
  132. assert operations.get_json()["data"]["grants_data_access"] is False
  133. evaluated = client.post(
  134. "/api/system/responsibilities/data_product/product-1/joint-review/evaluate",
  135. json={"decisions": []},
  136. headers=_headers("admin"),
  137. )
  138. assert evaluated.status_code == 200
  139. assert evaluated.get_json()["data"]["deterministic"] is True