Repository: zulip/zulip

Issue:
Clean up get_cross_realm_emails vs. CROSS_REALM_BOT_EMAILS
We have (at least) two ways of getting the set of cross-realm bots:
* `zerver.models.get_cross_realm_emails` is used in `zerver/lib/actions.py` and `zerver/models.py`
* `settings.CROSS_REALM_BOT_EMAILS` is used directly in `zerver/lib/actions.py`, plus used to implement `get_cross_realm_emails`

Moreover, in some places we explicitly lower-case the email address before looking for it in that set, and in others we don't.

There should be just one way we do this -- probably a function like `is_cross_realm_bot_email`. That would help make sure we handle things in a uniform way with respect to casing, and also help a reader be confident there isn't a subtle distinction they're missing. To do this, I'd do something like:
* Write an `is_cross_realm_bot_email`, next to `get_cross_realm_emails`. Write tests, being sure to test the case handling.
* Change all the users of `get_cross_realm_emails` and `CROSS_REALM_BOT_EMAILS` to use that instead, where applicable.
* I think the only exception is `get_cross_realm_dicts`, which actually wants the list rather than checking membership. This can use `CROSS_REALM_BOT_EMAILS` directly.
* Delete `get_cross_realm_emails`.



Discussion:
Hello @zulip/server-refactoring members, this issue was labeled with the **area: refactoring** label, so you may want to check it out!

<!-- areaLabelNotification -->
@zulipbot claim

Resolving patch:
diff --git a/zerver/lib/actions.py b/zerver/lib/actions.py
--- a/zerver/lib/actions.py
+++ b/zerver/lib/actions.py
@@ -66,7 +66,7 @@
     UserActivityInterval, active_user_ids, get_active_streams, \
     realm_filters_for_realm, RealmFilter, \
     get_owned_bot_dicts, stream_name_in_use, \
-    get_old_unclaimed_attachments, get_cross_realm_emails, \
+    get_old_unclaimed_attachments, is_cross_realm_bot_email, \
     Reaction, EmailChangeStatus, CustomProfileField, \
     custom_profile_fields_for_realm, get_huddle_user_ids, \
     CustomProfileFieldValue, validate_attachment_request, get_system_bot, \
@@ -1531,8 +1531,7 @@ def validate_recipient_user_profiles(user_profiles: List[UserProfile],
     # We exempt cross-realm bots from the check that all the recipients
     # are in the same realm.
     realms = set()
-    exempt_emails = get_cross_realm_emails()
-    if sender.email not in exempt_emails:
+    if not is_cross_realm_bot_email(sender.email):
         realms.add(sender.realm_id)
 
     for user_profile in user_profiles:
@@ -1540,7 +1539,7 @@ def validate_recipient_user_profiles(user_profiles: List[UserProfile],
                 user_profile.realm.deactivated:
             raise ValidationError(_("'%s' is no longer using Zulip.") % (user_profile.email,))
         recipient_profile_ids.add(user_profile.id)
-        if user_profile.email not in exempt_emails:
+        if not is_cross_realm_bot_email(user_profile.email):
             realms.add(user_profile.realm_id)
 
     if len(realms) > 1:
@@ -1892,8 +1891,7 @@ def internal_send_message(realm, sender_email, recipient_type_name, recipients,
     system bot."""
 
     # Verify the user is in fact a system bot
-    assert(sender_email.lower() in settings.CROSS_REALM_BOT_EMAILS or
-           sender_email == settings.ERROR_BOT)
+    assert(is_cross_realm_bot_email(sender_email) or sender_email == settings.ERROR_BOT)
 
     sender = get_system_bot(sender_email)
     parsed_recipients = extract_recipients(recipients)
@@ -3843,7 +3841,7 @@ def get_status_dict(requesting_user_profile: UserProfile) -> Dict[Text, Dict[Tex
     return UserPresence.get_status_dict_by_realm(requesting_user_profile.realm_id)
 
 def get_cross_realm_dicts() -> List[Dict[str, Any]]:
-    users = bulk_get_users(list(get_cross_realm_emails()), None,
+    users = bulk_get_users(list(settings.CROSS_REALM_BOT_EMAILS), None,
                            base_query=UserProfile.objects.filter(
                                realm__string_id=settings.SYSTEM_BOT_REALM)).values()
     return [{'email': user.email,
@@ -3874,7 +3872,7 @@ def do_send_confirmation_email(invitee: PreregistrationUser,
                from_address=FromAddress.NOREPLY, context=context)
 
 def email_not_system_bot(email: Text) -> None:
-    if email.lower() in settings.CROSS_REALM_BOT_EMAILS:
+    if is_cross_realm_bot_email(email):
         raise ValidationError('%s is an email address reserved for system bots' % (email,))
 
 def validate_email_for_realm(target_realm: Realm, email: Text) -> None:
diff --git a/zerver/models.py b/zerver/models.py
--- a/zerver/models.py
+++ b/zerver/models.py
@@ -1404,7 +1404,7 @@ def get_user(email: Text, realm: Realm) -> UserProfile:
     return UserProfile.objects.select_related().get(email__iexact=email.strip(), realm=realm)
 
 def get_user_including_cross_realm(email: Text, realm: Optional[Realm]=None) -> UserProfile:
-    if email in get_cross_realm_emails():
+    if is_cross_realm_bot_email(email):
         return get_system_bot(email)
     assert realm is not None
     return get_user(email, realm)
@@ -1455,8 +1455,8 @@ def get_owned_bot_dicts(user_profile: UserProfile,
              }
             for botdict in result]
 
-def get_cross_realm_emails() -> Set[Text]:
-    return set(settings.CROSS_REALM_BOT_EMAILS)
+def is_cross_realm_bot_email(email: Text) -> bool:
+    return email.lower() in settings.CROSS_REALM_BOT_EMAILS
 
 # The Huddle class represents a group of individuals who have had a
 # Group Private Message conversation together.  The actual membership


Test patch:
diff --git a/zerver/tests/test_bots.py b/zerver/tests/test_bots.py
--- a/zerver/tests/test_bots.py
+++ b/zerver/tests/test_bots.py
@@ -11,7 +11,8 @@
 
 from zerver.lib.actions import do_change_stream_invite_only
 from zerver.models import get_realm, get_stream, \
-    Realm, Stream, UserProfile, get_user, get_bot_services, Service
+    Realm, Stream, UserProfile, get_user, get_bot_services, Service, \
+    is_cross_realm_bot_email
 from zerver.lib.test_classes import ZulipTestCase, UploadSerializeMixin
 from zerver.lib.test_helpers import (
     avatar_disk_path,
@@ -988,3 +989,12 @@ def test_create_embedded_bot(self, **extras: Any) -> None:
         bot_info.update(extras)
         result = self.client_post("/json/bots", bot_info)
         self.assert_json_error(result, 'Invalid embedded bot name.')
+
+    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"))