Files
zulip/zerver/tests/test_handle_push_notification.py
Anders Kaseorg f24a0a6b81 ruff: Fix RUF059 Unpacked variable is never used.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2025-09-30 16:47:54 -07:00

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()