test_wechat_cert_notices.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. """证件每日汇总以真实证件、当前角色和现有列表数据权限为准。"""
  2. import importlib
  3. import importlib.util
  4. import json
  5. from datetime import datetime, timedelta, timezone
  6. from types import SimpleNamespace
  7. import pytest
  8. from sqlalchemy import create_engine
  9. from sqlalchemy.orm import sessionmaker
  10. def test_cert_notice_module_exists():
  11. assert importlib.util.find_spec("app.services.wechat_cert_notices") is not None
  12. @pytest.fixture
  13. def cert_notices(monkeypatch):
  14. assert importlib.util.find_spec("app.services.wechat_cert_notices") is not None
  15. svc = importlib.import_module("app.services.wechat_cert_notices")
  16. from app.services import wechat_notice_events
  17. from app.models.models import SysRole, SysRoleDataScopeDetail, SysUser, SysUserRole
  18. from app.models.pl_models import PlCert, PlDocumentReminder
  19. from app.models.wechat_notice import WechatNotice
  20. engine = create_engine("sqlite://")
  21. for model in (SysRole, SysRoleDataScopeDetail, SysUser, SysUserRole,
  22. PlCert, PlDocumentReminder, WechatNotice):
  23. model.__table__.create(engine)
  24. db = sessionmaker(bind=engine, autoflush=False)()
  25. monkeypatch.setattr(svc, "settings", SimpleNamespace(
  26. WECHAT_OA_ENABLED=True, WECHAT_OA_CERT_REMINDER_ENABLED=True,
  27. WECHAT_OA_CERT_REMINDER_HOUR=10, WECHAT_OA_CERT_REMINDER_MINUTE=0,
  28. WECHAT_OA_TEMPLATE_CERT_REMINDER="aYzLZ_EqI8hq7RXrlf73BTAfNvzgJHeg_xWKar01Md0",
  29. WECHAT_OA_CERT_NOTICE_ROLE_NAME="证件到期通知接收人",
  30. ))
  31. monkeypatch.setattr(wechat_notice_events.settings, "WECHAT_OA_ENABLED", True)
  32. for tenant in ("t1", "t2"):
  33. for role_id, name, scope in (("notice", "证件到期通知接收人", "5"),
  34. ("self", "本人", "4")):
  35. db.add(SysRole(id=f"{tenant}-{role_id}", tenant_no=tenant,
  36. role_name=name, data_scope=scope))
  37. for uid in ("a", "b"):
  38. user_id = f"{tenant}-{uid}"
  39. db.add(SysUser(id=user_id, tenant_no=tenant, user_no=user_id,
  40. nick_name=user_id, dept_id=f"{tenant}-dept"))
  41. for role_id in ("notice", "self"):
  42. db.add(SysUserRole(id=f"{user_id}-{role_id}", tenant_no=tenant,
  43. user_id=user_id, role_id=f"{tenant}-{role_id}"))
  44. db.commit()
  45. now = datetime(2026, 10, 26, 10, 0)
  46. def add_cert(cid="cert-1", tenant="t1", creator="t1-a", days=10,
  47. reminder_creator=None, **kwargs):
  48. cert = PlCert(id=cid, tenant_no=tenant, owner_type="personnel", owner_id="owner",
  49. cert_name="从业资格证", expire_date=now.date() + timedelta(days=days),
  50. remind_days=30, create_by=creator, **kwargs)
  51. db.add(cert)
  52. db.add(PlDocumentReminder(id=f"reminder-{cid}", tenant_no=tenant, cert_id=cid,
  53. document_type=cert.cert_name, expire_date=cert.expire_date,
  54. days_remaining=days, create_by=reminder_creator or creator))
  55. db.flush()
  56. return cert
  57. yield svc, db, now, add_cert, WechatNotice
  58. db.close()
  59. engine.dispose()
  60. @pytest.mark.parametrize("flag", ["WECHAT_OA_ENABLED", "WECHAT_OA_CERT_REMINDER_ENABLED"])
  61. def test_disabled_skips_database(cert_notices, flag):
  62. svc, _, now, _, _ = cert_notices
  63. setattr(svc.settings, flag, False)
  64. assert svc.enqueue_daily_cert_notices(None, now=now) == 0
  65. assert svc.refresh_cert_notice(None, None, now=now) is False
  66. def test_daily_dedup_template_and_tenant_scope(cert_notices):
  67. svc, db, now, add_cert, Notice = cert_notices
  68. add_cert()
  69. add_cert("cert-2", tenant="t2", creator="t2-b")
  70. assert svc.enqueue_daily_cert_notices(db, now=now) == 2
  71. assert svc.enqueue_daily_cert_notices(db, now=now + timedelta(hours=1)) == 0
  72. rows = db.query(Notice).all()
  73. assert {(n.tenant_no, n.recipient_id) for n in rows} == {("t1", "t1-a"), ("t2", "t2-b")}
  74. notice = rows[0]
  75. assert notice.kind == "cert_daily"
  76. assert notice.business_id == notice.recipient_id
  77. assert notice.cycle_id == "2026-10-26"
  78. assert notice.template_id == svc.settings.WECHAT_OA_TEMPLATE_CERT_REMINDER
  79. assert json.loads(notice.payload) == {
  80. "thing5": {"value": "1项即将到期"},
  81. "time6": {"value": "2026-10-26 10:00:00"},
  82. "thing3": {"value": "系统默认发出"},
  83. }
  84. def test_no_commit_and_no_warning_no_send(cert_notices):
  85. svc, db, now, add_cert, Notice = cert_notices
  86. add_cert(days=60)
  87. assert svc.enqueue_daily_cert_notices(db, now=now) == 0
  88. add_cert("near")
  89. assert svc.enqueue_daily_cert_notices(db, now=now) == 1
  90. db.rollback()
  91. assert db.query(Notice).count() == 0
  92. def test_union_of_roles_and_scope_matches_reminder_list(cert_notices):
  93. from app.models.models import SysRole, SysRoleDataScopeDetail, SysUserRole
  94. svc, db, now, add_cert, Notice = cert_notices
  95. add_cert("by-self")
  96. add_cert("by-other", creator="other", reminder_creator="other")
  97. db.add(SysRole(id="custom", tenant_no="t1", role_name="附加可见", data_scope="5"))
  98. db.add(SysUserRole(id="a-custom", tenant_no="t1", user_id="t1-a", role_id="custom"))
  99. db.add(SysRoleDataScopeDetail(id="custom-user", tenant_no="t1", role_id="custom",
  100. scope_type="user", scope_value="other"))
  101. db.flush()
  102. assert svc.enqueue_daily_cert_notices(db, now=now) == 1
  103. notice = db.query(Notice).one()
  104. # 移除本人范围后,另一角色的数据范围仍然有效(并集)。
  105. db.query(SysUserRole).filter_by(id="t1-a-self").update({"del_flag": 1})
  106. assert svc.refresh_cert_notice(db, notice, now=now) is True
  107. db.query(SysRoleDataScopeDetail).filter_by(id="custom-user").update({"del_flag": 1})
  108. assert svc.refresh_cert_notice(db, notice, now=now) is False
  109. @pytest.mark.parametrize("change", ["renew", "deleted", "revoked", "no_expiry", "resolved",
  110. "user_disabled", "role_disabled", "role_removed", "scope_removed"])
  111. def test_refresh_rechecks_current_business_and_recipient(cert_notices, change):
  112. from app.models.models import SysRole, SysUser, SysUserRole
  113. from app.models.pl_models import PlDocumentReminder
  114. svc, db, now, add_cert, Notice = cert_notices
  115. cert = add_cert()
  116. svc.enqueue_daily_cert_notices(db, now=now)
  117. notice = db.query(Notice).one()
  118. if change == "renew":
  119. cert.expire_date = now.date() + timedelta(days=90)
  120. elif change == "deleted":
  121. cert.del_flag = 1
  122. elif change == "revoked":
  123. cert.status = "revoked"
  124. elif change == "no_expiry":
  125. cert.expire_date = None
  126. elif change == "resolved":
  127. db.query(PlDocumentReminder).update({"status": "resolved"})
  128. elif change == "user_disabled":
  129. db.query(SysUser).filter_by(id="t1-a").update({"status": "0"})
  130. elif change == "role_disabled":
  131. db.query(SysRole).filter_by(id="t1-notice").update({"status": "0"})
  132. else:
  133. role = "notice" if change == "role_removed" else "self"
  134. db.query(SysUserRole).filter_by(id=f"t1-a-{role}").update({"del_flag": 1})
  135. assert svc.refresh_cert_notice(db, notice, now=now) is False
  136. def test_refresh_rebuilds_payload_at_shanghai_send_time_and_expires_next_day(cert_notices):
  137. svc, db, now, add_cert, Notice = cert_notices
  138. add_cert()
  139. svc.enqueue_daily_cert_notices(db, now=now)
  140. notice = db.query(Notice).one()
  141. utc_send = datetime(2026, 10, 26, 2, 2, 3, tzinfo=timezone.utc)
  142. assert svc.refresh_cert_notice(db, notice, now=utc_send) is True
  143. assert json.loads(notice.payload)["time6"]["value"] == "2026-10-26 10:02:03"
  144. assert svc.refresh_cert_notice(db, notice, now=datetime(2026, 10, 26, 16, tzinfo=timezone.utc)) is False
  145. assert svc.enqueue_daily_cert_notices(db, now=now + timedelta(days=2)) == 1
  146. def test_reminder_requires_live_same_tenant_certificate(cert_notices):
  147. from app.models.pl_models import PlDocumentReminder
  148. svc, db, now, add_cert, Notice = cert_notices
  149. add_cert(tenant="t2", creator="t2-a")
  150. reminder = db.query(PlDocumentReminder).one()
  151. reminder.tenant_no = "t1"
  152. reminder.create_by = "t1-a"
  153. assert svc.enqueue_daily_cert_notices(db, now=now) == 0
  154. reminder.cert_id = "missing"
  155. assert svc.enqueue_daily_cert_notices(db, now=now) == 0
  156. assert db.query(Notice).count() == 0
  157. def test_foreign_tenant_role_assignment_is_ignored(cert_notices):
  158. from app.models.models import SysUserRole
  159. svc, db, now, add_cert, _ = cert_notices
  160. add_cert()
  161. assignment = db.query(SysUserRole).filter_by(id="t1-a-notice").one()
  162. assignment.role_id = "t2-notice"
  163. assert svc.enqueue_daily_cert_notices(db, now=now) == 0
  164. def test_duplicate_reminders_and_stale_remaining_days_do_not_change_daily_gate(cert_notices):
  165. from app.models.pl_models import PlDocumentReminder
  166. svc, db, now, add_cert, Notice = cert_notices
  167. cert = add_cert(days=30)
  168. reminder = db.query(PlDocumentReminder).one()
  169. reminder.days_remaining = 900
  170. db.add(PlDocumentReminder(id="duplicate", tenant_no="t1", cert_id=cert.id,
  171. document_type=cert.cert_name, expire_date=cert.expire_date,
  172. days_remaining=900, create_by="t1-a"))
  173. assert svc.enqueue_daily_cert_notices(db, now=now) == 1
  174. assert db.query(Notice).count() == 1
  175. assert json.loads(db.query(Notice).one().payload)["thing5"]["value"] == "1项即将到期"
  176. cert.expire_date = now.date() + timedelta(days=31)
  177. assert svc.refresh_cert_notice(db, db.query(Notice).one(), now=now) is False
  178. def test_summary_counts_expired_and_upcoming_and_refreshes_after_renewal(cert_notices):
  179. svc, db, now, add_cert, Notice = cert_notices
  180. add_cert("upcoming", days=2)
  181. expired = add_cert("expired", days=-2)
  182. add_cert("today", days=0)
  183. add_cert("hidden", creator="other", days=-1)
  184. svc.enqueue_daily_cert_notices(db, now=now)
  185. notice = db.query(Notice).one()
  186. assert json.loads(notice.payload)["thing5"]["value"] == "1项即将到期,2项已到期"
  187. expired.expire_date = now.date() + timedelta(days=60)
  188. assert svc.refresh_cert_notice(db, notice, now=now) is True
  189. assert json.loads(notice.payload)["thing5"]["value"] == "1项即将到期,1项已到期"
  190. def test_long_summary_uses_short_total_title(cert_notices):
  191. svc, _, now, _, _ = cert_notices
  192. payload = svc._payload(now, 1234567, 1234567)
  193. assert payload["thing5"]["value"] == "2469134项证件到期提醒"
  194. assert len(payload["thing5"]["value"]) <= 20
  195. def test_scope_is_based_on_reminder_creator_as_in_list(cert_notices):
  196. svc, db, now, add_cert, Notice = cert_notices
  197. add_cert(creator="t1-a", reminder_creator="t1-b")
  198. svc.enqueue_daily_cert_notices(db, now=now)
  199. assert db.query(Notice).one().recipient_id == "t1-b"
  200. @pytest.mark.parametrize("day,hour,expected", [
  201. (26, 9, 0), (26, 10, 1), (27, 10, 0), (28, 10, 1),
  202. (29, 10, 0), (30, 10, 1), (31, 10, 0),
  203. ])
  204. def test_only_monday_wednesday_friday_after_ten_enqueue(cert_notices, day, hour, expected):
  205. svc, db, _, add_cert, Notice = cert_notices
  206. add_cert()
  207. assert svc.enqueue_daily_cert_notices(db, now=datetime(2026, 10, day, hour)) == expected
  208. assert db.query(Notice).count() == expected
  209. def test_sunday_and_old_non_schedule_day_notice_never_send(cert_notices):
  210. svc, db, now, add_cert, Notice = cert_notices
  211. add_cert()
  212. svc.enqueue_daily_cert_notices(db, now=now)
  213. notice = db.query(Notice).one()
  214. for when in (datetime(2026, 10, 27, 10), datetime(2026, 11, 1, 10),
  215. datetime(2026, 10, 26, 9)):
  216. notice.cycle_id = when.date().isoformat()
  217. assert not svc.refresh_cert_notice(db, notice, now=when)
  218. assert svc.enqueue_daily_cert_notices(db, now=datetime(2026, 11, 1, 10)) == 0
  219. def test_enqueue_uses_shanghai_weekday_and_hour(cert_notices):
  220. svc, db, _, add_cert, Notice = cert_notices
  221. add_cert()
  222. # UTC周二晚上是上海周三,但必须等到上海10点。
  223. assert svc.enqueue_daily_cert_notices(db, now=datetime(2026, 10, 27, 23, tzinfo=timezone.utc)) == 0
  224. assert svc.enqueue_daily_cert_notices(db, now=datetime(2026, 10, 28, 2, tzinfo=timezone.utc)) == 1
  225. assert db.query(Notice).one().cycle_id == '2026-10-28'