from django.test import TestCase from django.contrib.contenttypes.models import ContentType from types import SimpleNamespace from unittest.mock import patch from django.utils import timezone from basic_info.models import Employee, Merchant, MerchantTypeEnum from mission.models import Mission, MissionCategory, MissionParticipant, MissionReply from mission.services import ( cancel_mission, create_mission, create_mission_reply, reject_reply, reopen_mission, set_mission_participants, set_mission_urgent, update_mission, ) from mission.signals import ( mission_cancelled, mission_completed, mission_created, mission_reopened, mission_replied, mission_reply_rejected, ) from mission.payload_processors import _split_structured_description from notifier.models import NotificationEventKeyEnum, Notifier, NotifierChannelEnum, NotifierRoute from notifier.services import dispatch_notification_event class MissionModelTestCase(TestCase): def setUp(self): self.merchant = Merchant.objects.create(name="测试商户", type=MerchantTypeEnum.STORE) self.other_merchant = Merchant.objects.create(name="其他商户", type=MerchantTypeEnum.STORE) self.default_category = MissionCategory.objects.create(merchant=self.merchant, name="通用") self.custom_category = MissionCategory.objects.create(merchant=self.merchant, name="售后") self.other_category = MissionCategory.objects.create(merchant=self.other_merchant, name="通用") self.creator = Employee.objects.create(merchant=self.merchant, name="创建者") self.responder = Employee.objects.create(merchant=self.merchant, name="回应者") self.other_employee = Employee.objects.create(merchant=self.other_merchant, name="外部员工") self.mission = Mission.objects.create( merchant=self.merchant, category=self.default_category, description="测试任务", creator=self.creator, ) def test_participant_helpers(self): MissionParticipant.objects.create( merchant=self.merchant, mission=self.mission, employee=self.responder, ) self.assertEqual(self.mission.get_participants().count(), 1) self.assertEqual( self.mission.filter_participants(employee=self.responder).first().employee, self.responder, ) def test_create_ending_reply_marks_mission_completed(self): reply = create_mission_reply( mission=self.mission, responder=self.responder, content="完成任务", ends_task=True, ) self.mission.refresh_from_db() reply.refresh_from_db() self.assertTrue(reply.ends_task) self.assertEqual(reply.merchant, self.merchant) self.assertTrue(self.mission.is_completed) self.assertTrue(self.mission.has_ending_reply) self.assertFalse(self.mission.can_reply) def test_reopen_mission_rejects_ending_reply(self): reply = create_mission_reply( mission=self.mission, responder=self.responder, content="完成任务", ends_task=True, ) reopen_mission(mission=self.mission, reopened_by=self.creator) self.mission.refresh_from_db() reply.refresh_from_db() self.assertFalse(self.mission.is_completed) self.assertFalse(reply.ends_task) self.assertTrue(reply.is_rejected) self.assertEqual(reply.rejected_by, self.creator) self.assertIsNotNone(reply.rejected_at) self.assertTrue(self.mission.can_reply) def test_reopen_cancelled_mission_is_rejected(self): create_mission_reply( mission=self.mission, responder=self.responder, content="完成任务", ends_task=True, ) self.mission.is_cancelled = True self.mission.save(update_fields=["is_cancelled", "updated_at"]) with self.assertRaises(ValueError): reopen_mission(mission=self.mission, reopened_by=self.creator) def test_cross_merchant_responder_is_rejected(self): with self.assertRaises(ValueError): create_mission_reply( mission=self.mission, responder=self.other_employee, content="跨商户回应", ) def test_create_reply_rejects_empty_responder(self): with self.assertRaises(ValueError): create_mission_reply( mission=self.mission, responder=None, content="无回应者", ) def test_reject_reply_marks_reply_rejected_and_reopens_mission(self): reply = create_mission_reply( mission=self.mission, responder=self.responder, content="完成任务", ends_task=True, ) reject_reply(reply=reply, rejected_by=self.creator) self.mission.refresh_from_db() reply.refresh_from_db() self.assertFalse(self.mission.is_completed) self.assertFalse(reply.ends_task) self.assertTrue(reply.is_rejected) self.assertEqual(reply.rejected_by, self.creator) self.assertIsNotNone(reply.rejected_at) def test_cancel_mission_records_employee_and_time(self): cancel_mission(mission=self.mission, cancelled_by=self.creator) self.mission.refresh_from_db() self.assertTrue(self.mission.is_cancelled) self.assertEqual(self.mission.cancelled_by, self.creator) self.assertIsNotNone(self.mission.cancelled_at) def test_cross_merchant_cancel_is_rejected(self): with self.assertRaises(ValueError): cancel_mission(mission=self.mission, cancelled_by=self.other_employee) def test_create_mission_sets_defaults_and_participants(self): mission = create_mission( creator=self.creator, description="service 创建任务", participant_ids=[self.responder.id, self.responder.id], ) self.assertEqual(mission.merchant, self.merchant) self.assertEqual(mission.category, self.default_category) self.assertFalse(mission.is_urgent) self.assertEqual(list(mission.participants.values_list("employee_id", flat=True)), [self.responder.id]) def test_create_mission_supports_unreplied_notification_fields(self): mission = create_mission( creator=self.creator, description="带未回复提醒的任务", notify_if_unreplied=True, unreplied_notify_interval_minutes=15, ) self.assertTrue(mission.notify_if_unreplied) self.assertEqual(mission.unreplied_notify_interval_minutes, 15) self.assertEqual(mission.unreplied_notify_max_count, 5) self.assertEqual(mission.unreplied_notify_sent_count, 0) self.assertIsNone(mission.unreplied_last_notified_at) def test_create_mission_requires_interval_when_unreplied_notification_enabled(self): with self.assertRaisesMessage(ValueError, "开启未回复提醒时必须设置提醒间隔"): create_mission( creator=self.creator, description="缺少提醒间隔", notify_if_unreplied=True, ) def test_create_mission_rejects_empty_creator(self): with self.assertRaises(ValueError): create_mission(creator=None, description="无创建人") def test_create_mission_rejects_partial_content_object(self): with self.assertRaises(ValueError): create_mission( creator=self.creator, description="缺少 content_id", content_type=ContentType.objects.get_for_model(Mission), ) def test_create_mission_rejects_missing_content_object(self): with self.assertRaises(ValueError): create_mission( creator=self.creator, description="不存在的关联对象", content_type=ContentType.objects.get_for_model(Mission), content_id=999999, ) def test_create_mission_rejects_cross_merchant_content_object(self): other_mission = Mission.objects.create( merchant=self.other_merchant, category=self.other_category, creator=self.other_employee, description="其他商户任务", ) with self.assertRaises(ValueError): create_mission( creator=self.creator, description="跨商户关联对象", content_type=ContentType.objects.get_for_model(Mission), content_id=other_mission.id, ) def test_update_mission_updates_content_object_and_participants(self): related = Mission.objects.create( merchant=self.merchant, category=self.default_category, creator=self.creator, description="同商户关联对象", ) update_mission( mission=self.mission, updated_by=self.creator, description="更新描述", category=self.custom_category, content_type=ContentType.objects.get_for_model(Mission), content_id=related.id, update_content_object=True, participant_ids=[self.responder.id], ) self.mission.refresh_from_db() self.assertEqual(self.mission.description, "更新描述") self.assertEqual(self.mission.category, self.custom_category) self.assertEqual(self.mission.content_id, related.id) self.assertEqual(list(self.mission.participants.values_list("employee_id", flat=True)), [self.responder.id]) def test_update_mission_updates_unreplied_notification_fields(self): update_mission( mission=self.mission, updated_by=self.creator, notify_if_unreplied=True, unreplied_notify_interval_minutes=20, unreplied_notify_max_count=8, ) self.mission.refresh_from_db() self.assertTrue(self.mission.notify_if_unreplied) self.assertEqual(self.mission.unreplied_notify_interval_minutes, 20) self.assertEqual(self.mission.unreplied_notify_max_count, 8) def test_create_mission_rejects_cross_merchant_category(self): with self.assertRaises(ValueError): create_mission( creator=self.creator, description="跨商户分类", category=self.other_category, ) def test_update_mission_rejects_cross_merchant_employee(self): with self.assertRaises(ValueError): update_mission( mission=self.mission, updated_by=self.other_employee, description="非法更新", ) def test_set_participants_rejects_cross_merchant_employee(self): with self.assertRaises(ValueError): set_mission_participants( mission=self.mission, participant_ids=[self.other_employee.id], ) def test_update_mission_rejects_cross_merchant_category(self): with self.assertRaises(ValueError): update_mission( mission=self.mission, updated_by=self.creator, category=self.other_category, ) def test_create_reply_rejects_after_ending_reply(self): create_mission_reply( mission=self.mission, responder=self.responder, content="结束任务", ends_task=True, ) with self.assertRaises(ValueError): create_mission_reply( mission=self.mission, responder=self.responder, content="后续回应", ) def test_reopen_uncompleted_mission_is_rejected(self): with self.assertRaises(ValueError): reopen_mission(mission=self.mission, reopened_by=self.creator) def test_reopen_completed_mission_without_ending_reply_is_rejected(self): self.mission.is_completed = True self.mission.save(update_fields=["is_completed", "updated_at"]) with self.assertRaises(ValueError): reopen_mission(mission=self.mission, reopened_by=self.creator) def test_reopen_cross_merchant_employee_is_rejected(self): create_mission_reply( mission=self.mission, responder=self.responder, content="结束任务", ends_task=True, ) with self.assertRaises(ValueError): reopen_mission(mission=self.mission, reopened_by=self.other_employee) def test_reject_reply_rejects_cancelled_mission(self): reply = create_mission_reply( mission=self.mission, responder=self.responder, content="普通回应", ) self.mission.is_cancelled = True self.mission.save(update_fields=["is_cancelled", "updated_at"]) with self.assertRaises(ValueError): reject_reply(reply=reply, rejected_by=self.creator) def test_reject_reply_rejects_already_rejected_reply(self): reply = create_mission_reply( mission=self.mission, responder=self.responder, content="普通回应", ) reject_reply(reply=reply, rejected_by=self.creator) with self.assertRaises(ValueError): reject_reply(reply=reply, rejected_by=self.creator) def test_reject_reply_rejects_cross_merchant_employee(self): reply = create_mission_reply( mission=self.mission, responder=self.responder, content="普通回应", ) with self.assertRaises(ValueError): reject_reply(reply=reply, rejected_by=self.other_employee) def test_reject_non_ending_reply_keeps_completed_state(self): self.mission.is_completed = True self.mission.save(update_fields=["is_completed", "updated_at"]) reply = MissionReply.objects.create( merchant=self.merchant, mission=self.mission, responder=self.responder, content="普通回应", ends_task=False, ) reject_reply(reply=reply, rejected_by=self.creator) self.mission.refresh_from_db() self.assertTrue(self.mission.is_completed) def test_create_reply_resets_unreplied_notification_state(self): self.mission.notify_if_unreplied = True self.mission.unreplied_notify_interval_minutes = 10 self.mission.unreplied_notify_sent_count = 3 self.mission.unreplied_last_notified_at = timezone.now() self.mission.save( update_fields=[ "notify_if_unreplied", "unreplied_notify_interval_minutes", "unreplied_notify_sent_count", "unreplied_last_notified_at", "updated_at", ] ) create_mission_reply( mission=self.mission, responder=self.responder, content="收到,处理中", ) self.mission.refresh_from_db() self.assertEqual(self.mission.unreplied_notify_sent_count, 0) self.assertIsNone(self.mission.unreplied_last_notified_at) def test_reject_reply_without_other_effective_reply_resets_unreplied_notification_state(self): self.mission.notify_if_unreplied = True self.mission.unreplied_notify_interval_minutes = 10 self.mission.unreplied_notify_sent_count = 2 self.mission.unreplied_last_notified_at = timezone.now() self.mission.save( update_fields=[ "notify_if_unreplied", "unreplied_notify_interval_minutes", "unreplied_notify_sent_count", "unreplied_last_notified_at", "updated_at", ] ) reply = create_mission_reply( mission=self.mission, responder=self.responder, content="普通回应", ) reject_reply(reply=reply, rejected_by=self.creator) self.mission.refresh_from_db() self.assertEqual(self.mission.unreplied_notify_sent_count, 0) self.assertIsNone(self.mission.unreplied_last_notified_at) def test_cancel_already_cancelled_mission_is_rejected(self): cancel_mission(mission=self.mission, cancelled_by=self.creator) with self.assertRaises(ValueError): cancel_mission(mission=self.mission, cancelled_by=self.creator) def test_set_urgent_updates_status_and_rejects_cross_merchant_employee(self): set_mission_urgent(mission=self.mission, updated_by=self.creator, is_urgent=True) self.mission.refresh_from_db() self.assertTrue(self.mission.is_urgent) with self.assertRaises(ValueError): set_mission_urgent(mission=self.mission, updated_by=self.other_employee, is_urgent=False) def test_create_mission_emits_mission_created_signal(self): received = [] def receiver(sender, instance, created_by=None, **kwargs): received.append((sender, instance.id, created_by.id)) mission_created.connect(receiver, sender=Mission, dispatch_uid="test_mission_created") try: with self.captureOnCommitCallbacks(execute=True): mission = create_mission(creator=self.creator, description="触发创建信号") finally: mission_created.disconnect(sender=Mission, dispatch_uid="test_mission_created") self.assertEqual(received, [(Mission, mission.id, self.creator.id)]) def test_create_reply_emits_replied_and_completed_signals(self): received = [] def on_replied(sender, instance, mission=None, responder=None, **kwargs): received.append(("replied", sender, instance.id, mission.id, responder.id)) def on_completed(sender, instance, completed_by=None, reply=None, **kwargs): received.append(("completed", sender, instance.id, completed_by.id, reply.id)) mission_replied.connect(on_replied, sender=MissionReply, dispatch_uid="test_mission_replied") mission_completed.connect(on_completed, sender=Mission, dispatch_uid="test_mission_completed") try: with self.captureOnCommitCallbacks(execute=True): reply = create_mission_reply( mission=self.mission, responder=self.responder, content="完成并触发信号", ends_task=True, ) finally: mission_replied.disconnect(sender=MissionReply, dispatch_uid="test_mission_replied") mission_completed.disconnect(sender=Mission, dispatch_uid="test_mission_completed") self.assertIn(("completed", Mission, self.mission.id, self.responder.id, reply.id), received) self.assertIn(("replied", MissionReply, reply.id, self.mission.id, self.responder.id), received) def test_reject_reply_emits_reply_rejected_signal(self): received = [] reply = create_mission_reply( mission=self.mission, responder=self.responder, content="待撤销回应", ) def receiver(sender, instance, mission=None, rejected_by=None, reason=None, **kwargs): received.append((sender, instance.id, mission.id, rejected_by.id, reason)) mission_reply_rejected.connect(receiver, sender=MissionReply, dispatch_uid="test_reply_rejected") try: with self.captureOnCommitCallbacks(execute=True): reject_reply(reply=reply, rejected_by=self.creator) finally: mission_reply_rejected.disconnect(sender=MissionReply, dispatch_uid="test_reply_rejected") self.assertEqual(received, [(MissionReply, reply.id, self.mission.id, self.creator.id, "reject_reply")]) def test_reopen_emits_reopened_and_reply_rejected_signals(self): received = [] reply = create_mission_reply( mission=self.mission, responder=self.responder, content="结束并等待 reopen", ends_task=True, ) def on_reply_rejected(sender, instance, mission=None, rejected_by=None, reason=None, **kwargs): received.append(("reply_rejected", sender, instance.id, mission.id, rejected_by.id, reason)) def on_reopened(sender, instance, reopened_by=None, rejected_reply_ids=None, **kwargs): received.append(("reopened", sender, instance.id, reopened_by.id, rejected_reply_ids)) mission_reply_rejected.connect(on_reply_rejected, sender=MissionReply, dispatch_uid="test_reopen_reply_rejected") mission_reopened.connect(on_reopened, sender=Mission, dispatch_uid="test_mission_reopened") try: with self.captureOnCommitCallbacks(execute=True): reopen_mission(mission=self.mission, reopened_by=self.creator) finally: mission_reply_rejected.disconnect(sender=MissionReply, dispatch_uid="test_reopen_reply_rejected") mission_reopened.disconnect(sender=Mission, dispatch_uid="test_mission_reopened") self.assertIn(("reply_rejected", MissionReply, reply.id, self.mission.id, self.creator.id, "reopen"), received) self.assertIn(("reopened", Mission, self.mission.id, self.creator.id, [reply.id]), received) def test_cancel_mission_emits_cancelled_signal(self): received = [] def receiver(sender, instance, cancelled_by=None, **kwargs): received.append((sender, instance.id, cancelled_by.id)) mission_cancelled.connect(receiver, sender=Mission, dispatch_uid="test_mission_cancelled") try: with self.captureOnCommitCallbacks(execute=True): cancel_mission(mission=self.mission, cancelled_by=self.creator) finally: mission_cancelled.disconnect(sender=Mission, dispatch_uid="test_mission_cancelled") self.assertEqual(received, [(Mission, self.mission.id, self.creator.id)]) def test_split_structured_description_v1(self): title, body, url, image_url = _split_structured_description( "系统首行\n标题一\n款式图:https://images.yuwen.cloud/abc.jpg\n标题二\n\n正文一\n正文二\n手机端链接: https://example.com/detail" ) self.assertEqual(title, "标题一\n标题二") self.assertEqual(body, "正文一\n正文二") self.assertEqual(url, "https://example.com/detail") self.assertEqual(image_url, "https://images.yuwen.cloud/abc.jpg") def test_split_structured_description_v1_without_blank_line_uses_title_only(self): title, body, url, image_url = _split_structured_description( "系统首行\n标题一\n款式图:https://images.yuwen.cloud/abc.jpg\n标题二\n手机端链接: https://example.com/detail" ) self.assertEqual(title, "标题一\n标题二") self.assertEqual(body, "") self.assertEqual(url, "https://example.com/detail") self.assertEqual(image_url, "https://images.yuwen.cloud/abc.jpg") @patch("mission.handlers.enqueue_notification_event") def test_mission_created_handler_enqueues_notifier_event(self, mock_enqueue): with self.captureOnCommitCallbacks(execute=True): mission = create_mission(creator=self.creator, description="触发任务创建通知") mock_enqueue.assert_called_once() self.assertEqual(mock_enqueue.call_args.kwargs["event_key"], "mission.created") self.assertEqual(mock_enqueue.call_args.kwargs["merchant_id"], self.merchant.id) self.assertEqual(mock_enqueue.call_args.kwargs["payload"]["mission_id"], mission.id) @patch("mission.tasks.notify_mission_speech.delay") @patch("mission.handlers.enqueue_notification_event") def test_mission_created_speech_requires_category_enabled(self, mock_enqueue, mock_speech_delay): with self.captureOnCommitCallbacks(execute=True): create_mission(creator=self.creator, description="不播报任务") mock_enqueue.assert_called_once() mock_speech_delay.assert_not_called() @patch("mission.tasks.notify_mission_speech.delay") @patch("mission.handlers.enqueue_notification_event") def test_mission_created_speech_enqueues_when_category_enabled(self, mock_enqueue, mock_speech_delay): self.default_category.speech_enabled = True self.default_category.save(update_fields=["speech_enabled", "updated_at"]) with self.captureOnCommitCallbacks(execute=True): mission = create_mission(creator=self.creator, description="需要播报任务") mock_enqueue.assert_called_once() mock_speech_delay.assert_called_once() self.assertIn(str(mission.description), mock_speech_delay.call_args.kwargs["text"]) @patch("mission.handlers.enqueue_notification_event") def test_mission_created_handler_applies_category_payload_processor(self, mock_enqueue): self.default_category.payload_processor = "structured_description_v1" self.default_category.save(update_fields=["payload_processor", "updated_at"]) with self.captureOnCommitCallbacks(execute=True): mission = create_mission( creator=self.creator, description=( "系统首行\n标题一\n款式图:https://images.yuwen.cloud/abc.jpg\n标题二\n\n正文一\n正文二\n" "手机端链接: https://example.com/detail" ), ) payload = mock_enqueue.call_args.kwargs["payload"] self.assertEqual(payload["mission_id"], mission.id) 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("mission.handlers.enqueue_notification_event") def test_create_ending_reply_handler_enqueues_replied_and_completed_notifications(self, mock_enqueue): with self.captureOnCommitCallbacks(execute=True): reply = create_mission_reply( mission=self.mission, responder=self.responder, content="完成任务并通知", ends_task=True, ) self.assertEqual(mock_enqueue.call_count, 2) event_keys = {call.kwargs["event_key"] for call in mock_enqueue.call_args_list} self.assertEqual(event_keys, {"mission.replied", "mission.completed"}) payloads = [call.kwargs["payload"] for call in mock_enqueue.call_args_list] self.assertTrue(any(payload.get("reply_id") == reply.id for payload in payloads)) @patch("mission.handlers.enqueue_notification_event") def test_reject_reply_handler_enqueues_notification(self, mock_enqueue): reply = create_mission_reply( mission=self.mission, responder=self.responder, content="待撤销回应", ) with self.captureOnCommitCallbacks(execute=True): reject_reply(reply=reply, rejected_by=self.creator) mock_enqueue.assert_called_once() self.assertEqual(mock_enqueue.call_args.kwargs["event_key"], "mission.reply_rejected") self.assertEqual(mock_enqueue.call_args.kwargs["payload"]["reply_id"], reply.id) @patch("mission.handlers.enqueue_notification_event") def test_reopen_mission_handler_enqueues_rejected_and_reopened_notifications(self, mock_enqueue): reply = create_mission_reply( mission=self.mission, responder=self.responder, content="结束任务", ends_task=True, ) with self.captureOnCommitCallbacks(execute=True): reopen_mission(mission=self.mission, reopened_by=self.creator) self.assertEqual(mock_enqueue.call_count, 2) event_keys = {call.kwargs["event_key"] for call in mock_enqueue.call_args_list} self.assertEqual(event_keys, {"mission.reply_rejected", "mission.reopened"}) self.assertTrue( any(reply.id in call.kwargs["payload"].get("rejected_reply_ids", []) for call in mock_enqueue.call_args_list) ) @patch("mission.handlers.enqueue_notification_event") def test_cancel_mission_handler_enqueues_notification(self, mock_enqueue): with self.captureOnCommitCallbacks(execute=True): cancel_mission(mission=self.mission, cancelled_by=self.creator) mock_enqueue.assert_called_once() self.assertEqual(mock_enqueue.call_args.kwargs["event_key"], "mission.cancelled") self.assertEqual(mock_enqueue.call_args.kwargs["payload"]["mission_id"], self.mission.id) @patch("notifier.backends.send_message_api_news_to_agents") @patch("notifier.tasks.dispatch_notification_event_task.delay") def test_mission_created_can_flow_to_message_api_news_with_structured_description_template( self, mock_delay, mock_send_news, ): self.default_category.payload_processor = "structured_description_v1" self.default_category.save(update_fields=["payload_processor", "updated_at"]) 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", }, ) NotifierRoute.objects.create( merchant=self.merchant, notifier=notifier, event_key=NotificationEventKeyEnum.MISSION_CREATED, mission_category=self.default_category, ) mock_send_news.return_value = [ { "agent_id": 1000007, "ok": True, "response": {"errcode": 0, "errmsg": "ok"}, "error": None, } ] def inline_delay(*, event_key, merchant_id, payload): dispatch_notification_event( event_key=event_key, merchant_id=merchant_id, payload=payload, ) return SimpleNamespace(id="task-inline-1") mock_delay.side_effect = inline_delay with self.captureOnCommitCallbacks(execute=True): create_mission( creator=self.creator, category=self.default_category, description=( "系统首行\n标题一\n款式图:https://images.yuwen.cloud/abc.jpg\n标题二\n\n正文一\n正文二\n" "手机端链接: https://example.com/detail" ), ) mock_send_news.assert_called_once() self.assertEqual(mock_send_news.call_args.kwargs["agent_ids"], [1000007]) self.assertEqual(mock_send_news.call_args.kwargs["title"], "标题一\n标题二") self.assertEqual(mock_send_news.call_args.kwargs["description"], "正文一\n正文二") self.assertEqual(mock_send_news.call_args.kwargs["url"], "https://example.com/detail") self.assertEqual( mock_send_news.call_args.kwargs["image_url"], "https://images.yuwen.cloud/abc.jpg", )