mirror of
https://github.com/zulip/zulip.git
synced 2025-10-23 04:52:12 +00:00
1048 lines
44 KiB
Python
1048 lines
44 KiB
Python
from unittest import mock
|
|
|
|
import responses
|
|
from django.conf import settings
|
|
from django.http.response import ResponseHeaders
|
|
from django.test import override_settings
|
|
from requests.exceptions import ConnectionError
|
|
from requests.models import PreparedRequest
|
|
from typing_extensions import override
|
|
|
|
from analytics.models import RealmCount
|
|
from zerver.actions.message_delete import do_delete_messages
|
|
from zerver.actions.user_groups import add_subgroups_to_user_group, check_add_user_group
|
|
from zerver.actions.user_settings import do_change_user_setting
|
|
from zerver.actions.user_topics import do_set_user_topic_visibility_policy
|
|
from zerver.lib.push_notifications import (
|
|
UserPushIdentityCompat,
|
|
handle_push_notification,
|
|
handle_remove_push_notification,
|
|
)
|
|
from zerver.lib.remote_server import PushNotificationBouncerRetryLaterError
|
|
from zerver.lib.test_classes import PushNotificationTestCase
|
|
from zerver.lib.test_helpers import activate_push_notification_service
|
|
from zerver.models import PushDeviceToken, Recipient, UserMessage, UserTopic
|
|
from zerver.models.realms import get_realm
|
|
from zerver.models.scheduled_jobs import NotificationTriggers
|
|
from zerver.models.streams import get_stream
|
|
from zilencer.views import DevicesToCleanUpDict
|
|
|
|
if settings.ZILENCER_ENABLED:
|
|
from zilencer.models import RemotePushDeviceToken
|
|
|
|
|
|
class HandlePushNotificationTest(PushNotificationTestCase):
|
|
DEFAULT_SUBDOMAIN = ""
|
|
|
|
def soft_deactivate_main_user(self) -> None:
|
|
self.user_profile = self.example_user("hamlet")
|
|
self.soft_deactivate_user(self.user_profile)
|
|
|
|
@override
|
|
def request_callback(self, request: PreparedRequest) -> tuple[int, ResponseHeaders, bytes]:
|
|
assert request.url is not None # allow mypy to infer url is present.
|
|
assert settings.ZULIP_SERVICES_URL is not None
|
|
local_url = request.url.replace(settings.ZULIP_SERVICES_URL, "")
|
|
assert isinstance(request.body, bytes)
|
|
result = self.uuid_post(
|
|
self.server_uuid, local_url, request.body, content_type="application/json"
|
|
)
|
|
return (result.status_code, result.headers, result.content)
|
|
|
|
@activate_push_notification_service()
|
|
@responses.activate
|
|
def test_end_to_end(self) -> None:
|
|
self.add_mock_response()
|
|
self.setup_apns_tokens()
|
|
self.setup_fcm_tokens()
|
|
|
|
message = self.get_message(
|
|
Recipient.PERSONAL,
|
|
type_id=self.personal_recipient_user.id,
|
|
realm_id=self.personal_recipient_user.realm_id,
|
|
)
|
|
UserMessage.objects.create(
|
|
user_profile=self.user_profile,
|
|
message=message,
|
|
)
|
|
|
|
missed_message = {
|
|
"message_id": message.id,
|
|
"trigger": NotificationTriggers.DIRECT_MESSAGE,
|
|
}
|
|
with (
|
|
self.mock_fcm() as (
|
|
_mock_fcm_app,
|
|
mock_fcm_messaging,
|
|
),
|
|
self.mock_apns() as (_apns_context, send_notification),
|
|
mock.patch(
|
|
"corporate.lib.stripe.RemoteRealmBillingSession.current_count_for_billed_licenses",
|
|
return_value=10,
|
|
),
|
|
self.assertLogs("zerver.lib.push_notifications", level="INFO") as pn_logger,
|
|
self.assertLogs("zilencer.views", level="INFO") as views_logger,
|
|
):
|
|
apns_devices = list(
|
|
RemotePushDeviceToken.objects.filter(kind=PushDeviceToken.APNS)
|
|
.order_by("id")
|
|
.values_list("token", flat=True)
|
|
)
|
|
fcm_devices = list(
|
|
RemotePushDeviceToken.objects.filter(kind=PushDeviceToken.FCM)
|
|
.order_by("id")
|
|
.values_list("token", flat=True)
|
|
)
|
|
mock_fcm_messaging.send_each.return_value = self.make_fcm_success_response(fcm_devices)
|
|
send_notification.return_value.is_successful = True
|
|
handle_push_notification(self.user_profile.id, missed_message)
|
|
self.assertEqual(
|
|
{
|
|
(args[0][0].device_token, args[0][0].apns_topic)
|
|
for args in send_notification.call_args_list
|
|
},
|
|
{
|
|
(device.token, device.ios_app_id)
|
|
for device in RemotePushDeviceToken.objects.filter(kind=PushDeviceToken.APNS)
|
|
},
|
|
)
|
|
self.assertEqual(
|
|
views_logger.output,
|
|
[
|
|
"INFO:zilencer.views:"
|
|
f"Sending mobile push notifications for remote user 6cde5f7a-1f7e-4978-9716-49f69ebfc9fe:<id:{self.user_profile.id}><uuid:{self.user_profile.uuid}>: "
|
|
f"{len(fcm_devices)} via FCM devices, {len(apns_devices)} via APNs devices",
|
|
],
|
|
)
|
|
for token in apns_devices:
|
|
self.assertIn(
|
|
"INFO:zerver.lib.push_notifications:"
|
|
f"APNs: Success sending for user <id:{self.user_profile.id}><uuid:{self.user_profile.uuid}> to device {token}",
|
|
pn_logger.output,
|
|
)
|
|
for idx, token in enumerate(fcm_devices):
|
|
self.assertIn(
|
|
f"INFO:zerver.lib.push_notifications:FCM: Sent message with ID: {idx} to {token}",
|
|
pn_logger.output,
|
|
)
|
|
|
|
remote_realm_count = RealmCount.objects.values("property", "subgroup", "value").last()
|
|
self.assertEqual(
|
|
remote_realm_count,
|
|
dict(
|
|
property="mobile_pushes_sent::day",
|
|
subgroup=None,
|
|
value=len(fcm_devices) + len(apns_devices),
|
|
),
|
|
)
|
|
|
|
self.assertIn(
|
|
"INFO:zerver.lib.push_notifications:"
|
|
f"Skipping E2EE push notifications for user {self.user_profile.id} because there are no registered devices",
|
|
pn_logger.output,
|
|
)
|
|
|
|
@activate_push_notification_service()
|
|
@responses.activate
|
|
def test_end_to_end_failure_due_to_no_plan(self) -> None:
|
|
self.add_mock_response()
|
|
|
|
self.setup_apns_tokens()
|
|
self.setup_fcm_tokens()
|
|
|
|
self.server.last_api_feature_level = 237
|
|
self.server.save()
|
|
|
|
realm = self.user_profile.realm
|
|
realm.push_notifications_enabled = True
|
|
realm.save()
|
|
|
|
message = self.get_message(
|
|
Recipient.PERSONAL,
|
|
type_id=self.personal_recipient_user.id,
|
|
realm_id=self.personal_recipient_user.realm_id,
|
|
)
|
|
UserMessage.objects.create(
|
|
user_profile=self.user_profile,
|
|
message=message,
|
|
)
|
|
|
|
missed_message = {
|
|
"message_id": message.id,
|
|
"trigger": NotificationTriggers.DIRECT_MESSAGE,
|
|
}
|
|
with (
|
|
mock.patch(
|
|
"corporate.lib.stripe.RemoteRealmBillingSession.current_count_for_billed_licenses",
|
|
return_value=100,
|
|
) as mock_current_count,
|
|
self.assertLogs("zerver.lib.push_notifications", level="INFO") as pn_logger,
|
|
self.assertLogs("zilencer.views", level="INFO"),
|
|
):
|
|
handle_push_notification(self.user_profile.id, missed_message)
|
|
|
|
self.assertEqual(
|
|
pn_logger.output,
|
|
[
|
|
f"INFO:zerver.lib.push_notifications:Sending push notifications to mobile clients for user {self.user_profile.id}",
|
|
"WARNING:zerver.lib.push_notifications:Bouncer refused to send push notification: Your plan doesn't allow sending push notifications. Reason provided by the server: Push notifications access with 10+ users requires signing up for a plan. https://zulip.com/plans/",
|
|
f"INFO:zerver.lib.push_notifications:Skipping E2EE push notifications for user {self.user_profile.id} because there are no registered devices",
|
|
],
|
|
)
|
|
realm.refresh_from_db()
|
|
self.assertEqual(realm.push_notifications_enabled, False)
|
|
self.assertEqual(realm.push_notifications_enabled_end_timestamp, None)
|
|
|
|
# Now verify the flag will correctly get flipped back if the server stops
|
|
# rejecting our notification.
|
|
|
|
# This will put us within the allowed number of users to use push notifications
|
|
# for free, so the server will accept our next request.
|
|
mock_current_count.return_value = 5
|
|
|
|
new_message_id = self.send_personal_message(
|
|
self.example_user("othello"), self.user_profile
|
|
)
|
|
new_missed_message = {
|
|
"message_id": new_message_id,
|
|
"trigger": NotificationTriggers.DIRECT_MESSAGE,
|
|
}
|
|
|
|
handle_push_notification(self.user_profile.id, new_missed_message)
|
|
self.assertIn(
|
|
f"Sent mobile push notifications for user {self.user_profile.id}",
|
|
pn_logger.output[-2],
|
|
)
|
|
realm.refresh_from_db()
|
|
self.assertEqual(realm.push_notifications_enabled, True)
|
|
self.assertEqual(realm.push_notifications_enabled_end_timestamp, None)
|
|
|
|
@activate_push_notification_service()
|
|
@responses.activate
|
|
def test_unregistered_client(self) -> None:
|
|
self.add_mock_response()
|
|
self.setup_apns_tokens()
|
|
self.setup_fcm_tokens()
|
|
|
|
message = self.get_message(
|
|
Recipient.PERSONAL,
|
|
type_id=self.personal_recipient_user.id,
|
|
realm_id=self.personal_recipient_user.realm_id,
|
|
)
|
|
UserMessage.objects.create(
|
|
user_profile=self.user_profile,
|
|
message=message,
|
|
)
|
|
|
|
missed_message = {
|
|
"message_id": message.id,
|
|
"trigger": NotificationTriggers.DIRECT_MESSAGE,
|
|
}
|
|
with (
|
|
self.mock_fcm() as (
|
|
_mock_fcm_app,
|
|
mock_fcm_messaging,
|
|
),
|
|
self.mock_apns() as (_apns_context, send_notification),
|
|
mock.patch(
|
|
"corporate.lib.stripe.RemoteRealmBillingSession.current_count_for_billed_licenses",
|
|
return_value=10,
|
|
),
|
|
self.assertLogs("zerver.lib.push_notifications", level="INFO") as pn_logger,
|
|
self.assertLogs("zilencer.views", level="INFO") as views_logger,
|
|
):
|
|
apns_devices = list(
|
|
RemotePushDeviceToken.objects.filter(kind=PushDeviceToken.APNS)
|
|
.order_by("id")
|
|
.values_list("token", flat=True)
|
|
)
|
|
fcm_devices = list(
|
|
RemotePushDeviceToken.objects.filter(kind=PushDeviceToken.FCM)
|
|
.order_by("id")
|
|
.values_list("token", flat=True)
|
|
)
|
|
|
|
# Reset the local registrations for the user to make them compatible
|
|
# with the RemotePushDeviceToken entries.
|
|
PushDeviceToken.objects.filter(kind=PushDeviceToken.APNS).delete()
|
|
[
|
|
PushDeviceToken.objects.create(
|
|
kind=PushDeviceToken.APNS,
|
|
token=device.token,
|
|
user=self.user_profile,
|
|
ios_app_id=device.ios_app_id,
|
|
)
|
|
for device in RemotePushDeviceToken.objects.filter(kind=PushDeviceToken.APNS)
|
|
]
|
|
PushDeviceToken.objects.filter(kind=PushDeviceToken.FCM).delete()
|
|
[
|
|
PushDeviceToken.objects.create(
|
|
kind=PushDeviceToken.FCM,
|
|
token=device.token,
|
|
user=self.user_profile,
|
|
ios_app_id=device.ios_app_id,
|
|
)
|
|
for device in RemotePushDeviceToken.objects.filter(kind=PushDeviceToken.FCM)
|
|
]
|
|
|
|
mock_fcm_messaging.send_each.return_value = self.make_fcm_success_response(
|
|
[fcm_devices[0]]
|
|
)
|
|
send_notification.return_value.is_successful = False
|
|
send_notification.return_value.description = "Unregistered"
|
|
|
|
# Ensure the setup is as expected:
|
|
self.assertNotEqual(
|
|
PushDeviceToken.objects.filter(kind=PushDeviceToken.APNS).count(), 0
|
|
)
|
|
handle_push_notification(self.user_profile.id, missed_message)
|
|
self.assertEqual(
|
|
views_logger.output,
|
|
[
|
|
"INFO:zilencer.views:"
|
|
f"Sending mobile push notifications for remote user 6cde5f7a-1f7e-4978-9716-49f69ebfc9fe:<id:{self.user_profile.id}><uuid:{self.user_profile.uuid}>: "
|
|
f"{len(fcm_devices)} via FCM devices, {len(apns_devices)} via APNs devices",
|
|
],
|
|
)
|
|
for token in apns_devices:
|
|
self.assertIn(
|
|
"INFO:zerver.lib.push_notifications:"
|
|
f"APNs: Removing invalid/expired token {token} (Unregistered)",
|
|
pn_logger.output,
|
|
)
|
|
self.assertIn(
|
|
"INFO:zerver.lib.push_notifications:Deleting push tokens based on response from bouncer: "
|
|
f"Android: [], Apple: {sorted(apns_devices)}",
|
|
pn_logger.output,
|
|
)
|
|
self.assertEqual(
|
|
RemotePushDeviceToken.objects.filter(kind=PushDeviceToken.APNS).count(), 0
|
|
)
|
|
# Local registrations have also been deleted:
|
|
self.assertEqual(PushDeviceToken.objects.filter(kind=PushDeviceToken.APNS).count(), 0)
|
|
|
|
@activate_push_notification_service()
|
|
@responses.activate
|
|
def test_connection_error(self) -> None:
|
|
self.setup_apns_tokens()
|
|
self.setup_fcm_tokens()
|
|
|
|
message = self.get_message(
|
|
Recipient.PERSONAL,
|
|
type_id=self.personal_recipient_user.id,
|
|
realm_id=self.personal_recipient_user.realm_id,
|
|
)
|
|
UserMessage.objects.create(
|
|
user_profile=self.user_profile,
|
|
message=message,
|
|
)
|
|
|
|
missed_message = {
|
|
"user_profile_id": self.user_profile.id,
|
|
"message_id": message.id,
|
|
"trigger": NotificationTriggers.DIRECT_MESSAGE,
|
|
}
|
|
assert settings.ZULIP_SERVICES_URL is not None
|
|
URL = settings.ZULIP_SERVICES_URL + "/api/v1/remotes/push/notify"
|
|
responses.add(responses.POST, URL, body=ConnectionError())
|
|
with self.assertRaises(PushNotificationBouncerRetryLaterError):
|
|
handle_push_notification(self.user_profile.id, missed_message)
|
|
|
|
@mock.patch("zerver.lib.push_notifications.push_notifications_configured", return_value=True)
|
|
@override_settings(ZULIP_SERVICE_PUSH_NOTIFICATIONS=False, ZULIP_SERVICES=set())
|
|
def test_read_message(self, mock_push_notifications: mock.MagicMock) -> None:
|
|
self.setup_apns_tokens()
|
|
self.setup_fcm_tokens()
|
|
|
|
user_profile = self.example_user("hamlet")
|
|
message = self.get_message(
|
|
Recipient.PERSONAL,
|
|
type_id=self.personal_recipient_user.id,
|
|
realm_id=self.personal_recipient_user.realm_id,
|
|
)
|
|
|
|
usermessage = UserMessage.objects.create(
|
|
user_profile=user_profile,
|
|
message=message,
|
|
)
|
|
|
|
missed_message = {
|
|
"message_id": message.id,
|
|
"trigger": NotificationTriggers.DIRECT_MESSAGE,
|
|
}
|
|
|
|
# If the message is unread, we should send push notifications.
|
|
with (
|
|
mock.patch(
|
|
"zerver.lib.push_notifications.send_apple_push_notification", return_value=1
|
|
) as mock_send_apple,
|
|
mock.patch(
|
|
"zerver.lib.push_notifications.send_android_push_notification", return_value=1
|
|
) as mock_send_android,
|
|
):
|
|
handle_push_notification(user_profile.id, missed_message)
|
|
mock_send_apple.assert_called_once()
|
|
mock_send_android.assert_called_once()
|
|
|
|
# If the message has been read, don't send push notifications.
|
|
usermessage.flags.read = True
|
|
usermessage.save()
|
|
with (
|
|
mock.patch(
|
|
"zerver.lib.push_notifications.send_apple_push_notification", return_value=1
|
|
) as mock_send_apple,
|
|
mock.patch(
|
|
"zerver.lib.push_notifications.send_android_push_notification", return_value=1
|
|
) as mock_send_android,
|
|
):
|
|
handle_push_notification(user_profile.id, missed_message)
|
|
mock_send_apple.assert_not_called()
|
|
mock_send_android.assert_not_called()
|
|
|
|
def test_deleted_message(self) -> None:
|
|
"""Simulates the race where message is deleted before handling push notifications"""
|
|
user_profile = self.example_user("hamlet")
|
|
message = self.get_message(
|
|
Recipient.PERSONAL,
|
|
type_id=self.personal_recipient_user.id,
|
|
realm_id=self.personal_recipient_user.realm_id,
|
|
)
|
|
UserMessage.objects.create(
|
|
user_profile=user_profile,
|
|
flags=UserMessage.flags.read,
|
|
message=message,
|
|
)
|
|
missed_message = {
|
|
"message_id": message.id,
|
|
"trigger": NotificationTriggers.DIRECT_MESSAGE,
|
|
}
|
|
# Now, delete the message the normal way
|
|
do_delete_messages(user_profile.realm, [message], acting_user=None)
|
|
|
|
# This mock.patch() should be assertNoLogs once that feature
|
|
# is added to Python.
|
|
with (
|
|
mock.patch("zerver.lib.push_notifications.uses_notification_bouncer") as mock_check,
|
|
mock.patch("logging.error") as mock_logging_error,
|
|
mock.patch(
|
|
"zerver.lib.push_notifications.push_notifications_configured", return_value=True
|
|
) as mock_push_notifications,
|
|
):
|
|
handle_push_notification(user_profile.id, missed_message)
|
|
mock_push_notifications.assert_called_once()
|
|
# Check we didn't proceed through and didn't log anything.
|
|
mock_check.assert_not_called()
|
|
mock_logging_error.assert_not_called()
|
|
|
|
def test_missing_message(self) -> None:
|
|
"""Simulates the race where message is missing when handling push notifications"""
|
|
user_profile = self.example_user("hamlet")
|
|
message = self.get_message(
|
|
Recipient.PERSONAL,
|
|
type_id=self.personal_recipient_user.id,
|
|
realm_id=self.personal_recipient_user.realm_id,
|
|
)
|
|
UserMessage.objects.create(
|
|
user_profile=user_profile,
|
|
flags=UserMessage.flags.read,
|
|
message=message,
|
|
)
|
|
missed_message = {
|
|
"message_id": message.id,
|
|
"trigger": NotificationTriggers.DIRECT_MESSAGE,
|
|
}
|
|
# Now delete the message forcefully, so it just doesn't exist.
|
|
message.delete()
|
|
|
|
# This should log an error
|
|
with (
|
|
mock.patch("zerver.lib.push_notifications.uses_notification_bouncer") as mock_check,
|
|
self.assertLogs(level="INFO") as mock_logging_info,
|
|
mock.patch(
|
|
"zerver.lib.push_notifications.push_notifications_configured", return_value=True
|
|
) as mock_push_notifications,
|
|
):
|
|
handle_push_notification(user_profile.id, missed_message)
|
|
mock_push_notifications.assert_called_once()
|
|
# Check we didn't proceed through.
|
|
mock_check.assert_not_called()
|
|
self.assertEqual(
|
|
mock_logging_info.output,
|
|
[
|
|
f"INFO:root:Unexpected message access failure handling push notifications: {user_profile.id} {missed_message['message_id']}"
|
|
],
|
|
)
|
|
|
|
def test_send_notifications_to_bouncer(self) -> None:
|
|
self.setup_apns_tokens()
|
|
self.setup_fcm_tokens()
|
|
|
|
user_profile = self.user_profile
|
|
message = self.get_message(
|
|
Recipient.PERSONAL,
|
|
type_id=self.personal_recipient_user.id,
|
|
realm_id=self.personal_recipient_user.realm_id,
|
|
)
|
|
UserMessage.objects.create(
|
|
user_profile=user_profile,
|
|
message=message,
|
|
)
|
|
|
|
missed_message = {
|
|
"message_id": message.id,
|
|
"trigger": NotificationTriggers.DIRECT_MESSAGE,
|
|
}
|
|
with (
|
|
activate_push_notification_service(),
|
|
mock.patch(
|
|
"zerver.lib.push_notifications.get_message_payload_apns",
|
|
return_value={"apns": True},
|
|
),
|
|
mock.patch(
|
|
"zerver.lib.push_notifications.get_message_payload_gcm",
|
|
return_value=({"gcm": True}, {}),
|
|
),
|
|
mock.patch(
|
|
"zerver.lib.push_notifications.send_json_to_push_bouncer",
|
|
return_value=dict(
|
|
total_android_devices=3,
|
|
total_apple_devices=5,
|
|
deleted_devices=DevicesToCleanUpDict(android_devices=[], apple_devices=[]),
|
|
realm=None,
|
|
),
|
|
) as mock_send,
|
|
self.assertLogs("zerver.lib.push_notifications", level="INFO") as mock_logging_info,
|
|
):
|
|
handle_push_notification(user_profile.id, missed_message)
|
|
mock_send.assert_called_with(
|
|
"POST",
|
|
"push/notify",
|
|
{
|
|
"user_uuid": str(user_profile.uuid),
|
|
"user_id": user_profile.id,
|
|
"realm_uuid": str(user_profile.realm.uuid),
|
|
"apns_payload": {"apns": True},
|
|
"gcm_payload": {"gcm": True},
|
|
"gcm_options": {},
|
|
"android_devices": list(
|
|
PushDeviceToken.objects.filter(user=user_profile, kind=PushDeviceToken.FCM)
|
|
.order_by("id")
|
|
.values_list("token", flat=True)
|
|
),
|
|
"apple_devices": list(
|
|
PushDeviceToken.objects.filter(user=user_profile, kind=PushDeviceToken.APNS)
|
|
.order_by("id")
|
|
.values_list("token", flat=True)
|
|
),
|
|
},
|
|
)
|
|
|
|
self.assertEqual(
|
|
mock_logging_info.output,
|
|
[
|
|
f"INFO:zerver.lib.push_notifications:Sending push notifications to mobile clients for user {user_profile.id}",
|
|
f"INFO:zerver.lib.push_notifications:Sent mobile push notifications for user {user_profile.id} through bouncer: 3 via FCM devices, 5 via APNs devices",
|
|
f"INFO:zerver.lib.push_notifications:Skipping E2EE push notifications for user {self.user_profile.id} because there are no registered devices",
|
|
],
|
|
)
|
|
|
|
def test_non_bouncer_push(self) -> None:
|
|
self.setup_apns_tokens()
|
|
self.setup_fcm_tokens()
|
|
message = self.get_message(
|
|
Recipient.PERSONAL,
|
|
type_id=self.personal_recipient_user.id,
|
|
realm_id=self.personal_recipient_user.realm_id,
|
|
)
|
|
UserMessage.objects.create(
|
|
user_profile=self.user_profile,
|
|
message=message,
|
|
)
|
|
|
|
android_devices = list(
|
|
PushDeviceToken.objects.filter(user=self.user_profile, kind=PushDeviceToken.FCM)
|
|
)
|
|
|
|
apple_devices = list(
|
|
PushDeviceToken.objects.filter(user=self.user_profile, kind=PushDeviceToken.APNS)
|
|
)
|
|
|
|
missed_message = {
|
|
"message_id": message.id,
|
|
"trigger": NotificationTriggers.DIRECT_MESSAGE,
|
|
}
|
|
with (
|
|
mock.patch(
|
|
"zerver.lib.push_notifications.get_message_payload_apns",
|
|
return_value={"apns": True},
|
|
),
|
|
mock.patch(
|
|
"zerver.lib.push_notifications.get_message_payload_gcm",
|
|
return_value=({"gcm": True}, {}),
|
|
),
|
|
mock.patch(
|
|
# Simulate the send...push_notification functions returning a number of successes
|
|
# lesser than the number of devices, so that we can verify correct CountStat counting.
|
|
"zerver.lib.push_notifications.send_apple_push_notification",
|
|
return_value=len(apple_devices) - 1,
|
|
) as mock_send_apple,
|
|
mock.patch(
|
|
"zerver.lib.push_notifications.send_android_push_notification",
|
|
return_value=len(android_devices) - 1,
|
|
) as mock_send_android,
|
|
mock.patch(
|
|
"zerver.lib.push_notifications.push_notifications_configured", return_value=True
|
|
) as mock_push_notifications,
|
|
):
|
|
handle_push_notification(self.user_profile.id, missed_message)
|
|
user_identity = UserPushIdentityCompat(user_id=self.user_profile.id)
|
|
mock_send_apple.assert_called_with(user_identity, apple_devices, {"apns": True})
|
|
mock_send_android.assert_called_with(user_identity, android_devices, {"gcm": True}, {})
|
|
mock_push_notifications.assert_called_once()
|
|
|
|
remote_realm_count = RealmCount.objects.values("property", "subgroup", "value").last()
|
|
self.assertEqual(
|
|
remote_realm_count,
|
|
dict(
|
|
property="mobile_pushes_sent::day",
|
|
subgroup=None,
|
|
value=len(android_devices) + len(apple_devices) - 2,
|
|
),
|
|
)
|
|
|
|
def test_send_remove_notifications_to_bouncer(self) -> None:
|
|
self.setup_apns_tokens()
|
|
self.setup_fcm_tokens()
|
|
|
|
user_profile = self.user_profile
|
|
message = self.get_message(
|
|
Recipient.PERSONAL,
|
|
type_id=self.personal_recipient_user.id,
|
|
realm_id=self.personal_recipient_user.realm_id,
|
|
)
|
|
UserMessage.objects.create(
|
|
user_profile=user_profile,
|
|
message=message,
|
|
flags=UserMessage.flags.active_mobile_push_notification,
|
|
)
|
|
|
|
with (
|
|
activate_push_notification_service(),
|
|
mock.patch("zerver.lib.push_notifications.send_notifications_to_bouncer") as mock_send,
|
|
):
|
|
handle_remove_push_notification(user_profile.id, [message.id])
|
|
mock_send.assert_called_with(
|
|
user_profile,
|
|
{
|
|
"badge": 0,
|
|
"custom": {
|
|
"zulip": {
|
|
"realm_name": self.sender.realm.name,
|
|
"realm_uri": "http://zulip.testserver",
|
|
"realm_url": "http://zulip.testserver",
|
|
"user_id": self.user_profile.id,
|
|
"event": "remove",
|
|
"zulip_message_ids": str(message.id),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"server": "testserver",
|
|
"realm_id": self.sender.realm.id,
|
|
"realm_name": self.sender.realm.name,
|
|
"realm_uri": "http://zulip.testserver",
|
|
"realm_url": "http://zulip.testserver",
|
|
"user_id": self.user_profile.id,
|
|
"event": "remove",
|
|
"zulip_message_ids": str(message.id),
|
|
},
|
|
{"priority": "normal"},
|
|
list(
|
|
PushDeviceToken.objects.filter(
|
|
user=user_profile, kind=PushDeviceToken.FCM
|
|
).order_by("id")
|
|
),
|
|
list(
|
|
PushDeviceToken.objects.filter(
|
|
user=user_profile, kind=PushDeviceToken.APNS
|
|
).order_by("id")
|
|
),
|
|
)
|
|
user_message = UserMessage.objects.get(user_profile=self.user_profile, message=message)
|
|
self.assertEqual(user_message.flags.active_mobile_push_notification, False)
|
|
|
|
def test_non_bouncer_push_remove(self) -> None:
|
|
self.setup_apns_tokens()
|
|
self.setup_fcm_tokens()
|
|
message = self.get_message(
|
|
Recipient.PERSONAL,
|
|
type_id=self.personal_recipient_user.id,
|
|
realm_id=self.personal_recipient_user.realm_id,
|
|
)
|
|
UserMessage.objects.create(
|
|
user_profile=self.user_profile,
|
|
message=message,
|
|
flags=UserMessage.flags.active_mobile_push_notification,
|
|
)
|
|
|
|
android_devices = list(
|
|
PushDeviceToken.objects.filter(user=self.user_profile, kind=PushDeviceToken.FCM)
|
|
)
|
|
|
|
apple_devices = list(
|
|
PushDeviceToken.objects.filter(user=self.user_profile, kind=PushDeviceToken.APNS)
|
|
)
|
|
|
|
with (
|
|
mock.patch(
|
|
"zerver.lib.push_notifications.push_notifications_configured", return_value=True
|
|
) as mock_push_notifications,
|
|
mock.patch(
|
|
# Simulate the send...push_notification functions returning a number of successes
|
|
# lesser than the number of devices, so that we can verify correct CountStat counting.
|
|
"zerver.lib.push_notifications.send_android_push_notification",
|
|
return_value=len(apple_devices) - 1,
|
|
) as mock_send_android,
|
|
mock.patch(
|
|
"zerver.lib.push_notifications.send_apple_push_notification",
|
|
return_value=len(apple_devices) - 1,
|
|
) as mock_send_apple,
|
|
):
|
|
handle_remove_push_notification(self.user_profile.id, [message.id])
|
|
mock_push_notifications.assert_called_once()
|
|
user_identity = UserPushIdentityCompat(user_id=self.user_profile.id)
|
|
mock_send_android.assert_called_with(
|
|
user_identity,
|
|
android_devices,
|
|
{
|
|
"server": "testserver",
|
|
"realm_id": self.sender.realm.id,
|
|
"realm_name": self.sender.realm.name,
|
|
"realm_uri": "http://zulip.testserver",
|
|
"realm_url": "http://zulip.testserver",
|
|
"user_id": self.user_profile.id,
|
|
"event": "remove",
|
|
"zulip_message_ids": str(message.id),
|
|
},
|
|
{"priority": "normal"},
|
|
)
|
|
mock_send_apple.assert_called_with(
|
|
user_identity,
|
|
apple_devices,
|
|
{
|
|
"badge": 0,
|
|
"custom": {
|
|
"zulip": {
|
|
"realm_name": self.sender.realm.name,
|
|
"realm_uri": "http://zulip.testserver",
|
|
"realm_url": "http://zulip.testserver",
|
|
"user_id": self.user_profile.id,
|
|
"event": "remove",
|
|
"zulip_message_ids": str(message.id),
|
|
}
|
|
},
|
|
},
|
|
)
|
|
user_message = UserMessage.objects.get(user_profile=self.user_profile, message=message)
|
|
self.assertEqual(user_message.flags.active_mobile_push_notification, False)
|
|
|
|
remote_realm_count = RealmCount.objects.values("property", "subgroup", "value").last()
|
|
self.assertEqual(
|
|
remote_realm_count,
|
|
dict(
|
|
property="mobile_pushes_sent::day",
|
|
subgroup=None,
|
|
value=len(android_devices) + len(apple_devices) - 2,
|
|
),
|
|
)
|
|
|
|
def test_user_message_does_not_exist(self) -> None:
|
|
"""This simulates a condition that should only be an error if the user is
|
|
not long-term idle; we fake it, though, in the sense that the user should
|
|
not have received the message in the first place"""
|
|
self.make_stream("public_stream")
|
|
sender = self.example_user("iago")
|
|
self.subscribe(sender, "public_stream")
|
|
message_id = self.send_stream_message(sender, "public_stream", "test")
|
|
missed_message = {"message_id": message_id}
|
|
with (
|
|
self.assertLogs("zerver.lib.push_notifications", level="ERROR") as logger,
|
|
mock.patch(
|
|
"zerver.lib.push_notifications.push_notifications_configured", return_value=True
|
|
) as mock_push_notifications,
|
|
):
|
|
handle_push_notification(self.user_profile.id, missed_message)
|
|
self.assertEqual(
|
|
"ERROR:zerver.lib.push_notifications:"
|
|
f"Could not find UserMessage with message_id {message_id} and user_id {self.user_profile.id}",
|
|
logger.output[0],
|
|
)
|
|
mock_push_notifications.assert_called_once()
|
|
|
|
def test_user_message_does_not_exist_remove(self) -> None:
|
|
"""This simulates a condition that should only be an error if the user is
|
|
not long-term idle; we fake it, though, in the sense that the user should
|
|
not have received the message in the first place"""
|
|
self.setup_apns_tokens()
|
|
self.setup_fcm_tokens()
|
|
self.make_stream("public_stream")
|
|
sender = self.example_user("iago")
|
|
self.subscribe(sender, "public_stream")
|
|
message_id = self.send_stream_message(sender, "public_stream", "test")
|
|
with (
|
|
mock.patch(
|
|
"zerver.lib.push_notifications.push_notifications_configured", return_value=True
|
|
) as mock_push_notifications,
|
|
mock.patch(
|
|
"zerver.lib.push_notifications.send_android_push_notification", return_value=1
|
|
) as mock_send_android,
|
|
mock.patch(
|
|
"zerver.lib.push_notifications.send_apple_push_notification", return_value=1
|
|
) as mock_send_apple,
|
|
):
|
|
handle_remove_push_notification(self.user_profile.id, [message_id])
|
|
mock_push_notifications.assert_called_once()
|
|
mock_send_android.assert_called_once()
|
|
mock_send_apple.assert_called_once()
|
|
|
|
def test_user_message_soft_deactivated(self) -> None:
|
|
"""This simulates a condition that should only be an error if the user is
|
|
not long-term idle; we fake it, though, in the sense that the user should
|
|
not have received the message in the first place"""
|
|
self.setup_apns_tokens()
|
|
self.setup_fcm_tokens()
|
|
self.make_stream("public_stream")
|
|
sender = self.example_user("iago")
|
|
self.subscribe(self.user_profile, "public_stream")
|
|
self.subscribe(sender, "public_stream")
|
|
logger_string = "zulip.soft_deactivation"
|
|
with self.assertLogs(logger_string, level="INFO") as info_logs:
|
|
self.soft_deactivate_main_user()
|
|
|
|
self.assertEqual(
|
|
info_logs.output,
|
|
[
|
|
f"INFO:{logger_string}:Soft deactivated user {self.user_profile.id}",
|
|
f"INFO:{logger_string}:Soft-deactivated batch of 1 users; 0 remain to process",
|
|
],
|
|
)
|
|
message_id = self.send_stream_message(sender, "public_stream", "test")
|
|
missed_message = {
|
|
"message_id": message_id,
|
|
"trigger": NotificationTriggers.STREAM_PUSH,
|
|
}
|
|
|
|
android_devices = list(
|
|
PushDeviceToken.objects.filter(user=self.user_profile, kind=PushDeviceToken.FCM)
|
|
)
|
|
|
|
apple_devices = list(
|
|
PushDeviceToken.objects.filter(user=self.user_profile, kind=PushDeviceToken.APNS)
|
|
)
|
|
|
|
with (
|
|
mock.patch(
|
|
"zerver.lib.push_notifications.get_message_payload_apns",
|
|
return_value={"apns": True},
|
|
),
|
|
mock.patch(
|
|
"zerver.lib.push_notifications.get_message_payload_gcm",
|
|
return_value=({"gcm": True}, {}),
|
|
),
|
|
mock.patch(
|
|
"zerver.lib.push_notifications.send_apple_push_notification", return_value=1
|
|
) as mock_send_apple,
|
|
mock.patch(
|
|
"zerver.lib.push_notifications.send_android_push_notification", return_value=1
|
|
) as mock_send_android,
|
|
mock.patch("zerver.lib.push_notifications.logger.error") as mock_logger,
|
|
mock.patch(
|
|
"zerver.lib.push_notifications.push_notifications_configured", return_value=True
|
|
) as mock_push_notifications,
|
|
):
|
|
handle_push_notification(self.user_profile.id, missed_message)
|
|
mock_logger.assert_not_called()
|
|
user_identity = UserPushIdentityCompat(user_id=self.user_profile.id)
|
|
mock_send_apple.assert_called_with(user_identity, apple_devices, {"apns": True})
|
|
mock_send_android.assert_called_with(user_identity, android_devices, {"gcm": True}, {})
|
|
mock_push_notifications.assert_called_once()
|
|
|
|
@override_settings(MAX_GROUP_SIZE_FOR_MENTION_REACTIVATION=2)
|
|
@mock.patch("zerver.lib.push_notifications.push_notifications_configured", return_value=True)
|
|
def test_user_push_soft_reactivate_soft_deactivated_user(
|
|
self, mock_push_notifications: mock.MagicMock
|
|
) -> None:
|
|
othello = self.example_user("othello")
|
|
cordelia = self.example_user("cordelia")
|
|
zulip_realm = get_realm("zulip")
|
|
self.register_push_device_token(self.user_profile.id)
|
|
|
|
# user groups having upto 'MAX_GROUP_SIZE_FOR_MENTION_REACTIVATION'
|
|
# members are small user groups.
|
|
small_user_group = check_add_user_group(
|
|
zulip_realm,
|
|
"small_user_group",
|
|
[self.user_profile, othello],
|
|
acting_user=othello,
|
|
)
|
|
|
|
large_user_group = check_add_user_group(
|
|
zulip_realm, "large_user_group", [self.user_profile], acting_user=othello
|
|
)
|
|
subgroup = check_add_user_group(
|
|
zulip_realm, "subgroup", [othello, cordelia], acting_user=othello
|
|
)
|
|
add_subgroups_to_user_group(large_user_group, [subgroup], acting_user=None)
|
|
|
|
# Personal mention in a stream message should soft reactivate the user
|
|
def mention_in_stream() -> None:
|
|
mention = f"@**{self.user_profile.full_name}**"
|
|
stream_mentioned_message_id = self.send_stream_message(othello, "Denmark", mention)
|
|
handle_push_notification(
|
|
self.user_profile.id,
|
|
{
|
|
"message_id": stream_mentioned_message_id,
|
|
"trigger": NotificationTriggers.MENTION,
|
|
},
|
|
)
|
|
|
|
self.soft_deactivate_main_user()
|
|
self.expect_soft_reactivation(self.user_profile, mention_in_stream)
|
|
|
|
# Direct message should soft reactivate the user
|
|
def direct_message() -> None:
|
|
# Soft reactivate the user by sending a personal message
|
|
personal_message_id = self.send_personal_message(othello, self.user_profile, "Message")
|
|
handle_push_notification(
|
|
self.user_profile.id,
|
|
{
|
|
"message_id": personal_message_id,
|
|
"trigger": NotificationTriggers.DIRECT_MESSAGE,
|
|
},
|
|
)
|
|
|
|
self.soft_deactivate_main_user()
|
|
self.expect_soft_reactivation(self.user_profile, direct_message)
|
|
|
|
# User FOLLOWS the topic.
|
|
# 'wildcard_mentions_notify' is disabled to verify the corner case when only
|
|
# 'enable_followed_topic_wildcard_mentions_notify' is enabled (True by default).
|
|
do_set_user_topic_visibility_policy(
|
|
self.user_profile,
|
|
get_stream("Denmark", self.user_profile.realm),
|
|
"test",
|
|
visibility_policy=UserTopic.VisibilityPolicy.FOLLOWED,
|
|
)
|
|
do_change_user_setting(
|
|
self.user_profile, "wildcard_mentions_notify", False, acting_user=None
|
|
)
|
|
|
|
# Topic wildcard mention in followed topic should soft reactivate the user
|
|
# user should be a topic participant
|
|
self.send_stream_message(self.user_profile, "Denmark", "topic participant")
|
|
|
|
def send_topic_wildcard_mention() -> None:
|
|
mention = "@**topic**"
|
|
stream_mentioned_message_id = self.send_stream_message(othello, "Denmark", mention)
|
|
handle_push_notification(
|
|
self.user_profile.id,
|
|
{
|
|
"message_id": stream_mentioned_message_id,
|
|
"trigger": NotificationTriggers.TOPIC_WILDCARD_MENTION_IN_FOLLOWED_TOPIC,
|
|
},
|
|
)
|
|
|
|
self.soft_deactivate_main_user()
|
|
self.expect_soft_reactivation(self.user_profile, send_topic_wildcard_mention)
|
|
|
|
# Stream wildcard mention in followed topic should NOT soft reactivate the user
|
|
def send_stream_wildcard_mention() -> None:
|
|
mention = "@**all**"
|
|
stream_mentioned_message_id = self.send_stream_message(othello, "Denmark", mention)
|
|
handle_push_notification(
|
|
self.user_profile.id,
|
|
{
|
|
"message_id": stream_mentioned_message_id,
|
|
"trigger": NotificationTriggers.STREAM_WILDCARD_MENTION_IN_FOLLOWED_TOPIC,
|
|
},
|
|
)
|
|
|
|
self.soft_deactivate_main_user()
|
|
self.expect_to_stay_long_term_idle(self.user_profile, send_stream_wildcard_mention)
|
|
|
|
# Reset
|
|
do_set_user_topic_visibility_policy(
|
|
self.user_profile,
|
|
get_stream("Denmark", self.user_profile.realm),
|
|
"test",
|
|
visibility_policy=UserTopic.VisibilityPolicy.INHERIT,
|
|
)
|
|
do_change_user_setting(
|
|
self.user_profile, "wildcard_mentions_notify", True, acting_user=None
|
|
)
|
|
|
|
# Topic Wildcard mention should soft reactivate the user
|
|
self.expect_soft_reactivation(self.user_profile, send_topic_wildcard_mention)
|
|
|
|
# Stream Wildcard mention should NOT soft reactivate the user
|
|
self.soft_deactivate_main_user()
|
|
self.expect_to_stay_long_term_idle(self.user_profile, send_stream_wildcard_mention)
|
|
|
|
# Small group mention should soft reactivate the user
|
|
def send_small_group_mention() -> None:
|
|
mention = "@*small_user_group*"
|
|
stream_mentioned_message_id = self.send_stream_message(othello, "Denmark", mention)
|
|
handle_push_notification(
|
|
self.user_profile.id,
|
|
{
|
|
"message_id": stream_mentioned_message_id,
|
|
"trigger": NotificationTriggers.MENTION,
|
|
"mentioned_user_group_id": small_user_group.id,
|
|
},
|
|
)
|
|
|
|
self.soft_deactivate_main_user()
|
|
self.expect_soft_reactivation(self.user_profile, send_small_group_mention)
|
|
|
|
# Large group mention should NOT soft reactivate the user
|
|
def send_large_group_mention() -> None:
|
|
mention = "@*large_user_group*"
|
|
stream_mentioned_message_id = self.send_stream_message(othello, "Denmark", mention)
|
|
handle_push_notification(
|
|
self.user_profile.id,
|
|
{
|
|
"message_id": stream_mentioned_message_id,
|
|
"trigger": NotificationTriggers.MENTION,
|
|
"mentioned_user_group_id": large_user_group.id,
|
|
},
|
|
)
|
|
|
|
self.soft_deactivate_main_user()
|
|
self.expect_to_stay_long_term_idle(self.user_profile, send_large_group_mention)
|
|
|
|
@mock.patch("zerver.lib.push_notifications.logger.info")
|
|
@mock.patch("zerver.lib.push_notifications.push_notifications_configured", return_value=True)
|
|
def test_user_push_notification_already_active(
|
|
self, mock_push_notifications: mock.MagicMock, mock_info: mock.MagicMock
|
|
) -> None:
|
|
user_profile = self.example_user("hamlet")
|
|
message = self.get_message(
|
|
Recipient.PERSONAL,
|
|
type_id=self.personal_recipient_user.id,
|
|
realm_id=self.personal_recipient_user.realm_id,
|
|
)
|
|
UserMessage.objects.create(
|
|
user_profile=user_profile,
|
|
flags=UserMessage.flags.active_mobile_push_notification,
|
|
message=message,
|
|
)
|
|
|
|
missed_message = {
|
|
"message_id": message.id,
|
|
"trigger": NotificationTriggers.DIRECT_MESSAGE,
|
|
}
|
|
handle_push_notification(user_profile.id, missed_message)
|
|
mock_push_notifications.assert_called_once()
|
|
# Check we didn't proceed ahead and function returned.
|
|
mock_info.assert_not_called()
|