194 lines
7.6 KiB
Python
194 lines
7.6 KiB
Python
from datetime import datetime, timedelta
|
|
|
|
import pytest
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
|
|
@pytest.fixture
|
|
def notices(monkeypatch):
|
|
from app.services import wechat_approval_notices as svc
|
|
from app.services.wechat_notice_types import NoticeSnapshot, PendingApprover
|
|
from app.models.wechat_notice import WechatApprovalCycle, WechatNotice, WechatBinding
|
|
from app.models.models import SysUser
|
|
engine = create_engine('sqlite://')
|
|
for model in (SysUser, WechatApprovalCycle, WechatNotice, WechatBinding):
|
|
model.__table__.create(engine)
|
|
db = sessionmaker(bind=engine, autoflush=False)()
|
|
for uid in ('applicant', 'a', 'b'):
|
|
db.add(SysUser(id=uid, tenant_no='t1', user_no=uid, nick_name=uid))
|
|
db.commit()
|
|
monkeypatch.setattr(svc.settings, 'WECHAT_OA_ENABLED', True)
|
|
snapshot = NoticeSnapshot('报销审批', 'BX001', 'applicant', [PendingApprover('n1', 'a')])
|
|
monkeypatch.setattr(svc, '_snapshot', lambda *args: snapshot)
|
|
yield svc, db, snapshot, WechatNotice, WechatBinding
|
|
db.close()
|
|
|
|
|
|
def test_disabled_does_not_access_new_tables(monkeypatch):
|
|
from app.services import wechat_approval_notices as svc
|
|
monkeypatch.setattr(svc.settings, 'WECHAT_OA_ENABLED', False)
|
|
svc.sync_approval_notifications(None, 'bx_reimburse', 'x', 't1', new_cycle=True)
|
|
|
|
|
|
def test_template_mapping_and_transactional_rollback(notices):
|
|
svc, db, snapshot, Notice, _ = notices
|
|
svc.sync_approval_notifications(db, 'bx_reimburse', 'x', 't1', new_cycle=True)
|
|
db.flush()
|
|
row = db.query(Notice).one()
|
|
import json
|
|
assert json.loads(row.payload)['thing2']['value'] == '报销审批'
|
|
assert json.loads(row.payload)['character_string6']['value'] == 'BX001'
|
|
assert row.recipient_id == 'a'
|
|
db.rollback()
|
|
assert db.query(Notice).count() == 0
|
|
|
|
|
|
def test_dedup_next_step_result_and_resubmit(notices):
|
|
svc, db, snapshot, Notice, _ = notices
|
|
from app.services.wechat_notice_types import PendingApprover
|
|
svc.sync_approval_notifications(db, 'bx_reimburse', 'x', 't1', new_cycle=True)
|
|
svc.sync_approval_notifications(db, 'bx_reimburse', 'x', 't1')
|
|
db.flush()
|
|
assert db.query(Notice).count() == 1
|
|
snapshot.pending = [PendingApprover('n2', 'b')]
|
|
svc.sync_approval_notifications(db, 'bx_reimburse', 'x', 't1')
|
|
db.flush()
|
|
assert db.query(Notice).filter_by(status='pending').one().recipient_id == 'b'
|
|
snapshot.pending = []
|
|
snapshot.result = 'rejected'
|
|
snapshot.approver_id = 'b'
|
|
snapshot.occurred_at = datetime(2026, 10, 27, 14, 0)
|
|
svc.sync_approval_notifications(db, 'bx_reimburse', 'x', 't1')
|
|
svc.sync_approval_notifications(db, 'bx_reimburse', 'x', 't1')
|
|
db.flush()
|
|
import json
|
|
result = db.query(Notice).filter_by(kind='result').one()
|
|
assert result.recipient_id == 'applicant'
|
|
assert json.loads(result.payload)['const5']['value'] == '打回'
|
|
assert json.loads(result.payload)['thing6']['value'] == 'b'
|
|
snapshot.result = None
|
|
snapshot.pending = [PendingApprover('n1', 'a')]
|
|
svc.sync_approval_notifications(db, 'bx_reimburse', 'x', 't1', new_cycle=True)
|
|
db.flush()
|
|
assert db.query(Notice).filter_by(status='pending').count() == 1
|
|
|
|
|
|
def test_no_history_backfill_and_tenant_recipient_guard(notices):
|
|
svc, db, snapshot, Notice, _ = notices
|
|
svc.sync_approval_notifications(db, 'bx_reimburse', 'x', 't1')
|
|
db.flush()
|
|
assert db.query(Notice).count() == 0
|
|
svc.sync_approval_notifications(db, 'bx_reimburse', 'x', 'other', new_cycle=True)
|
|
db.flush()
|
|
assert db.query(Notice).count() == 0
|
|
|
|
|
|
class FakeClient:
|
|
def __init__(self, failure=None):
|
|
self.calls = []
|
|
self.failure = failure
|
|
|
|
def send(self, **kwargs):
|
|
self.calls.append(kwargs)
|
|
if self.failure:
|
|
raise self.failure
|
|
return 'wx-msg-id'
|
|
|
|
|
|
def bind(db, Binding):
|
|
from app.core.config import settings
|
|
db.add(Binding(id='binding', tenant_no='t1', user_id='a', appid=settings.WECHAT_OA_APPID,
|
|
openid='oa-openid', subscribed=1))
|
|
db.commit()
|
|
|
|
|
|
def test_worker_rechecks_withdrawal_before_send(notices):
|
|
svc, db, snapshot, Notice, Binding = notices
|
|
bind(db, Binding)
|
|
svc.sync_approval_notifications(db, 'bx_reimburse', 'x', 't1', new_cycle=True)
|
|
db.commit()
|
|
snapshot.pending = []
|
|
client = FakeClient()
|
|
svc.deliver_pending(db, client=client)
|
|
assert client.calls == []
|
|
assert db.query(Notice).one().status == 'cancelled'
|
|
|
|
|
|
def test_worker_sends_once_with_oa_openid(notices):
|
|
svc, db, snapshot, Notice, Binding = notices
|
|
bind(db, Binding)
|
|
svc.sync_approval_notifications(db, 'bx_reimburse', 'x', 't1', new_cycle=True)
|
|
db.commit()
|
|
client = FakeClient()
|
|
svc.deliver_pending(db, client=client)
|
|
svc.deliver_pending(db, client=client)
|
|
assert len(client.calls) == 1
|
|
assert client.calls[0]['openid'] == 'oa-openid'
|
|
assert db.query(Notice).one().status == 'sent'
|
|
|
|
|
|
def test_ambiguous_send_is_not_automatically_repeated(notices):
|
|
svc, db, snapshot, Notice, Binding = notices
|
|
from app.services.wechat_oa_client import WechatDeliveryUnknown
|
|
bind(db, Binding)
|
|
svc.sync_approval_notifications(db, 'bx_reimburse', 'x', 't1', new_cycle=True)
|
|
db.commit()
|
|
client = FakeClient(WechatDeliveryUnknown('发送结果待核对'))
|
|
svc.deliver_pending(db, client=client)
|
|
svc.deliver_pending(db, client=client)
|
|
assert len(client.calls) == 1
|
|
assert db.query(Notice).one().status == 'unknown'
|
|
|
|
|
|
def test_explicit_transient_failure_is_bounded_retry(notices):
|
|
svc, db, snapshot, Notice, Binding = notices
|
|
from app.services.wechat_oa_client import WechatAPIError
|
|
bind(db, Binding)
|
|
svc.sync_approval_notifications(db, 'bx_reimburse', 'x', 't1', new_cycle=True)
|
|
db.commit()
|
|
client = FakeClient(WechatAPIError(-1))
|
|
for _ in range(6):
|
|
svc.deliver_pending(db, client=client)
|
|
row = db.query(Notice).one()
|
|
row.next_attempt_at = datetime.now() - timedelta(seconds=1)
|
|
db.commit()
|
|
assert len(client.calls) == 5
|
|
assert db.query(Notice).one().status == 'failed'
|
|
|
|
|
|
@pytest.mark.parametrize('biz_type,page', [
|
|
('bx_advance', ''), ('bx_reimburse', ''), ('pl_recharge', ''),
|
|
('pl_freight', ''),
|
|
('crm_quote', 'pages/common/mp-webview?path=quote-detail'),
|
|
('crm_contract', 'pages/common/mp-webview?path=contract-detail'),
|
|
])
|
|
def test_worker_preserves_mini_page_without_web_fallback(notices, monkeypatch, biz_type, page):
|
|
svc, db, snapshot, Notice, Binding = notices
|
|
monkeypatch.setattr(svc.settings, 'WECHAT_OA_PUBLIC_BASE_URL', 'https://portal.example.test')
|
|
snapshot.page = page
|
|
bind(db, Binding)
|
|
svc.sync_approval_notifications(db, biz_type, 'x', 't1', new_cycle=True)
|
|
db.commit()
|
|
client = FakeClient()
|
|
assert svc.deliver_pending(db, client=client) == 1
|
|
assert client.calls[0]['page'] == page
|
|
assert not client.calls[0].get('url')
|
|
|
|
|
|
def test_claimed_assignment_invalid_after_transfer_back(notices):
|
|
svc, db, snapshot, Notice, _ = notices
|
|
from app.services.wechat_notice_types import PendingApprover
|
|
svc.sync_approval_notifications(db, 'bx_reimburse', 'x', 't1', new_cycle=True)
|
|
db.flush()
|
|
old = db.query(Notice).one()
|
|
old.status = 'sending'
|
|
db.commit()
|
|
snapshot.pending = [PendingApprover('n1', 'b')]
|
|
svc.sync_approval_notifications(db, 'bx_reimburse', 'x', 't1')
|
|
snapshot.pending = [PendingApprover('n1', 'a')]
|
|
svc.sync_approval_notifications(db, 'bx_reimburse', 'x', 't1')
|
|
db.commit()
|
|
assert not svc._valid_now(db, old)
|
|
assert svc._valid_now(db, db.query(Notice).filter_by(status='pending', recipient_id='a').one())
|