1
0
forked from erp-dev/erp
Files
erpnew/notifier/tests.py
2026-07-11 00:05:05 +08:00

674 lines
28 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import json
from unittest.mock import MagicMock, patch
from django.test import TestCase
from django.utils import timezone
from basic_info.models import Employee, Merchant, MerchantTypeEnum
from mission.models import Mission, MissionCategory, MissionReply
from notifier.models import NotificationEventKeyEnum, Notifier, NotifierChannelEnum, NotifierRoute
from notifier.services import (
dispatch_notification_event,
enqueue_notification_event,
render_notification_content,
send_notification_with_notifier,
)
from notifier.tasks import notify_unreplied_missions_task
class NotifierServiceTestCase(TestCase):
def setUp(self):
self.merchant = Merchant.objects.create(name="通知商户", type=MerchantTypeEnum.STORE)
self.general_category = MissionCategory.objects.create(merchant=self.merchant, name="通用")
self.after_sale_category = MissionCategory.objects.create(merchant=self.merchant, name="售后")
self.notifier = Notifier.objects.create(
merchant=self.merchant,
name="任务创建通知",
channel=NotifierChannelEnum.WECOM_WEBHOOK,
template_key="mission_created",
config={"key": "abc123", "msgtype": "markdown"},
)
self.global_route = NotifierRoute.objects.create(
merchant=self.merchant,
notifier=self.notifier,
event_key=NotificationEventKeyEnum.MISSION_CREATED,
)
self.creator = Employee.objects.create(merchant=self.merchant, name="任务创建者")
def test_render_notification_content(self):
content = render_notification_content(
notifier=self.notifier,
payload={
"mission_id": 12,
"created_by_name": "张三",
"creator_name": "张三",
"category_name": "通用",
"is_urgent": False,
"participant_names_display": "李四、王五",
"description": "检查打印质量",
},
)
self.assertIn("任务已创建", content)
self.assertIn("任务ID12", content)
self.assertIn("创建人:张三", content)
def test_render_notification_content_with_structured_description_template(self):
notifier = Notifier.objects.create(
merchant=self.merchant,
name="结构化描述通知",
channel=NotifierChannelEnum.MESSAGE_API,
template_key="mission_structured_description_text",
config={"agent_id": 1000007},
)
content = render_notification_content(
notifier=notifier,
payload={
"mission_id": 12,
"creator_name": "张三",
"category_name": "通用",
"description": "原始描述",
"parsed_description_title": "标题一\n标题二",
"parsed_description_body": "正文一\n正文二",
"parsed_description_url": "https://example.com/detail",
"parsed_description_image_url": "https://images.yuwen.cloud/abc.jpg",
},
)
rendered_payload = json.loads(content)
self.assertEqual(rendered_payload["msgtype"], "text")
self.assertIn("标题:标题一\n标题二", rendered_payload["content"])
self.assertIn("正文:正文一\n正文二", rendered_payload["content"])
self.assertIn("链接https://example.com/detail", rendered_payload["content"])
def test_render_notification_content_with_structured_description_news_template(self):
notifier = Notifier.objects.create(
merchant=self.merchant,
name="结构化描述图文通知",
channel=NotifierChannelEnum.MESSAGE_API,
template_key="mission_structured_description_news",
config={
"agent_ids": [1000007],
"image_url": "https://cdn.example.com/covers/mission-news.png",
"url": "https://erp.example.com/missions/fallback",
},
)
content = render_notification_content(
notifier=notifier,
payload={
"mission_id": 12,
"creator_name": "张三",
"category_name": "通用",
"description": "原始描述",
"parsed_description_title": "标题一\n标题二",
"parsed_description_body": "正文一\n正文二",
"parsed_description_url": "https://example.com/detail",
"parsed_description_image_url": "https://images.yuwen.cloud/abc.jpg",
},
)
rendered_payload = json.loads(content)
self.assertEqual(rendered_payload["msgtype"], "news")
self.assertEqual(rendered_payload["title"], "标题一\n标题二")
self.assertEqual(rendered_payload["description"], "正文一\n正文二")
self.assertEqual(rendered_payload["url"], "https://example.com/detail")
self.assertEqual(rendered_payload["image_url"], "https://images.yuwen.cloud/abc.jpg")
def test_render_notification_content_with_structured_description_news_template_falls_back_to_default_image(self):
notifier = Notifier.objects.create(
merchant=self.merchant,
name="结构化描述图文通知-默认图",
channel=NotifierChannelEnum.MESSAGE_API,
template_key="mission_structured_description_news",
config={
"agent_ids": [1000007],
"url": "https://erp.example.com/missions/fallback",
},
)
with self.settings(MESSAGE_API_DEFAULT_NEWS_IMAGE_URL="https://via.placeholder.com/640x360.png?text=No+Image"):
content = render_notification_content(
notifier=notifier,
payload={
"mission_id": 12,
"creator_name": "张三",
"category_name": "通用",
"description": "原始描述",
"parsed_description_title": "标题一\n标题二",
"parsed_description_body": "正文一\n正文二",
"parsed_description_url": "",
"parsed_description_image_url": "",
},
)
rendered_payload = json.loads(content)
self.assertEqual(rendered_payload["url"], "https://erp.example.com/missions/fallback")
self.assertEqual(
rendered_payload["image_url"],
"https://via.placeholder.com/640x360.png?text=No+Image",
)
@patch("notifier.backends.send_wecom_webhook_message")
def test_send_notification_with_notifier_uses_wecom_backend(self, mock_send):
mock_send.return_value.ok = True
mock_send.return_value.errcode = 0
mock_send.return_value.errmsg = "ok"
result = send_notification_with_notifier(
notifier=self.notifier,
payload={
"mission_id": 12,
"created_by_name": "张三",
"creator_name": "张三",
"category_name": "通用",
"is_urgent": True,
"participant_names_display": "",
"description": "检查打印质量",
},
)
self.assertEqual(result["status"], "sent")
self.assertEqual(result["channel"], NotifierChannelEnum.WECOM_WEBHOOK)
mock_send.assert_called_once()
@patch("notifier.services.send_notification_with_notifier")
def test_dispatch_notification_event_filters_by_route_event_and_enabled(self, mock_send):
mock_send.side_effect = lambda *, notifier, payload: {
"notifier_id": notifier.id,
"status": "sent",
}
replied_notifier = Notifier.objects.create(
merchant=self.merchant,
name="任务回应通知",
channel=NotifierChannelEnum.WECOM_WEBHOOK,
template_key="mission_replied",
config={"key": "def456"},
)
NotifierRoute.objects.create(
merchant=self.merchant,
notifier=replied_notifier,
event_key=NotificationEventKeyEnum.MISSION_REPLIED,
)
disabled_notifier = Notifier.objects.create(
merchant=self.merchant,
name="停用通知器",
channel=NotifierChannelEnum.WECOM_WEBHOOK,
template_key="mission_created",
is_enabled=False,
config={"key": "ghi789"},
)
NotifierRoute.objects.create(
merchant=self.merchant,
notifier=disabled_notifier,
event_key=NotificationEventKeyEnum.MISSION_CREATED,
)
results = dispatch_notification_event(
event_key=NotificationEventKeyEnum.MISSION_CREATED,
merchant_id=self.merchant.id,
payload={"mission_id": 99},
)
self.assertEqual(
results,
[
{
"notifier_id": self.notifier.id,
"status": "sent",
"event_key": NotificationEventKeyEnum.MISSION_CREATED,
"route_id": self.global_route.id,
"route_event_key": NotificationEventKeyEnum.MISSION_CREATED,
"route_mission_category_id": None,
}
],
)
self.assertEqual(mock_send.call_count, 1)
self.assertEqual(mock_send.call_args.kwargs["notifier"].id, self.notifier.id)
@patch("notifier.services.send_notification_with_notifier")
def test_dispatch_notification_event_prefers_category_specific_route(self, mock_send):
mock_send.side_effect = lambda *, notifier, payload: {
"notifier_id": notifier.id,
"status": "sent",
}
specific_route = NotifierRoute.objects.create(
merchant=self.merchant,
notifier=self.notifier,
event_key=NotificationEventKeyEnum.MISSION_CREATED,
mission_category=self.after_sale_category,
)
results = dispatch_notification_event(
event_key=NotificationEventKeyEnum.MISSION_CREATED,
merchant_id=self.merchant.id,
payload={"mission_id": 99, "category_id": self.after_sale_category.id},
)
self.assertEqual(mock_send.call_count, 1)
self.assertEqual(results[0]["route_id"], specific_route.id)
self.assertEqual(results[0]["route_mission_category_id"], self.after_sale_category.id)
@patch("notifier.services.send_notification_with_notifier")
def test_dispatch_notification_event_falls_back_to_global_route(self, mock_send):
mock_send.side_effect = lambda *, notifier, payload: {
"notifier_id": notifier.id,
"status": "sent",
}
results = dispatch_notification_event(
event_key=NotificationEventKeyEnum.MISSION_CREATED,
merchant_id=self.merchant.id,
payload={"mission_id": 99, "category_id": self.general_category.id},
)
self.assertEqual(mock_send.call_count, 1)
self.assertEqual(results[0]["route_id"], self.global_route.id)
self.assertIsNone(results[0]["route_mission_category_id"])
@patch("notifier.tasks.dispatch_notification_event_task.delay")
def test_enqueue_notification_event_returns_task_id(self, mock_delay):
mock_delay.return_value.id = "task-123"
task_id = enqueue_notification_event(
event_key=NotificationEventKeyEnum.MISSION_CREATED,
merchant_id=self.merchant.id,
payload={"mission_id": 1},
)
self.assertEqual(task_id, "task-123")
mock_delay.assert_called_once()
@patch("notifier.tasks.dispatch_notification_event_task.delay", side_effect=RuntimeError("broker down"))
def test_enqueue_notification_event_fails_open(self, mock_delay):
task_id = enqueue_notification_event(
event_key=NotificationEventKeyEnum.MISSION_CREATED,
merchant_id=self.merchant.id,
payload={"mission_id": 1},
)
self.assertIsNone(task_id)
mock_delay.assert_called_once()
@patch("notifier.tasks.dispatch_notification_event")
def test_notify_unreplied_missions_task_dispatches_due_mission_and_updates_state(self, mock_dispatch):
mock_dispatch.return_value = [{"status": "sent"}]
notifier = Notifier.objects.create(
merchant=self.merchant,
name="未回复提醒通知器",
channel=NotifierChannelEnum.WECOM_WEBHOOK,
template_key="mission_unreplied",
config={"key": "unreplied-key"},
)
NotifierRoute.objects.create(
merchant=self.merchant,
notifier=notifier,
event_key=NotificationEventKeyEnum.MISSION_UNREPLIED,
)
mission = Mission.objects.create(
merchant=self.merchant,
category=self.general_category,
creator=self.creator,
description="超时未回复任务",
notify_if_unreplied=True,
unreplied_notify_interval_minutes=1,
)
mission.created_at = timezone.now() - timezone.timedelta(minutes=3)
mission.save(update_fields=["created_at", "updated_at"])
result = notify_unreplied_missions_task.run(limit=10)
mission.refresh_from_db()
self.assertEqual(result["sent_count"], 1)
self.assertEqual(mission.unreplied_notify_sent_count, 1)
self.assertIsNotNone(mission.unreplied_last_notified_at)
mock_dispatch.assert_called_once()
self.assertEqual(mock_dispatch.call_args.kwargs["event_key"], NotificationEventKeyEnum.MISSION_UNREPLIED)
@patch("notifier.tasks.dispatch_notification_event")
def test_notify_unreplied_missions_task_skips_mission_with_effective_reply(self, mock_dispatch):
mission = Mission.objects.create(
merchant=self.merchant,
category=self.general_category,
creator=self.creator,
description="已有回复任务",
notify_if_unreplied=True,
unreplied_notify_interval_minutes=1,
)
mission.created_at = timezone.now() - timezone.timedelta(minutes=3)
mission.save(update_fields=["created_at", "updated_at"])
responder = Employee.objects.create(merchant=self.merchant, name="回应人")
MissionReply.objects.create(
merchant=self.merchant,
mission=mission,
responder=responder,
content="收到",
)
result = notify_unreplied_missions_task.run(limit=10)
self.assertEqual(result["sent_count"], 0)
mock_dispatch.assert_not_called()
@patch("notifier.tasks.dispatch_notification_event")
def test_notify_unreplied_missions_task_applies_category_payload_processor(self, mock_dispatch):
mock_dispatch.return_value = [{"status": "sent"}]
self.general_category.payload_processor = "structured_description_v1"
self.general_category.save(update_fields=["payload_processor", "updated_at"])
notifier = Notifier.objects.create(
merchant=self.merchant,
name="未回复提醒通知器",
channel=NotifierChannelEnum.WECOM_WEBHOOK,
template_key="mission_unreplied",
config={"key": "unreplied-key"},
)
NotifierRoute.objects.create(
merchant=self.merchant,
notifier=notifier,
event_key=NotificationEventKeyEnum.MISSION_UNREPLIED,
)
mission = Mission.objects.create(
merchant=self.merchant,
category=self.general_category,
creator=self.creator,
description=(
"系统首行\n标题一\n款式图:https://images.yuwen.cloud/abc.jpg\n标题二\n\n正文一\n正文二\n"
"手机端链接: https://example.com/detail"
),
notify_if_unreplied=True,
unreplied_notify_interval_minutes=1,
)
mission.created_at = timezone.now() - timezone.timedelta(minutes=3)
mission.save(update_fields=["created_at", "updated_at"])
result = notify_unreplied_missions_task.run(limit=10)
self.assertEqual(result["sent_count"], 1)
payload = mock_dispatch.call_args.kwargs["payload"]
self.assertEqual(payload["payload_processor"], "structured_description_v1")
self.assertEqual(payload["parsed_description_title"], "标题一\n标题二")
self.assertEqual(payload["parsed_description_body"], "正文一\n正文二")
self.assertEqual(payload["parsed_description_url"], "https://example.com/detail")
self.assertEqual(payload["parsed_description_image_url"], "https://images.yuwen.cloud/abc.jpg")
@patch("notifier.backends.send_message_api_text_message")
def test_send_notification_with_notifier_uses_message_api_text_backend(self, mock_send):
mock_send.return_value.errcode = 0
mock_send.return_value.errmsg = "ok"
mock_send.return_value.raw = {"errcode": 0, "errmsg": "ok"}
notifier = Notifier.objects.create(
merchant=self.merchant,
name="任务创建消息 API 通知",
channel=NotifierChannelEnum.MESSAGE_API,
template_key="mission_created",
config={"agent_id": 1000007},
)
result = send_notification_with_notifier(
notifier=notifier,
payload={
"mission_id": 12,
"created_by_name": "张三",
"creator_name": "张三",
"category_name": "通用",
"is_urgent": True,
"participant_names_display": "",
"description": "检查打印质量",
},
)
self.assertEqual(result["status"], "sent")
self.assertEqual(result["channel"], NotifierChannelEnum.MESSAGE_API)
self.assertEqual(result["msgtype"], "text")
self.assertEqual(mock_send.call_count, 1)
self.assertEqual(mock_send.call_args.kwargs["agent_id"], 1000007)
self.assertEqual(mock_send.call_args.kwargs["user_ids"], None)
self.assertEqual(mock_send.call_args.kwargs["timeout_seconds"], 10.0)
self.assertIn("任务已创建", mock_send.call_args.kwargs["content"])
@patch("notifier.backends.send_message_api_text_message")
def test_send_notification_with_notifier_uses_structured_description_template(self, mock_send):
mock_send.return_value.errcode = 0
mock_send.return_value.errmsg = "ok"
mock_send.return_value.raw = {"errcode": 0, "errmsg": "ok"}
notifier = Notifier.objects.create(
merchant=self.merchant,
name="结构化描述消息 API 通知",
channel=NotifierChannelEnum.MESSAGE_API,
template_key="mission_structured_description_text",
config={"agent_id": 1000007},
)
result = send_notification_with_notifier(
notifier=notifier,
payload={
"mission_id": 12,
"creator_name": "张三",
"category_name": "通用",
"description": "原始描述",
"parsed_description_title": "标题一\n标题二",
"parsed_description_body": "正文一\n正文二",
"parsed_description_url": "https://example.com/detail",
"parsed_description_image_url": "https://images.yuwen.cloud/abc.jpg",
},
)
self.assertEqual(result["status"], "sent")
self.assertEqual(result["channel"], NotifierChannelEnum.MESSAGE_API)
self.assertEqual(result["msgtype"], "text")
self.assertEqual(mock_send.call_count, 1)
self.assertIn("标题:标题一\n标题二", mock_send.call_args.kwargs["content"])
self.assertIn("正文:正文一\n正文二", mock_send.call_args.kwargs["content"])
self.assertIn("链接https://example.com/detail", mock_send.call_args.kwargs["content"])
@patch("notifier.backends.send_message_api_news_to_agents")
def test_send_notification_with_notifier_uses_structured_description_news_template(self, mock_send):
mock_send.return_value = [
{
"agent_id": 1000007,
"ok": True,
"response": {"errcode": 0, "errmsg": "ok"},
"error": None,
}
]
notifier = Notifier.objects.create(
merchant=self.merchant,
name="结构化描述图文消息 API 通知",
channel=NotifierChannelEnum.MESSAGE_API,
template_key="mission_structured_description_news",
config={
"agent_ids": [1000007],
"image_url": "https://cdn.example.com/covers/mission-news.png",
},
)
result = send_notification_with_notifier(
notifier=notifier,
payload={
"mission_id": 12,
"description": "原始描述",
"parsed_description_title": "标题一\n标题二",
"parsed_description_body": "正文一\n正文二",
"parsed_description_url": "https://example.com/detail",
"parsed_description_image_url": "https://images.yuwen.cloud/abc.jpg",
},
)
self.assertEqual(result["status"], "sent")
self.assertEqual(result["msgtype"], "news")
mock_send.assert_called_once()
self.assertEqual(mock_send.call_args.kwargs["agent_ids"], [1000007])
self.assertEqual(mock_send.call_args.kwargs["title"], "标题一\n标题二")
self.assertEqual(mock_send.call_args.kwargs["description"], "正文一\n正文二")
self.assertEqual(mock_send.call_args.kwargs["url"], "https://example.com/detail")
self.assertEqual(
mock_send.call_args.kwargs["image_url"],
"https://images.yuwen.cloud/abc.jpg",
)
@patch("notifier.backends.send_message_api_news_to_agents")
def test_send_notification_with_notifier_uses_message_api_news_backend(self, mock_send):
mock_send.return_value = [
{
"agent_id": 1000007,
"ok": True,
"response": {"errcode": 0, "errmsg": "ok"},
"error": None,
}
]
notifier = Notifier.objects.create(
merchant=self.merchant,
name="任务创建图文通知",
channel=NotifierChannelEnum.MESSAGE_API,
template_key="test_message_news",
config={"agent_ids": [1000007]},
)
result = send_notification_with_notifier(
notifier=notifier,
payload={
"mission_id": 12,
"description": "检查打印质量",
"category_name": "通用",
},
)
self.assertEqual(result["status"], "sent")
self.assertEqual(result["msgtype"], "news")
self.assertEqual(result["ok_count"], 1)
mock_send.assert_called_once()
def test_send_notification_with_notifier_rejects_invalid_message_api_template(self):
notifier = Notifier.objects.create(
merchant=self.merchant,
name="非法消息模板",
channel=NotifierChannelEnum.MESSAGE_API,
template_key="test_message_invalid",
config={"agent_id": 1000007},
)
with self.assertRaisesMessage(Exception, "content"):
send_notification_with_notifier(
notifier=notifier,
payload={"mission_id": 12},
)
@patch("notifier.backends.send_jpush_payload")
def test_send_notification_with_notifier_uses_jpush_backend(self, mock_send):
mock_send.return_value.raw = {"sendno": "123", "msg_id": "456"}
notifier = Notifier.objects.create(
merchant=self.merchant,
name="任务创建极光推送",
channel=NotifierChannelEnum.JPUSH,
template_key="mission_created_jpush",
config={
"app_key": "app-key-1",
"master_secret": "master-secret-1",
"alias_prefix": "emp_",
"recipient_source": "participants",
"platform": "all",
"apns_production": True,
"android_channel_id": "mission",
},
)
result = send_notification_with_notifier(
notifier=notifier,
payload={
"mission_id": 12,
"merchant_id": self.merchant.id,
"description": "检查打印质量",
"participant_ids": [21, 22, 21],
},
)
self.assertEqual(result["status"], "sent")
self.assertEqual(result["channel"], NotifierChannelEnum.JPUSH)
self.assertEqual(result["aliases"], ["emp_21", "emp_22"])
mock_send.assert_called_once()
self.assertEqual(mock_send.call_args.kwargs["app_key"], "app-key-1")
self.assertEqual(mock_send.call_args.kwargs["master_secret"], "master-secret-1")
payload = mock_send.call_args.kwargs["payload"]
self.assertEqual(payload["audience"], {"alias": ["emp_21", "emp_22"]})
self.assertEqual(payload["notification"]["android"]["channel_id"], "mission")
self.assertEqual(payload["notification"]["ios"]["sound"], "default")
self.assertTrue(payload["options"]["apns_production"])
self.assertEqual(payload["notification"]["android"]["extras"]["mission_id"], "12")
@patch("notifier.backends.send_jpush_payload")
def test_send_notification_with_notifier_rejects_jpush_without_aliases(self, mock_send):
notifier = Notifier.objects.create(
merchant=self.merchant,
name="无接收人极光推送",
channel=NotifierChannelEnum.JPUSH,
template_key="mission_created_jpush",
config={"app_key": "app-key-1", "master_secret": "master-secret-1"},
)
with self.assertRaisesMessage(ValueError, "alias 不能为空"):
send_notification_with_notifier(
notifier=notifier,
payload={"mission_id": 12, "description": "检查打印质量"},
)
mock_send.assert_not_called()
class MessageAPIServiceTestCase(TestCase):
@patch("notifier.message_api.urlopen")
def test_send_message_api_text_message_calls_message_api_endpoint(self, mock_urlopen):
from notifier.message_api import send_message_api_text_message
response_obj = MagicMock()
response_obj.read.return_value = b'{"errcode":0,"errmsg":"ok"}'
response_obj.__enter__.return_value = response_obj
response_obj.__exit__.return_value = False
mock_urlopen.return_value = response_obj
with self.settings(
MESSAGE_API_BASE_URL="http://message-api.internal:8198",
MESSAGE_API_AUTHORIZATION="secret-1",
):
response = send_message_api_text_message(agent_id=1000007, content="hello")
self.assertTrue(response.ok)
self.assertEqual(response.errcode, 0)
self.assertEqual(mock_urlopen.call_count, 1)
request = mock_urlopen.call_args.args[0]
self.assertEqual(request.full_url, "http://message-api.internal:8198/api/message/send")
self.assertEqual(request.get_method(), "POST")
self.assertEqual(request.get_header("Authorization"), "secret-1")
@patch("notifier.message_api.urlopen")
def test_send_message_api_news_to_agents_calls_message_api_endpoint(self, mock_urlopen):
from notifier.message_api import send_message_api_news_to_agents
response_obj = MagicMock()
response_obj.read.return_value = (
b'{"results":[{"agent_id":1000007,"ok":true,"response":{"errcode":0,"errmsg":"ok"},"error":null}]}'
)
response_obj.__enter__.return_value = response_obj
response_obj.__exit__.return_value = False
mock_urlopen.return_value = response_obj
with self.settings(
MESSAGE_API_BASE_URL="http://message-api.internal:8198",
MESSAGE_API_AUTHORIZATION="secret-1",
):
results = send_message_api_news_to_agents(
agent_ids=[1000007],
title="销售日报",
description="点击查看今日各区域销售汇总",
url="https://example.com/reports/daily-sales",
image_url="https://example.com/static/daily-sales-cover.png",
)
self.assertEqual(len(results), 1)
request = mock_urlopen.call_args.args[0]
self.assertEqual(request.full_url, "http://message-api.internal:8198/api/message/send/news")
self.assertEqual(request.get_header("Authorization"), "secret-1")