| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263 |
- """证件每日汇总以真实证件、当前角色和现有列表数据权限为准。"""
- import importlib
- import importlib.util
- import json
- from datetime import datetime, timedelta, timezone
- from types import SimpleNamespace
- import pytest
- from sqlalchemy import create_engine
- from sqlalchemy.orm import sessionmaker
- def test_cert_notice_module_exists():
- assert importlib.util.find_spec("app.services.wechat_cert_notices") is not None
- @pytest.fixture
- def cert_notices(monkeypatch):
- assert importlib.util.find_spec("app.services.wechat_cert_notices") is not None
- svc = importlib.import_module("app.services.wechat_cert_notices")
- from app.services import wechat_notice_events
- from app.models.models import SysRole, SysRoleDataScopeDetail, SysUser, SysUserRole
- from app.models.pl_models import PlCert, PlDocumentReminder
- from app.models.wechat_notice import WechatNotice
- engine = create_engine("sqlite://")
- for model in (SysRole, SysRoleDataScopeDetail, SysUser, SysUserRole,
- PlCert, PlDocumentReminder, WechatNotice):
- model.__table__.create(engine)
- db = sessionmaker(bind=engine, autoflush=False)()
- monkeypatch.setattr(svc, "settings", SimpleNamespace(
- WECHAT_OA_ENABLED=True, WECHAT_OA_CERT_REMINDER_ENABLED=True,
- WECHAT_OA_CERT_REMINDER_HOUR=10, WECHAT_OA_CERT_REMINDER_MINUTE=0,
- WECHAT_OA_TEMPLATE_CERT_REMINDER="aYzLZ_EqI8hq7RXrlf73BTAfNvzgJHeg_xWKar01Md0",
- WECHAT_OA_CERT_NOTICE_ROLE_NAME="证件到期通知接收人",
- ))
- monkeypatch.setattr(wechat_notice_events.settings, "WECHAT_OA_ENABLED", True)
- for tenant in ("t1", "t2"):
- for role_id, name, scope in (("notice", "证件到期通知接收人", "5"),
- ("self", "本人", "4")):
- db.add(SysRole(id=f"{tenant}-{role_id}", tenant_no=tenant,
- role_name=name, data_scope=scope))
- for uid in ("a", "b"):
- user_id = f"{tenant}-{uid}"
- db.add(SysUser(id=user_id, tenant_no=tenant, user_no=user_id,
- nick_name=user_id, dept_id=f"{tenant}-dept"))
- for role_id in ("notice", "self"):
- db.add(SysUserRole(id=f"{user_id}-{role_id}", tenant_no=tenant,
- user_id=user_id, role_id=f"{tenant}-{role_id}"))
- db.commit()
- now = datetime(2026, 10, 26, 10, 0)
- def add_cert(cid="cert-1", tenant="t1", creator="t1-a", days=10,
- reminder_creator=None, **kwargs):
- cert = PlCert(id=cid, tenant_no=tenant, owner_type="personnel", owner_id="owner",
- cert_name="从业资格证", expire_date=now.date() + timedelta(days=days),
- remind_days=30, create_by=creator, **kwargs)
- db.add(cert)
- db.add(PlDocumentReminder(id=f"reminder-{cid}", tenant_no=tenant, cert_id=cid,
- document_type=cert.cert_name, expire_date=cert.expire_date,
- days_remaining=days, create_by=reminder_creator or creator))
- db.flush()
- return cert
- yield svc, db, now, add_cert, WechatNotice
- db.close()
- engine.dispose()
- @pytest.mark.parametrize("flag", ["WECHAT_OA_ENABLED", "WECHAT_OA_CERT_REMINDER_ENABLED"])
- def test_disabled_skips_database(cert_notices, flag):
- svc, _, now, _, _ = cert_notices
- setattr(svc.settings, flag, False)
- assert svc.enqueue_daily_cert_notices(None, now=now) == 0
- assert svc.refresh_cert_notice(None, None, now=now) is False
- def test_daily_dedup_template_and_tenant_scope(cert_notices):
- svc, db, now, add_cert, Notice = cert_notices
- add_cert()
- add_cert("cert-2", tenant="t2", creator="t2-b")
- assert svc.enqueue_daily_cert_notices(db, now=now) == 2
- assert svc.enqueue_daily_cert_notices(db, now=now + timedelta(hours=1)) == 0
- rows = db.query(Notice).all()
- assert {(n.tenant_no, n.recipient_id) for n in rows} == {("t1", "t1-a"), ("t2", "t2-b")}
- notice = rows[0]
- assert notice.kind == "cert_daily"
- assert notice.business_id == notice.recipient_id
- assert notice.cycle_id == "2026-10-26"
- assert notice.template_id == svc.settings.WECHAT_OA_TEMPLATE_CERT_REMINDER
- assert json.loads(notice.payload) == {
- "thing5": {"value": "1项即将到期"},
- "time6": {"value": "2026-10-26 10:00:00"},
- "thing3": {"value": "系统默认发出"},
- }
- def test_no_commit_and_no_warning_no_send(cert_notices):
- svc, db, now, add_cert, Notice = cert_notices
- add_cert(days=60)
- assert svc.enqueue_daily_cert_notices(db, now=now) == 0
- add_cert("near")
- assert svc.enqueue_daily_cert_notices(db, now=now) == 1
- db.rollback()
- assert db.query(Notice).count() == 0
- def test_union_of_roles_and_scope_matches_reminder_list(cert_notices):
- from app.models.models import SysRole, SysRoleDataScopeDetail, SysUserRole
- svc, db, now, add_cert, Notice = cert_notices
- add_cert("by-self")
- add_cert("by-other", creator="other", reminder_creator="other")
- db.add(SysRole(id="custom", tenant_no="t1", role_name="附加可见", data_scope="5"))
- db.add(SysUserRole(id="a-custom", tenant_no="t1", user_id="t1-a", role_id="custom"))
- db.add(SysRoleDataScopeDetail(id="custom-user", tenant_no="t1", role_id="custom",
- scope_type="user", scope_value="other"))
- db.flush()
- assert svc.enqueue_daily_cert_notices(db, now=now) == 1
- notice = db.query(Notice).one()
- # 移除本人范围后,另一角色的数据范围仍然有效(并集)。
- db.query(SysUserRole).filter_by(id="t1-a-self").update({"del_flag": 1})
- assert svc.refresh_cert_notice(db, notice, now=now) is True
- db.query(SysRoleDataScopeDetail).filter_by(id="custom-user").update({"del_flag": 1})
- assert svc.refresh_cert_notice(db, notice, now=now) is False
- @pytest.mark.parametrize("change", ["renew", "deleted", "revoked", "no_expiry", "resolved",
- "user_disabled", "role_disabled", "role_removed", "scope_removed"])
- def test_refresh_rechecks_current_business_and_recipient(cert_notices, change):
- from app.models.models import SysRole, SysUser, SysUserRole
- from app.models.pl_models import PlDocumentReminder
- svc, db, now, add_cert, Notice = cert_notices
- cert = add_cert()
- svc.enqueue_daily_cert_notices(db, now=now)
- notice = db.query(Notice).one()
- if change == "renew":
- cert.expire_date = now.date() + timedelta(days=90)
- elif change == "deleted":
- cert.del_flag = 1
- elif change == "revoked":
- cert.status = "revoked"
- elif change == "no_expiry":
- cert.expire_date = None
- elif change == "resolved":
- db.query(PlDocumentReminder).update({"status": "resolved"})
- elif change == "user_disabled":
- db.query(SysUser).filter_by(id="t1-a").update({"status": "0"})
- elif change == "role_disabled":
- db.query(SysRole).filter_by(id="t1-notice").update({"status": "0"})
- else:
- role = "notice" if change == "role_removed" else "self"
- db.query(SysUserRole).filter_by(id=f"t1-a-{role}").update({"del_flag": 1})
- assert svc.refresh_cert_notice(db, notice, now=now) is False
- def test_refresh_rebuilds_payload_at_shanghai_send_time_and_expires_next_day(cert_notices):
- svc, db, now, add_cert, Notice = cert_notices
- add_cert()
- svc.enqueue_daily_cert_notices(db, now=now)
- notice = db.query(Notice).one()
- utc_send = datetime(2026, 10, 26, 2, 2, 3, tzinfo=timezone.utc)
- assert svc.refresh_cert_notice(db, notice, now=utc_send) is True
- assert json.loads(notice.payload)["time6"]["value"] == "2026-10-26 10:02:03"
- assert svc.refresh_cert_notice(db, notice, now=datetime(2026, 10, 26, 16, tzinfo=timezone.utc)) is False
- assert svc.enqueue_daily_cert_notices(db, now=now + timedelta(days=2)) == 1
- def test_reminder_requires_live_same_tenant_certificate(cert_notices):
- from app.models.pl_models import PlDocumentReminder
- svc, db, now, add_cert, Notice = cert_notices
- add_cert(tenant="t2", creator="t2-a")
- reminder = db.query(PlDocumentReminder).one()
- reminder.tenant_no = "t1"
- reminder.create_by = "t1-a"
- assert svc.enqueue_daily_cert_notices(db, now=now) == 0
- reminder.cert_id = "missing"
- assert svc.enqueue_daily_cert_notices(db, now=now) == 0
- assert db.query(Notice).count() == 0
- def test_foreign_tenant_role_assignment_is_ignored(cert_notices):
- from app.models.models import SysUserRole
- svc, db, now, add_cert, _ = cert_notices
- add_cert()
- assignment = db.query(SysUserRole).filter_by(id="t1-a-notice").one()
- assignment.role_id = "t2-notice"
- assert svc.enqueue_daily_cert_notices(db, now=now) == 0
- def test_duplicate_reminders_and_stale_remaining_days_do_not_change_daily_gate(cert_notices):
- from app.models.pl_models import PlDocumentReminder
- svc, db, now, add_cert, Notice = cert_notices
- cert = add_cert(days=30)
- reminder = db.query(PlDocumentReminder).one()
- reminder.days_remaining = 900
- db.add(PlDocumentReminder(id="duplicate", tenant_no="t1", cert_id=cert.id,
- document_type=cert.cert_name, expire_date=cert.expire_date,
- days_remaining=900, create_by="t1-a"))
- assert svc.enqueue_daily_cert_notices(db, now=now) == 1
- assert db.query(Notice).count() == 1
- assert json.loads(db.query(Notice).one().payload)["thing5"]["value"] == "1项即将到期"
- cert.expire_date = now.date() + timedelta(days=31)
- assert svc.refresh_cert_notice(db, db.query(Notice).one(), now=now) is False
- def test_summary_counts_expired_and_upcoming_and_refreshes_after_renewal(cert_notices):
- svc, db, now, add_cert, Notice = cert_notices
- add_cert("upcoming", days=2)
- expired = add_cert("expired", days=-2)
- add_cert("today", days=0)
- add_cert("hidden", creator="other", days=-1)
- svc.enqueue_daily_cert_notices(db, now=now)
- notice = db.query(Notice).one()
- assert json.loads(notice.payload)["thing5"]["value"] == "1项即将到期,2项已到期"
- expired.expire_date = now.date() + timedelta(days=60)
- assert svc.refresh_cert_notice(db, notice, now=now) is True
- assert json.loads(notice.payload)["thing5"]["value"] == "1项即将到期,1项已到期"
- def test_long_summary_uses_short_total_title(cert_notices):
- svc, _, now, _, _ = cert_notices
- payload = svc._payload(now, 1234567, 1234567)
- assert payload["thing5"]["value"] == "2469134项证件到期提醒"
- assert len(payload["thing5"]["value"]) <= 20
- def test_scope_is_based_on_reminder_creator_as_in_list(cert_notices):
- svc, db, now, add_cert, Notice = cert_notices
- add_cert(creator="t1-a", reminder_creator="t1-b")
- svc.enqueue_daily_cert_notices(db, now=now)
- assert db.query(Notice).one().recipient_id == "t1-b"
- @pytest.mark.parametrize("day,hour,expected", [
- (26, 9, 0), (26, 10, 1), (27, 10, 0), (28, 10, 1),
- (29, 10, 0), (30, 10, 1), (31, 10, 0),
- ])
- def test_only_monday_wednesday_friday_after_ten_enqueue(cert_notices, day, hour, expected):
- svc, db, _, add_cert, Notice = cert_notices
- add_cert()
- assert svc.enqueue_daily_cert_notices(db, now=datetime(2026, 10, day, hour)) == expected
- assert db.query(Notice).count() == expected
- def test_sunday_and_old_non_schedule_day_notice_never_send(cert_notices):
- svc, db, now, add_cert, Notice = cert_notices
- add_cert()
- svc.enqueue_daily_cert_notices(db, now=now)
- notice = db.query(Notice).one()
- for when in (datetime(2026, 10, 27, 10), datetime(2026, 11, 1, 10),
- datetime(2026, 10, 26, 9)):
- notice.cycle_id = when.date().isoformat()
- assert not svc.refresh_cert_notice(db, notice, now=when)
- assert svc.enqueue_daily_cert_notices(db, now=datetime(2026, 11, 1, 10)) == 0
- def test_enqueue_uses_shanghai_weekday_and_hour(cert_notices):
- svc, db, _, add_cert, Notice = cert_notices
- add_cert()
- # UTC周二晚上是上海周三,但必须等到上海10点。
- assert svc.enqueue_daily_cert_notices(db, now=datetime(2026, 10, 27, 23, tzinfo=timezone.utc)) == 0
- assert svc.enqueue_daily_cert_notices(db, now=datetime(2026, 10, 28, 2, tzinfo=timezone.utc)) == 1
- assert db.query(Notice).one().cycle_id == '2026-10-28'
|