test_data_element_lifecycle.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. from __future__ import annotations
  2. from dataclasses import replace
  3. import pytest
  4. class MemoryElementRepository:
  5. def __init__(self):
  6. self.records = {}
  7. def get(self, uid):
  8. return self.records.get(uid)
  9. def get_by_code(self, code):
  10. return next((item for item in self.records.values() if item.code == code), None)
  11. def list(self, *, status=None, business_domain_uid=None):
  12. records = list(self.records.values())
  13. if status:
  14. records = [item for item in records if item.status == status]
  15. if business_domain_uid:
  16. records = [
  17. item
  18. for item in records
  19. if business_domain_uid
  20. in set(item.snapshot.get("business_domain_uids") or [])
  21. ]
  22. return records
  23. def save(self, record):
  24. self.records[record.uid] = record
  25. return record
  26. def valid_payload(**overrides):
  27. payload = {
  28. "code": "ORDER_ID",
  29. "name_zh": "订单编号",
  30. "name_en": "order_id",
  31. "definition": "订单的唯一业务标识",
  32. "logical_type": "string",
  33. "evidence_uids": ["evidence-1"],
  34. "business_domain_uids": ["domain-1"],
  35. }
  36. payload.update(overrides)
  37. return payload
  38. def test_data_element_requires_definition_and_starts_as_draft():
  39. from app.core.data_research.data_elements import DataElementService
  40. from app.core.data_research.errors import DataElementInvalid
  41. repository = MemoryElementRepository()
  42. service = DataElementService(repository, uid_factory=lambda: "element-1")
  43. with pytest.raises(DataElementInvalid, match="definition"):
  44. service.create_draft(valid_payload(definition=""), actor_uid="editor-1")
  45. element = service.create_draft(valid_payload(), actor_uid="editor-1")
  46. assert element.uid == "element-1"
  47. assert element.status == "draft"
  48. assert element.current_version == 1
  49. assert element.snapshot["evidence_uids"] == ["evidence-1"]
  50. def test_lifecycle_uses_optimistic_version_and_emits_publish_event():
  51. from app.core.data_research.data_elements import DataElementService
  52. from app.core.data_research.errors import DataElementVersionConflict
  53. repository = MemoryElementRepository()
  54. events = []
  55. service = DataElementService(
  56. repository,
  57. uid_factory=lambda: "element-1",
  58. outbox_enqueue=lambda **event: events.append(event),
  59. )
  60. element = service.create_draft(valid_payload(), actor_uid="editor-1")
  61. review = service.transition(
  62. element.uid,
  63. "in_review",
  64. expected_version=1,
  65. actor_uid="editor-1",
  66. )
  67. published = service.transition(
  68. element.uid,
  69. "published",
  70. expected_version=2,
  71. actor_uid="admin-1",
  72. )
  73. assert review.current_version == 2
  74. assert published.current_version == 3
  75. assert events == [
  76. {
  77. "aggregate_type": "data_element",
  78. "aggregate_id": "element-1",
  79. "event_type": "data_element.version_published",
  80. "payload": {"uid": "element-1", "version": 3},
  81. }
  82. ]
  83. with pytest.raises(DataElementVersionConflict):
  84. service.transition(
  85. element.uid,
  86. "deprecated",
  87. expected_version=2,
  88. actor_uid="admin-1",
  89. )
  90. def test_invalid_lifecycle_transition_is_rejected():
  91. from app.core.data_research.data_elements import DataElementService
  92. from app.core.data_research.errors import DataElementTransitionInvalid
  93. repository = MemoryElementRepository()
  94. service = DataElementService(repository, uid_factory=lambda: "element-1")
  95. element = service.create_draft(valid_payload(), actor_uid="editor-1")
  96. with pytest.raises(DataElementTransitionInvalid, match="draft -> published"):
  97. service.transition(
  98. element.uid,
  99. "published",
  100. expected_version=1,
  101. actor_uid="admin-1",
  102. )
  103. def test_elements_can_be_filtered_for_ontology_mapping():
  104. from app.core.data_research.data_elements import DataElementService
  105. repository = MemoryElementRepository()
  106. service = DataElementService(
  107. repository, uid_factory=iter(("element-1", "element-2")).__next__
  108. )
  109. first = service.create_draft(valid_payload(), actor_uid="editor-1")
  110. service.transition(
  111. first.uid, "in_review", expected_version=1, actor_uid="editor-1"
  112. )
  113. service.transition(
  114. first.uid, "published", expected_version=2, actor_uid="admin-1"
  115. )
  116. service.create_draft(
  117. valid_payload(code="OTHER_ID", business_domain_uids=["domain-2"]),
  118. actor_uid="editor-1",
  119. )
  120. assert [
  121. item.uid
  122. for item in service.list(
  123. status="published", business_domain_uid="domain-1"
  124. )
  125. ] == ["element-1"]