Files
zkxw/backend/tests/test_sample_collect_wx_notify.py
T
zlwuandCursor 5e563a6669 fix: 子表保存改为就地同步,避免整批换 id 打断外键。
协议样本、生产产出和盘点明细按行更新并保留原 id;补齐存储证明质检预填、离职人员显示名,以及样本采集通知模板字段。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-28 10:51:53 +08:00

406 lines
14 KiB
Python

"""协议新增样本服务号通知单测。"""
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"}