"""协议新增样本服务号通知单测。""" from datetime import datetime from unittest.mock import MagicMock, patch from app.services.sample_collect_wx_notify import ( PROTOCOL_KIND_CELL, PROTOCOL_KIND_DONATION, TASK_TYPE_CELL, TASK_TYPE_DONATION, diff_new_sample_codes, format_protocol_create_time, format_sample_collect_title, protocol_status_blocks_notify, run_sample_collect_notify, sample_status_blocks_notify, sample_codes_from_orm_rows, sample_codes_from_payload, schedule_sample_collect_notify, ) def test_sample_codes_from_payload_camel_and_snake(): assert sample_codes_from_payload( [ {"sampleCode": "A1"}, {"sample_code": "A2"}, {"sampleCode": ""}, {}, ] ) == {"A1", "A2"} def test_sample_codes_from_orm_rows(): a = MagicMock(sample_code="X1") b = MagicMock(sample_code=" ") c = MagicMock(sample_code="X2") assert sample_codes_from_orm_rows([a, b, c]) == {"X1", "X2"} def test_diff_new_sample_codes_create_and_edit(): assert diff_new_sample_codes(before_codes=[], submitted_codes=["b", "a"]) == [ "a", "b", ] assert diff_new_sample_codes( before_codes=["a", "b"], submitted_codes=["a", "b", "c"] ) == ["c"] assert ( diff_new_sample_codes(before_codes=["a"], submitted_codes=["a"]) == [] ) def test_format_sample_collect_title_with_and_without_eta(): eta = datetime(2022, 11, 22, 16, 17) assert format_sample_collect_title(eta) == "样本采集(预计到达 11月22日)" assert format_sample_collect_title(None) == "样本采集(到达待定)" assert len(format_sample_collect_title(eta)) <= 20 assert len(format_sample_collect_title(None)) <= 20 def test_sample_type_name_by_code_uses_type_name_or_blank(): from app.services.sample_collect_wx_notify import _sample_type_name_by_code db = MagicMock() type_row = MagicMock() type_row.id = "t1" type_row.type_name = " 胎盘亚全能干细胞 " db.query.return_value.filter.return_value.all.return_value = [type_row] named = MagicMock(sample_code="A", sample_type_id="t1") blank = MagicMock(sample_code="B", sample_type_id="") missing = MagicMock(sample_code="C", sample_type_id="gone") out = _sample_type_name_by_code( db, tenant_no="zkxw", samples=[named, blank, missing] ) assert out == {"A": "胎盘亚全能干细胞", "B": "", "C": ""} def test_format_protocol_create_time(): assert ( format_protocol_create_time(datetime(2022, 11, 22, 16, 17)) == "2022年11月22日 16:17" ) assert format_protocol_create_time(None) == "" def test_send_sample_collect_notify_builds_data_and_truncates_title(): with patch( "app.services.wechat_mp_message._send_template" ) as send_tpl, patch("app.services.wechat_mp_message.settings") as st: st.WX_MP_TEMPLATE_SAMPLE_COLLECT = "tpl-sc" st.WX_MP_TPL_SC_FIELD_TITLE = "thing2" st.WX_MP_TPL_SC_FIELD_APPLICANT = "thing8" st.WX_MP_TPL_SC_FIELD_TIME = "time3" st.WX_MP_TPL_SC_FIELD_TYPE = "phrase10" st.WX_MP_TPL_SC_FIELD_SAMPLE_TYPE = "thing17" send_tpl.return_value = {"success": True, "msgid": "1"} from app.services.wechat_mp_message import send_sample_collect_notify long_title = "样本采集(预计到达 11月22日)额外超长文字会被截断" result = send_sample_collect_notify( mp_openid="oid", title=long_title, applicant="彭某某", time_text="2022年11月22日 16:17", task_type=TASK_TYPE_CELL, sample_type="脐带间充质干细胞" + "甲" * 20, ) assert result["success"] is True kwargs = send_tpl.call_args.kwargs assert kwargs["template_id"] == "tpl-sc" data = kwargs["data"] assert len(data["thing2"]["value"]) <= 20 assert data["thing8"]["value"] == "彭某某" assert data["time3"]["value"] == "2022年11月22日 16:17" assert data["phrase10"]["value"] == "细胞存储" assert data["thing17"]["value"] == ("脐带间充质干细胞" + "甲" * 20)[:20] assert len(data["thing17"]["value"]) == 20 send_sample_collect_notify( mp_openid="oid", title="样本采集(到达待定)", applicant="彭某某", time_text="2022年11月22日 16:17", task_type=TASK_TYPE_DONATION, sample_type=" ", ) empty_data = send_tpl.call_args.kwargs["data"] assert empty_data["phrase10"]["value"] == "科研捐赠" assert "thing17" not in empty_data def test_schedule_skips_when_samples_not_in_payload(): with patch( "app.services.sample_collect_wx_notify._notify_enabled", return_value=True ), patch( "app.services.sample_collect_wx_notify.threading.Thread" ) as th: schedule_sample_collect_notify( tenant_no="zkxw", protocol_id="p1", protocol_kind=PROTOCOL_KIND_CELL, before_codes=["a"], submitted_codes=None, ) th.assert_not_called() def test_schedule_skips_when_no_new_codes(): with patch( "app.services.sample_collect_wx_notify._notify_enabled", return_value=True ), patch( "app.services.sample_collect_wx_notify.threading.Thread" ) as th: schedule_sample_collect_notify( tenant_no="zkxw", protocol_id="p1", protocol_kind=PROTOCOL_KIND_CELL, before_codes=["a"], submitted_codes=["a"], ) th.assert_not_called() def test_schedule_starts_thread_for_new_codes(): with patch( "app.services.sample_collect_wx_notify._notify_enabled", return_value=True ), patch( "app.services.sample_collect_wx_notify.threading.Thread" ) as th: th.return_value = MagicMock() schedule_sample_collect_notify( tenant_no="zkxw", protocol_id="p1", protocol_kind=PROTOCOL_KIND_DONATION, before_codes=["a"], submitted_codes=["a", "b"], ) th.assert_called_once() kwargs = th.call_args.kwargs assert kwargs["kwargs"]["new_sample_codes"] == ["b"] assert kwargs["kwargs"]["protocol_kind"] == PROTOCOL_KIND_DONATION th.return_value.start.assert_called_once() def test_run_notify_sends_per_code_per_user_and_idempotent(): db = MagicMock() user = MagicMock() user.id = "u1" user.wx_mp_openid = "mp1" user.nick_name = "通知人" with patch( "app.services.sample_collect_wx_notify._notify_enabled", return_value=True ), patch( "app.services.sample_collect_wx_notify._load_cell_context", return_value={ "create_time": datetime(2022, 11, 22, 16, 17), "applicant": "彭某某", "eta_by_code": {"S1": datetime(2022, 11, 22, 16, 17), "S2": None}, "sample_type_by_code": {"S1": "围产期造血干细胞", "S2": ""}, }, ), patch( "app.services.sample_collect_wx_notify.list_notify_recipient_users", return_value=[user], ), patch( "app.services.sample_collect_wx_notify._already_success", side_effect=[False, True], ) as already, patch( "app.services.wechat_mp_message.send_sample_collect_notify", return_value={"success": True, "msgid": "1"}, ) as send, patch( "app.services.sample_collect_wx_notify._write_success_log" ) as write_log: stats = run_sample_collect_notify( db, tenant_no="zkxw", protocol_id="p1", protocol_kind=PROTOCOL_KIND_CELL, new_sample_codes=["S1", "S2"], ) assert stats["codes"] == 2 assert stats["sent"] == 1 assert stats["skipped"] == 1 assert send.call_count == 1 assert send.call_args.kwargs["task_type"] == TASK_TYPE_CELL assert send.call_args.kwargs["sample_type"] == "围产期造血干细胞" assert send.call_args.kwargs["title"] == "样本采集(预计到达 11月22日)" assert write_log.call_count == 1 assert already.call_count == 2 db.commit.assert_called_once() def test_run_notify_donation_task_type(): db = MagicMock() user = MagicMock() user.id = "u1" user.wx_mp_openid = "mp1" with patch( "app.services.sample_collect_wx_notify._notify_enabled", return_value=True ), patch( "app.services.sample_collect_wx_notify._load_donation_context", return_value={ "create_time": datetime(2022, 11, 22, 16, 17), "applicant": "李某", "eta_by_code": {"D1": None}, "sample_type_by_code": {"D1": "脐带间充质干细胞"}, }, ), patch( "app.services.sample_collect_wx_notify.list_notify_recipient_users", return_value=[user], ), patch( "app.services.sample_collect_wx_notify._already_success", return_value=False, ), patch( "app.services.wechat_mp_message.send_sample_collect_notify", return_value={"success": True}, ) as send, patch( "app.services.sample_collect_wx_notify._write_success_log" ): run_sample_collect_notify( db, tenant_no="zkxw", protocol_id="d1", protocol_kind=PROTOCOL_KIND_DONATION, new_sample_codes=["D1"], ) assert send.call_args.kwargs["task_type"] == TASK_TYPE_DONATION assert send.call_args.kwargs["sample_type"] == "脐带间充质干细胞" assert send.call_args.kwargs["title"] == "样本采集(到达待定)" def test_protocol_and_sample_void_status_block_notify(): assert protocol_status_blocks_notify("yzf") is True assert protocol_status_blocks_notify("sdfq") is True assert protocol_status_blocks_notify("sxz") is False assert protocol_status_blocks_notify("") is False assert sample_status_blocks_notify("yzf") is True assert sample_status_blocks_notify("ycj") is False assert sample_status_blocks_notify(None) is False def test_run_notify_skips_inactive_protocol(): db = MagicMock() with patch( "app.services.sample_collect_wx_notify._notify_enabled", return_value=True ), patch( "app.services.sample_collect_wx_notify._load_cell_context", return_value={ "create_time": datetime(2022, 11, 22, 16, 17), "applicant": "彭某某", "protocol_status": "sdfq", "eta_by_code": {"S1": None}, "sample_status_by_code": {"S1": "ycj"}, "sample_type_by_code": {"S1": "围产期造血干细胞"}, }, ), patch( "app.services.sample_collect_wx_notify.list_notify_recipient_users" ) as recipients, patch( "app.services.wechat_mp_message.send_sample_collect_notify" ) as send: stats = run_sample_collect_notify( db, tenant_no="zkxw", protocol_id="p1", protocol_kind=PROTOCOL_KIND_CELL, new_sample_codes=["S1"], ) assert stats["sent"] == 0 assert stats["skipped"] == 1 recipients.assert_not_called() send.assert_not_called() db.commit.assert_not_called() def test_run_notify_skips_voided_sample_and_sends_the_rest(): db = MagicMock() user = MagicMock() user.id = "u1" user.wx_mp_openid = "mp1" with patch( "app.services.sample_collect_wx_notify._notify_enabled", return_value=True ), patch( "app.services.sample_collect_wx_notify._load_donation_context", return_value={ "create_time": datetime(2022, 11, 22, 16, 17), "applicant": "李某", "protocol_status": "sxz", "eta_by_code": {"D1": None, "D2": None}, "sample_status_by_code": {"D1": "yzf", "D2": "ycj"}, "sample_type_by_code": {"D1": "脐带间充质干细胞", "D2": "胎盘亚全能干细胞"}, }, ), patch( "app.services.sample_collect_wx_notify.list_notify_recipient_users", return_value=[user], ), patch( "app.services.sample_collect_wx_notify._already_success", return_value=False, ), patch( "app.services.wechat_mp_message.send_sample_collect_notify", return_value={"success": True}, ) as send, patch( "app.services.sample_collect_wx_notify._write_success_log" ): stats = run_sample_collect_notify( db, tenant_no="zkxw", protocol_id="d1", protocol_kind=PROTOCOL_KIND_DONATION, new_sample_codes=["D1", "D2"], ) assert stats["sent"] == 1 assert stats["skipped"] == 1 assert send.call_count == 1 assert send.call_args.kwargs["sample_type"] == "胎盘亚全能干细胞" assert send.call_args.kwargs["task_type"] == TASK_TYPE_DONATION def test_list_notify_recipient_users_uses_effective_role_including_post(): """接收人须走有效角色(含职位继承),不能只查 sys_user_role。""" db = MagicMock() user = MagicMock() user.id = "u-post" user.wx_mp_openid = "mp-openid" db.query.return_value.filter.return_value.all.return_value = [user] with patch( "app.services.sample_collect_wx_notify.list_user_ids_with_effective_role_name", return_value={"u-post"}, ) as list_ids: from app.services.sample_collect_wx_notify import list_notify_recipient_users rows = list_notify_recipient_users(db, tenant_no="zkxw") list_ids.assert_called_once_with( db, tenant_no="zkxw", role_name="样本采集通知人员" ) assert rows == [user] def test_list_notify_recipient_users_dedupes_same_openid(): db = MagicMock() u1 = MagicMock() u1.id = "u1" u1.wx_mp_openid = "same-oid" u2 = MagicMock() u2.id = "u2" u2.wx_mp_openid = "same-oid" u3 = MagicMock() u3.id = "u3" u3.wx_mp_openid = "other-oid" db.query.return_value.filter.return_value.all.return_value = [u1, u2, u3] with patch( "app.services.sample_collect_wx_notify.list_user_ids_with_effective_role_name", return_value={"u1", "u2", "u3"}, ): from app.services.sample_collect_wx_notify import list_notify_recipient_users rows = list_notify_recipient_users(db, tenant_no="zkxw") assert len(rows) == 2 assert {r.wx_mp_openid for r in rows} == {"same-oid", "other-oid"}