import filecmp import os from typing import Any from unittest.mock import MagicMock, patch import orjson from django.core import mail from django.test import override_settings from zulip_bots.custom_exceptions import ConfigValidationError from zerver.actions.bots import do_change_bot_owner, do_change_default_sending_stream from zerver.actions.realm_settings import do_set_realm_user_default_setting from zerver.actions.streams import do_change_stream_permission from zerver.actions.users import do_change_can_create_users, do_change_user_role, do_deactivate_user from zerver.lib.bot_config import ConfigError, get_bot_config from zerver.lib.bot_lib import get_bot_handler from zerver.lib.integrations import EMBEDDED_BOTS, WebhookIntegration from zerver.lib.test_classes import UploadSerializeMixin, ZulipTestCase from zerver.lib.test_helpers import avatar_disk_path, get_test_image_file from zerver.lib.utils import assert_is_not_none from zerver.lib.webhooks.common import WebhookConfigOption from zerver.models import RealmUserDefault, Service, Subscription, UserProfile from zerver.models.bots import get_bot_services from zerver.models.realms import BotCreationPolicyEnum, get_realm from zerver.models.streams import get_stream from zerver.models.users import bot_owner_user_ids, get_user, is_cross_realm_bot_email # A test validator def _check_string(var_name: str, val: str) -> str | None: if val.startswith("_"): return f'{var_name} starts with a "_" and is hence invalid.' return None stripe_sample_config_options = [ WebhookIntegration( "stripe", ["financial"], display_name="Stripe", config_options=[ WebhookConfigOption( name="stripe_api_key", description="Stripe API key", validator=_check_string ) ], ), ] class BotTest(ZulipTestCase, UploadSerializeMixin): def get_bot_user(self, email: str) -> UserProfile: realm = get_realm("zulip") bot = get_user(email, realm) return bot def assert_num_bots_equal(self, count: int) -> None: result = self.client_get("/json/bots") response_dict = self.assert_json_success(result) self.assert_length(response_dict["bots"], count) def create_bot(self, **extras: Any) -> dict[str, Any]: bot_info = { "full_name": "The Bot of Hamlet", "short_name": "hambot", "bot_type": "1", } bot_info.update(extras) result = self.client_post("/json/bots", bot_info) response_dict = self.assert_json_success(result) return response_dict def test_bot_domain(self) -> None: self.login("hamlet") self.create_bot() self.assertTrue(UserProfile.objects.filter(email="hambot-bot@zulip.testserver").exists()) # The other cases are hard to test directly, since we don't allow creating bots from # the wrong subdomain, and because 'testserver.example.com' is not a valid domain for the bot's email. # So we just test the Realm.get_bot_domain function. realm = get_realm("zulip") self.assertEqual(realm.get_bot_domain(), "zulip.testserver") def deactivate_bot(self) -> None: email = "hambot-bot@zulip.testserver" result = self.client_delete(f"/json/bots/{self.get_bot_user(email).id}") self.assert_json_success(result) def test_add_bot_with_bad_username(self) -> None: self.login("hamlet") self.assert_num_bots_equal(0) # Invalid username bot_info = dict( full_name="My bot name", short_name="my\nbot", ) result = self.client_post("/json/bots", bot_info) self.assert_json_error(result, "Bad name or username") self.assert_num_bots_equal(0) # Empty username bot_info = dict( full_name="My bot name", short_name="", ) result = self.client_post("/json/bots", bot_info) self.assert_json_error(result, "Bad name or username") self.assert_num_bots_equal(0) @override_settings(FAKE_EMAIL_DOMAIN="invaliddomain", REALM_HOSTS={"zulip": "127.0.0.1"}) def test_add_bot_with_invalid_fake_email_domain(self) -> None: self.login("hamlet") self.assert_num_bots_equal(0) bot_info = { "full_name": "The Bot of Hamlet", "short_name": "hambot", "bot_type": "1", } result = self.client_post("/json/bots", bot_info) error_message = ( "Can't create bots until FAKE_EMAIL_DOMAIN is correctly configured.\n" "Please contact your server administrator." ) self.assert_json_error(result, error_message) self.assert_num_bots_equal(0) def test_add_bot_with_no_name(self) -> None: self.login("hamlet") self.assert_num_bots_equal(0) bot_info = dict( full_name="a", short_name="bot", ) result = self.client_post("/json/bots", bot_info) self.assert_json_error(result, "Name too short!") self.assert_num_bots_equal(0) def test_json_users_with_bots(self) -> None: hamlet = self.example_user("hamlet") self.login_user(hamlet) self.assert_num_bots_equal(0) num_bots = 3 for i in range(num_bots): full_name = f"Bot {i}" short_name = f"bot-{i}" bot_info = dict( full_name=full_name, short_name=short_name, bot_type=1, ) result = self.client_post("/json/bots", bot_info) self.assert_json_success(result) self.assert_num_bots_equal(num_bots) with self.assert_database_query_count(4): users_result = self.client_get("/json/users") self.assert_json_success(users_result) def test_add_bot(self) -> None: hamlet = self.example_user("hamlet") self.login("hamlet") self.assert_num_bots_equal(0) with self.capture_send_event_calls(expected_num_events=4) as events: result = self.create_bot() self.assert_num_bots_equal(1) email = "hambot-bot@zulip.testserver" bot = self.get_bot_user(email) (event,) = (e for e in events if e["event"]["type"] == "realm_bot") self.assertEqual(result["user_id"], bot.id) self.assertEqual( dict( type="realm_bot", op="add", bot=dict( email="hambot-bot@zulip.testserver", user_id=bot.id, bot_type=bot.bot_type, full_name="The Bot of Hamlet", is_active=True, api_key=result["api_key"], avatar_url=result["avatar_url"], default_sending_stream=None, default_events_register_stream=None, default_all_public_streams=False, services=[], owner_id=hamlet.id, ), ), event["event"], ) users_result = self.client_get("/json/users") members = orjson.loads(users_result.content)["members"] [bot_dict] = [m for m in members if m["email"] == "hambot-bot@zulip.testserver"] self.assertEqual(bot_dict["bot_owner_id"], self.example_user("hamlet").id) self.assertEqual(bot_dict["user_id"], self.get_bot_user(email).id) @override_settings(FAKE_EMAIL_DOMAIN="fakedomain.com", REALM_HOSTS={"zulip": "127.0.0.1"}) def test_add_bot_with_fake_email_domain(self) -> None: self.login("hamlet") self.assert_num_bots_equal(0) self.create_bot() self.assert_num_bots_equal(1) email = "hambot-bot@fakedomain.com" self.get_bot_user(email) @override_settings(EXTERNAL_HOST="example.com") def test_add_bot_verify_subdomain_in_email_address(self) -> None: self.login("hamlet") self.assert_num_bots_equal(0) self.create_bot() self.assert_num_bots_equal(1) email = "hambot-bot@zulip.example.com" self.get_bot_user(email) @override_settings( FAKE_EMAIL_DOMAIN="fakedomain.com", REALM_HOSTS={"zulip": "zulip.example.com"} ) def test_add_bot_host_used_as_domain_if_valid(self) -> None: self.login("hamlet") self.assert_num_bots_equal(0) self.create_bot() self.assert_num_bots_equal(1) email = "hambot-bot@zulip.example.com" self.get_bot_user(email) def test_add_bot_with_username_in_use(self) -> None: self.login("hamlet") self.assert_num_bots_equal(0) self.create_bot() self.assert_num_bots_equal(1) # The short_name is used in the email, which we call # "Username" for legacy reasons. bot_info = dict( full_name="whatever", short_name="hambot", ) result = self.client_post("/json/bots", bot_info) self.assert_json_error(result, "Username already in use") dup_full_name = "The Bot of Hamlet" bot_info = dict( full_name=dup_full_name, short_name="whatever", ) result = self.client_post("/json/bots", bot_info) self.assert_json_error(result, "Name is already in use!") def test_add_bot_with_user_avatar(self) -> None: email = "hambot-bot@zulip.testserver" realm = get_realm("zulip") self.login("hamlet") self.assert_num_bots_equal(0) with get_test_image_file("img.png") as fp: self.create_bot(file=fp) profile = get_user(email, realm) # Make sure that avatar image that we've uploaded is same with avatar image in the server self.assertTrue( filecmp.cmp(fp.name, os.path.splitext(avatar_disk_path(profile))[0] + ".original") ) self.assert_num_bots_equal(1) self.assertEqual(profile.avatar_source, UserProfile.AVATAR_FROM_USER) self.assertTrue(os.path.exists(avatar_disk_path(profile))) def test_add_bot_with_too_many_files(self) -> None: self.login("hamlet") self.assert_num_bots_equal(0) with get_test_image_file("img.png") as fp1, get_test_image_file("img.gif") as fp2: bot_info = dict( full_name="whatever", short_name="whatever", file1=fp1, file2=fp2, ) result = self.client_post("/json/bots", bot_info) self.assert_json_error(result, "You may only upload one file at a time") self.assert_num_bots_equal(0) def test_add_bot_with_default_sending_stream(self) -> None: email = "hambot-bot@zulip.testserver" realm = get_realm("zulip") self.login("hamlet") self.assert_num_bots_equal(0) result = self.create_bot(default_sending_stream="Denmark") self.assert_num_bots_equal(1) self.assertEqual(result["default_sending_stream"], "Denmark") profile = get_user(email, realm) assert profile.default_sending_stream is not None self.assertEqual(profile.default_sending_stream.name, "Denmark") def test_add_bot_with_default_sending_stream_not_subscribed(self) -> None: email = "hambot-bot@zulip.testserver" realm = get_realm("zulip") self.login("hamlet") self.assert_num_bots_equal(0) result = self.create_bot(default_sending_stream="Rome") self.assert_num_bots_equal(1) self.assertEqual(result["default_sending_stream"], "Rome") profile = get_user(email, realm) assert profile.default_sending_stream is not None self.assertEqual(profile.default_sending_stream.name, "Rome") def test_add_bot_email_address_visibility(self) -> None: # Test that we don't mangle the email field with # email_address_visibility limited to admins user = self.example_user("hamlet") realm_user_default = RealmUserDefault.objects.get(realm=user.realm) do_set_realm_user_default_setting( realm_user_default, "email_address_visibility", RealmUserDefault.EMAIL_ADDRESS_VISIBILITY_ADMINS, acting_user=None, ) user.refresh_from_db() self.login_user(user) self.assert_num_bots_equal(0) with self.capture_send_event_calls(expected_num_events=4) as events: result = self.create_bot() self.assert_num_bots_equal(1) email = "hambot-bot@zulip.testserver" bot = self.get_bot_user(email) (event,) = (e for e in events if e["event"]["type"] == "realm_bot") self.assertEqual( dict( type="realm_bot", op="add", bot=dict( email="hambot-bot@zulip.testserver", user_id=bot.id, bot_type=bot.bot_type, full_name="The Bot of Hamlet", is_active=True, api_key=result["api_key"], avatar_url=result["avatar_url"], default_sending_stream=None, default_events_register_stream=None, default_all_public_streams=False, services=[], owner_id=user.id, ), ), event["event"], ) users_result = self.client_get("/json/users") members = orjson.loads(users_result.content)["members"] [bot_dict] = [m for m in members if m["email"] == "hambot-bot@zulip.testserver"] self.assertEqual(bot_dict["bot_owner_id"], user.id) self.assertEqual(bot_dict["user_id"], self.get_bot_user(email).id) def test_bot_add_subscription(self) -> None: """ Calling POST /json/users/me/subscriptions should successfully add streams, and a stream to the list of subscriptions and confirm the right number of events are generated. When 'principals' has a bot, no notification message event or invitation email is sent when add_subscriptions_backend is called in the above API call. """ hamlet = self.example_user("hamlet") iago = self.example_user("iago") self.login_user(hamlet) # Normal user i.e. not a bot. request_data = { "principals": '["' + iago.email + '"]', } with self.capture_send_event_calls(expected_num_events=3) as events: result = self.common_subscribe_to_streams(hamlet, ["Rome"], request_data) self.assert_json_success(result) msg_event = [e for e in events if e["event"]["type"] == "message"] self.assert_length(msg_event, 1) # Notification message event is sent. # Create a bot. self.assert_num_bots_equal(0) self.create_bot() self.assert_num_bots_equal(1) # A bot bot_request_data = { "principals": '["hambot-bot@zulip.testserver"]', } with self.capture_send_event_calls(expected_num_events=2) as events_bot: result = self.common_subscribe_to_streams(hamlet, ["Rome"], bot_request_data) self.assert_json_success(result) # No notification message event or invitation email is sent because of bot. msg_event = [e for e in events_bot if e["event"]["type"] == "message"] self.assert_length(msg_event, 0) self.assert_length(events_bot, len(events) - 1) # Test runner automatically redirects all sent email to a dummy 'outbox'. self.assert_length(mail.outbox, 0) def test_add_bot_with_default_sending_stream_private_allowed(self) -> None: self.login("hamlet") user_profile = self.example_user("hamlet") stream = get_stream("Denmark", user_profile.realm) self.subscribe(user_profile, stream.name) do_change_stream_permission( stream, invite_only=True, history_public_to_subscribers=False, is_web_public=False, acting_user=user_profile, ) self.assert_num_bots_equal(0) with self.capture_send_event_calls(expected_num_events=4) as events: result = self.create_bot(default_sending_stream="Denmark") self.assert_num_bots_equal(1) self.assertEqual(result["default_sending_stream"], "Denmark") email = "hambot-bot@zulip.testserver" realm = get_realm("zulip") profile = get_user(email, realm) assert profile.default_sending_stream is not None self.assertEqual(profile.default_sending_stream.name, "Denmark") (event,) = (e for e in events if e["event"]["type"] == "realm_bot") self.assertEqual( dict( type="realm_bot", op="add", bot=dict( email="hambot-bot@zulip.testserver", user_id=profile.id, full_name="The Bot of Hamlet", bot_type=profile.bot_type, is_active=True, api_key=result["api_key"], avatar_url=result["avatar_url"], default_sending_stream="Denmark", default_events_register_stream=None, default_all_public_streams=False, services=[], owner_id=user_profile.id, ), ), event["event"], ) self.assertEqual(event["users"], [user_profile.id]) def test_add_bot_with_default_sending_stream_private_denied(self) -> None: self.login("hamlet") realm = self.example_user("hamlet").realm stream = get_stream("Denmark", realm) self.unsubscribe(self.example_user("hamlet"), "Denmark") do_change_stream_permission( stream, invite_only=True, history_public_to_subscribers=False, is_web_public=False, acting_user=self.example_user("hamlet"), ) bot_info = { "full_name": "The Bot of Hamlet", "short_name": "hambot", "default_sending_stream": "Denmark", } result = self.client_post("/json/bots", bot_info) self.assert_json_error(result, "Invalid channel name 'Denmark'") def test_add_bot_with_default_events_register_stream(self) -> None: bot_email = "hambot-bot@zulip.testserver" bot_realm = get_realm("zulip") self.login("hamlet") self.assert_num_bots_equal(0) result = self.create_bot(default_events_register_stream="Denmark") self.assert_num_bots_equal(1) self.assertEqual(result["default_events_register_stream"], "Denmark") profile = get_user(bot_email, bot_realm) assert profile.default_events_register_stream is not None self.assertEqual(profile.default_events_register_stream.name, "Denmark") def test_add_bot_with_default_events_register_stream_private_allowed(self) -> None: self.login("hamlet") user_profile = self.example_user("hamlet") stream = self.subscribe(user_profile, "Denmark") do_change_stream_permission( stream, invite_only=True, history_public_to_subscribers=False, is_web_public=False, acting_user=user_profile, ) self.assert_num_bots_equal(0) with self.capture_send_event_calls(expected_num_events=4) as events: result = self.create_bot(default_events_register_stream="Denmark") self.assert_num_bots_equal(1) self.assertEqual(result["default_events_register_stream"], "Denmark") bot_email = "hambot-bot@zulip.testserver" bot_realm = get_realm("zulip") bot_profile = get_user(bot_email, bot_realm) assert bot_profile.default_events_register_stream is not None self.assertEqual(bot_profile.default_events_register_stream.name, "Denmark") (event,) = (e for e in events if e["event"]["type"] == "realm_bot") self.assertEqual( dict( type="realm_bot", op="add", bot=dict( email="hambot-bot@zulip.testserver", full_name="The Bot of Hamlet", user_id=bot_profile.id, bot_type=bot_profile.bot_type, is_active=True, api_key=result["api_key"], avatar_url=result["avatar_url"], default_sending_stream=None, default_events_register_stream="Denmark", default_all_public_streams=False, services=[], owner_id=user_profile.id, ), ), event["event"], ) self.assertEqual(event["users"], [user_profile.id]) def test_add_bot_with_default_events_register_stream_private_denied(self) -> None: self.login("hamlet") realm = self.example_user("hamlet").realm stream = get_stream("Denmark", realm) self.unsubscribe(self.example_user("hamlet"), "Denmark") do_change_stream_permission( stream, invite_only=True, history_public_to_subscribers=False, is_web_public=False, acting_user=self.example_user("hamlet"), ) self.assert_num_bots_equal(0) bot_info = { "full_name": "The Bot of Hamlet", "short_name": "hambot", "default_events_register_stream": "Denmark", } result = self.client_post("/json/bots", bot_info) self.assert_json_error(result, "Invalid channel name 'Denmark'") def test_add_bot_with_default_all_public_streams(self) -> None: self.login("hamlet") self.assert_num_bots_equal(0) result = self.create_bot(default_all_public_streams=orjson.dumps(True).decode()) self.assert_num_bots_equal(1) self.assertTrue(result["default_all_public_streams"]) bot_email = "hambot-bot@zulip.testserver" bot_realm = get_realm("zulip") profile = get_user(bot_email, bot_realm) self.assertEqual(profile.default_all_public_streams, True) def test_deactivate_bot(self) -> None: self.login("hamlet") self.assert_num_bots_equal(0) self.create_bot() self.assert_num_bots_equal(1) self.deactivate_bot() # You can deactivate the same bot twice. self.deactivate_bot() self.assert_num_bots_equal(0) def test_deactivate_bot_invalid_id(self) -> None: self.login("hamlet") self.assert_num_bots_equal(0) self.create_bot() self.assert_num_bots_equal(1) invalid_bot_id = 1000 result = self.client_delete(f"/json/bots/{invalid_bot_id}") self.assert_json_error(result, "No such bot") self.assert_num_bots_equal(1) def test_deactivate_bot_with_no_owners(self) -> None: iago = self.example_user("iago") self.login("iago") self.create_bot() self.assert_num_bots_equal(1) # Set up the bot to be a private bot, as otherwise realm admins # are used as default owners in the absence of .bot_owner_id. stream = get_stream("Denmark", get_realm("zulip")) do_change_stream_permission( stream, invite_only=True, history_public_to_subscribers=False, is_web_public=False, acting_user=iago, ) new_bot = assert_is_not_none(UserProfile.objects.last()) do_change_default_sending_stream(new_bot, stream, acting_user=iago) new_bot.bot_owner_id = None new_bot.save() result = self.client_delete(f"/json/bots/{new_bot.id}") self.assert_json_success(result) self.assert_num_bots_equal(0) def test_deactivate_bot_with_owner_deactivation(self) -> None: user = self.example_user("hamlet") self.login_user(user) bot_info = { "full_name": "The Bot of Hamlet", "short_name": "hambot", } result = self.client_post("/json/bots", bot_info) self.assert_json_success(result) bot_info = { "full_name": "The Another Bot of Hamlet", "short_name": "hambot-another", } result = self.client_post("/json/bots", bot_info) self.assert_json_success(result) self.assertEqual( UserProfile.objects.filter(is_bot=True, bot_owner=user, is_active=True).count(), 2 ) result = self.client_delete("/json/users/me") self.assert_json_success(result) user = self.example_user("hamlet") self.assertFalse(user.is_active) self.login("iago") self.assertFalse( UserProfile.objects.filter(is_bot=True, bot_owner=user, is_active=True).exists() ) def test_cannot_deactivate_other_realm_bot(self) -> None: user = self.mit_user("starnine") self.login_user(user) bot_info = { "full_name": "The Bot in zephyr", "short_name": "starn-bot", "bot_type": "1", } result = self.client_post("/json/bots", bot_info, subdomain="zephyr") self.assert_json_success(result) result = self.client_get("/json/bots", subdomain="zephyr") response_dict = self.assert_json_success(result) bot_email = response_dict["bots"][0]["username"] bot = get_user(bot_email, user.realm) self.login("iago") result = self.client_delete(f"/json/bots/{bot.id}") self.assert_json_error(result, "No such bot") def test_bot_deactivation_attacks(self) -> None: """You cannot deactivate somebody else's bot.""" self.login("hamlet") self.assert_num_bots_equal(0) self.create_bot() self.assert_num_bots_equal(1) # Have Othello try to deactivate both Hamlet and # Hamlet's bot. self.login("othello") # Cannot deactivate a user as a bot result = self.client_delete("/json/bots/{}".format(self.example_user("hamlet").id)) self.assert_json_error(result, "No such bot") email = "hambot-bot@zulip.testserver" result = self.client_delete(f"/json/bots/{self.get_bot_user(email).id}") self.assert_json_error(result, "Insufficient permission") # But we don't actually deactivate the other person's bot. self.login("hamlet") self.assert_num_bots_equal(1) # Cannot deactivate a bot as a user result = self.client_delete(f"/json/users/{self.get_bot_user(email).id}") self.assert_json_error(result, "No such user") self.assert_num_bots_equal(1) def test_activate_bot_with_duplicate_name(self) -> None: self.login("iago") # Create a bot and then deactivate it original_name = "Hamlet" bot_info = { "full_name": original_name, "short_name": "hambot", } result = self.client_post("/json/bots", bot_info) self.assert_json_success(result) bot_email = "hambot-bot@zulip.testserver" bot = self.get_bot_user(bot_email) do_deactivate_user(bot, False, acting_user=None) self.assertFalse( UserProfile.objects.filter(is_bot=True, id=bot.id, is_active=True).exists() ) # Create another bot with the same name bot_info2 = { "full_name": original_name, "short_name": "hambot2", } result = self.client_post("/json/bots", bot_info2) self.assert_json_success(result) result = self.client_post(f"/json/users/{bot.id}/reactivate") self.assert_json_error( result, 'There is already an active bot named "Hamlet" in this organization. To reactivate this bot, you must rename or deactivate the other one first.', ) def test_bot_permissions(self) -> None: self.login("hamlet") self.assert_num_bots_equal(0) self.create_bot() self.assert_num_bots_equal(1) # Have Othello try to mess with Hamlet's bots. self.login("othello") email = "hambot-bot@zulip.testserver" result = self.client_post(f"/json/bots/{self.get_bot_user(email).id}/api_key/regenerate") self.assert_json_error(result, "Insufficient permission") bot_info = { "full_name": "Fred", } result = self.client_patch(f"/json/bots/{self.get_bot_user(email).id}", bot_info) self.assert_json_error(result, "Insufficient permission") def get_bot(self) -> dict[str, Any]: result = self.client_get("/json/bots") return self.assert_json_success(result)["bots"][0] def test_update_api_key(self) -> None: self.login("hamlet") self.create_bot() bot = self.get_bot() old_api_key = bot["api_key"] email = "hambot-bot@zulip.testserver" result = self.client_post(f"/json/bots/{self.get_bot_user(email).id}/api_key/regenerate") new_api_key = self.assert_json_success(result)["api_key"] self.assertNotEqual(old_api_key, new_api_key) bot = self.get_bot() self.assertEqual(new_api_key, bot["api_key"]) def test_update_api_key_for_invalid_user(self) -> None: self.login("hamlet") invalid_user_id = 1000 result = self.client_post(f"/json/bots/{invalid_user_id}/api_key/regenerate") self.assert_json_error(result, "No such bot") def test_add_bot_with_bot_type_default(self) -> None: bot_email = "hambot-bot@zulip.testserver" bot_realm = get_realm("zulip") self.login("hamlet") self.assert_num_bots_equal(0) self.create_bot(bot_type=UserProfile.DEFAULT_BOT) self.assert_num_bots_equal(1) profile = get_user(bot_email, bot_realm) self.assertEqual(profile.bot_type, UserProfile.DEFAULT_BOT) def test_add_bot_with_bot_type_incoming_webhook(self) -> None: bot_email = "hambot-bot@zulip.testserver" bot_realm = get_realm("zulip") self.login("hamlet") self.assert_num_bots_equal(0) self.create_bot(bot_type=UserProfile.INCOMING_WEBHOOK_BOT) self.assert_num_bots_equal(1) profile = get_user(bot_email, bot_realm) self.assertEqual(profile.bot_type, UserProfile.INCOMING_WEBHOOK_BOT) def test_add_bot_with_bot_type_invalid(self) -> None: bot_info = { "full_name": "The Bot of Hamlet", "short_name": "hambot", "bot_type": 7, } self.login("hamlet") self.assert_num_bots_equal(0) result = self.client_post("/json/bots", bot_info) self.assert_num_bots_equal(0) self.assert_json_error(result, "Invalid bot type") def test_no_generic_bots_allowed_for_non_admins(self) -> None: bot_info = { "full_name": "The Bot of Hamlet", "short_name": "hambot", "bot_type": 1, } bot_email = "hambot-bot@zulip.testserver" bot_realm = get_realm("zulip") bot_realm.bot_creation_policy = BotCreationPolicyEnum.LIMIT_GENERIC_BOTS bot_realm.save(update_fields=["bot_creation_policy"]) # A regular user cannot create a generic bot self.login("hamlet") self.assert_num_bots_equal(0) result = self.client_post("/json/bots", bot_info) self.assert_num_bots_equal(0) self.assert_json_error(result, "Must be an organization administrator") # But can create an incoming webhook self.assert_num_bots_equal(0) self.create_bot(bot_type=UserProfile.INCOMING_WEBHOOK_BOT) self.assert_num_bots_equal(1) profile = get_user(bot_email, bot_realm) self.assertEqual(profile.bot_type, UserProfile.INCOMING_WEBHOOK_BOT) def test_no_generic_bot_reactivation_allowed_for_non_admins(self) -> None: self.login("hamlet") self.create_bot(bot_type=UserProfile.DEFAULT_BOT) bot_realm = get_realm("zulip") bot_realm.bot_creation_policy = BotCreationPolicyEnum.LIMIT_GENERIC_BOTS bot_realm.save(update_fields=["bot_creation_policy"]) bot_email = "hambot-bot@zulip.testserver" bot_user = get_user(bot_email, bot_realm) do_deactivate_user(bot_user, acting_user=None) # A regular user cannot reactivate a generic bot self.assert_num_bots_equal(0) result = self.client_post(f"/json/users/{bot_user.id}/reactivate") self.assert_json_error(result, "Must be an organization administrator") self.assert_num_bots_equal(0) def test_no_generic_bots_allowed_for_admins(self) -> None: bot_email = "hambot-bot@zulip.testserver" bot_realm = get_realm("zulip") bot_realm.bot_creation_policy = BotCreationPolicyEnum.LIMIT_GENERIC_BOTS bot_realm.save(update_fields=["bot_creation_policy"]) # An administrator can create any type of bot self.login("iago") self.assert_num_bots_equal(0) self.create_bot(bot_type=UserProfile.DEFAULT_BOT) self.assert_num_bots_equal(1) profile = get_user(bot_email, bot_realm) self.assertEqual(profile.bot_type, UserProfile.DEFAULT_BOT) def test_no_bots_allowed_for_non_admins(self) -> None: bot_info = { "full_name": "The Bot of Hamlet", "short_name": "hambot", "bot_type": 1, } bot_realm = get_realm("zulip") bot_realm.bot_creation_policy = BotCreationPolicyEnum.ADMINS_ONLY bot_realm.save(update_fields=["bot_creation_policy"]) # A regular user cannot create a generic bot self.login("hamlet") self.assert_num_bots_equal(0) result = self.client_post("/json/bots", bot_info) self.assert_num_bots_equal(0) self.assert_json_error(result, "Must be an organization administrator") # Also, a regular user cannot create a incoming bot bot_info["bot_type"] = 2 self.login("hamlet") self.assert_num_bots_equal(0) result = self.client_post("/json/bots", bot_info) self.assert_num_bots_equal(0) self.assert_json_error(result, "Must be an organization administrator") def test_no_bots_allowed_for_admins(self) -> None: bot_email = "hambot-bot@zulip.testserver" bot_realm = get_realm("zulip") bot_realm.bot_creation_policy = BotCreationPolicyEnum.ADMINS_ONLY bot_realm.save(update_fields=["bot_creation_policy"]) # An administrator can create any type of bot self.login("iago") self.assert_num_bots_equal(0) self.create_bot(bot_type=UserProfile.DEFAULT_BOT) self.assert_num_bots_equal(1) profile = get_user(bot_email, bot_realm) self.assertEqual(profile.bot_type, UserProfile.DEFAULT_BOT) def test_reactivating_bot_with_deactivated_owner(self) -> None: self.login("hamlet") bot_info = { "full_name": "Test bot", "short_name": "testbot", "bot_type": "1", } result = self.client_post("/json/bots", bot_info) bot_id = result.json()["user_id"] test_bot = UserProfile.objects.get(id=bot_id, is_bot=True) private_stream = self.make_stream("private_stream", invite_only=True) public_stream = self.make_stream("public_stream") self.subscribe(test_bot, "private_stream") self.subscribe(self.example_user("hamlet"), "private_stream") self.subscribe(test_bot, "public_stream") self.subscribe(self.example_user("hamlet"), "public_stream") private_stream_test = self.make_stream("private_stream_test", invite_only=True) self.subscribe(self.example_user("iago"), "private_stream_test") self.subscribe(test_bot, "private_stream_test") do_deactivate_user(test_bot, acting_user=None) # Deactivate the bot owner do_deactivate_user(self.example_user("hamlet"), acting_user=None) self.login("iago") result = self.client_post(f"/json/users/{bot_id}/reactivate") self.assert_json_success(result) test_bot = UserProfile.objects.get(id=bot_id, is_bot=True) assert test_bot.bot_owner is not None self.assertEqual(test_bot.bot_owner.id, self.example_user("iago").id) self.assertFalse( Subscription.objects.filter( user_profile=test_bot, recipient__type_id=private_stream.id, active=True ).exists() ) self.assertTrue( Subscription.objects.filter( user_profile=test_bot, recipient__type_id=private_stream_test.id, active=True ).exists() ) self.assertTrue( Subscription.objects.filter( user_profile=test_bot, recipient__type_id=public_stream.id, active=True ).exists() ) def test_patch_bot_full_name(self) -> None: self.login("hamlet") bot_info = { "full_name": "The Bot of Hamlet", "short_name": "hambot", } result = self.client_post("/json/bots", bot_info) self.assert_json_success(result) bot_info = { "full_name": "Fred", } email = "hambot-bot@zulip.testserver" result = self.client_patch(f"/json/bots/{self.get_bot_user(email).id}", bot_info) response_dict = self.assert_json_success(result) self.assertEqual("Fred", response_dict["full_name"]) bot = self.get_bot() self.assertEqual("Fred", bot["full_name"]) def test_patch_bot_full_name_in_use(self) -> None: self.login("hamlet") original_name = "The Bot of Hamlet" bot_info = { "full_name": original_name, "short_name": "hambot", } result = self.client_post("/json/bots", bot_info) self.assert_json_success(result) bot_email = "hambot-bot@zulip.testserver" bot = self.get_bot_user(bot_email) url = f"/json/bots/{bot.id}" # It doesn't matter whether a name is taken by a human # or a bot, we can't use it. already_taken_name = self.example_user("cordelia").full_name bot_info = { "full_name": already_taken_name, } result = self.client_patch(url, bot_info) self.assert_json_error(result, "Name is already in use!") # We can use our own name (with extra whitespace), and the # server should silently do nothing. original_name_with_padding = " " + original_name + " " bot_info = { "full_name": original_name_with_padding, } result = self.client_patch(url, bot_info) self.assert_json_success(result) bot = self.get_bot_user(bot_email) self.assertEqual(bot.full_name, original_name) # And let's do a sanity check with an actual name change # after our various attempts that either failed or did # nothing. bot_info = { "full_name": "Hal", } result = self.client_patch(url, bot_info) self.assert_json_success(result) bot = self.get_bot_user(bot_email) self.assertEqual(bot.full_name, "Hal") def test_patch_bot_full_name_non_bot(self) -> None: self.login("iago") bot_info = { "full_name": "Fred", } result = self.client_patch("/json/bots/{}".format(self.example_user("hamlet").id), bot_info) self.assert_json_error(result, "No such bot") def test_patch_bot_owner(self) -> None: self.login("hamlet") othello = self.example_user("othello") bot_info: dict[str, object] = { "full_name": "The Bot of Hamlet", "short_name": "hambot", } result = self.client_post("/json/bots", bot_info) self.assert_json_success(result) bot_info = { "bot_owner_id": othello.id, } email = "hambot-bot@zulip.testserver" result = self.client_patch(f"/json/bots/{self.get_bot_user(email).id}", bot_info) response_dict = self.assert_json_success(result) # Test bot's owner has been changed successfully. self.assertEqual(response_dict["bot_owner"], othello.email) self.login("othello") bot = self.get_bot() self.assertEqual("The Bot of Hamlet", bot["full_name"]) def test_patch_bot_owner_bad_user_id(self) -> None: self.login("hamlet") self.create_bot() self.assert_num_bots_equal(1) email = "hambot-bot@zulip.testserver" profile = get_user("hambot-bot@zulip.testserver", get_realm("zulip")) bad_bot_owner_id = 999999 bot_info = { "bot_owner_id": bad_bot_owner_id, } result = self.client_patch(f"/json/bots/{self.get_bot_user(email).id}", bot_info) self.assert_json_error(result, "Failed to change owner, no such user") profile = get_user("hambot-bot@zulip.testserver", get_realm("zulip")) self.assertEqual(profile.bot_owner, self.example_user("hamlet")) def test_patch_bot_owner_deactivated(self) -> None: self.login("hamlet") self.create_bot() self.assert_num_bots_equal(1) target_user_profile = self.example_user("othello") do_deactivate_user(target_user_profile, acting_user=None) target_user_profile = self.example_user("othello") self.assertFalse(target_user_profile.is_active) bot_info = { "bot_owner_id": self.example_user("othello").id, } email = "hambot-bot@zulip.testserver" result = self.client_patch(f"/json/bots/{self.get_bot_user(email).id}", bot_info) self.assert_json_error(result, "Failed to change owner, user is deactivated") profile = self.get_bot_user(email) self.assertEqual(profile.bot_owner, self.example_user("hamlet")) def test_patch_bot_owner_must_be_in_same_realm(self) -> None: self.login("hamlet") self.create_bot() self.assert_num_bots_equal(1) bot_info = { "bot_owner_id": self.mit_user("starnine").id, } email = "hambot-bot@zulip.testserver" result = self.client_patch(f"/json/bots/{self.get_bot_user(email).id}", bot_info) self.assert_json_error(result, "Failed to change owner, no such user") profile = self.get_bot_user(email) self.assertEqual(profile.bot_owner, self.example_user("hamlet")) def test_patch_bot_owner_noop(self) -> None: self.login("hamlet") self.create_bot() self.assert_num_bots_equal(1) bot_info = { "bot_owner_id": self.example_user("hamlet").id, } email = "hambot-bot@zulip.testserver" result = self.client_patch(f"/json/bots/{self.get_bot_user(email).id}", bot_info) # Check that we're still the owner self.assert_json_success(result) profile = self.get_bot_user(email) self.assertEqual(profile.bot_owner, self.example_user("hamlet")) def test_patch_bot_owner_a_bot(self) -> None: self.login("hamlet") self.create_bot() self.assert_num_bots_equal(1) bot_info: dict[str, object] = { "full_name": "Another Bot of Hamlet", "short_name": "hamelbot", } result = self.client_post("/json/bots", bot_info) self.assert_json_success(result) bot_info = { "bot_owner_id": self.get_bot_user("hamelbot-bot@zulip.testserver").id, } email = "hambot-bot@zulip.testserver" result = self.client_patch(f"/json/bots/{self.get_bot_user(email).id}", bot_info) self.assert_json_error(result, "Failed to change owner, bots can't own other bots") profile = get_user(email, get_realm("zulip")) self.assertEqual(profile.bot_owner, self.example_user("hamlet")) def test_patch_bot_owner_of_bot_with_can_create_users(self) -> None: """ can_create_users is granted to organizations upon approval, and thus should be thought of as something that only organization owners should have control over. """ cordelia = self.example_user("cordelia") self.login("hamlet") self.create_bot() bot_realm = get_realm("zulip") bot_email = "hambot-bot@zulip.testserver" bot_user = get_user(bot_email, bot_realm) do_change_can_create_users(bot_user, True) self.logout() # iago is an ordinary organization administrator, and thus doesn't have # sufficient permissions to change ownership of this bot. self.login("iago") bot_info = { "bot_owner_id": cordelia.id, } result = self.client_patch(f"/json/bots/{bot_user.id}", bot_info) self.assert_json_error( result, "Must be an organization owner", ) self.logout() # desdemona is the organization owner and should be allowed to change the bot's ownership. self.login("desdemona") result = self.client_patch(f"/json/bots/{bot_user.id}", bot_info) self.assert_json_success(result) bot_user.refresh_from_db() self.assertEqual(bot_user.bot_owner, cordelia) def test_patch_bot_owner_with_private_streams(self) -> None: self.login("iago") hamlet = self.example_user("hamlet") self.create_bot() bot_realm = get_realm("zulip") bot_email = "hambot-bot@zulip.testserver" bot_user = get_user(bot_email, bot_realm) private_stream = self.make_stream("private_stream", invite_only=True) public_stream = self.make_stream("public_stream") self.subscribe(bot_user, "private_stream") self.subscribe(self.example_user("iago"), "private_stream") self.subscribe(bot_user, "public_stream") self.subscribe(self.example_user("iago"), "public_stream") private_stream_test = self.make_stream("private_stream_test", invite_only=True) self.subscribe(self.example_user("hamlet"), "private_stream_test") self.subscribe(bot_user, "private_stream_test") bot_info = { "bot_owner_id": hamlet.id, } result = self.client_patch(f"/json/bots/{bot_user.id}", bot_info) self.assert_json_success(result) bot_user = get_user(bot_email, bot_realm) assert bot_user.bot_owner is not None self.assertEqual(bot_user.bot_owner.id, hamlet.id) assert private_stream.recipient_id is not None self.assertFalse( Subscription.objects.filter( user_profile=bot_user, recipient_id=private_stream.recipient_id, active=True ).exists() ) assert private_stream_test.recipient_id is not None self.assertTrue( Subscription.objects.filter( user_profile=bot_user, recipient_id=private_stream_test.recipient_id, active=True ).exists() ) assert public_stream.recipient_id is not None self.assertTrue( Subscription.objects.filter( user_profile=bot_user, recipient_id=public_stream.recipient_id, active=True ).exists() ) def test_patch_bot_avatar(self) -> None: self.login("hamlet") bot_info = { "full_name": "The Bot of Hamlet", "short_name": "hambot", } result = self.client_post("/json/bots", bot_info) self.assert_json_success(result) bot_email = "hambot-bot@zulip.testserver" bot_realm = get_realm("zulip") profile = get_user(bot_email, bot_realm) self.assertEqual(profile.avatar_source, UserProfile.AVATAR_FROM_GRAVATAR) email = "hambot-bot@zulip.testserver" # Try error case first (too many files): with get_test_image_file("img.png") as fp1, get_test_image_file("img.gif") as fp2: result = self.client_patch_multipart( f"/json/bots/{self.get_bot_user(email).id}", dict(file1=fp1, file2=fp2) ) self.assert_json_error(result, "You may only upload one file at a time") profile = get_user(bot_email, bot_realm) self.assertEqual(profile.avatar_version, 1) # HAPPY PATH with get_test_image_file("img.png") as fp: result = self.client_patch_multipart( f"/json/bots/{self.get_bot_user(email).id}", dict(file=fp) ) profile = get_user(bot_email, bot_realm) self.assertEqual(profile.avatar_version, 2) # Make sure that avatar image that we've uploaded is same with avatar image in the server self.assertTrue( filecmp.cmp(fp.name, os.path.splitext(avatar_disk_path(profile))[0] + ".original") ) self.assert_json_success(result) self.assertEqual(profile.avatar_source, UserProfile.AVATAR_FROM_USER) self.assertTrue(os.path.exists(avatar_disk_path(profile))) def test_patch_bot_to_stream(self) -> None: self.login("hamlet") bot_info = { "full_name": "The Bot of Hamlet", "short_name": "hambot", } result = self.client_post("/json/bots", bot_info) self.assert_json_success(result) bot_info = { "default_sending_stream": "Denmark", } email = "hambot-bot@zulip.testserver" result = self.client_patch(f"/json/bots/{self.get_bot_user(email).id}", bot_info) response_dict = self.assert_json_success(result) self.assertEqual("Denmark", response_dict["default_sending_stream"]) bot = self.get_bot() self.assertEqual("Denmark", bot["default_sending_stream"]) def test_patch_bot_to_stream_not_subscribed(self) -> None: self.login("hamlet") bot_info = { "full_name": "The Bot of Hamlet", "short_name": "hambot", } result = self.client_post("/json/bots", bot_info) self.assert_json_success(result) bot_info = { "default_sending_stream": "Rome", } email = "hambot-bot@zulip.testserver" result = self.client_patch(f"/json/bots/{self.get_bot_user(email).id}", bot_info) response_dict = self.assert_json_success(result) self.assertEqual("Rome", response_dict["default_sending_stream"]) bot = self.get_bot() self.assertEqual("Rome", bot["default_sending_stream"]) def test_patch_bot_to_stream_none(self) -> None: self.login("hamlet") bot_info = { "full_name": "The Bot of Hamlet", "short_name": "hambot", } result = self.client_post("/json/bots", bot_info) self.assert_json_success(result) bot_info = { "default_sending_stream": "", } email = "hambot-bot@zulip.testserver" result = self.client_patch(f"/json/bots/{self.get_bot_user(email).id}", bot_info) self.assert_json_success(result) bot_email = "hambot-bot@zulip.testserver" bot_realm = get_realm("zulip") default_sending_stream = get_user(bot_email, bot_realm).default_sending_stream self.assertEqual(None, default_sending_stream) bot = self.get_bot() self.assertEqual(None, bot["default_sending_stream"]) def test_patch_bot_role(self) -> None: self.login("desdemona") email = "default-bot@zulip.com" user_profile = self.get_bot_user(email) do_change_user_role(user_profile, UserProfile.ROLE_MEMBER, acting_user=user_profile) req = dict(role=UserProfile.ROLE_GUEST) result = self.client_patch(f"/json/bots/{self.get_bot_user(email).id}", req) self.assert_json_success(result) user_profile = self.get_bot_user(email) self.assertEqual(user_profile.role, UserProfile.ROLE_GUEST) # Test for not allowing a non-owner user to make assign a bot an owner role desdemona = self.example_user("desdemona") do_change_user_role(desdemona, UserProfile.ROLE_REALM_ADMINISTRATOR, acting_user=None) req = dict(role=UserProfile.ROLE_REALM_OWNER) result = self.client_patch(f"/json/users/{user_profile.id}", req) self.assert_json_error(result, "Must be an organization owner") result = self.client_patch(f"/json/bots/{user_profile.id}", req) self.assert_json_error(result, "Must be an organization owner") # Test for not allowing a non-administrator user to assign a bot an administrator role shiva = self.example_user("shiva") self.assertEqual(shiva.role, UserProfile.ROLE_MODERATOR) self.login_user(shiva) do_change_bot_owner(user_profile, shiva, acting_user=None) req = dict(role=UserProfile.ROLE_REALM_ADMINISTRATOR) result = self.client_patch(f"/json/users/{user_profile.id}", req) self.assert_json_error(result, "Must be an organization administrator") result = self.client_patch(f"/json/bots/{user_profile.id}", req) self.assert_json_error(result, "Must be an organization administrator") def test_patch_bot_to_stream_private_allowed(self) -> None: self.login("hamlet") user_profile = self.example_user("hamlet") stream = self.subscribe(user_profile, "Denmark") do_change_stream_permission( stream, invite_only=True, history_public_to_subscribers=False, is_web_public=False, acting_user=user_profile, ) bot_info = { "full_name": "The Bot of Hamlet", "short_name": "hambot", } result = self.client_post("/json/bots", bot_info) self.assert_json_success(result) bot_info = { "default_sending_stream": "Denmark", } email = "hambot-bot@zulip.testserver" result = self.client_patch(f"/json/bots/{self.get_bot_user(email).id}", bot_info) response_dict = self.assert_json_success(result) self.assertEqual("Denmark", response_dict["default_sending_stream"]) bot = self.get_bot() self.assertEqual("Denmark", bot["default_sending_stream"]) def test_patch_bot_to_stream_private_denied(self) -> None: self.login("hamlet") realm = self.example_user("hamlet").realm stream = get_stream("Denmark", realm) self.unsubscribe(self.example_user("hamlet"), "Denmark") do_change_stream_permission( stream, invite_only=True, history_public_to_subscribers=False, is_web_public=False, acting_user=self.example_user("hamlet"), ) bot_info = { "full_name": "The Bot of Hamlet", "short_name": "hambot", } result = self.client_post("/json/bots", bot_info) self.assert_json_success(result) bot_info = { "default_sending_stream": "Denmark", } email = "hambot-bot@zulip.testserver" result = self.client_patch(f"/json/bots/{self.get_bot_user(email).id}", bot_info) self.assert_json_error(result, "Invalid channel name 'Denmark'") def test_patch_bot_to_stream_not_found(self) -> None: self.login("hamlet") bot_info = { "full_name": "The Bot of Hamlet", "short_name": "hambot", } result = self.client_post("/json/bots", bot_info) self.assert_json_success(result) bot_info = { "default_sending_stream": "missing", } email = "hambot-bot@zulip.testserver" result = self.client_patch(f"/json/bots/{self.get_bot_user(email).id}", bot_info) self.assert_json_error(result, "Invalid channel name 'missing'") def test_patch_bot_events_register_stream(self) -> None: hamlet = self.example_user("hamlet") self.login_user(hamlet) bot_info = { "full_name": "The Bot of Hamlet", "short_name": "hambot", } result = self.client_post("/json/bots", bot_info) self.assert_json_success(result) email = "hambot-bot@zulip.testserver" bot_user = self.get_bot_user(email) url = f"/json/bots/{bot_user.id}" # Successfully give the bot a default stream. stream_name = "Denmark" bot_info = dict(default_events_register_stream=stream_name) result = self.client_patch(url, bot_info) response_dict = self.assert_json_success(result) self.assertEqual(stream_name, response_dict["default_events_register_stream"]) bot = self.get_bot() self.assertEqual(stream_name, bot["default_events_register_stream"]) # Make sure we are locked out of an unsubscribed private stream. # We'll subscribe the bot but not the owner (since the check is # on owner). stream_name = "private_stream" self.make_stream(stream_name, hamlet.realm, invite_only=True) self.subscribe(bot_user, stream_name) bot_info = dict(default_events_register_stream=stream_name) result = self.client_patch(url, bot_info) self.assert_json_error_contains(result, "Invalid channel name") # Subscribing the owner allows us to patch the stream. self.subscribe(hamlet, stream_name) bot_info = dict(default_events_register_stream=stream_name) result = self.client_patch(url, bot_info) self.assert_json_success(result) # Make sure the bot cannot create their own default stream. url = f"/api/v1/bots/{bot_user.id}" result = self.api_patch(bot_user, url, bot_info) self.assert_json_error_contains(result, "endpoint does not accept") def test_patch_bot_events_register_stream_allowed(self) -> None: self.login("hamlet") user_profile = self.example_user("hamlet") stream = self.subscribe(user_profile, "Denmark") do_change_stream_permission( stream, invite_only=True, history_public_to_subscribers=False, is_web_public=False, acting_user=user_profile, ) bot_info = { "full_name": "The Bot of Hamlet", "short_name": "hambot", } result = self.client_post("/json/bots", bot_info) self.assert_json_success(result) bot_info = { "default_events_register_stream": "Denmark", } email = "hambot-bot@zulip.testserver" result = self.client_patch(f"/json/bots/{self.get_bot_user(email).id}", bot_info) response_dict = self.assert_json_success(result) self.assertEqual("Denmark", response_dict["default_events_register_stream"]) bot = self.get_bot() self.assertEqual("Denmark", bot["default_events_register_stream"]) def test_patch_bot_events_register_stream_denied(self) -> None: self.login("hamlet") realm = self.example_user("hamlet").realm stream = get_stream("Denmark", realm) self.unsubscribe(self.example_user("hamlet"), "Denmark") do_change_stream_permission( stream, invite_only=True, history_public_to_subscribers=False, is_web_public=False, acting_user=self.example_user("hamlet"), ) bot_info = { "full_name": "The Bot of Hamlet", "short_name": "hambot", } result = self.client_post("/json/bots", bot_info) self.assert_json_success(result) bot_info = { "default_events_register_stream": "Denmark", } email = "hambot-bot@zulip.testserver" result = self.client_patch(f"/json/bots/{self.get_bot_user(email).id}", bot_info) self.assert_json_error(result, "Invalid channel name 'Denmark'") def test_patch_bot_events_register_stream_none(self) -> None: self.login("hamlet") bot_info = { "full_name": "The Bot of Hamlet", "short_name": "hambot", } result = self.client_post("/json/bots", bot_info) self.assert_json_success(result) bot_info = { "default_events_register_stream": "", } email = "hambot-bot@zulip.testserver" result = self.client_patch(f"/json/bots/{self.get_bot_user(email).id}", bot_info) self.assert_json_success(result) bot_email = "hambot-bot@zulip.testserver" bot_realm = get_realm("zulip") default_events_register_stream = get_user( bot_email, bot_realm ).default_events_register_stream self.assertEqual(None, default_events_register_stream) bot = self.get_bot() self.assertEqual(None, bot["default_events_register_stream"]) def test_patch_bot_events_register_stream_not_found(self) -> None: self.login("hamlet") bot_info = { "full_name": "The Bot of Hamlet", "short_name": "hambot", } result = self.client_post("/json/bots", bot_info) self.assert_json_success(result) bot_info = { "default_events_register_stream": "missing", } email = "hambot-bot@zulip.testserver" result = self.client_patch(f"/json/bots/{self.get_bot_user(email).id}", bot_info) self.assert_json_error(result, "Invalid channel name 'missing'") def test_patch_bot_default_all_public_streams_true(self) -> None: self.login("hamlet") bot_info = { "full_name": "The Bot of Hamlet", "short_name": "hambot", } result = self.client_post("/json/bots", bot_info) self.assert_json_success(result) bot_info = { "default_all_public_streams": orjson.dumps(True).decode(), } email = "hambot-bot@zulip.testserver" result = self.client_patch(f"/json/bots/{self.get_bot_user(email).id}", bot_info) response_dict = self.assert_json_success(result) self.assertEqual(response_dict["default_all_public_streams"], True) bot = self.get_bot() self.assertEqual(bot["default_all_public_streams"], True) def test_patch_bot_default_all_public_streams_false(self) -> None: self.login("hamlet") bot_info = { "full_name": "The Bot of Hamlet", "short_name": "hambot", } result = self.client_post("/json/bots", bot_info) self.assert_json_success(result) bot_info = { "default_all_public_streams": orjson.dumps(False).decode(), } email = "hambot-bot@zulip.testserver" result = self.client_patch(f"/json/bots/{self.get_bot_user(email).id}", bot_info) response_dict = self.assert_json_success(result) self.assertEqual(response_dict["default_all_public_streams"], False) bot = self.get_bot() self.assertEqual(bot["default_all_public_streams"], False) def test_patch_bot_via_post(self) -> None: self.login("hamlet") bot_info = { "full_name": "The Bot of Hamlet", "short_name": "hambot", } result = self.client_post("/json/bots", bot_info) self.assert_json_success(result) bot_info = { "full_name": "Fred", "method": "PATCH", } email = "hambot-bot@zulip.testserver" # Important: We intentionally use the wrong method, post, here. result = self.client_post(f"/json/bots/{self.get_bot_user(email).id}", bot_info) # TODO: The "method" parameter is not currently tracked as a processed parameter # by typed_endpoint. Assert it is returned as an ignored parameter. response_dict = self.assert_json_success(result, ignored_parameters=["method"]) self.assertEqual("Fred", response_dict["full_name"]) bot = self.get_bot() self.assertEqual("Fred", bot["full_name"]) def test_patch_bogus_bot(self) -> None: """Deleting a bogus bot will succeed silently.""" self.login("hamlet") self.create_bot() bot_info = { "full_name": "Fred", } invalid_user_id = 1000 result = self.client_patch(f"/json/bots/{invalid_user_id}", bot_info) self.assert_json_error(result, "No such bot") self.assert_num_bots_equal(1) def test_patch_outgoing_webhook_bot(self) -> None: self.login("hamlet") bot_info = { "full_name": "The Bot of Hamlet", "short_name": "hambot", "bot_type": UserProfile.OUTGOING_WEBHOOK_BOT, "payload_url": orjson.dumps("http://foo.bar.com").decode(), "interface_type": Service.GENERIC, } result = self.client_post("/json/bots", bot_info) self.assert_json_success(result) bot_info = { "service_payload_url": orjson.dumps("http://foo.bar2.com").decode(), "service_interface": Service.SLACK, } email = "hambot-bot@zulip.testserver" result = self.client_patch(f"/json/bots/{self.get_bot_user(email).id}", bot_info) self.assert_json_success(result) service_interface = orjson.loads(result.content)["service_interface"] self.assertEqual(service_interface, Service.SLACK) service_payload_url = orjson.loads(result.content)["service_payload_url"] self.assertEqual(service_payload_url, "http://foo.bar2.com") @patch("zulip_bots.bots.giphy.giphy.GiphyHandler.validate_config") def test_patch_bot_config_data(self, mock_validate_config: MagicMock) -> None: self.create_test_bot( "test", self.example_user("hamlet"), full_name="Bot with config data", bot_type=UserProfile.EMBEDDED_BOT, service_name="giphy", config_data=orjson.dumps({"key": "12345678"}).decode(), ) bot_info = {"config_data": orjson.dumps({"key": "87654321"}).decode()} email = "test-bot@zulip.testserver" result = self.client_patch(f"/json/bots/{self.get_bot_user(email).id}", bot_info) self.assert_json_success(result) config_data = orjson.loads(result.content)["config_data"] self.assertEqual(config_data, orjson.loads(bot_info["config_data"])) def test_outgoing_webhook_invalid_interface(self) -> None: self.login("hamlet") bot_info = { "full_name": "Outgoing Webhook test bot", "short_name": "outgoingservicebot", "bot_type": UserProfile.OUTGOING_WEBHOOK_BOT, "payload_url": orjson.dumps("http://127.0.0.1:5002").decode(), "interface_type": -1, } result = self.client_post("/json/bots", bot_info) self.assert_json_error(result, "Invalid interface type") bot_info["interface_type"] = Service.GENERIC result = self.client_post("/json/bots", bot_info) self.assert_json_success(result) def test_create_outgoing_webhook_bot(self, **extras: Any) -> None: self.login("hamlet") bot_info = { "full_name": "Outgoing Webhook test bot", "short_name": "outgoingservicebot", "bot_type": UserProfile.OUTGOING_WEBHOOK_BOT, "payload_url": orjson.dumps("http://127.0.0.1:5002").decode(), } bot_info.update(extras) result = self.client_post("/json/bots", bot_info) self.assert_json_success(result) bot_email = "outgoingservicebot-bot@zulip.testserver" bot_realm = get_realm("zulip") bot = get_user(bot_email, bot_realm) [service] = get_bot_services(bot.id) self.assertEqual(service.name, "outgoingservicebot") self.assertEqual(service.base_url, "http://127.0.0.1:5002") self.assertEqual(service.user_profile, bot) # invalid URL test case. bot_info["payload_url"] = orjson.dumps("http://127.0.0.:5002").decode() result = self.client_post("/json/bots", bot_info) self.assert_json_error(result, "Invalid payload_url: Value error, Not a URL") def test_get_bot_handler(self) -> None: # Test for valid service. test_service_name = "converter" test_bot_handler = get_bot_handler(test_service_name) self.assertEqual( str(type(test_bot_handler)), "", ) # Test for invalid service. test_service_name = "incorrect_bot_service_foo" test_bot_handler = get_bot_handler(test_service_name) self.assertEqual(test_bot_handler, None) def test_if_each_embedded_bot_service_exists(self) -> None: for embedded_bot in EMBEDDED_BOTS: self.assertIsNotNone(get_bot_handler(embedded_bot.name)) def test_outgoing_webhook_interface_type(self) -> None: self.login("hamlet") bot_info = { "full_name": "Outgoing Webhook test bot", "short_name": "outgoingservicebot", "bot_type": UserProfile.OUTGOING_WEBHOOK_BOT, "payload_url": orjson.dumps("http://127.0.0.1:5002").decode(), "interface_type": -1, } result = self.client_post("/json/bots", bot_info) self.assert_json_error(result, "Invalid interface type") bot_info["interface_type"] = Service.GENERIC result = self.client_post("/json/bots", bot_info) self.assert_json_success(result) def test_create_embedded_bot_with_disabled_embedded_bots(self, **extras: Any) -> None: with self.settings(EMBEDDED_BOTS_ENABLED=False): self.fail_to_create_test_bot( short_name="embeddedservicebot", user_profile=self.example_user("hamlet"), bot_type=UserProfile.EMBEDDED_BOT, service_name="followup", config_data=orjson.dumps({"key": "value"}).decode(), assert_json_error_msg="Embedded bots are not enabled.", **extras, ) def test_create_embedded_bot(self, **extras: Any) -> None: bot_config_info = {"key": "value"} self.create_test_bot( short_name="embeddedservicebot", user_profile=self.example_user("hamlet"), bot_type=UserProfile.EMBEDDED_BOT, service_name="followup", config_data=orjson.dumps(bot_config_info).decode(), **extras, ) bot_email = "embeddedservicebot-bot@zulip.testserver" bot_realm = get_realm("zulip") bot = get_user(bot_email, bot_realm) [service] = get_bot_services(bot.id) bot_config = get_bot_config(bot) self.assertEqual(bot_config, bot_config_info) self.assertEqual(service.name, "followup") self.assertEqual(service.user_profile, bot) def test_create_embedded_bot_with_incorrect_service_name(self, **extras: Any) -> None: self.fail_to_create_test_bot( short_name="embeddedservicebot", user_profile=self.example_user("hamlet"), bot_type=UserProfile.EMBEDDED_BOT, service_name="not_existing_service", assert_json_error_msg="Invalid embedded bot name.", **extras, ) def test_create_embedded_bot_with_invalid_config_value(self, **extras: Any) -> None: self.fail_to_create_test_bot( short_name="embeddedservicebot", user_profile=self.example_user("hamlet"), service_name="followup", config_data=orjson.dumps({"invalid": ["config", "value"]}).decode(), assert_json_error_msg='config_data["invalid"] is not a string', **extras, ) # Test to create embedded bot with an incorrect config value incorrect_bot_config_info = {"key": "incorrect key"} bot_info = { "full_name": "Embedded test bot", "short_name": "embeddedservicebot3", "bot_type": UserProfile.EMBEDDED_BOT, "service_name": "giphy", "config_data": orjson.dumps(incorrect_bot_config_info).decode(), } bot_info.update(extras) with patch( "zulip_bots.bots.giphy.giphy.GiphyHandler.validate_config", side_effect=ConfigValidationError, ): result = self.client_post("/json/bots", bot_info) self.assert_json_error(result, "Invalid configuration data!") def test_is_cross_realm_bot_email(self) -> None: self.assertTrue(is_cross_realm_bot_email("notification-bot@zulip.com")) self.assertTrue(is_cross_realm_bot_email("notification-BOT@zulip.com")) self.assertFalse(is_cross_realm_bot_email("random-bot@zulip.com")) with self.settings(CROSS_REALM_BOT_EMAILS={"random-bot@zulip.com"}): self.assertTrue(is_cross_realm_bot_email("random-bot@zulip.com")) self.assertFalse(is_cross_realm_bot_email("notification-bot@zulip.com")) def test_private_bot_empty_bot_owner_user_ids(self) -> None: hamlet = self.example_user("hamlet") self.login("hamlet") self.create_bot() stream = get_stream("Denmark", get_realm("zulip")) do_change_stream_permission( stream, invite_only=True, history_public_to_subscribers=False, is_web_public=False, acting_user=hamlet, ) new_bot = assert_is_not_none(UserProfile.objects.last()) do_change_default_sending_stream(new_bot, stream, acting_user=hamlet) new_bot.bot_owner_id = None new_bot.save() self.assertEqual(bot_owner_user_ids(new_bot), set()) @patch("zerver.lib.integrations.WEBHOOK_INTEGRATIONS", stripe_sample_config_options) def test_create_incoming_webhook_bot_with_service_name_and_with_keys(self) -> None: self.login("hamlet") bot_metadata = { "full_name": "My Stripe Bot", "short_name": "my-stripe", "bot_type": UserProfile.INCOMING_WEBHOOK_BOT, "service_name": "stripe", "config_data": orjson.dumps({"stripe_api_key": "sample-api-key"}).decode(), } self.create_bot(**bot_metadata) new_bot = UserProfile.objects.get(full_name="My Stripe Bot") config_data = get_bot_config(new_bot) self.assertEqual( config_data, {"integration_id": "stripe", "stripe_api_key": "sample-api-key"} ) @patch("zerver.lib.integrations.WEBHOOK_INTEGRATIONS", stripe_sample_config_options) def test_create_incoming_webhook_bot_with_service_name_incorrect_keys(self) -> None: self.login("hamlet") bot_metadata = { "full_name": "My Stripe Bot", "short_name": "my-stripe", "bot_type": UserProfile.INCOMING_WEBHOOK_BOT, "service_name": "stripe", "config_data": orjson.dumps({"stripe_api_key": "_invalid_key"}).decode(), } response = self.client_post("/json/bots", bot_metadata) self.assertEqual(response.status_code, 400) expected_error_message = 'Invalid stripe_api_key value _invalid_key (stripe_api_key starts with a "_" and is hence invalid.)' self.assertEqual(orjson.loads(response.content)["msg"], expected_error_message) with self.assertRaises(UserProfile.DoesNotExist): UserProfile.objects.get(full_name="My Stripe Bot") @patch("zerver.lib.integrations.WEBHOOK_INTEGRATIONS", stripe_sample_config_options) def test_create_incoming_webhook_bot_with_service_name_without_keys(self) -> None: self.login("hamlet") bot_metadata = { "full_name": "My Stripe Bot", "short_name": "my-stripe", "bot_type": UserProfile.INCOMING_WEBHOOK_BOT, "service_name": "stripe", } response = self.client_post("/json/bots", bot_metadata) self.assertEqual(response.status_code, 400) expected_error_message = "Missing configuration parameters: {'stripe_api_key'}" self.assertEqual(orjson.loads(response.content)["msg"], expected_error_message) with self.assertRaises(UserProfile.DoesNotExist): UserProfile.objects.get(full_name="My Stripe Bot") @patch("zerver.lib.integrations.WEBHOOK_INTEGRATIONS", stripe_sample_config_options) def test_create_incoming_webhook_bot_without_service_name(self) -> None: self.login("hamlet") bot_metadata = { "full_name": "My Stripe Bot", "short_name": "my-stripe", "bot_type": UserProfile.INCOMING_WEBHOOK_BOT, } self.create_bot(**bot_metadata) new_bot = UserProfile.objects.get(full_name="My Stripe Bot") with self.assertRaises(ConfigError): get_bot_config(new_bot) @patch("zerver.lib.integrations.WEBHOOK_INTEGRATIONS", stripe_sample_config_options) def test_create_incoming_webhook_bot_with_incorrect_service_name(self) -> None: self.login("hamlet") bot_metadata = { "full_name": "My Stripe Bot", "short_name": "my-stripe", "bot_type": UserProfile.INCOMING_WEBHOOK_BOT, "service_name": "stripes", } response = self.client_post("/json/bots", bot_metadata) self.assertEqual(response.status_code, 400) expected_error_message = "Invalid integration 'stripes'." self.assertEqual(orjson.loads(response.content)["msg"], expected_error_message) with self.assertRaises(UserProfile.DoesNotExist): UserProfile.objects.get(full_name="My Stripe Bot")