snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
This commit is contained in:
381
zerver/tests/test_message_triggered_bots.py
Normal file
381
zerver/tests/test_message_triggered_bots.py
Normal file
@@ -0,0 +1,381 @@
|
||||
from collections.abc import Callable
|
||||
from functools import wraps
|
||||
from typing import Any, Concatenate
|
||||
from unittest import mock
|
||||
|
||||
import responses
|
||||
from typing_extensions import ParamSpec, override
|
||||
|
||||
from zerver.actions.create_user import do_create_user
|
||||
from zerver.actions.message_send import get_message_triggered_bot_events
|
||||
from zerver.lib.test_classes import ZulipTestCase
|
||||
from zerver.lib.test_helpers import mock_queue_publish
|
||||
from zerver.models import Recipient, UserProfile
|
||||
from zerver.models.messages import UserMessage
|
||||
from zerver.models.realms import get_realm
|
||||
from zerver.models.scheduled_jobs import NotificationTriggers
|
||||
|
||||
BOT_TYPE_TO_QUEUE_NAME = {
|
||||
UserProfile.OUTGOING_WEBHOOK_BOT: "outgoing_webhooks",
|
||||
UserProfile.EMBEDDED_BOT: "embedded_bots",
|
||||
}
|
||||
|
||||
|
||||
class TestMessageTriggeredBotBasics(ZulipTestCase):
|
||||
def _get_outgoing_bot(self) -> UserProfile:
|
||||
outgoing_bot = do_create_user(
|
||||
email="bar-bot@zulip.com",
|
||||
password="test",
|
||||
realm=get_realm("zulip"),
|
||||
full_name="BarBot",
|
||||
bot_type=UserProfile.OUTGOING_WEBHOOK_BOT,
|
||||
bot_owner=self.example_user("cordelia"),
|
||||
acting_user=None,
|
||||
)
|
||||
|
||||
return outgoing_bot
|
||||
|
||||
def test_message_triggered_bot_events_for_direct_messages(self) -> None:
|
||||
sender = self.example_user("hamlet")
|
||||
assert not sender.is_bot
|
||||
|
||||
outgoing_bot = self._get_outgoing_bot()
|
||||
assert outgoing_bot.bot_type is not None
|
||||
|
||||
event_dict = get_message_triggered_bot_events(
|
||||
sender=sender,
|
||||
message_triggered_bot_tuples=[
|
||||
(outgoing_bot.id, outgoing_bot.bot_type),
|
||||
],
|
||||
active_user_ids={outgoing_bot.id},
|
||||
mentioned_user_ids=set(),
|
||||
recipient_type=Recipient.DIRECT_MESSAGE_GROUP,
|
||||
)
|
||||
|
||||
expected = dict(
|
||||
outgoing_webhooks=[
|
||||
dict(trigger=NotificationTriggers.DIRECT_MESSAGE, user_profile_id=outgoing_bot.id),
|
||||
],
|
||||
)
|
||||
|
||||
self.assertEqual(event_dict, expected)
|
||||
|
||||
def test_spurious_mentions(self) -> None:
|
||||
sender = self.example_user("hamlet")
|
||||
assert not sender.is_bot
|
||||
|
||||
outgoing_bot = self._get_outgoing_bot()
|
||||
assert outgoing_bot.bot_type is not None
|
||||
|
||||
# If outgoing_bot is not in mentioned_user_ids,
|
||||
# we will skip over it. This tests an anomaly
|
||||
# of the code that our query for bots can include
|
||||
# bots that may not actually be mentioned, and it's
|
||||
# easiest to just filter them in get_message_triggered_bot_events.
|
||||
event_dict = get_message_triggered_bot_events(
|
||||
sender=sender,
|
||||
message_triggered_bot_tuples=[
|
||||
(outgoing_bot.id, outgoing_bot.bot_type),
|
||||
],
|
||||
active_user_ids={outgoing_bot.id},
|
||||
mentioned_user_ids=set(),
|
||||
recipient_type=Recipient.STREAM,
|
||||
)
|
||||
|
||||
self.assert_length(event_dict, 0)
|
||||
|
||||
def test_message_triggered_bot_events_for_channel_mentions(self) -> None:
|
||||
sender = self.example_user("hamlet")
|
||||
assert not sender.is_bot
|
||||
|
||||
outgoing_bot = self._get_outgoing_bot()
|
||||
assert outgoing_bot.bot_type is not None
|
||||
|
||||
cordelia = self.example_user("cordelia")
|
||||
|
||||
red_herring_bot = self.create_test_bot(
|
||||
short_name="whatever",
|
||||
user_profile=cordelia,
|
||||
)
|
||||
|
||||
event_dict = get_message_triggered_bot_events(
|
||||
sender=sender,
|
||||
message_triggered_bot_tuples=[
|
||||
(outgoing_bot.id, outgoing_bot.bot_type),
|
||||
(red_herring_bot.id, UserProfile.OUTGOING_WEBHOOK_BOT),
|
||||
],
|
||||
active_user_ids=set(),
|
||||
mentioned_user_ids={outgoing_bot.id},
|
||||
recipient_type=Recipient.STREAM,
|
||||
)
|
||||
|
||||
expected = dict(
|
||||
outgoing_webhooks=[
|
||||
dict(trigger="mention", user_profile_id=outgoing_bot.id),
|
||||
],
|
||||
)
|
||||
|
||||
self.assertEqual(event_dict, expected)
|
||||
|
||||
def test_no_bot_events_when_mentioned_without_being_recipient(self) -> None:
|
||||
"""Message-triggered bots should not get access to mentions if they
|
||||
aren't a direct recipient."""
|
||||
sender = self.example_user("hamlet")
|
||||
assert not sender.is_bot
|
||||
|
||||
outgoing_bot = self._get_outgoing_bot()
|
||||
assert outgoing_bot.bot_type is not None
|
||||
|
||||
event_dict = get_message_triggered_bot_events(
|
||||
sender=sender,
|
||||
message_triggered_bot_tuples=[
|
||||
(outgoing_bot.id, outgoing_bot.bot_type),
|
||||
],
|
||||
active_user_ids=set(),
|
||||
mentioned_user_ids={outgoing_bot.id},
|
||||
recipient_type=Recipient.DIRECT_MESSAGE_GROUP,
|
||||
)
|
||||
|
||||
self.assert_length(event_dict, 0)
|
||||
|
||||
def test_message_triggered_bot_events_with_unexpected_bot_type(self) -> None:
|
||||
hamlet = self.example_user("hamlet")
|
||||
cordelia = self.example_user("cordelia")
|
||||
|
||||
bot = self.create_test_bot(
|
||||
short_name="whatever",
|
||||
user_profile=cordelia,
|
||||
)
|
||||
wrong_bot_type = UserProfile.INCOMING_WEBHOOK_BOT
|
||||
bot.bot_type = wrong_bot_type
|
||||
bot.save()
|
||||
|
||||
with self.assertLogs(level="ERROR") as m:
|
||||
event_dict = get_message_triggered_bot_events(
|
||||
sender=hamlet,
|
||||
message_triggered_bot_tuples=[
|
||||
(bot.id, wrong_bot_type),
|
||||
],
|
||||
active_user_ids=set(),
|
||||
mentioned_user_ids={bot.id},
|
||||
recipient_type=Recipient.DIRECT_MESSAGE_GROUP,
|
||||
)
|
||||
|
||||
self.assert_length(event_dict, 0)
|
||||
self.assertEqual(
|
||||
m.output,
|
||||
[
|
||||
f"ERROR:root:Unexpected bot_type for message-triggered bot id={bot.id}: {wrong_bot_type}"
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
ParamT = ParamSpec("ParamT")
|
||||
|
||||
|
||||
def for_all_bot_types(
|
||||
test_func: Callable[Concatenate["TestMessageTriggeredBotEventTriggers", ParamT], None],
|
||||
) -> Callable[Concatenate["TestMessageTriggeredBotEventTriggers", ParamT], None]:
|
||||
@wraps(test_func)
|
||||
def _wrapped(
|
||||
self: "TestMessageTriggeredBotEventTriggers", /, *args: ParamT.args, **kwargs: ParamT.kwargs
|
||||
) -> None:
|
||||
for bot_type in BOT_TYPE_TO_QUEUE_NAME:
|
||||
self.bot_profile.bot_type = bot_type
|
||||
self.bot_profile.save()
|
||||
test_func(self, *args, **kwargs)
|
||||
|
||||
return _wrapped
|
||||
|
||||
|
||||
def patch_queue_publish(
|
||||
method_to_patch: str,
|
||||
) -> Callable[
|
||||
[Callable[["TestMessageTriggeredBotEventTriggers", mock.Mock], None]],
|
||||
Callable[["TestMessageTriggeredBotEventTriggers"], None],
|
||||
]:
|
||||
def inner(
|
||||
func: Callable[["TestMessageTriggeredBotEventTriggers", mock.Mock], None],
|
||||
) -> Callable[["TestMessageTriggeredBotEventTriggers"], None]:
|
||||
@wraps(func)
|
||||
def _wrapped(self: "TestMessageTriggeredBotEventTriggers") -> None:
|
||||
with mock_queue_publish(method_to_patch) as m:
|
||||
func(self, m)
|
||||
|
||||
return _wrapped
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
class TestMessageTriggeredBotEventTriggers(ZulipTestCase):
|
||||
@override
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.user_profile = self.example_user("othello")
|
||||
self.bot_profile = do_create_user(
|
||||
email="foo-bot@zulip.com",
|
||||
password="test",
|
||||
realm=get_realm("zulip"),
|
||||
full_name="FooBot",
|
||||
bot_type=UserProfile.OUTGOING_WEBHOOK_BOT,
|
||||
bot_owner=self.user_profile,
|
||||
acting_user=None,
|
||||
)
|
||||
self.second_bot_profile = do_create_user(
|
||||
email="bar-bot@zulip.com",
|
||||
password="test",
|
||||
realm=get_realm("zulip"),
|
||||
full_name="BarBot",
|
||||
bot_type=UserProfile.OUTGOING_WEBHOOK_BOT,
|
||||
bot_owner=self.user_profile,
|
||||
acting_user=None,
|
||||
)
|
||||
|
||||
@for_all_bot_types
|
||||
@patch_queue_publish("zerver.actions.message_send.queue_event_on_commit")
|
||||
def test_trigger_on_stream_mention_from_user(
|
||||
self, mock_queue_event_on_commit: mock.Mock
|
||||
) -> None:
|
||||
content = "@**FooBot** foo bar!!!"
|
||||
recipient = "Denmark"
|
||||
trigger = "mention"
|
||||
recipient_type = "stream"
|
||||
|
||||
def check_values_passed(
|
||||
queue_name: Any,
|
||||
trigger_event: dict[str, Any],
|
||||
processor: Callable[[Any], None] | None = None,
|
||||
) -> None:
|
||||
assert self.bot_profile.bot_type
|
||||
self.assertEqual(queue_name, BOT_TYPE_TO_QUEUE_NAME[self.bot_profile.bot_type])
|
||||
self.assertEqual(trigger_event["message"]["content"], content)
|
||||
self.assertEqual(trigger_event["message"]["display_recipient"], recipient)
|
||||
self.assertEqual(trigger_event["message"]["sender_email"], self.user_profile.email)
|
||||
self.assertEqual(trigger_event["message"]["type"], recipient_type)
|
||||
self.assertEqual(trigger_event["trigger"], trigger)
|
||||
self.assertEqual(trigger_event["user_profile_id"], self.bot_profile.id)
|
||||
|
||||
mock_queue_event_on_commit.side_effect = check_values_passed
|
||||
|
||||
self.send_stream_message(self.user_profile, "Denmark", content)
|
||||
self.assertTrue(mock_queue_event_on_commit.called)
|
||||
|
||||
@patch_queue_publish("zerver.actions.message_send.queue_event_on_commit")
|
||||
def test_no_trigger_on_stream_message_without_mention(
|
||||
self, mock_queue_event_on_commit: mock.Mock
|
||||
) -> None:
|
||||
sender = self.user_profile
|
||||
self.send_stream_message(sender, "Denmark")
|
||||
self.assertFalse(mock_queue_event_on_commit.called)
|
||||
|
||||
@for_all_bot_types
|
||||
@patch_queue_publish("zerver.actions.message_send.queue_event_on_commit")
|
||||
def test_no_trigger_on_stream_mention_from_bot(
|
||||
self, mock_queue_event_on_commit: mock.Mock
|
||||
) -> None:
|
||||
self.send_stream_message(self.second_bot_profile, "Denmark", "@**FooBot** foo bar!!!")
|
||||
self.assertFalse(mock_queue_event_on_commit.called)
|
||||
|
||||
@for_all_bot_types
|
||||
@patch_queue_publish("zerver.actions.message_send.queue_event_on_commit")
|
||||
def test_trigger_on_personal_message_from_user(
|
||||
self, mock_queue_event_on_commit: mock.Mock
|
||||
) -> None:
|
||||
sender = self.user_profile
|
||||
recipient = self.bot_profile
|
||||
|
||||
def check_values_passed(
|
||||
queue_name: Any,
|
||||
trigger_event: dict[str, Any],
|
||||
processor: Callable[[Any], None] | None = None,
|
||||
) -> None:
|
||||
assert self.bot_profile.bot_type
|
||||
self.assertEqual(queue_name, BOT_TYPE_TO_QUEUE_NAME[self.bot_profile.bot_type])
|
||||
self.assertEqual(trigger_event["user_profile_id"], self.bot_profile.id)
|
||||
self.assertEqual(trigger_event["trigger"], NotificationTriggers.DIRECT_MESSAGE)
|
||||
self.assertEqual(trigger_event["message"]["sender_email"], sender.email)
|
||||
display_recipients = [
|
||||
trigger_event["message"]["display_recipient"][0]["email"],
|
||||
trigger_event["message"]["display_recipient"][1]["email"],
|
||||
]
|
||||
self.assertTrue(sender.email in display_recipients)
|
||||
self.assertTrue(recipient.email in display_recipients)
|
||||
|
||||
mock_queue_event_on_commit.side_effect = check_values_passed
|
||||
|
||||
self.send_personal_message(sender, recipient, "test")
|
||||
self.assertTrue(mock_queue_event_on_commit.called)
|
||||
|
||||
@for_all_bot_types
|
||||
@patch_queue_publish("zerver.actions.message_send.queue_event_on_commit")
|
||||
def test_no_trigger_on_personal_message_from_bot(
|
||||
self, mock_queue_event_on_commit: mock.Mock
|
||||
) -> None:
|
||||
sender = self.second_bot_profile
|
||||
recipient = self.bot_profile
|
||||
self.send_personal_message(sender, recipient)
|
||||
self.assertFalse(mock_queue_event_on_commit.called)
|
||||
|
||||
@for_all_bot_types
|
||||
@patch_queue_publish("zerver.actions.message_send.queue_event_on_commit")
|
||||
def test_trigger_on_group_direct_message_from_user(
|
||||
self, mock_queue_event_on_commit: mock.Mock
|
||||
) -> None:
|
||||
self.second_bot_profile.bot_type = self.bot_profile.bot_type
|
||||
self.second_bot_profile.save()
|
||||
|
||||
sender = self.user_profile
|
||||
recipients = [self.bot_profile, self.second_bot_profile]
|
||||
profile_ids = [self.bot_profile.id, self.second_bot_profile.id]
|
||||
|
||||
def check_values_passed(
|
||||
queue_name: Any,
|
||||
trigger_event: dict[str, Any],
|
||||
processor: Callable[[Any], None] | None = None,
|
||||
) -> None:
|
||||
assert self.bot_profile.bot_type
|
||||
self.assertEqual(queue_name, BOT_TYPE_TO_QUEUE_NAME[self.bot_profile.bot_type])
|
||||
self.assertIn(trigger_event["user_profile_id"], profile_ids)
|
||||
profile_ids.remove(trigger_event["user_profile_id"])
|
||||
self.assertEqual(trigger_event["trigger"], NotificationTriggers.DIRECT_MESSAGE)
|
||||
self.assertEqual(trigger_event["message"]["sender_email"], sender.email)
|
||||
self.assertEqual(trigger_event["message"]["type"], "private")
|
||||
|
||||
mock_queue_event_on_commit.side_effect = check_values_passed
|
||||
|
||||
self.send_group_direct_message(sender, recipients, "test")
|
||||
self.assertEqual(mock_queue_event_on_commit.call_count, 2)
|
||||
|
||||
@for_all_bot_types
|
||||
@patch_queue_publish("zerver.actions.message_send.queue_event_on_commit")
|
||||
def test_no_trigger_on_group_direct_message_from_bot(
|
||||
self, mock_queue_event_on_commit: mock.Mock
|
||||
) -> None:
|
||||
sender = self.second_bot_profile
|
||||
recipients = [self.user_profile, self.bot_profile]
|
||||
self.send_group_direct_message(sender, recipients)
|
||||
self.assertFalse(mock_queue_event_on_commit.called)
|
||||
|
||||
@responses.activate
|
||||
@for_all_bot_types
|
||||
def test_message_flagged_read_after_bot_processes_event(self) -> None:
|
||||
"""
|
||||
Verifies that once an event has been processed by the message-triggered
|
||||
bot's queue processor, the message is marked as processed (flagged with
|
||||
`read`).
|
||||
"""
|
||||
sender = self.user_profile
|
||||
recipients = [self.user_profile, self.bot_profile, self.second_bot_profile]
|
||||
responses.add(
|
||||
responses.POST,
|
||||
"https://bot.example.com/",
|
||||
json="",
|
||||
)
|
||||
message_id = self.send_group_direct_message(
|
||||
sender, recipients, content=f"@**{self.bot_profile.full_name}** foo"
|
||||
)
|
||||
# message = Message.objects.get(id=message_id, sender=sender)
|
||||
bot_user_message = UserMessage.objects.get(
|
||||
user_profile=self.bot_profile, message=message_id
|
||||
)
|
||||
self.assertIn("read", bot_user_message.flags_list())
|
||||
Reference in New Issue
Block a user