test_responsibility_postgres.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. from __future__ import annotations
  2. import os
  3. import uuid
  4. import pytest
  5. from sqlalchemy import create_engine, text
  6. pytestmark = pytest.mark.integration
  7. def _login(client, username: str, password: str):
  8. response = client.post(
  9. "/api/system/auth/login",
  10. json={"username": username, "password": password},
  11. )
  12. return response, (response.get_json().get("data") or {}).get("token")
  13. def test_device_responsibility_matrix_is_versioned_audited_and_readable(
  14. monkeypatch,
  15. ):
  16. database_url = os.environ.get("TEST_DATABASE_URL")
  17. admin_password = os.environ.get("TEST_ADMIN_PASSWORD")
  18. if not database_url or not admin_password:
  19. pytest.skip("TEST_DATABASE_URL and TEST_ADMIN_PASSWORD are required")
  20. monkeypatch.setenv("DATABASE_URL", database_url)
  21. monkeypatch.setenv("SECRET_KEY", "dataops-local-test-secret-key")
  22. from app import create_app
  23. app = create_app()
  24. app.config.update(TESTING=True)
  25. client = app.test_client()
  26. username = f"asset_manager_{uuid.uuid4().hex[:10]}"
  27. resource_uid = f"device-{uuid.uuid4().hex[:12]}"
  28. user_id = None
  29. engine = create_engine(database_url)
  30. try:
  31. login, admin_token = _login(client, "admin", admin_password)
  32. assert login.status_code == 200
  33. assert admin_token
  34. created = client.post(
  35. "/api/system/users",
  36. json={
  37. "username": username,
  38. "display_name": "设备资产管理员",
  39. "password": "AssetManager123",
  40. "roles": ["viewer"],
  41. },
  42. headers={"Authorization": f"Bearer {admin_token}"},
  43. )
  44. assert created.status_code == 201
  45. user_id = created.get_json()["data"]["id"]
  46. matrix = {
  47. "assignments": [
  48. {
  49. "user_id": user_id,
  50. "responsibility_role": "asset_manager",
  51. "raci_role": "accountable",
  52. }
  53. ]
  54. }
  55. updated = client.put(
  56. f"/api/system/responsibilities/device_asset/{resource_uid}",
  57. json=matrix,
  58. headers={
  59. "Authorization": f"Bearer {admin_token}",
  60. "If-Match": '"0"',
  61. },
  62. )
  63. assert updated.status_code == 200
  64. assert updated.headers["ETag"] == '"1"'
  65. viewer_login, viewer_token = _login(
  66. client,
  67. username,
  68. "AssetManager123",
  69. )
  70. assert viewer_login.status_code == 200
  71. readable = client.get(
  72. f"/api/system/responsibilities/device_asset/{resource_uid}",
  73. headers={"Authorization": f"Bearer {viewer_token}"},
  74. )
  75. assert readable.status_code == 200
  76. assert readable.get_json()["data"]["assignments"][0]["user_id"] == user_id
  77. forbidden = client.put(
  78. f"/api/system/responsibilities/device_asset/{resource_uid}",
  79. json=matrix,
  80. headers={
  81. "Authorization": f"Bearer {viewer_token}",
  82. "If-Match": '"1"',
  83. },
  84. )
  85. assert forbidden.status_code == 403
  86. stale = client.put(
  87. f"/api/system/responsibilities/device_asset/{resource_uid}",
  88. json=matrix,
  89. headers={
  90. "Authorization": f"Bearer {admin_token}",
  91. "If-Match": '"0"',
  92. },
  93. )
  94. assert stale.status_code == 409
  95. with engine.connect() as connection:
  96. audit_count = connection.execute(
  97. text(
  98. """
  99. SELECT COUNT(*)
  100. FROM public.governance_responsibility_audit_events
  101. WHERE resource_type = 'device_asset'
  102. AND resource_uid = :resource_uid
  103. """
  104. ),
  105. {"resource_uid": resource_uid},
  106. ).scalar_one()
  107. assert audit_count == 1
  108. finally:
  109. with engine.begin() as connection:
  110. connection.execute(
  111. text(
  112. """
  113. DELETE FROM public.governance_responsibility_scopes
  114. WHERE resource_type = 'device_asset'
  115. AND resource_uid = :resource_uid
  116. """
  117. ),
  118. {"resource_uid": resource_uid},
  119. )
  120. if user_id:
  121. connection.execute(
  122. text(
  123. "DELETE FROM public.users WHERE id = CAST(:user_id AS uuid)"
  124. ),
  125. {"user_id": user_id},
  126. )
  127. engine.dispose()