@@ -37,6 +37,7 @@ def mock_update():
3737 update .message .chat_id = - 1001234567890
3838 update .message .message_id = 999
3939 update .message .reply_to_message = None
40+ update .message .forum_topic_created = None
4041 update .effective_chat = MagicMock ()
4142 update .effective_chat .id = - 1001234567890
4243 update .effective_chat .type = "supergroup"
@@ -69,6 +70,7 @@ def _make_target_user(user_id=67890, full_name="Bad Member", username="badmember
6970def _make_reply_message (target_user ):
7071 msg = MagicMock ()
7172 msg .from_user = target_user
73+ msg .forum_topic_created = None
7274 return msg
7375
7476
@@ -322,11 +324,12 @@ async def test_delete_failure_does_not_break(
322324 async def test_reply_without_from_user_falls_to_id_mode (
323325 self , mock_update , mock_context , mock_registry
324326 ):
325- """Reply to a channel message (no from_user) falls through to ID mode."""
327+ """Reply to a channel message (no from_user) with ID arg uses ID mode."""
326328 target = _make_target_user ()
327329 mock_context .bot .get_chat_member .return_value = _make_chat_member (target )
328330 mock_update .message .reply_to_message = MagicMock ()
329331 mock_update .message .reply_to_message .from_user = None
332+ mock_update .message .reply_to_message .forum_topic_created = None
330333 mock_context .args = ["67890" , "spam" ]
331334
332335 with patch ("bot.handlers.warn.get_group_config_for_update" , return_value = mock_registry .get (- 1001234567890 )):
@@ -338,7 +341,7 @@ async def test_reply_without_from_user_falls_to_id_mode(
338341 mock_context .bot .send_message .assert_called_once ()
339342 call_kwargs = mock_context .bot .send_message .call_args .kwargs
340343 assert "spam" in call_kwargs ["text" ]
341- assert "67890" not in call_kwargs [ "text" ] or " badmember" in call_kwargs ["text" ]
344+ assert "badmember" in call_kwargs ["text" ]
342345
343346 async def test_reason_with_markdown_is_escaped (
344347 self , mock_update , mock_context , mock_registry
@@ -464,3 +467,123 @@ async def mock_get_member(**kwargs):
464467 await handle_warn_command (mock_update , mock_context )
465468
466469 assert call_order [0 ] == "delete"
470+
471+ async def test_id_mode_takes_priority_over_reply (
472+ self , mock_update , mock_context , mock_registry
473+ ):
474+ """Bug fix: /warn USER_ID reason as a reply warns the ID target, not the replied-to user."""
475+ reply_target = _make_target_user (user_id = 11111 , full_name = "Replied User" , username = "replieduser" )
476+ id_target = _make_target_user (user_id = 67890 , full_name = "ID Target" , username = "idtarget" )
477+ mock_update .message .reply_to_message = _make_reply_message (reply_target )
478+ mock_context .bot .get_chat_member .return_value = _make_chat_member (id_target )
479+ mock_context .args = ["67890" , "spamming" ]
480+
481+ with patch ("bot.handlers.warn.get_group_config_for_update" , return_value = mock_registry .get (- 1001234567890 )):
482+ await handle_warn_command (mock_update , mock_context )
483+
484+ mock_context .bot .get_chat_member .assert_called_once_with (
485+ chat_id = - 1001234567890 , user_id = 67890
486+ )
487+ mock_context .bot .send_message .assert_called_once ()
488+ call_kwargs = mock_context .bot .send_message .call_args .kwargs
489+ assert "idtarget" in call_kwargs ["text" ]
490+ assert "replieduser" not in call_kwargs ["text" ]
491+ assert "spamming" in call_kwargs ["text" ]
492+
493+ async def test_forum_topic_anchor_not_treated_as_reply (
494+ self , mock_update , mock_context , mock_registry
495+ ):
496+ """Bug fix: forum topic anchor (auto reply_to_message) is not treated as a real reply."""
497+ target = _make_target_user ()
498+ mock_context .bot .get_chat_member .return_value = _make_chat_member (target )
499+ # Simulate a forum topic anchor: reply_to_message exists with forum_topic_created set
500+ mock_update .message .reply_to_message = MagicMock ()
501+ mock_update .message .reply_to_message .from_user = _make_target_user (user_id = 11111 , full_name = "Topic Creator" )
502+ mock_update .message .reply_to_message .forum_topic_created = MagicMock ()
503+ mock_context .args = ["67890" , "reason" ]
504+
505+ with patch ("bot.handlers.warn.get_group_config_for_update" , return_value = mock_registry .get (- 1001234567890 )):
506+ await handle_warn_command (mock_update , mock_context )
507+
508+ mock_context .bot .get_chat_member .assert_called_once_with (
509+ chat_id = - 1001234567890 , user_id = 67890
510+ )
511+ mock_context .bot .send_message .assert_called_once ()
512+ call_kwargs = mock_context .bot .send_message .call_args .kwargs
513+ assert "badmember" in call_kwargs ["text" ]
514+ assert "11111" not in call_kwargs ["text" ]
515+
516+ async def test_forum_topic_anchor_no_args_shows_usage (
517+ self , mock_update , mock_context , mock_registry
518+ ):
519+ """Forum topic anchor with no args shows usage (not treated as reply)."""
520+ mock_update .message .reply_to_message = MagicMock ()
521+ mock_update .message .reply_to_message .from_user = _make_target_user (user_id = 11111 )
522+ mock_update .message .reply_to_message .forum_topic_created = MagicMock ()
523+ mock_context .args = []
524+
525+ with patch ("bot.handlers.warn.get_group_config_for_update" , return_value = mock_registry .get (- 1001234567890 )):
526+ await handle_warn_command (mock_update , mock_context )
527+
528+ mock_update .message .reply_text .assert_called_once ()
529+ assert "Penggunaan" in mock_update .message .reply_text .call_args .args [0 ]
530+ mock_context .bot .send_message .assert_not_called ()
531+
532+ async def test_username_mode_with_reason (
533+ self , mock_update , mock_context , mock_registry
534+ ):
535+ """Admin uses /warn @username <reason>."""
536+ mock_context .args = ["@badmember" , "stop" , "spamming" ]
537+
538+ with patch ("bot.handlers.warn.get_group_config_for_update" , return_value = mock_registry .get (- 1001234567890 )):
539+ await handle_warn_command (mock_update , mock_context )
540+
541+ mock_context .bot .get_chat_member .assert_not_called ()
542+ mock_context .bot .send_message .assert_called_once ()
543+ call_kwargs = mock_context .bot .send_message .call_args .kwargs
544+ assert "@badmember" in call_kwargs ["text" ]
545+ assert "stop spamming" in call_kwargs ["text" ]
546+
547+ async def test_username_mode_without_reason (
548+ self , mock_update , mock_context , mock_registry
549+ ):
550+ """Admin uses /warn @username with no reason."""
551+ mock_context .args = ["@badmember" ]
552+
553+ with patch ("bot.handlers.warn.get_group_config_for_update" , return_value = mock_registry .get (- 1001234567890 )):
554+ await handle_warn_command (mock_update , mock_context )
555+
556+ mock_context .bot .get_chat_member .assert_not_called ()
557+ mock_context .bot .send_message .assert_called_once ()
558+ call_kwargs = mock_context .bot .send_message .call_args .kwargs
559+ assert "@badmember" in call_kwargs ["text" ]
560+ assert "patuhi aturan grup" in call_kwargs ["text" ]
561+
562+ async def test_username_mode_takes_priority_over_reply (
563+ self , mock_update , mock_context , mock_registry
564+ ):
565+ """@username takes priority over a real reply."""
566+ reply_target = _make_target_user (user_id = 11111 , full_name = "Replied User" , username = "replieduser" )
567+ mock_update .message .reply_to_message = _make_reply_message (reply_target )
568+ mock_context .args = ["@badmember" , "reason" ]
569+
570+ with patch ("bot.handlers.warn.get_group_config_for_update" , return_value = mock_registry .get (- 1001234567890 )):
571+ await handle_warn_command (mock_update , mock_context )
572+
573+ mock_context .bot .get_chat_member .assert_not_called ()
574+ mock_context .bot .send_message .assert_called_once ()
575+ call_kwargs = mock_context .bot .send_message .call_args .kwargs
576+ assert "@badmember" in call_kwargs ["text" ]
577+ assert "replieduser" not in call_kwargs ["text" ]
578+ assert "reason" in call_kwargs ["text" ]
579+
580+ async def test_username_mode_skips_bot_and_self_checks (
581+ self , mock_update , mock_context , mock_registry
582+ ):
583+ """Username mode cannot check is_bot/self since we don't have the User object."""
584+ mock_context .args = ["@somebot" ]
585+
586+ with patch ("bot.handlers.warn.get_group_config_for_update" , return_value = mock_registry .get (- 1001234567890 )):
587+ await handle_warn_command (mock_update , mock_context )
588+
589+ mock_context .bot .send_message .assert_called_once ()
0 commit comments