test_active_metadata_api.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. from __future__ import annotations
  2. ACTOR_UID = "01900000-0000-7000-8000-000000000101"
  3. PLAN_UID = "01900000-0000-7000-8000-000000000201"
  4. ASSET_UID = "01900000-0000-7000-8000-000000000301"
  5. CORRECTION_UID = "01900000-0000-7000-8000-000000000401"
  6. class FakeService:
  7. def __init__(self):
  8. self.calls = []
  9. def list_plans(self):
  10. return [{"uid": PLAN_UID, "name": "主动发现"}]
  11. def create_plan(self, payload, *, actor_uid):
  12. self.calls.append(("create_plan", payload, actor_uid))
  13. return {"uid": PLAN_UID, **payload}
  14. def execute(self, plan_uid, payload, *, batch_key, actor_uid):
  15. self.calls.append(("execute", plan_uid, batch_key, actor_uid))
  16. return {"uid": "run-1", "status": "completed"}
  17. def list_assets(self, source_uid):
  18. return [{"uid": ASSET_UID, "source_uid": source_uid}]
  19. def list_runs(self, plan_uid):
  20. return [{"uid": "run-1", "plan_uid": plan_uid}]
  21. def list_changes(self, run_uid):
  22. return [{"run_uid": run_uid, "change_type": "field_changed"}]
  23. def list_lineage(self, run_uid):
  24. return [{"run_uid": run_uid, "parse_status": "resolved"}]
  25. def list_health_signals(self, run_uid):
  26. return [{"run_uid": run_uid, "signal_type": "quality"}]
  27. def list_corrections(self, asset_uid=None):
  28. return [{"uid": CORRECTION_UID, "asset_uid": asset_uid}]
  29. def submit_correction(self, asset_uid, payload, *, actor_uid):
  30. return {"uid": CORRECTION_UID, "asset_uid": asset_uid, "status": "pending"}
  31. def resolve_correction(self, correction_uid, **kwargs):
  32. self.calls.append(("resolve", correction_uid, kwargs))
  33. return {"uid": correction_uid, "status": "resolved", "current_version": 2}
  34. def _headers(role, **extra):
  35. return {"Authorization": f"Bearer {role}", **extra}
  36. def test_active_metadata_read_operate_and_admin_permissions(monkeypatch):
  37. from app import create_app
  38. from app.api.meta_data import active_metadata
  39. service = FakeService()
  40. monkeypatch.setattr(active_metadata, "_service", lambda: service)
  41. monkeypatch.setattr(
  42. "app.core.system.auth.load_identity_from_token",
  43. lambda token, secret: (
  44. {"id": ACTOR_UID, "username": token, "roles": [token]}
  45. if token in {"viewer", "editor", "admin"}
  46. else None
  47. ),
  48. )
  49. app = create_app()
  50. app.config.update(TESTING=True)
  51. client = app.test_client()
  52. assert client.get(
  53. "/api/meta/active-metadata/plans", headers=_headers("viewer")
  54. ).status_code == 200
  55. assert client.post(
  56. "/api/meta/active-metadata/plans",
  57. json={"name": "plan"},
  58. headers=_headers("editor"),
  59. ).status_code == 403
  60. assert client.post(
  61. "/api/meta/active-metadata/plans",
  62. json={"name": "plan"},
  63. headers=_headers("admin"),
  64. ).status_code == 201
  65. assert client.post(
  66. f"/api/meta/active-metadata/plans/{PLAN_UID}/runs",
  67. json={"snapshot": {"assets": []}},
  68. headers=_headers("viewer", **{"Idempotency-Key": "batch-1"}),
  69. ).status_code == 403
  70. executed = client.post(
  71. f"/api/meta/active-metadata/plans/{PLAN_UID}/runs",
  72. json={"snapshot": {"assets": []}},
  73. headers=_headers("editor", **{"Idempotency-Key": "batch-1"}),
  74. )
  75. assert executed.status_code == 200
  76. assert service.calls[-1] == ("execute", PLAN_UID, "batch-1", ACTOR_UID)
  77. resolved = client.post(
  78. f"/api/meta/active-metadata/corrections/{CORRECTION_UID}/resolve",
  79. json={
  80. "expected_version": 1,
  81. "decision": "accept",
  82. "resolution": {"comment": "accepted"},
  83. },
  84. headers=_headers("editor"),
  85. )
  86. assert resolved.status_code == 200
  87. def test_active_metadata_paths_have_dedicated_policy():
  88. from app.core.system.permissions import (
  89. ACTIVE_METADATA_MANAGE,
  90. ACTIVE_METADATA_OPERATE,
  91. ACTIVE_METADATA_READ,
  92. permission_for_request,
  93. )
  94. base = "/api/meta/active-metadata"
  95. assert permission_for_request(f"{base}/plans", "GET") == (ACTIVE_METADATA_READ,)
  96. assert permission_for_request(f"{base}/plans", "POST") == (
  97. ACTIVE_METADATA_MANAGE,
  98. )
  99. assert permission_for_request(f"{base}/plans/{PLAN_UID}/runs", "POST") == (
  100. ACTIVE_METADATA_OPERATE,
  101. )
  102. assert permission_for_request(f"{base}/runs/run-1/lineage", "GET") == (
  103. ACTIVE_METADATA_READ,
  104. )