mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-03 21:43:21 +00:00 
			
		
		
		
	remote_server: Eliminate separate realms-only code path.
Given that most of the use cases for realms-only code path would really like to upload audit logs too, and the others would likely produce a better user experience if they upoaded audit logs, we should just have a single main code path here i.e. 'send_analytics_to_push_bouncer'. We still only upload usage statistics according to documented option, and only from the analytics cron job. The error handling takes place in 'send_analytics_to_push_bouncer' itself.
This commit is contained in:
		
				
					committed by
					
						
						Tim Abbott
					
				
			
			
				
	
			
			
			
						parent
						
							6f93ab72c0
						
					
				
				
					commit
					d763fae9d0
				
			@@ -106,4 +106,4 @@ class Command(BaseCommand):
 | 
			
		||||
            logger.info("Sleeping %d seconds before reporting...", delay)
 | 
			
		||||
            time.sleep(delay)
 | 
			
		||||
 | 
			
		||||
            send_analytics_to_push_bouncer()
 | 
			
		||||
            send_analytics_to_push_bouncer(consider_usage_statistics=True)
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ from corporate.lib.remote_billing_util import (
 | 
			
		||||
    RemoteBillingIdentityDict,
 | 
			
		||||
    RemoteBillingUserDict,
 | 
			
		||||
)
 | 
			
		||||
from zerver.lib.remote_server import send_realms_only_to_push_bouncer
 | 
			
		||||
from zerver.lib.remote_server import send_analytics_to_push_bouncer
 | 
			
		||||
from zerver.lib.test_classes import BouncerTestCase
 | 
			
		||||
from zerver.lib.timestamp import datetime_to_timestamp
 | 
			
		||||
from zerver.models import UserProfile
 | 
			
		||||
@@ -163,7 +163,7 @@ class RemoteBillingAuthenticationTest(BouncerTestCase):
 | 
			
		||||
        realm = desdemona.realm
 | 
			
		||||
 | 
			
		||||
        self.add_mock_response()
 | 
			
		||||
        send_realms_only_to_push_bouncer()
 | 
			
		||||
        send_analytics_to_push_bouncer(consider_usage_statistics=False)
 | 
			
		||||
 | 
			
		||||
        result = self.execute_remote_billing_authentication_flow(desdemona)
 | 
			
		||||
 | 
			
		||||
@@ -193,14 +193,14 @@ class RemoteBillingAuthenticationTest(BouncerTestCase):
 | 
			
		||||
        # and successfully completing the flow - transparently to the user.
 | 
			
		||||
        self.assertFalse(RemoteRealm.objects.filter(uuid=realm.uuid).exists())
 | 
			
		||||
 | 
			
		||||
        # send_realms_only_to_push_bouncer will be called within the endpoint's
 | 
			
		||||
        # send_analytics_to_push_bouncer will be called within the endpoint's
 | 
			
		||||
        # error handling to register realms with the bouncer. We mock.patch it
 | 
			
		||||
        # to be able to assert that it was called - but also use side_effect
 | 
			
		||||
        # to maintain the original behavior of the function, instead of
 | 
			
		||||
        # replacing it with a Mock.
 | 
			
		||||
        with mock.patch(
 | 
			
		||||
            "zerver.views.push_notifications.send_realms_only_to_push_bouncer",
 | 
			
		||||
            side_effect=send_realms_only_to_push_bouncer,
 | 
			
		||||
            "zerver.views.push_notifications.send_analytics_to_push_bouncer",
 | 
			
		||||
            side_effect=send_analytics_to_push_bouncer,
 | 
			
		||||
        ) as m:
 | 
			
		||||
            result = self.execute_remote_billing_authentication_flow(desdemona)
 | 
			
		||||
 | 
			
		||||
@@ -219,7 +219,7 @@ class RemoteBillingAuthenticationTest(BouncerTestCase):
 | 
			
		||||
        desdemona = self.example_user("desdemona")
 | 
			
		||||
 | 
			
		||||
        self.add_mock_response()
 | 
			
		||||
        send_realms_only_to_push_bouncer()
 | 
			
		||||
        send_analytics_to_push_bouncer(consider_usage_statistics=False)
 | 
			
		||||
 | 
			
		||||
        result = self.execute_remote_billing_authentication_flow(
 | 
			
		||||
            desdemona,
 | 
			
		||||
@@ -235,7 +235,7 @@ class RemoteBillingAuthenticationTest(BouncerTestCase):
 | 
			
		||||
        desdemona = self.example_user("desdemona")
 | 
			
		||||
 | 
			
		||||
        self.add_mock_response()
 | 
			
		||||
        send_realms_only_to_push_bouncer()
 | 
			
		||||
        send_analytics_to_push_bouncer(consider_usage_statistics=False)
 | 
			
		||||
 | 
			
		||||
        with self.settings(TERMS_OF_SERVICE_VERSION="1.0"):
 | 
			
		||||
            result = self.execute_remote_billing_authentication_flow(
 | 
			
		||||
@@ -280,7 +280,7 @@ class RemoteBillingAuthenticationTest(BouncerTestCase):
 | 
			
		||||
        realm = desdemona.realm
 | 
			
		||||
 | 
			
		||||
        self.add_mock_response()
 | 
			
		||||
        send_realms_only_to_push_bouncer()
 | 
			
		||||
        send_analytics_to_push_bouncer(consider_usage_statistics=False)
 | 
			
		||||
 | 
			
		||||
        with time_machine.travel(now, tick=False):
 | 
			
		||||
            result = self.execute_remote_billing_authentication_flow(desdemona)
 | 
			
		||||
@@ -336,7 +336,7 @@ class RemoteBillingAuthenticationTest(BouncerTestCase):
 | 
			
		||||
        realm = desdemona.realm
 | 
			
		||||
 | 
			
		||||
        self.add_mock_response()
 | 
			
		||||
        send_realms_only_to_push_bouncer()
 | 
			
		||||
        send_analytics_to_push_bouncer(consider_usage_statistics=False)
 | 
			
		||||
 | 
			
		||||
        # Straight-up access without authing at all:
 | 
			
		||||
        result = self.client_get(f"/realm/{realm.uuid!s}/plans/", subdomain="selfhosting")
 | 
			
		||||
@@ -388,7 +388,7 @@ class RemoteBillingAuthenticationTest(BouncerTestCase):
 | 
			
		||||
        realm = desdemona.realm
 | 
			
		||||
 | 
			
		||||
        self.add_mock_response()
 | 
			
		||||
        send_realms_only_to_push_bouncer()
 | 
			
		||||
        send_analytics_to_push_bouncer(consider_usage_statistics=False)
 | 
			
		||||
 | 
			
		||||
        result = self.execute_remote_billing_authentication_flow(desdemona, "sponsorship")
 | 
			
		||||
 | 
			
		||||
@@ -408,7 +408,7 @@ class RemoteBillingAuthenticationTest(BouncerTestCase):
 | 
			
		||||
        realm = desdemona.realm
 | 
			
		||||
 | 
			
		||||
        self.add_mock_response()
 | 
			
		||||
        send_realms_only_to_push_bouncer()
 | 
			
		||||
        send_analytics_to_push_bouncer(consider_usage_statistics=False)
 | 
			
		||||
 | 
			
		||||
        result = self.execute_remote_billing_authentication_flow(desdemona, "upgrade")
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -44,9 +44,8 @@ from zerver.lib.exceptions import ErrorCode, JsonableError
 | 
			
		||||
from zerver.lib.message import access_message, huddle_users
 | 
			
		||||
from zerver.lib.outgoing_http import OutgoingSession
 | 
			
		||||
from zerver.lib.remote_server import (
 | 
			
		||||
    PushNotificationBouncerServerError,
 | 
			
		||||
    send_analytics_to_push_bouncer,
 | 
			
		||||
    send_json_to_push_bouncer,
 | 
			
		||||
    send_realms_only_to_push_bouncer,
 | 
			
		||||
    send_to_push_bouncer,
 | 
			
		||||
)
 | 
			
		||||
from zerver.lib.soft_deactivation import soft_reactivate_if_personal_notification
 | 
			
		||||
@@ -783,37 +782,9 @@ def initialize_push_notifications() -> None:
 | 
			
		||||
 | 
			
		||||
    if uses_notification_bouncer():
 | 
			
		||||
        # If we're using the notification bouncer, check if we can
 | 
			
		||||
        # actually send push notifications.
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            realms = send_realms_only_to_push_bouncer()
 | 
			
		||||
        except PushNotificationBouncerServerError:  # nocoverage
 | 
			
		||||
            # 50x errors from the bouncer cannot be addressed by the
 | 
			
		||||
            # administrator of this server, and may be localized to
 | 
			
		||||
            # this endpoint; don't rashly mark push notifications as
 | 
			
		||||
            # disabled when they are likely working perfectly fine.
 | 
			
		||||
            return
 | 
			
		||||
        except Exception:
 | 
			
		||||
            # An exception was thrown trying to ask the bouncer service whether we can send
 | 
			
		||||
            # push notifications or not. There may be certain transient failures that we could
 | 
			
		||||
            # ignore here, but the default explanation is that there is something wrong either
 | 
			
		||||
            # with our credentials being corrupted or our ability to reach the bouncer service
 | 
			
		||||
            # over the network, so we immediately move to reporting push notifications as likely not working,
 | 
			
		||||
            # as whatever failed here is likely to also fail when trying to send a push notification.
 | 
			
		||||
            for realm in Realm.objects.filter(push_notifications_enabled=True):
 | 
			
		||||
                do_set_realm_property(realm, "push_notifications_enabled", False, acting_user=None)
 | 
			
		||||
                do_set_push_notifications_enabled_end_timestamp(realm, None, acting_user=None)
 | 
			
		||||
            logger.exception("Exception while sending realms only data to push bouncer")
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        for realm_uuid, data in realms.items():
 | 
			
		||||
            realm = Realm.objects.get(uuid=realm_uuid)
 | 
			
		||||
            do_set_realm_property(
 | 
			
		||||
                realm, "push_notifications_enabled", data["can_push"], acting_user=None
 | 
			
		||||
            )
 | 
			
		||||
            do_set_push_notifications_enabled_end_timestamp(
 | 
			
		||||
                realm, data["expected_end_timestamp"], acting_user=None
 | 
			
		||||
            )
 | 
			
		||||
        # actually send push notifications, and update our
 | 
			
		||||
        # understanding of that state for each realm accordingly.
 | 
			
		||||
        send_analytics_to_push_bouncer(consider_usage_statistics=False)
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    logger.warning(  # nocoverage
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,10 @@ from pydantic import UUID4, BaseModel, ConfigDict, Field, Json, field_validator
 | 
			
		||||
 | 
			
		||||
from analytics.models import InstallationCount, RealmCount
 | 
			
		||||
from version import API_FEATURE_LEVEL, ZULIP_VERSION
 | 
			
		||||
from zerver.actions.realm_settings import (
 | 
			
		||||
    do_set_push_notifications_enabled_end_timestamp,
 | 
			
		||||
    do_set_realm_property,
 | 
			
		||||
)
 | 
			
		||||
from zerver.lib.exceptions import (
 | 
			
		||||
    JsonableError,
 | 
			
		||||
    MissingRemoteRealmError,
 | 
			
		||||
@@ -18,7 +22,6 @@ from zerver.lib.exceptions import (
 | 
			
		||||
)
 | 
			
		||||
from zerver.lib.outgoing_http import OutgoingSession
 | 
			
		||||
from zerver.lib.queue import queue_json_publish
 | 
			
		||||
from zerver.lib.types import RemoteRealmDictValue
 | 
			
		||||
from zerver.models import OrgTypeEnum, Realm, RealmAuditLog
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -311,7 +314,7 @@ def get_realms_info_for_push_bouncer(realm_id: Optional[int] = None) -> List[Rea
 | 
			
		||||
    return realm_info_list
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def send_analytics_to_push_bouncer() -> None:
 | 
			
		||||
def send_analytics_to_push_bouncer(consider_usage_statistics: bool = True) -> None:
 | 
			
		||||
    logger = logging.getLogger("zulip.analytics")
 | 
			
		||||
    # first, check what's latest
 | 
			
		||||
    try:
 | 
			
		||||
@@ -326,7 +329,10 @@ def send_analytics_to_push_bouncer() -> None:
 | 
			
		||||
    last_acked_installation_count_id = result["last_installation_count_id"]
 | 
			
		||||
    last_acked_realmauditlog_id = result["last_realmauditlog_id"]
 | 
			
		||||
 | 
			
		||||
    if settings.SUBMIT_USAGE_STATISTICS:
 | 
			
		||||
    if settings.SUBMIT_USAGE_STATISTICS and consider_usage_statistics:
 | 
			
		||||
        # Only upload usage statistics, which is relatively expensive,
 | 
			
		||||
        # if called from the analytics cron job and the server has
 | 
			
		||||
        # uploading such statistics enabled.
 | 
			
		||||
        installation_count_query = InstallationCount.objects.filter(
 | 
			
		||||
            id__gt=last_acked_installation_count_id
 | 
			
		||||
        )
 | 
			
		||||
@@ -353,32 +359,48 @@ def send_analytics_to_push_bouncer() -> None:
 | 
			
		||||
        api_feature_level=API_FEATURE_LEVEL,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # Send the actual request, and process the response.
 | 
			
		||||
    try:
 | 
			
		||||
        send_to_push_bouncer("POST", "server/analytics", request.model_dump(round_trip=True))
 | 
			
		||||
    except JsonableError as e:
 | 
			
		||||
        logger.warning(e.msg)
 | 
			
		||||
    logger.info("Reported %d records", record_count)
 | 
			
		||||
        response = send_to_push_bouncer(
 | 
			
		||||
            "POST", "server/analytics", request.model_dump(round_trip=True)
 | 
			
		||||
        )
 | 
			
		||||
    except PushNotificationBouncerServerError:  # nocoverage
 | 
			
		||||
        # 50x errors from the bouncer cannot be addressed by the
 | 
			
		||||
        # administrator of this server, and may be localized to
 | 
			
		||||
        # this endpoint; don't rashly mark push notifications as
 | 
			
		||||
        # disabled when they are likely working perfectly fine.
 | 
			
		||||
        return
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        if isinstance(e, JsonableError):
 | 
			
		||||
            # Log exceptions with server error messages to the analytics log.
 | 
			
		||||
            logger.warning(e.msg)
 | 
			
		||||
 | 
			
		||||
        # An exception was thrown trying to ask the bouncer service whether we can send
 | 
			
		||||
        # push notifications or not. There may be certain transient failures that we could
 | 
			
		||||
        # ignore here, but the default explanation is that there is something wrong either
 | 
			
		||||
        # with our credentials being corrupted or our ability to reach the bouncer service
 | 
			
		||||
        # over the network, so we immediately move to reporting push notifications as likely not working,
 | 
			
		||||
        # as whatever failed here is likely to also fail when trying to send a push notification.
 | 
			
		||||
        for realm in Realm.objects.filter(push_notifications_enabled=True):
 | 
			
		||||
            do_set_realm_property(realm, "push_notifications_enabled", False, acting_user=None)
 | 
			
		||||
            do_set_push_notifications_enabled_end_timestamp(realm, None, acting_user=None)
 | 
			
		||||
        if not isinstance(e, JsonableError):
 | 
			
		||||
            # Log this generic error only if we haven't already logged specific error above.
 | 
			
		||||
            logger.exception("Exception asking push bouncer if notifications will work.")
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
def send_realms_only_to_push_bouncer() -> Dict[str, RemoteRealmDictValue]:
 | 
			
		||||
    request = AnalyticsRequest.model_construct(
 | 
			
		||||
        realm_counts=[],
 | 
			
		||||
        installation_counts=[],
 | 
			
		||||
        realms=get_realms_info_for_push_bouncer(),
 | 
			
		||||
        version=ZULIP_VERSION,
 | 
			
		||||
        api_feature_level=API_FEATURE_LEVEL,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # We don't catch JsonableError here, because we want it to propagate further
 | 
			
		||||
    # to either explicitly, loudly fail or be error-handled by the caller.
 | 
			
		||||
    response = send_to_push_bouncer(
 | 
			
		||||
        "POST",
 | 
			
		||||
        "server/analytics",
 | 
			
		||||
        request.model_dump(round_trip=True, exclude={"realmauditlog_rows"}),
 | 
			
		||||
    )
 | 
			
		||||
    assert isinstance(response["realms"], dict)  # for mypy
 | 
			
		||||
    realms = response["realms"]
 | 
			
		||||
    for realm_uuid, data in realms.items():
 | 
			
		||||
        realm = Realm.objects.get(uuid=realm_uuid)
 | 
			
		||||
        do_set_realm_property(
 | 
			
		||||
            realm, "push_notifications_enabled", data["can_push"], acting_user=None
 | 
			
		||||
        )
 | 
			
		||||
        do_set_push_notifications_enabled_end_timestamp(
 | 
			
		||||
            realm, data["expected_end_timestamp"], acting_user=None
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    return response["realms"]
 | 
			
		||||
    logger.info("Reported %d records", record_count)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def enqueue_register_realm_with_push_bouncer_if_needed(realm: Realm) -> None:
 | 
			
		||||
 
 | 
			
		||||
@@ -1399,14 +1399,28 @@ class RealmImportExportTest(ExportFile):
 | 
			
		||||
        with self.settings(BILLING_ENABLED=False), self.assertLogs(level="INFO"), patch(
 | 
			
		||||
            "zerver.lib.remote_server.send_to_push_bouncer"
 | 
			
		||||
        ) as m:
 | 
			
		||||
            get_response = {
 | 
			
		||||
                "last_realm_count_id": 0,
 | 
			
		||||
                "last_installation_count_id": 0,
 | 
			
		||||
                "last_realmauditlog_id": 0,
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            def mock_send_to_push_bouncer_response(  # type: ignore[return]
 | 
			
		||||
                method: str, *args: Any
 | 
			
		||||
            ) -> Optional[Dict[str, int]]:
 | 
			
		||||
                if method == "GET":
 | 
			
		||||
                    return get_response
 | 
			
		||||
 | 
			
		||||
            m.side_effect = mock_send_to_push_bouncer_response
 | 
			
		||||
 | 
			
		||||
            new_realm = do_import_realm(get_output_dir(), "test-zulip")
 | 
			
		||||
 | 
			
		||||
        self.assertTrue(Realm.objects.filter(string_id="test-zulip").exists())
 | 
			
		||||
        calls_args_for_assert = m.call_args_list[0][0]
 | 
			
		||||
        calls_args_for_assert = m.call_args_list[1][0]
 | 
			
		||||
        self.assertEqual(calls_args_for_assert[0], "POST")
 | 
			
		||||
        self.assertEqual(calls_args_for_assert[1], "server/analytics")
 | 
			
		||||
        self.assertIn(
 | 
			
		||||
            new_realm.id, [realm["id"] for realm in json.loads(m.call_args_list[0][0][2]["realms"])]
 | 
			
		||||
            new_realm.id, [realm["id"] for realm in json.loads(m.call_args_list[1][0][2]["realms"])]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_import_files_from_local(self) -> None:
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ from typing_extensions import override
 | 
			
		||||
from analytics.lib.counts import CountStat, LoggingCountStat
 | 
			
		||||
from analytics.models import InstallationCount, RealmCount
 | 
			
		||||
from corporate.models import CustomerPlan
 | 
			
		||||
from version import API_FEATURE_LEVEL, ZULIP_VERSION
 | 
			
		||||
from version import ZULIP_VERSION
 | 
			
		||||
from zerver.actions.message_delete import do_delete_messages
 | 
			
		||||
from zerver.actions.message_flags import do_mark_stream_messages_as_read, do_update_message_flags
 | 
			
		||||
from zerver.actions.realm_settings import (
 | 
			
		||||
@@ -67,7 +67,6 @@ from zerver.lib.remote_server import (
 | 
			
		||||
    build_analytics_data,
 | 
			
		||||
    get_realms_info_for_push_bouncer,
 | 
			
		||||
    send_analytics_to_push_bouncer,
 | 
			
		||||
    send_realms_only_to_push_bouncer,
 | 
			
		||||
    send_to_push_bouncer,
 | 
			
		||||
)
 | 
			
		||||
from zerver.lib.response import json_response_from_error
 | 
			
		||||
@@ -780,30 +779,29 @@ class PushBouncerNotificationTest(BouncerTestCase):
 | 
			
		||||
        with mock.patch(
 | 
			
		||||
            "zerver.lib.push_notifications.uses_notification_bouncer", return_value=True
 | 
			
		||||
        ):
 | 
			
		||||
            realms_response = {realm.uuid: {"can_push": True, "expected_end_timestamp": None}}
 | 
			
		||||
            with mock.patch(
 | 
			
		||||
                "zerver.lib.push_notifications.send_realms_only_to_push_bouncer",
 | 
			
		||||
                return_value=realms_response,
 | 
			
		||||
            ):
 | 
			
		||||
            with mock.patch("zerver.lib.remote_server.send_to_push_bouncer") as m:
 | 
			
		||||
                post_response = {
 | 
			
		||||
                    "realms": {realm.uuid: {"can_push": True, "expected_end_timestamp": None}}
 | 
			
		||||
                }
 | 
			
		||||
                get_response = {
 | 
			
		||||
                    "last_realm_count_id": 0,
 | 
			
		||||
                    "last_installation_count_id": 0,
 | 
			
		||||
                    "last_realmauditlog_id": 0,
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                def mock_send_to_push_bouncer_response(method: str, *args: Any) -> Dict[str, Any]:
 | 
			
		||||
                    if method == "POST":
 | 
			
		||||
                        return post_response
 | 
			
		||||
                    return get_response
 | 
			
		||||
 | 
			
		||||
                m.side_effect = mock_send_to_push_bouncer_response
 | 
			
		||||
 | 
			
		||||
                initialize_push_notifications()
 | 
			
		||||
 | 
			
		||||
                realm = get_realm("zulip")
 | 
			
		||||
                self.assertTrue(realm.push_notifications_enabled)
 | 
			
		||||
                self.assertEqual(realm.push_notifications_enabled_end_timestamp, None)
 | 
			
		||||
 | 
			
		||||
            with mock.patch(
 | 
			
		||||
                "zerver.lib.push_notifications.send_realms_only_to_push_bouncer",
 | 
			
		||||
                side_effect=Exception,
 | 
			
		||||
            ), self.assertLogs("zerver.lib.push_notifications", level="ERROR") as exception_log:
 | 
			
		||||
                initialize_push_notifications()
 | 
			
		||||
 | 
			
		||||
                realm = get_realm("zulip")
 | 
			
		||||
                self.assertFalse(realm.push_notifications_enabled)
 | 
			
		||||
                self.assertIn(
 | 
			
		||||
                    "ERROR:zerver.lib.push_notifications:Exception while sending realms only data to push bouncer",
 | 
			
		||||
                    exception_log.output[0],
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
    @override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com")
 | 
			
		||||
    @responses.activate
 | 
			
		||||
    def test_register_token_realm_uuid_belongs_to_different_server(self) -> None:
 | 
			
		||||
@@ -1044,10 +1042,6 @@ class AnalyticsBouncerTest(BouncerTestCase):
 | 
			
		||||
 | 
			
		||||
        self.add_mock_response()
 | 
			
		||||
        # Send any existing data over, so that we can start the test with a "clean" slate
 | 
			
		||||
        audit_log = RealmAuditLog.objects.all().order_by("id").last()
 | 
			
		||||
        assert audit_log is not None
 | 
			
		||||
        audit_log_max_id = audit_log.id
 | 
			
		||||
 | 
			
		||||
        remote_server = RemoteZulipServer.objects.get(uuid=self.server_uuid)
 | 
			
		||||
        assert remote_server is not None
 | 
			
		||||
        assert remote_server.last_version is None
 | 
			
		||||
@@ -1055,6 +1049,10 @@ class AnalyticsBouncerTest(BouncerTestCase):
 | 
			
		||||
        send_analytics_to_push_bouncer()
 | 
			
		||||
        self.assertTrue(responses.assert_call_count(ANALYTICS_STATUS_URL, 1))
 | 
			
		||||
 | 
			
		||||
        audit_log = RealmAuditLog.objects.all().order_by("id").last()
 | 
			
		||||
        assert audit_log is not None
 | 
			
		||||
        audit_log_max_id = audit_log.id
 | 
			
		||||
 | 
			
		||||
        remote_server = RemoteZulipServer.objects.get(uuid=self.server_uuid)
 | 
			
		||||
        assert remote_server.last_version == ZULIP_VERSION
 | 
			
		||||
 | 
			
		||||
@@ -1126,8 +1124,14 @@ class AnalyticsBouncerTest(BouncerTestCase):
 | 
			
		||||
            send_analytics_to_push_bouncer()
 | 
			
		||||
        check_counts(2, 2, 0, 0, 1)
 | 
			
		||||
 | 
			
		||||
        with self.settings(SUBMIT_USAGE_STATISTICS=True):
 | 
			
		||||
            # With 'SUBMIT_USAGE_STATISTICS=True' but 'consider_usage_statistics=False',
 | 
			
		||||
            # we don't send RealmCount and InstallationCounts.
 | 
			
		||||
            send_analytics_to_push_bouncer(consider_usage_statistics=False)
 | 
			
		||||
        check_counts(3, 3, 0, 0, 1)
 | 
			
		||||
 | 
			
		||||
        send_analytics_to_push_bouncer()
 | 
			
		||||
        check_counts(3, 3, 1, 1, 1)
 | 
			
		||||
        check_counts(4, 4, 1, 1, 1)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            list(
 | 
			
		||||
@@ -1204,7 +1208,7 @@ class AnalyticsBouncerTest(BouncerTestCase):
 | 
			
		||||
        do_deactivate_realm(zephyr_realm, acting_user=None)
 | 
			
		||||
 | 
			
		||||
        send_analytics_to_push_bouncer()
 | 
			
		||||
        check_counts(4, 4, 1, 1, 7)
 | 
			
		||||
        check_counts(5, 5, 1, 1, 7)
 | 
			
		||||
 | 
			
		||||
        zephyr_remote_realm = RemoteRealm.objects.get(uuid=zephyr_realm.uuid)
 | 
			
		||||
        self.assertEqual(zephyr_remote_realm.host, zephyr_realm.host)
 | 
			
		||||
@@ -1282,7 +1286,7 @@ class AnalyticsBouncerTest(BouncerTestCase):
 | 
			
		||||
 | 
			
		||||
        # Test having no new rows
 | 
			
		||||
        send_analytics_to_push_bouncer()
 | 
			
		||||
        check_counts(5, 5, 1, 1, 7)
 | 
			
		||||
        check_counts(6, 6, 1, 1, 7)
 | 
			
		||||
 | 
			
		||||
        # Test only having new RealmCount rows
 | 
			
		||||
        RealmCount.objects.create(
 | 
			
		||||
@@ -1298,14 +1302,14 @@ class AnalyticsBouncerTest(BouncerTestCase):
 | 
			
		||||
            value=9,
 | 
			
		||||
        )
 | 
			
		||||
        send_analytics_to_push_bouncer()
 | 
			
		||||
        check_counts(6, 6, 3, 1, 7)
 | 
			
		||||
        check_counts(7, 7, 3, 1, 7)
 | 
			
		||||
 | 
			
		||||
        # Test only having new InstallationCount rows
 | 
			
		||||
        InstallationCount.objects.create(
 | 
			
		||||
            property=realm_stat.property, end_time=end_time + timedelta(days=1), value=6
 | 
			
		||||
        )
 | 
			
		||||
        send_analytics_to_push_bouncer()
 | 
			
		||||
        check_counts(7, 7, 3, 2, 7)
 | 
			
		||||
        check_counts(8, 8, 3, 2, 7)
 | 
			
		||||
 | 
			
		||||
        # Test only having new RealmAuditLog rows
 | 
			
		||||
        # Non-synced event
 | 
			
		||||
@@ -1317,7 +1321,7 @@ class AnalyticsBouncerTest(BouncerTestCase):
 | 
			
		||||
            extra_data={"data": "foo"},
 | 
			
		||||
        )
 | 
			
		||||
        send_analytics_to_push_bouncer()
 | 
			
		||||
        check_counts(8, 8, 3, 2, 7)
 | 
			
		||||
        check_counts(9, 9, 3, 2, 7)
 | 
			
		||||
        # Synced event
 | 
			
		||||
        RealmAuditLog.objects.create(
 | 
			
		||||
            realm=user.realm,
 | 
			
		||||
@@ -1329,7 +1333,7 @@ class AnalyticsBouncerTest(BouncerTestCase):
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
        send_analytics_to_push_bouncer()
 | 
			
		||||
        check_counts(9, 9, 3, 2, 8)
 | 
			
		||||
        check_counts(10, 10, 3, 2, 8)
 | 
			
		||||
 | 
			
		||||
        # Now create an InstallationCount with a property that's not supposed
 | 
			
		||||
        # to be tracked by the remote server - since the bouncer itself tracks
 | 
			
		||||
@@ -1349,7 +1353,7 @@ class AnalyticsBouncerTest(BouncerTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        # The analytics endpoint call counts increase by 1, but the actual RemoteCounts remain unchanged,
 | 
			
		||||
        # since syncing the data failed.
 | 
			
		||||
        check_counts(10, 10, 3, 2, 8)
 | 
			
		||||
        check_counts(11, 11, 3, 2, 8)
 | 
			
		||||
        forbidden_installation_count.delete()
 | 
			
		||||
 | 
			
		||||
        (realm_count_data, installation_count_data, realmauditlog_data) = build_analytics_data(
 | 
			
		||||
@@ -1387,7 +1391,7 @@ class AnalyticsBouncerTest(BouncerTestCase):
 | 
			
		||||
            ],
 | 
			
		||||
        )
 | 
			
		||||
        # Only the request counts go up -- all of the other rows' duplicates are dropped
 | 
			
		||||
        check_counts(11, 11, 3, 2, 8)
 | 
			
		||||
        check_counts(12, 12, 3, 2, 8)
 | 
			
		||||
 | 
			
		||||
        # Test that only valid org_type values are accepted - integers defined in OrgTypeEnum.
 | 
			
		||||
        realms_data = get_realms_info_for_push_bouncer()
 | 
			
		||||
@@ -1785,28 +1789,30 @@ class AnalyticsBouncerTest(BouncerTestCase):
 | 
			
		||||
 | 
			
		||||
    @override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com")
 | 
			
		||||
    @responses.activate
 | 
			
		||||
    def test_send_realms_only_to_push_bouncer(self) -> None:
 | 
			
		||||
    def test_realm_properties_after_send_analytics(self) -> None:
 | 
			
		||||
        self.add_mock_response()
 | 
			
		||||
 | 
			
		||||
        with mock.patch(
 | 
			
		||||
            "zilencer.views.RemoteRealmBillingSession.get_customer", return_value=None
 | 
			
		||||
        ) as m:
 | 
			
		||||
            realms = send_realms_only_to_push_bouncer()
 | 
			
		||||
            send_analytics_to_push_bouncer(consider_usage_statistics=False)
 | 
			
		||||
            m.assert_called()
 | 
			
		||||
            for data in realms.values():
 | 
			
		||||
                self.assertEqual(data["can_push"], True)
 | 
			
		||||
                self.assertEqual(data["expected_end_timestamp"], None)
 | 
			
		||||
            realms = Realm.objects.all()
 | 
			
		||||
            for realm in realms:
 | 
			
		||||
                self.assertEqual(realm.push_notifications_enabled, True)
 | 
			
		||||
                self.assertEqual(realm.push_notifications_enabled_end_timestamp, None)
 | 
			
		||||
 | 
			
		||||
        dummy_customer = mock.MagicMock()
 | 
			
		||||
        with mock.patch(
 | 
			
		||||
            "zilencer.views.RemoteRealmBillingSession.get_customer", return_value=dummy_customer
 | 
			
		||||
        ):
 | 
			
		||||
            with mock.patch("zilencer.views.get_current_plan_by_customer", return_value=None) as m:
 | 
			
		||||
                realms = send_realms_only_to_push_bouncer()
 | 
			
		||||
                send_analytics_to_push_bouncer(consider_usage_statistics=False)
 | 
			
		||||
                m.assert_called()
 | 
			
		||||
                for data in realms.values():
 | 
			
		||||
                    self.assertEqual(data["can_push"], True)
 | 
			
		||||
                    self.assertEqual(data["expected_end_timestamp"], None)
 | 
			
		||||
                realms = Realm.objects.all()
 | 
			
		||||
                for realm in realms:
 | 
			
		||||
                    self.assertEqual(realm.push_notifications_enabled, True)
 | 
			
		||||
                    self.assertEqual(realm.push_notifications_enabled_end_timestamp, None)
 | 
			
		||||
 | 
			
		||||
        dummy_customer_plan = mock.MagicMock()
 | 
			
		||||
        dummy_customer_plan.status = CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE
 | 
			
		||||
@@ -1820,16 +1826,48 @@ class AnalyticsBouncerTest(BouncerTestCase):
 | 
			
		||||
                with mock.patch(
 | 
			
		||||
                    "zilencer.views.RemoteRealmBillingSession.get_next_billing_cycle",
 | 
			
		||||
                    return_value=dummy_date,
 | 
			
		||||
                ) as m:
 | 
			
		||||
                    realms = send_realms_only_to_push_bouncer()
 | 
			
		||||
                ) as m, self.assertLogs("zulip.analytics", level="INFO") as info_log:
 | 
			
		||||
                    send_analytics_to_push_bouncer(consider_usage_statistics=False)
 | 
			
		||||
                    m.assert_called()
 | 
			
		||||
                    for data in realms.values():
 | 
			
		||||
                        self.assertEqual(data["can_push"], True)
 | 
			
		||||
                    realms = Realm.objects.all()
 | 
			
		||||
                    for realm in realms:
 | 
			
		||||
                        self.assertEqual(realm.push_notifications_enabled, True)
 | 
			
		||||
                        self.assertEqual(
 | 
			
		||||
                            data["expected_end_timestamp"], datetime_to_timestamp(dummy_date)
 | 
			
		||||
                            realm.push_notifications_enabled_end_timestamp,
 | 
			
		||||
                            dummy_date,
 | 
			
		||||
                        )
 | 
			
		||||
                    self.assertIn(
 | 
			
		||||
                        "INFO:zulip.analytics:Reported 0 records",
 | 
			
		||||
                        info_log.output[0],
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
        send_realms_only_to_push_bouncer()
 | 
			
		||||
        with mock.patch("zerver.lib.remote_server.send_to_push_bouncer") as m, self.assertLogs(
 | 
			
		||||
            "zulip.analytics", level="ERROR"
 | 
			
		||||
        ) as exception_log:
 | 
			
		||||
            get_response = {
 | 
			
		||||
                "last_realm_count_id": 0,
 | 
			
		||||
                "last_installation_count_id": 0,
 | 
			
		||||
                "last_realmauditlog_id": 0,
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            def mock_send_to_push_bouncer_response(method: str, *args: Any) -> Dict[str, int]:
 | 
			
		||||
                if method == "POST":
 | 
			
		||||
                    raise Exception
 | 
			
		||||
                return get_response
 | 
			
		||||
 | 
			
		||||
            m.side_effect = mock_send_to_push_bouncer_response
 | 
			
		||||
 | 
			
		||||
            send_analytics_to_push_bouncer(consider_usage_statistics=False)
 | 
			
		||||
 | 
			
		||||
            realms = Realm.objects.all()
 | 
			
		||||
            for realm in realms:
 | 
			
		||||
                self.assertFalse(realm.push_notifications_enabled)
 | 
			
		||||
            self.assertIn(
 | 
			
		||||
                "ERROR:zulip.analytics:Exception asking push bouncer if notifications will work.",
 | 
			
		||||
                exception_log.output[0],
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        send_analytics_to_push_bouncer(consider_usage_statistics=False)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            list(
 | 
			
		||||
@@ -1859,38 +1897,6 @@ class AnalyticsBouncerTest(BouncerTestCase):
 | 
			
		||||
            ],
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Use a mock to assert exactly the data that gets sent.
 | 
			
		||||
        dummy_send_realms_only_response = {
 | 
			
		||||
            "result": "success",
 | 
			
		||||
            "msg": "",
 | 
			
		||||
            "realms": {
 | 
			
		||||
                "f9535515-84d0-489e-80d5-9ae97c3c7ec1": {
 | 
			
		||||
                    "can_push": True,
 | 
			
		||||
                    "expected_end_timestamp": None,
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
        with mock.patch(
 | 
			
		||||
            "zerver.lib.remote_server.send_to_push_bouncer",
 | 
			
		||||
            return_value=dummy_send_realms_only_response,
 | 
			
		||||
        ) as mock_send_to_push_bouncer:
 | 
			
		||||
            send_realms_only_to_push_bouncer()
 | 
			
		||||
 | 
			
		||||
        post_data = {
 | 
			
		||||
            "realm_counts": "[]",
 | 
			
		||||
            "installation_counts": "[]",
 | 
			
		||||
            "realms": orjson.dumps(
 | 
			
		||||
                [dict(realm_data) for realm_data in get_realms_info_for_push_bouncer()]
 | 
			
		||||
            ).decode(),
 | 
			
		||||
            "version": orjson.dumps(ZULIP_VERSION).decode(),
 | 
			
		||||
            "api_feature_level": orjson.dumps(API_FEATURE_LEVEL).decode(),
 | 
			
		||||
        }
 | 
			
		||||
        mock_send_to_push_bouncer.assert_called_with(
 | 
			
		||||
            "POST",
 | 
			
		||||
            "server/analytics",
 | 
			
		||||
            post_data,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PushNotificationTest(BouncerTestCase):
 | 
			
		||||
    @override
 | 
			
		||||
 
 | 
			
		||||
@@ -1089,18 +1089,30 @@ class RealmTest(ZulipTestCase):
 | 
			
		||||
        }
 | 
			
		||||
        with mock.patch(
 | 
			
		||||
            "zerver.lib.remote_server.send_to_push_bouncer",
 | 
			
		||||
            return_value=dummy_send_realms_only_response,
 | 
			
		||||
        ) as m:
 | 
			
		||||
            get_response = {
 | 
			
		||||
                "last_realm_count_id": 0,
 | 
			
		||||
                "last_installation_count_id": 0,
 | 
			
		||||
                "last_realmauditlog_id": 0,
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            def mock_send_to_push_bouncer_response(method: str, *args: Any) -> Dict[str, Any]:
 | 
			
		||||
                if method == "GET":
 | 
			
		||||
                    return get_response
 | 
			
		||||
                return dummy_send_realms_only_response
 | 
			
		||||
 | 
			
		||||
            m.side_effect = mock_send_to_push_bouncer_response
 | 
			
		||||
 | 
			
		||||
            realm = do_create_realm("realm_string_id", "realm name")
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(realm.string_id, "realm_string_id")
 | 
			
		||||
        self.assertEqual(m.call_count, 1)
 | 
			
		||||
        self.assertEqual(m.call_count, 2)
 | 
			
		||||
 | 
			
		||||
        calls_args_for_assert = m.call_args_list[0][0]
 | 
			
		||||
        calls_args_for_assert = m.call_args_list[1][0]
 | 
			
		||||
        self.assertEqual(calls_args_for_assert[0], "POST")
 | 
			
		||||
        self.assertEqual(calls_args_for_assert[1], "server/analytics")
 | 
			
		||||
        self.assertIn(
 | 
			
		||||
            realm.id, [realm["id"] for realm in json.loads(m.call_args_list[0][0][2]["realms"])]
 | 
			
		||||
            realm.id, [realm["id"] for realm in json.loads(m.call_args_list[1][0][2]["realms"])]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_changing_waiting_period_updates_system_groups(self) -> None:
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ from zerver.lib.push_notifications import (
 | 
			
		||||
from zerver.lib.remote_server import (
 | 
			
		||||
    UserDataForRemoteBilling,
 | 
			
		||||
    get_realms_info_for_push_bouncer,
 | 
			
		||||
    send_realms_only_to_push_bouncer,
 | 
			
		||||
    send_analytics_to_push_bouncer,
 | 
			
		||||
    send_to_push_bouncer,
 | 
			
		||||
)
 | 
			
		||||
from zerver.lib.request import REQ, has_request_variables
 | 
			
		||||
@@ -162,7 +162,7 @@ def self_hosting_auth_redirect(
 | 
			
		||||
        result = send_to_push_bouncer("POST", "server/billing", post_data)
 | 
			
		||||
    except MissingRemoteRealmError:
 | 
			
		||||
        # Upload realm info and re-try. It should work now.
 | 
			
		||||
        send_realms_only_to_push_bouncer()
 | 
			
		||||
        send_analytics_to_push_bouncer(consider_usage_statistics=False)
 | 
			
		||||
        result = send_to_push_bouncer("POST", "server/billing", post_data)
 | 
			
		||||
    except RemoteRealmServerMismatchError:
 | 
			
		||||
        return render(request, "zilencer/remote_realm_server_mismatch_error.html", status=403)
 | 
			
		||||
 
 | 
			
		||||
@@ -80,7 +80,7 @@ from zerver.lib.pysa import mark_sanitized
 | 
			
		||||
from zerver.lib.queue import SimpleQueueClient, retry_event
 | 
			
		||||
from zerver.lib.remote_server import (
 | 
			
		||||
    PushNotificationBouncerRetryLaterError,
 | 
			
		||||
    send_realms_only_to_push_bouncer,
 | 
			
		||||
    send_analytics_to_push_bouncer,
 | 
			
		||||
)
 | 
			
		||||
from zerver.lib.send_email import (
 | 
			
		||||
    EmailNotDeliveredError,
 | 
			
		||||
@@ -1174,9 +1174,9 @@ class DeferredWorker(QueueProcessingWorker):
 | 
			
		||||
            # In the future we may use the realm_id to send only that single realm's info.
 | 
			
		||||
            realm_id = event["realm_id"]
 | 
			
		||||
            logger.info(
 | 
			
		||||
                "Running send_realms_only_to_push_bouncer, requested due to realm %s", realm_id
 | 
			
		||||
                "Running send_analytics_to_push_bouncer, requested due to realm %s", realm_id
 | 
			
		||||
            )
 | 
			
		||||
            send_realms_only_to_push_bouncer()
 | 
			
		||||
            send_analytics_to_push_bouncer(consider_usage_statistics=False)
 | 
			
		||||
 | 
			
		||||
        end = time.time()
 | 
			
		||||
        logger.info(
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user