test_domain_template_postgres.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. from __future__ import annotations
  2. import json
  3. import os
  4. import uuid
  5. from pathlib import Path
  6. import pytest
  7. from sqlalchemy import text
  8. pytestmark = pytest.mark.integration
  9. ROOT = Path(__file__).resolve().parents[2]
  10. def test_domain_template_import_diff_and_rollback_are_atomic(monkeypatch):
  11. database_url = os.environ.get("TEST_DATABASE_URL")
  12. if not database_url:
  13. pytest.skip("TEST_DATABASE_URL is required")
  14. monkeypatch.setenv("DATABASE_URL", database_url)
  15. from app import create_app, db
  16. from app.core.governance.domain_template_repository import (
  17. SqlAlchemyDomainTemplateRepository,
  18. )
  19. from app.core.governance.domain_templates import DomainTemplateService
  20. app = create_app()
  21. app.config.update(TESTING=True)
  22. actor_uid = str(uuid.uuid4())
  23. definition = json.loads(
  24. (
  25. ROOT / "docs/phase2/P2_WP01_SPARE_PARTS_DOMAIN_TEMPLATE.json"
  26. ).read_text(encoding="utf-8")
  27. )
  28. definition["template_code"] = f"spare_parts_{uuid.uuid4().hex[:8]}"
  29. definition["name"] = f"集成测试模板 {definition['template_code']}"
  30. template_uid = None
  31. try:
  32. with app.app_context():
  33. db.session.execute(
  34. text(
  35. """
  36. INSERT INTO public.users (
  37. id, username, display_name, password_hash, status
  38. ) VALUES (
  39. CAST(:id AS uuid), :username, :username,
  40. 'p2-wp01-integration-hash', 'active'
  41. )
  42. """
  43. ),
  44. {"id": actor_uid, "username": f"p2-wp01-{actor_uid[:8]}"},
  45. )
  46. db.session.commit()
  47. service = DomainTemplateService(
  48. SqlAlchemyDomainTemplateRepository(db.session)
  49. )
  50. first = service.import_template(definition, actor_uid=actor_uid)
  51. db.session.commit()
  52. template_uid = first["uid"]
  53. changed = dict(definition)
  54. changed["description"] = "集成测试第二版"
  55. preview = service.dry_run(changed)
  56. assert preview["diff"]["added_object_types"] == []
  57. class FailingRepository(SqlAlchemyDomainTemplateRepository):
  58. def apply_import(self, record):
  59. super().apply_import(record)
  60. raise RuntimeError("simulated failure after persistence")
  61. failing_service = DomainTemplateService(FailingRepository(db.session))
  62. with pytest.raises(RuntimeError, match="simulated failure"):
  63. failing_service.import_template(changed, actor_uid=actor_uid)
  64. db.session.rollback()
  65. assert service.get_template(
  66. definition["template_code"]
  67. )["current_version"] == 1
  68. second = service.import_template(changed, actor_uid=actor_uid)
  69. db.session.commit()
  70. rolled_back = service.rollback(
  71. definition["template_code"],
  72. target_version=1,
  73. actor_uid=actor_uid,
  74. )
  75. db.session.commit()
  76. assert first["current_version"] == 1
  77. assert second["current_version"] == 2
  78. assert rolled_back["current_version"] == 3
  79. assert rolled_back["description"] == definition["description"]
  80. assert len(rolled_back["object_types"]) == 6
  81. assert [item["operation"] for item in service.list_imports(
  82. definition["template_code"]
  83. )] == ["rollback", "import", "import"]
  84. counts = db.session.execute(
  85. text(
  86. """
  87. SELECT
  88. (SELECT count(*) FROM public.governance_object_types
  89. WHERE template_uid = CAST(:uid AS uuid)) AS object_types,
  90. (SELECT count(*) FROM public.governance_domain_template_versions
  91. WHERE template_uid = CAST(:uid AS uuid)) AS versions,
  92. (SELECT count(*) FROM public.governance_domain_template_imports
  93. WHERE template_uid = CAST(:uid AS uuid)) AS imports
  94. """
  95. ),
  96. {"uid": template_uid},
  97. ).mappings().one()
  98. assert dict(counts) == {
  99. "object_types": 6,
  100. "versions": 3,
  101. "imports": 3,
  102. }
  103. finally:
  104. with app.app_context():
  105. db.session.rollback()
  106. if template_uid:
  107. for statement in (
  108. "DELETE FROM public.governance_domain_template_imports "
  109. "WHERE template_uid = CAST(:uid AS uuid)",
  110. "DELETE FROM public.governance_object_types "
  111. "WHERE template_uid = CAST(:uid AS uuid)",
  112. "DELETE FROM public.governance_domain_template_versions "
  113. "WHERE template_uid = CAST(:uid AS uuid)",
  114. "DELETE FROM public.governance_domain_templates "
  115. "WHERE uid = CAST(:uid AS uuid)",
  116. ):
  117. db.session.execute(
  118. text(statement),
  119. {"uid": template_uid},
  120. )
  121. db.session.execute(
  122. text("DELETE FROM public.users WHERE id = CAST(:id AS uuid)"),
  123. {"id": actor_uid},
  124. )
  125. db.session.commit()