test_wechat_approval_notices.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. from datetime import datetime, timedelta
  2. import pytest
  3. from sqlalchemy import create_engine
  4. from sqlalchemy.orm import sessionmaker
  5. @pytest.fixture
  6. def notices(monkeypatch):
  7. from app.services import wechat_approval_notices as svc
  8. from app.services.wechat_notice_types import NoticeSnapshot, PendingApprover
  9. from app.models.wechat_notice import WechatApprovalCycle, WechatNotice, WechatBinding
  10. from app.models.models import SysUser
  11. engine = create_engine('sqlite://')
  12. for model in (SysUser, WechatApprovalCycle, WechatNotice, WechatBinding):
  13. model.__table__.create(engine)
  14. db = sessionmaker(bind=engine, autoflush=False)()
  15. for uid in ('applicant', 'a', 'b'):
  16. db.add(SysUser(id=uid, tenant_no='t1', user_no=uid, nick_name=uid))
  17. db.commit()
  18. monkeypatch.setattr(svc.settings, 'WECHAT_OA_ENABLED', True)
  19. snapshot = NoticeSnapshot('报销审批', 'BX001', 'applicant', [PendingApprover('n1', 'a')])
  20. monkeypatch.setattr(svc, '_snapshot', lambda *args: snapshot)
  21. yield svc, db, snapshot, WechatNotice, WechatBinding
  22. db.close()
  23. def test_disabled_does_not_access_new_tables(monkeypatch):
  24. from app.services import wechat_approval_notices as svc
  25. monkeypatch.setattr(svc.settings, 'WECHAT_OA_ENABLED', False)
  26. svc.sync_approval_notifications(None, 'bx_reimburse', 'x', 't1', new_cycle=True)
  27. def test_template_mapping_and_transactional_rollback(notices):
  28. svc, db, snapshot, Notice, _ = notices
  29. svc.sync_approval_notifications(db, 'bx_reimburse', 'x', 't1', new_cycle=True)
  30. db.flush()
  31. row = db.query(Notice).one()
  32. import json
  33. assert json.loads(row.payload)['thing2']['value'] == '报销审批'
  34. assert json.loads(row.payload)['character_string6']['value'] == 'BX001'
  35. assert row.recipient_id == 'a'
  36. db.rollback()
  37. assert db.query(Notice).count() == 0
  38. def test_dedup_next_step_result_and_resubmit(notices):
  39. svc, db, snapshot, Notice, _ = notices
  40. from app.services.wechat_notice_types import PendingApprover
  41. svc.sync_approval_notifications(db, 'bx_reimburse', 'x', 't1', new_cycle=True)
  42. svc.sync_approval_notifications(db, 'bx_reimburse', 'x', 't1')
  43. db.flush()
  44. assert db.query(Notice).count() == 1
  45. snapshot.pending = [PendingApprover('n2', 'b')]
  46. svc.sync_approval_notifications(db, 'bx_reimburse', 'x', 't1')
  47. db.flush()
  48. assert db.query(Notice).filter_by(status='pending').one().recipient_id == 'b'
  49. snapshot.pending = []
  50. snapshot.result = 'rejected'
  51. snapshot.approver_id = 'b'
  52. snapshot.occurred_at = datetime(2026, 10, 27, 14, 0)
  53. svc.sync_approval_notifications(db, 'bx_reimburse', 'x', 't1')
  54. svc.sync_approval_notifications(db, 'bx_reimburse', 'x', 't1')
  55. db.flush()
  56. import json
  57. result = db.query(Notice).filter_by(kind='result').one()
  58. assert result.recipient_id == 'applicant'
  59. assert json.loads(result.payload)['const5']['value'] == '打回'
  60. assert json.loads(result.payload)['thing6']['value'] == 'b'
  61. snapshot.result = None
  62. snapshot.pending = [PendingApprover('n1', 'a')]
  63. svc.sync_approval_notifications(db, 'bx_reimburse', 'x', 't1', new_cycle=True)
  64. db.flush()
  65. assert db.query(Notice).filter_by(status='pending').count() == 1
  66. def test_no_history_backfill_and_tenant_recipient_guard(notices):
  67. svc, db, snapshot, Notice, _ = notices
  68. svc.sync_approval_notifications(db, 'bx_reimburse', 'x', 't1')
  69. db.flush()
  70. assert db.query(Notice).count() == 0
  71. svc.sync_approval_notifications(db, 'bx_reimburse', 'x', 'other', new_cycle=True)
  72. db.flush()
  73. assert db.query(Notice).count() == 0
  74. class FakeClient:
  75. def __init__(self, failure=None):
  76. self.calls = []
  77. self.failure = failure
  78. def send(self, **kwargs):
  79. self.calls.append(kwargs)
  80. if self.failure:
  81. raise self.failure
  82. return 'wx-msg-id'
  83. def bind(db, Binding):
  84. from app.core.config import settings
  85. db.add(Binding(id='binding', tenant_no='t1', user_id='a', appid=settings.WECHAT_OA_APPID,
  86. openid='oa-openid', subscribed=1))
  87. db.commit()
  88. def test_worker_rechecks_withdrawal_before_send(notices):
  89. svc, db, snapshot, Notice, Binding = notices
  90. bind(db, Binding)
  91. svc.sync_approval_notifications(db, 'bx_reimburse', 'x', 't1', new_cycle=True)
  92. db.commit()
  93. snapshot.pending = []
  94. client = FakeClient()
  95. svc.deliver_pending(db, client=client)
  96. assert client.calls == []
  97. assert db.query(Notice).one().status == 'cancelled'
  98. def test_worker_sends_once_with_oa_openid(notices):
  99. svc, db, snapshot, Notice, Binding = notices
  100. bind(db, Binding)
  101. svc.sync_approval_notifications(db, 'bx_reimburse', 'x', 't1', new_cycle=True)
  102. db.commit()
  103. client = FakeClient()
  104. svc.deliver_pending(db, client=client)
  105. svc.deliver_pending(db, client=client)
  106. assert len(client.calls) == 1
  107. assert client.calls[0]['openid'] == 'oa-openid'
  108. assert db.query(Notice).one().status == 'sent'
  109. def test_ambiguous_send_is_not_automatically_repeated(notices):
  110. svc, db, snapshot, Notice, Binding = notices
  111. from app.services.wechat_oa_client import WechatDeliveryUnknown
  112. bind(db, Binding)
  113. svc.sync_approval_notifications(db, 'bx_reimburse', 'x', 't1', new_cycle=True)
  114. db.commit()
  115. client = FakeClient(WechatDeliveryUnknown('发送结果待核对'))
  116. svc.deliver_pending(db, client=client)
  117. svc.deliver_pending(db, client=client)
  118. assert len(client.calls) == 1
  119. assert db.query(Notice).one().status == 'unknown'
  120. def test_explicit_transient_failure_is_bounded_retry(notices):
  121. svc, db, snapshot, Notice, Binding = notices
  122. from app.services.wechat_oa_client import WechatAPIError
  123. bind(db, Binding)
  124. svc.sync_approval_notifications(db, 'bx_reimburse', 'x', 't1', new_cycle=True)
  125. db.commit()
  126. client = FakeClient(WechatAPIError(-1))
  127. for _ in range(6):
  128. svc.deliver_pending(db, client=client)
  129. row = db.query(Notice).one()
  130. row.next_attempt_at = datetime.now() - timedelta(seconds=1)
  131. db.commit()
  132. assert len(client.calls) == 5
  133. assert db.query(Notice).one().status == 'failed'
  134. @pytest.mark.parametrize('biz_type,path', [
  135. ('bx_advance', '/bx/advance/approve'),
  136. ('bx_reimburse', '/bx/reimburse/approve'),
  137. ('pl_recharge', '/vehicle-mgmt/etc-recharge/vehicle-recharge-approval'),
  138. ])
  139. def test_pending_links_to_real_approval_screen(monkeypatch, biz_type, path):
  140. from types import SimpleNamespace
  141. from app.services import wechat_approval_notices as svc
  142. monkeypatch.setattr(svc.settings, 'WECHAT_OA_PUBLIC_BASE_URL', 'https://portal.example.test')
  143. assert svc._fallback_url(SimpleNamespace(business_type=biz_type, kind='pending')) == 'https://portal.example.test/#' + path
  144. def test_claimed_assignment_invalid_after_transfer_back(notices):
  145. svc, db, snapshot, Notice, _ = notices
  146. from app.services.wechat_notice_types import PendingApprover
  147. svc.sync_approval_notifications(db, 'bx_reimburse', 'x', 't1', new_cycle=True)
  148. db.flush()
  149. old = db.query(Notice).one()
  150. old.status = 'sending'
  151. db.commit()
  152. snapshot.pending = [PendingApprover('n1', 'b')]
  153. svc.sync_approval_notifications(db, 'bx_reimburse', 'x', 't1')
  154. snapshot.pending = [PendingApprover('n1', 'a')]
  155. svc.sync_approval_notifications(db, 'bx_reimburse', 'x', 't1')
  156. db.commit()
  157. assert not svc._valid_now(db, old)
  158. assert svc._valid_now(db, db.query(Notice).filter_by(status='pending', recipient_id='a').one())