test_sample_collect_wx_notify.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. """协议新增样本服务号通知单测。"""
  2. from datetime import datetime
  3. from unittest.mock import MagicMock, patch
  4. from app.services.sample_collect_wx_notify import (
  5. PROTOCOL_KIND_CELL,
  6. PROTOCOL_KIND_DONATION,
  7. TASK_TYPE_CELL,
  8. TASK_TYPE_DONATION,
  9. diff_new_sample_codes,
  10. format_protocol_create_time,
  11. format_sample_collect_title,
  12. protocol_status_blocks_notify,
  13. run_sample_collect_notify,
  14. sample_status_blocks_notify,
  15. sample_codes_from_orm_rows,
  16. sample_codes_from_payload,
  17. schedule_sample_collect_notify,
  18. )
  19. def test_sample_codes_from_payload_camel_and_snake():
  20. assert sample_codes_from_payload(
  21. [
  22. {"sampleCode": "A1"},
  23. {"sample_code": "A2"},
  24. {"sampleCode": ""},
  25. {},
  26. ]
  27. ) == {"A1", "A2"}
  28. def test_sample_codes_from_orm_rows():
  29. a = MagicMock(sample_code="X1")
  30. b = MagicMock(sample_code=" ")
  31. c = MagicMock(sample_code="X2")
  32. assert sample_codes_from_orm_rows([a, b, c]) == {"X1", "X2"}
  33. def test_diff_new_sample_codes_create_and_edit():
  34. assert diff_new_sample_codes(before_codes=[], submitted_codes=["b", "a"]) == [
  35. "a",
  36. "b",
  37. ]
  38. assert diff_new_sample_codes(
  39. before_codes=["a", "b"], submitted_codes=["a", "b", "c"]
  40. ) == ["c"]
  41. assert (
  42. diff_new_sample_codes(before_codes=["a"], submitted_codes=["a"]) == []
  43. )
  44. def test_format_sample_collect_title_with_and_without_eta():
  45. eta = datetime(2022, 11, 22, 16, 17)
  46. assert format_sample_collect_title(eta) == "样本采集(预计到达 11月22日)"
  47. assert format_sample_collect_title(None) == "样本采集(到达待定)"
  48. assert len(format_sample_collect_title(eta)) <= 20
  49. assert len(format_sample_collect_title(None)) <= 20
  50. def test_sample_type_name_by_code_uses_type_name_or_blank():
  51. from app.services.sample_collect_wx_notify import _sample_type_name_by_code
  52. db = MagicMock()
  53. type_row = MagicMock()
  54. type_row.id = "t1"
  55. type_row.type_name = " 胎盘亚全能干细胞 "
  56. db.query.return_value.filter.return_value.all.return_value = [type_row]
  57. named = MagicMock(sample_code="A", sample_type_id="t1")
  58. blank = MagicMock(sample_code="B", sample_type_id="")
  59. missing = MagicMock(sample_code="C", sample_type_id="gone")
  60. out = _sample_type_name_by_code(
  61. db, tenant_no="zkxw", samples=[named, blank, missing]
  62. )
  63. assert out == {"A": "胎盘亚全能干细胞", "B": "", "C": ""}
  64. def test_format_protocol_create_time():
  65. assert (
  66. format_protocol_create_time(datetime(2022, 11, 22, 16, 17))
  67. == "2022年11月22日 16:17"
  68. )
  69. assert format_protocol_create_time(None) == ""
  70. def test_send_sample_collect_notify_builds_data_and_truncates_title():
  71. with patch(
  72. "app.services.wechat_mp_message._send_template"
  73. ) as send_tpl, patch("app.services.wechat_mp_message.settings") as st:
  74. st.WX_MP_TEMPLATE_SAMPLE_COLLECT = "tpl-sc"
  75. st.WX_MP_TPL_SC_FIELD_TITLE = "thing2"
  76. st.WX_MP_TPL_SC_FIELD_APPLICANT = "thing8"
  77. st.WX_MP_TPL_SC_FIELD_TIME = "time3"
  78. st.WX_MP_TPL_SC_FIELD_TYPE = "phrase10"
  79. st.WX_MP_TPL_SC_FIELD_SAMPLE_TYPE = "thing17"
  80. send_tpl.return_value = {"success": True, "msgid": "1"}
  81. from app.services.wechat_mp_message import send_sample_collect_notify
  82. long_title = "样本采集(预计到达 11月22日)额外超长文字会被截断"
  83. result = send_sample_collect_notify(
  84. mp_openid="oid",
  85. title=long_title,
  86. applicant="彭某某",
  87. time_text="2022年11月22日 16:17",
  88. task_type=TASK_TYPE_CELL,
  89. sample_type="脐带间充质干细胞" + "甲" * 20,
  90. )
  91. assert result["success"] is True
  92. kwargs = send_tpl.call_args.kwargs
  93. assert kwargs["template_id"] == "tpl-sc"
  94. data = kwargs["data"]
  95. assert len(data["thing2"]["value"]) <= 20
  96. assert data["thing8"]["value"] == "彭某某"
  97. assert data["time3"]["value"] == "2022年11月22日 16:17"
  98. assert data["phrase10"]["value"] == "细胞存储"
  99. assert data["thing17"]["value"] == ("脐带间充质干细胞" + "甲" * 20)[:20]
  100. assert len(data["thing17"]["value"]) == 20
  101. send_sample_collect_notify(
  102. mp_openid="oid",
  103. title="样本采集(到达待定)",
  104. applicant="彭某某",
  105. time_text="2022年11月22日 16:17",
  106. task_type=TASK_TYPE_DONATION,
  107. sample_type=" ",
  108. )
  109. empty_data = send_tpl.call_args.kwargs["data"]
  110. assert empty_data["phrase10"]["value"] == "科研捐赠"
  111. assert "thing17" not in empty_data
  112. def test_schedule_skips_when_samples_not_in_payload():
  113. with patch(
  114. "app.services.sample_collect_wx_notify._notify_enabled", return_value=True
  115. ), patch(
  116. "app.services.sample_collect_wx_notify.threading.Thread"
  117. ) as th:
  118. schedule_sample_collect_notify(
  119. tenant_no="zkxw",
  120. protocol_id="p1",
  121. protocol_kind=PROTOCOL_KIND_CELL,
  122. before_codes=["a"],
  123. submitted_codes=None,
  124. )
  125. th.assert_not_called()
  126. def test_schedule_skips_when_no_new_codes():
  127. with patch(
  128. "app.services.sample_collect_wx_notify._notify_enabled", return_value=True
  129. ), patch(
  130. "app.services.sample_collect_wx_notify.threading.Thread"
  131. ) as th:
  132. schedule_sample_collect_notify(
  133. tenant_no="zkxw",
  134. protocol_id="p1",
  135. protocol_kind=PROTOCOL_KIND_CELL,
  136. before_codes=["a"],
  137. submitted_codes=["a"],
  138. )
  139. th.assert_not_called()
  140. def test_schedule_starts_thread_for_new_codes():
  141. with patch(
  142. "app.services.sample_collect_wx_notify._notify_enabled", return_value=True
  143. ), patch(
  144. "app.services.sample_collect_wx_notify.threading.Thread"
  145. ) as th:
  146. th.return_value = MagicMock()
  147. schedule_sample_collect_notify(
  148. tenant_no="zkxw",
  149. protocol_id="p1",
  150. protocol_kind=PROTOCOL_KIND_DONATION,
  151. before_codes=["a"],
  152. submitted_codes=["a", "b"],
  153. )
  154. th.assert_called_once()
  155. kwargs = th.call_args.kwargs
  156. assert kwargs["kwargs"]["new_sample_codes"] == ["b"]
  157. assert kwargs["kwargs"]["protocol_kind"] == PROTOCOL_KIND_DONATION
  158. th.return_value.start.assert_called_once()
  159. def test_run_notify_sends_per_code_per_user_and_idempotent():
  160. db = MagicMock()
  161. user = MagicMock()
  162. user.id = "u1"
  163. user.wx_mp_openid = "mp1"
  164. user.nick_name = "通知人"
  165. with patch(
  166. "app.services.sample_collect_wx_notify._notify_enabled", return_value=True
  167. ), patch(
  168. "app.services.sample_collect_wx_notify._load_cell_context",
  169. return_value={
  170. "create_time": datetime(2022, 11, 22, 16, 17),
  171. "applicant": "彭某某",
  172. "eta_by_code": {"S1": datetime(2022, 11, 22, 16, 17), "S2": None},
  173. "sample_type_by_code": {"S1": "围产期造血干细胞", "S2": ""},
  174. },
  175. ), patch(
  176. "app.services.sample_collect_wx_notify.list_notify_recipient_users",
  177. return_value=[user],
  178. ), patch(
  179. "app.services.sample_collect_wx_notify._already_success",
  180. side_effect=[False, True],
  181. ) as already, patch(
  182. "app.services.wechat_mp_message.send_sample_collect_notify",
  183. return_value={"success": True, "msgid": "1"},
  184. ) as send, patch(
  185. "app.services.sample_collect_wx_notify._write_success_log"
  186. ) as write_log:
  187. stats = run_sample_collect_notify(
  188. db,
  189. tenant_no="zkxw",
  190. protocol_id="p1",
  191. protocol_kind=PROTOCOL_KIND_CELL,
  192. new_sample_codes=["S1", "S2"],
  193. )
  194. assert stats["codes"] == 2
  195. assert stats["sent"] == 1
  196. assert stats["skipped"] == 1
  197. assert send.call_count == 1
  198. assert send.call_args.kwargs["task_type"] == TASK_TYPE_CELL
  199. assert send.call_args.kwargs["sample_type"] == "围产期造血干细胞"
  200. assert send.call_args.kwargs["title"] == "样本采集(预计到达 11月22日)"
  201. assert write_log.call_count == 1
  202. assert already.call_count == 2
  203. db.commit.assert_called_once()
  204. def test_run_notify_donation_task_type():
  205. db = MagicMock()
  206. user = MagicMock()
  207. user.id = "u1"
  208. user.wx_mp_openid = "mp1"
  209. with patch(
  210. "app.services.sample_collect_wx_notify._notify_enabled", return_value=True
  211. ), patch(
  212. "app.services.sample_collect_wx_notify._load_donation_context",
  213. return_value={
  214. "create_time": datetime(2022, 11, 22, 16, 17),
  215. "applicant": "李某",
  216. "eta_by_code": {"D1": None},
  217. "sample_type_by_code": {"D1": "脐带间充质干细胞"},
  218. },
  219. ), patch(
  220. "app.services.sample_collect_wx_notify.list_notify_recipient_users",
  221. return_value=[user],
  222. ), patch(
  223. "app.services.sample_collect_wx_notify._already_success",
  224. return_value=False,
  225. ), patch(
  226. "app.services.wechat_mp_message.send_sample_collect_notify",
  227. return_value={"success": True},
  228. ) as send, patch(
  229. "app.services.sample_collect_wx_notify._write_success_log"
  230. ):
  231. run_sample_collect_notify(
  232. db,
  233. tenant_no="zkxw",
  234. protocol_id="d1",
  235. protocol_kind=PROTOCOL_KIND_DONATION,
  236. new_sample_codes=["D1"],
  237. )
  238. assert send.call_args.kwargs["task_type"] == TASK_TYPE_DONATION
  239. assert send.call_args.kwargs["sample_type"] == "脐带间充质干细胞"
  240. assert send.call_args.kwargs["title"] == "样本采集(到达待定)"
  241. def test_protocol_and_sample_void_status_block_notify():
  242. assert protocol_status_blocks_notify("yzf") is True
  243. assert protocol_status_blocks_notify("sdfq") is True
  244. assert protocol_status_blocks_notify("sxz") is False
  245. assert protocol_status_blocks_notify("") is False
  246. assert sample_status_blocks_notify("yzf") is True
  247. assert sample_status_blocks_notify("ycj") is False
  248. assert sample_status_blocks_notify(None) is False
  249. def test_run_notify_skips_inactive_protocol():
  250. db = MagicMock()
  251. with patch(
  252. "app.services.sample_collect_wx_notify._notify_enabled", return_value=True
  253. ), patch(
  254. "app.services.sample_collect_wx_notify._load_cell_context",
  255. return_value={
  256. "create_time": datetime(2022, 11, 22, 16, 17),
  257. "applicant": "彭某某",
  258. "protocol_status": "sdfq",
  259. "eta_by_code": {"S1": None},
  260. "sample_status_by_code": {"S1": "ycj"},
  261. "sample_type_by_code": {"S1": "围产期造血干细胞"},
  262. },
  263. ), patch(
  264. "app.services.sample_collect_wx_notify.list_notify_recipient_users"
  265. ) as recipients, patch(
  266. "app.services.wechat_mp_message.send_sample_collect_notify"
  267. ) as send:
  268. stats = run_sample_collect_notify(
  269. db,
  270. tenant_no="zkxw",
  271. protocol_id="p1",
  272. protocol_kind=PROTOCOL_KIND_CELL,
  273. new_sample_codes=["S1"],
  274. )
  275. assert stats["sent"] == 0
  276. assert stats["skipped"] == 1
  277. recipients.assert_not_called()
  278. send.assert_not_called()
  279. db.commit.assert_not_called()
  280. def test_run_notify_skips_voided_sample_and_sends_the_rest():
  281. db = MagicMock()
  282. user = MagicMock()
  283. user.id = "u1"
  284. user.wx_mp_openid = "mp1"
  285. with patch(
  286. "app.services.sample_collect_wx_notify._notify_enabled", return_value=True
  287. ), patch(
  288. "app.services.sample_collect_wx_notify._load_donation_context",
  289. return_value={
  290. "create_time": datetime(2022, 11, 22, 16, 17),
  291. "applicant": "李某",
  292. "protocol_status": "sxz",
  293. "eta_by_code": {"D1": None, "D2": None},
  294. "sample_status_by_code": {"D1": "yzf", "D2": "ycj"},
  295. "sample_type_by_code": {"D1": "脐带间充质干细胞", "D2": "胎盘亚全能干细胞"},
  296. },
  297. ), patch(
  298. "app.services.sample_collect_wx_notify.list_notify_recipient_users",
  299. return_value=[user],
  300. ), patch(
  301. "app.services.sample_collect_wx_notify._already_success",
  302. return_value=False,
  303. ), patch(
  304. "app.services.wechat_mp_message.send_sample_collect_notify",
  305. return_value={"success": True},
  306. ) as send, patch(
  307. "app.services.sample_collect_wx_notify._write_success_log"
  308. ):
  309. stats = run_sample_collect_notify(
  310. db,
  311. tenant_no="zkxw",
  312. protocol_id="d1",
  313. protocol_kind=PROTOCOL_KIND_DONATION,
  314. new_sample_codes=["D1", "D2"],
  315. )
  316. assert stats["sent"] == 1
  317. assert stats["skipped"] == 1
  318. assert send.call_count == 1
  319. assert send.call_args.kwargs["sample_type"] == "胎盘亚全能干细胞"
  320. assert send.call_args.kwargs["task_type"] == TASK_TYPE_DONATION
  321. def test_list_notify_recipient_users_uses_effective_role_including_post():
  322. """接收人须走有效角色(含职位继承),不能只查 sys_user_role。"""
  323. db = MagicMock()
  324. user = MagicMock()
  325. user.id = "u-post"
  326. user.wx_mp_openid = "mp-openid"
  327. db.query.return_value.filter.return_value.all.return_value = [user]
  328. with patch(
  329. "app.services.sample_collect_wx_notify.list_user_ids_with_effective_role_name",
  330. return_value={"u-post"},
  331. ) as list_ids:
  332. from app.services.sample_collect_wx_notify import list_notify_recipient_users
  333. rows = list_notify_recipient_users(db, tenant_no="zkxw")
  334. list_ids.assert_called_once_with(
  335. db, tenant_no="zkxw", role_name="样本采集通知人员"
  336. )
  337. assert rows == [user]
  338. def test_list_notify_recipient_users_dedupes_same_openid():
  339. db = MagicMock()
  340. u1 = MagicMock()
  341. u1.id = "u1"
  342. u1.wx_mp_openid = "same-oid"
  343. u2 = MagicMock()
  344. u2.id = "u2"
  345. u2.wx_mp_openid = "same-oid"
  346. u3 = MagicMock()
  347. u3.id = "u3"
  348. u3.wx_mp_openid = "other-oid"
  349. db.query.return_value.filter.return_value.all.return_value = [u1, u2, u3]
  350. with patch(
  351. "app.services.sample_collect_wx_notify.list_user_ids_with_effective_role_name",
  352. return_value={"u1", "u2", "u3"},
  353. ):
  354. from app.services.sample_collect_wx_notify import list_notify_recipient_users
  355. rows = list_notify_recipient_users(db, tenant_no="zkxw")
  356. assert len(rows) == 2
  357. assert {r.wx_mp_openid for r in rows} == {"same-oid", "other-oid"}