test_notifications.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. """站内通知接口测试(契约 1.34)。"""
  2. from __future__ import annotations
  3. from datetime import datetime
  4. import pytest
  5. from sqlalchemy import delete, select
  6. from dms.extensions import db
  7. from dms.models.notification import Notification
  8. def auth(token: str) -> dict[str, str]:
  9. return {"Authorization": f"Bearer {token}"}
  10. @pytest.fixture()
  11. def notification_app(b2_app):
  12. """预置通知数据:admin 3 条(2 未读 1 已读、2 待办),user 1 条未读。"""
  13. with b2_app.app_context():
  14. db.session.execute(delete(Notification))
  15. from dms.models import User
  16. admin = db.session.scalar(select(User).where(User.username == "admin"))
  17. user = db.session.scalar(select(User).where(User.username == "user"))
  18. assert admin is not None and user is not None
  19. rows = [
  20. Notification(
  21. user_id=admin.id,
  22. title="待审批:作战方案A",
  23. content="作战方案A等待您审批",
  24. notify_type="APPROVAL",
  25. action="PENDING",
  26. status="UNREAD",
  27. created_at=datetime(2026, 9, 1, 0, 0),
  28. ),
  29. Notification(
  30. user_id=admin.id,
  31. title="已处理:通信方案B",
  32. content="通信方案B审批",
  33. notify_type="APPROVAL",
  34. action="AGREE",
  35. status="READ",
  36. created_at=datetime(2026, 9, 2, 0, 0),
  37. ),
  38. Notification(
  39. user_id=admin.id,
  40. title="无待办:系统公告",
  41. content="系统维护通知",
  42. notify_type="APPROVAL",
  43. action="NONE",
  44. status="UNREAD",
  45. created_at=datetime(2026, 9, 3, 0, 0),
  46. ),
  47. Notification(
  48. user_id=user.id,
  49. title="用户待办",
  50. content="用户自己的待办",
  51. notify_type="APPROVAL",
  52. action="PENDING",
  53. status="UNREAD",
  54. created_at=datetime(2026, 9, 4, 0, 0),
  55. ),
  56. ]
  57. db.session.add_all(rows)
  58. db.session.commit()
  59. b2_app.config["NOTIFICATION_IDS"] = {row.title: row.id for row in rows}
  60. yield b2_app
  61. with b2_app.app_context():
  62. db.session.execute(delete(Notification))
  63. db.session.commit()
  64. @pytest.fixture()
  65. def notification_client(notification_app):
  66. return notification_app.test_client()
  67. def test_notifications_require_valid_token(notification_client):
  68. response = notification_client.get("/api/v1/notifications")
  69. assert response.status_code == 401
  70. assert response.get_json()["code"] == "TOKEN_INVALID"
  71. def test_list_returns_only_own_notifications_id_desc(
  72. notification_client, token_for
  73. ):
  74. response = notification_client.get(
  75. "/api/v1/notifications", headers=auth(token_for("admin"))
  76. )
  77. assert response.status_code == 200
  78. items = response.get_json()["data"]["items"]
  79. assert [item["title"] for item in items] == [
  80. "无待办:系统公告",
  81. "已处理:通信方案B",
  82. "待审批:作战方案A",
  83. ]
  84. first = items[0]
  85. assert first["id"].isdecimal()
  86. assert first["notifyType"] == "APPROVAL"
  87. assert first["status"] == "UNREAD"
  88. assert first["createdAt"].endswith("Z")
  89. def test_unread_count_per_user(notification_client, token_for):
  90. admin = notification_client.get(
  91. "/api/v1/notifications/unread-count", headers=auth(token_for("admin"))
  92. )
  93. user = notification_client.get(
  94. "/api/v1/notifications/unread-count", headers=auth(token_for("user"))
  95. )
  96. assert admin.get_json()["data"] == {"count": 2}
  97. assert user.get_json()["data"] == {"count": 1}
  98. def test_read_all_clears_own_unread_only(notification_client, notification_app, token_for):
  99. response = notification_client.post(
  100. "/api/v1/notifications/read-all", headers=auth(token_for("admin"))
  101. )
  102. assert response.status_code == 200
  103. assert response.get_json()["data"]["updated"] == 2
  104. admin = notification_client.get(
  105. "/api/v1/notifications/unread-count", headers=auth(token_for("admin"))
  106. )
  107. user = notification_client.get(
  108. "/api/v1/notifications/unread-count", headers=auth(token_for("user"))
  109. )
  110. assert admin.get_json()["data"]["count"] == 0
  111. assert user.get_json()["data"]["count"] == 1
  112. # 已处理记录的 action 不受全部已读影响
  113. with notification_app.app_context():
  114. handled = db.session.get(
  115. Notification, notification_app.config["NOTIFICATION_IDS"]["已处理:通信方案B"]
  116. )
  117. assert handled is not None and handled.action == "AGREE"
  118. @pytest.mark.parametrize("action,expected", [("AGREE", "AGREE"), ("REJECT", "REJECT")])
  119. def test_handle_pending_notification(
  120. notification_client, notification_app, token_for, action, expected
  121. ):
  122. notification_id = notification_app.config["NOTIFICATION_IDS"]["待审批:作战方案A"]
  123. response = notification_client.post(
  124. f"/api/v1/notifications/{notification_id}/handle",
  125. headers=auth(token_for("admin")),
  126. json={"action": action},
  127. )
  128. assert response.status_code == 200
  129. data = response.get_json()["data"]
  130. assert data["action"] == expected
  131. assert data["status"] == "READ"
  132. assert data["handledAt"] is not None
  133. def test_handle_twice_returns_conflict(notification_client, notification_app, token_for):
  134. notification_id = notification_app.config["NOTIFICATION_IDS"]["待审批:作战方案A"]
  135. headers = auth(token_for("admin"))
  136. first = notification_client.post(
  137. f"/api/v1/notifications/{notification_id}/handle",
  138. headers=headers,
  139. json={"action": "AGREE"},
  140. )
  141. second = notification_client.post(
  142. f"/api/v1/notifications/{notification_id}/handle",
  143. headers=headers,
  144. json={"action": "REJECT"},
  145. )
  146. assert first.status_code == 200
  147. assert second.status_code == 409
  148. assert second.get_json()["code"] == "DATA_VERSION_CONFLICT"
  149. def test_handle_none_action_returns_conflict(
  150. notification_client, notification_app, token_for
  151. ):
  152. notification_id = notification_app.config["NOTIFICATION_IDS"]["无待办:系统公告"]
  153. response = notification_client.post(
  154. f"/api/v1/notifications/{notification_id}/handle",
  155. headers=auth(token_for("admin")),
  156. json={"action": "AGREE"},
  157. )
  158. assert response.status_code == 409
  159. def test_handle_other_users_notification_returns_404(
  160. notification_client, notification_app, token_for
  161. ):
  162. notification_id = notification_app.config["NOTIFICATION_IDS"]["用户待办"]
  163. response = notification_client.post(
  164. f"/api/v1/notifications/{notification_id}/handle",
  165. headers=auth(token_for("admin")),
  166. json={"action": "AGREE"},
  167. )
  168. assert response.status_code == 404
  169. assert response.get_json()["code"] == "RESOURCE_NOT_FOUND"
  170. @pytest.mark.parametrize(
  171. "path,body",
  172. [
  173. ("/api/v1/notifications/0/handle", {"action": "AGREE"}),
  174. ("/api/v1/notifications/abc/handle", {"action": "AGREE"}),
  175. ("/api/v1/notifications/1/handle", {"action": "FOO"}),
  176. ("/api/v1/notifications/1/handle", {}),
  177. ("/api/v1/notifications/1/handle", "not-a-dict"),
  178. ],
  179. )
  180. def test_handle_invalid_arguments(notification_client, token_for, path, body):
  181. response = notification_client.post(
  182. path, headers=auth(token_for("admin")), json=body
  183. )
  184. assert response.status_code == 400
  185. assert response.get_json()["code"] == "INVALID_ARGUMENT"