mirror of
https://github.com/zulip/zulip.git
synced 2025-10-24 08:33:43 +00:00
push_notification: Add support to send E2EE test push notification.
This commit adds an endpoint `/mobile_push/e2ee/test_notification` to send an end-to-end encrypted test push notification to the user's selected mobile device or all of their mobile devices.
This commit is contained in:
committed by
Tim Abbott
parent
f034a6c3b4
commit
3cbf0e70a2
@@ -20,6 +20,12 @@ format used by the Zulip server that they are interacting with.
|
||||
|
||||
## Changes in Zulip 11.0
|
||||
|
||||
**Feature level 420**
|
||||
|
||||
* [`POST /mobile_push/e2ee/test_notification`](/api/e2ee-test-notify):
|
||||
Added a new endpoint to send an end-to-end encrypted test push notification
|
||||
to the user's selected mobile device or all of their mobile devices.
|
||||
|
||||
**Feature level 419**
|
||||
|
||||
* [`POST /register`](/api/register-queue): Added `simplified_presence_events`
|
||||
|
||||
@@ -157,9 +157,10 @@
|
||||
|
||||
* [Fetch an API key (production)](/api/fetch-api-key)
|
||||
* [Fetch an API key (development only)](/api/dev-fetch-api-key)
|
||||
* [Send a test notification to mobile device(s)](/api/test-notify)
|
||||
* [Send an E2EE test notification to mobile device(s)](/api/e2ee-test-notify)
|
||||
* [Register E2EE push device](/api/register-push-device)
|
||||
* [Mobile notifications](/api/mobile-notifications)
|
||||
* [Send a test notification to mobile device(s)](/api/test-notify)
|
||||
* [Add an APNs device token](/api/add-apns-token)
|
||||
* [Remove an APNs device token](/api/remove-apns-token)
|
||||
* [Add an FCM registration token](/api/add-fcm-token)
|
||||
|
||||
@@ -59,7 +59,7 @@ Sample JSON data that gets encrypted:
|
||||
{
|
||||
"content": "test content",
|
||||
"message_id": 46,
|
||||
"pm_users": "6,10,12,15"
|
||||
"pm_users": "6,10,12,15",
|
||||
"realm_name": "Zulip Dev",
|
||||
"realm_url": "http://zulip.testserver",
|
||||
"recipient_type": "direct",
|
||||
@@ -106,6 +106,24 @@ Sample JSON data that gets encrypted:
|
||||
|
||||
**Changes**: New in Zulip 11.0 (feature level 413).
|
||||
|
||||
### Test push notification
|
||||
|
||||
A user can trigger [sending an E2EE test push notification](/api/e2ee-test-notify)
|
||||
to the user's selected mobile device or all of their mobile devices.
|
||||
|
||||
Sample JSON data that gets encrypted:
|
||||
```json
|
||||
{
|
||||
"realm_name": "Zulip Dev",
|
||||
"realm_url": "http://zulip.testserver",
|
||||
"time": 1754577820,
|
||||
"type": "test",
|
||||
"user_id": 10
|
||||
}
|
||||
```
|
||||
|
||||
**Changes**: New in Zulip 11.0 (feature level 420).
|
||||
|
||||
## Future work
|
||||
|
||||
This page will eventually also document the formats of the APNs and
|
||||
|
||||
@@ -34,7 +34,7 @@ DESKTOP_WARNING_VERSION = "5.9.3"
|
||||
# new level means in api_docs/changelog.md, as well as "**Changes**"
|
||||
# entries in the endpoint's documentation in `zulip.yaml`.
|
||||
|
||||
API_FEATURE_LEVEL = 419
|
||||
API_FEATURE_LEVEL = 420
|
||||
|
||||
# Bump the minor PROVISION_VERSION to indicate that folks should provision
|
||||
# only when going from an old version of the code to a newer version. Bump
|
||||
|
||||
@@ -63,6 +63,10 @@ class ErrorCode(Enum):
|
||||
INVALID_BOUNCER_PUBLIC_KEY = auto()
|
||||
REQUEST_EXPIRED = auto()
|
||||
PUSH_SERVICE_NOT_CONFIGURED = auto()
|
||||
NO_ACTIVE_PUSH_DEVICE = auto()
|
||||
FAILED_TO_CONNECT_BOUNCER = auto()
|
||||
INTERNAL_SERVER_ERROR_ON_BOUNCER = auto()
|
||||
ADMIN_ACTION_REQUIRED = auto()
|
||||
|
||||
|
||||
class JsonableError(Exception):
|
||||
|
||||
@@ -15,7 +15,7 @@ import orjson
|
||||
from aioapns.common import NotificationResult, PushType
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.db.models import F, Q
|
||||
from django.db.models import F, Q, QuerySet
|
||||
from django.db.models.functions import Lower
|
||||
from django.utils.timezone import now as timezone_now
|
||||
from django.utils.translation import gettext as _
|
||||
@@ -42,6 +42,9 @@ from zerver.lib.exceptions import ErrorCode, JsonableError, MissingRemoteRealmEr
|
||||
from zerver.lib.message import access_message_and_usermessage, direct_message_group_users
|
||||
from zerver.lib.notification_data import get_mentioned_user_group
|
||||
from zerver.lib.remote_server import (
|
||||
PushNotificationBouncerError,
|
||||
PushNotificationBouncerRetryLaterError,
|
||||
PushNotificationBouncerServerError,
|
||||
record_push_notifications_recently_working,
|
||||
send_json_to_push_bouncer,
|
||||
send_server_data_to_push_bouncer,
|
||||
@@ -1463,9 +1466,14 @@ def get_encrypted_data(payload_data_to_encrypt: dict[str, Any], public_key_str:
|
||||
def send_push_notifications(
|
||||
user_profile: UserProfile,
|
||||
payload_data_to_encrypt: dict[str, Any],
|
||||
test_notification_to_push_devices: QuerySet[PushDevice] | None = None,
|
||||
) -> None:
|
||||
# Uses 'zerver_pushdevice_user_bouncer_device_id_idx' index.
|
||||
push_devices = PushDevice.objects.filter(user=user_profile, bouncer_device_id__isnull=False)
|
||||
if test_notification_to_push_devices is not None:
|
||||
assert len(test_notification_to_push_devices) != 0
|
||||
push_devices = test_notification_to_push_devices
|
||||
else:
|
||||
# Uses 'zerver_pushdevice_user_bouncer_device_id_idx' index.
|
||||
push_devices = PushDevice.objects.filter(user=user_profile, bouncer_device_id__isnull=False)
|
||||
|
||||
if len(push_devices) == 0:
|
||||
logger.info(
|
||||
@@ -1475,6 +1483,7 @@ def send_push_notifications(
|
||||
return
|
||||
|
||||
is_removal = payload_data_to_encrypt["type"] == "remove"
|
||||
is_test_notification = payload_data_to_encrypt["type"] == "test"
|
||||
|
||||
# Note: The "Final" qualifier serves as a shorthand
|
||||
# for declaring that a variable is effectively Literal.
|
||||
@@ -1549,6 +1558,12 @@ def send_push_notifications(
|
||||
acting_user=None,
|
||||
)
|
||||
do_set_push_notifications_enabled_end_timestamp(user_profile.realm, None, acting_user=None)
|
||||
|
||||
if is_test_notification:
|
||||
# Propagate the exception to the caller to notify the client
|
||||
# about the error while attempting to send test push notification.
|
||||
raise e
|
||||
|
||||
return
|
||||
|
||||
# Handle success response data
|
||||
@@ -1597,6 +1612,12 @@ def send_push_notifications(
|
||||
if can_push:
|
||||
record_push_notifications_recently_working()
|
||||
|
||||
if is_test_notification and len(push_requests) == len(delete_device_ids):
|
||||
# While sending test push notification, the bouncer reported
|
||||
# that there's no active registered push device. Inform the
|
||||
# same to the client.
|
||||
raise NoActivePushDeviceError
|
||||
|
||||
|
||||
def handle_push_notification(user_profile_id: int, missed_message: dict[str, Any]) -> None:
|
||||
"""
|
||||
@@ -1797,6 +1818,39 @@ def send_test_push_notification(user_profile: UserProfile, devices: list[PushDev
|
||||
)
|
||||
|
||||
|
||||
def send_e2ee_test_push_notification(
|
||||
user_profile: UserProfile, push_devices: QuerySet[PushDevice]
|
||||
) -> None:
|
||||
payload_data_to_encrypt = get_base_payload(user_profile, for_legacy_clients=False)
|
||||
payload_data_to_encrypt["type"] = "test"
|
||||
payload_data_to_encrypt["time"] = datetime_to_timestamp(timezone_now())
|
||||
|
||||
logger.info("Sending E2EE test push notification for user %s", user_profile.id)
|
||||
|
||||
try:
|
||||
send_push_notifications(
|
||||
user_profile, payload_data_to_encrypt, test_notification_to_push_devices=push_devices
|
||||
)
|
||||
except PushNotificationBouncerServerError:
|
||||
# 5xx error response from bouncer server
|
||||
raise InternalBouncerServerError
|
||||
except PushNotificationBouncerRetryLaterError:
|
||||
# Network error
|
||||
raise FailedToConnectBouncerError
|
||||
except (
|
||||
# Need to resubmit realm info - `manage.py register_server`
|
||||
MissingRemoteRealmError,
|
||||
# Invalid credentials or unexpected status code
|
||||
PushNotificationBouncerError,
|
||||
# Plan doesn't allow sending push notifications
|
||||
PushNotificationsDisallowedByBouncerError,
|
||||
):
|
||||
# Server admins need to fix these set of errors, report them.
|
||||
error_msg = f"Sending E2EE test push notification for user_id={user_profile.id} failed."
|
||||
logger.error(error_msg)
|
||||
raise PushNotificationAdminActionRequiredError
|
||||
|
||||
|
||||
class InvalidPushDeviceTokenError(JsonableError):
|
||||
code = ErrorCode.INVALID_PUSH_DEVICE_TOKEN
|
||||
|
||||
@@ -1858,3 +1912,56 @@ def get_push_devices(user_profile: UserProfile) -> dict[str, PushDeviceInfoDict]
|
||||
str(row.push_account_id): {"status": row.status, "error_code": row.error_code}
|
||||
for row in rows
|
||||
}
|
||||
|
||||
|
||||
class NoActivePushDeviceError(JsonableError):
|
||||
code = ErrorCode.NO_ACTIVE_PUSH_DEVICE
|
||||
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
@override
|
||||
def msg_format() -> str:
|
||||
return _("No active registered push device")
|
||||
|
||||
|
||||
class FailedToConnectBouncerError(JsonableError):
|
||||
http_status_code = 502
|
||||
code = ErrorCode.FAILED_TO_CONNECT_BOUNCER
|
||||
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
@override
|
||||
def msg_format() -> str:
|
||||
return _("Network error while connecting to Zulip push notification service.")
|
||||
|
||||
|
||||
class InternalBouncerServerError(JsonableError):
|
||||
http_status_code = 502
|
||||
code = ErrorCode.INTERNAL_SERVER_ERROR_ON_BOUNCER
|
||||
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
@override
|
||||
def msg_format() -> str:
|
||||
return _("Internal server error on Zulip push notification service, retry later.")
|
||||
|
||||
|
||||
class PushNotificationAdminActionRequiredError(JsonableError):
|
||||
http_status_code = 403
|
||||
code = ErrorCode.ADMIN_ACTION_REQUIRED
|
||||
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
@override
|
||||
def msg_format() -> str:
|
||||
return _(
|
||||
"Push notification configuration issue on server, contact the server administrator or retry later."
|
||||
)
|
||||
|
||||
@@ -32,6 +32,7 @@ from zerver.lib.debug import maybe_tracemalloc_listen
|
||||
from zerver.lib.exceptions import ErrorCode, JsonableError, MissingAuthenticationError, WebhookError
|
||||
from zerver.lib.markdown import get_markdown_requests, get_markdown_time
|
||||
from zerver.lib.per_request_cache import flush_per_request_caches
|
||||
from zerver.lib.push_notifications import FailedToConnectBouncerError, InternalBouncerServerError
|
||||
from zerver.lib.rate_limiter import RateLimitResult
|
||||
from zerver.lib.request import RequestNotes
|
||||
from zerver.lib.response import (
|
||||
@@ -397,10 +398,16 @@ class JsonErrorHandler(MiddlewareMixin):
|
||||
|
||||
if isinstance(exception, JsonableError):
|
||||
response = json_response_from_error(exception)
|
||||
if response.status_code < 500 or isinstance(exception, WebhookError):
|
||||
if response.status_code < 500 or isinstance(
|
||||
exception, (FailedToConnectBouncerError | InternalBouncerServerError | WebhookError)
|
||||
):
|
||||
# Webhook errors are handled in
|
||||
# authenticated_rest_api_view / webhook_view, so we
|
||||
# just return the response without logging further.
|
||||
#
|
||||
# Return the response when `FailedToConnectBouncerError` or
|
||||
# `InternalBouncerServerError` raised (status code 502) as
|
||||
# it helps the client to show the user a more accurate error message.
|
||||
return response
|
||||
elif RequestNotes.get_notes(request).error_format == "JSON" and not settings.TEST_SUITE:
|
||||
response = json_response(
|
||||
|
||||
@@ -119,15 +119,15 @@ class PushDevice(AbstractPushDevice):
|
||||
# We treat (user, push_account_id) as a unique registration.
|
||||
#
|
||||
# Also, the unique index created is used by queries in `get_push_accounts`,
|
||||
# `register_push_device`, and `handle_register_push_device_to_bouncer`.
|
||||
# `register_push_device`, `handle_register_push_device_to_bouncer`, and
|
||||
# `send_e2ee_test_push_notification_api`.
|
||||
fields=["user", "push_account_id"],
|
||||
name="unique_push_device_user_push_account_id",
|
||||
),
|
||||
]
|
||||
indexes = [
|
||||
models.Index(
|
||||
# Used in 'send_push_notifications' function,
|
||||
# in 'zerver/lib/push_notifications'.
|
||||
# Used in 'send_push_notifications' and `send_e2ee_test_push_notification_api`.
|
||||
fields=["user", "bouncer_device_id"],
|
||||
condition=Q(bouncer_device_id__isnull=False),
|
||||
name="zerver_pushdevice_user_bouncer_device_id_idx",
|
||||
|
||||
@@ -470,7 +470,7 @@ def validate_test_response(request: Request, response: Response) -> bool:
|
||||
return True
|
||||
# Code is not declared but appears in various 400 responses. If
|
||||
# common, it can be added to 400 response schema
|
||||
if status_code.startswith("4"):
|
||||
if status_code.startswith("4") or status_code == "502":
|
||||
# This return statement should ideally be not here. But since
|
||||
# we have not defined 400 responses for various paths this has
|
||||
# been added as all 400 have the same schema. When all 400
|
||||
|
||||
@@ -26,7 +26,8 @@ from zerver.openapi.openapi import get_endpoint_from_operationid
|
||||
|
||||
UNTESTED_GENERATED_CURL_EXAMPLES = {
|
||||
# Would need push notification bouncer set up to test the
|
||||
# generated curl example for this endpoint.
|
||||
# generated curl example for the following two endpoints.
|
||||
"e2ee-test-notify",
|
||||
"test-notify",
|
||||
# Having a message for a specific user available to test this endpoint
|
||||
# is tricky for testing.
|
||||
|
||||
@@ -12636,6 +12636,69 @@ paths:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/InvalidPushDeviceTokenError"
|
||||
- $ref: "#/components/schemas/InvalidRemotePushDeviceTokenError"
|
||||
/mobile_push/e2ee/test_notification:
|
||||
post:
|
||||
operationId: e2ee-test-notify
|
||||
summary: Send an E2EE test notification to mobile device(s)
|
||||
tags: ["mobile"]
|
||||
description: |
|
||||
Trigger sending an end-to-end encrypted (E2EE) test push notification
|
||||
to the user's selected mobile device or all of their mobile devices.
|
||||
|
||||
**Changes**: New in Zulip 11.0 (feature level 420).
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
push_account_id:
|
||||
description: |
|
||||
The push account ID for the device to which to send the test notification.
|
||||
|
||||
If this parameter is not submitted, the E2EE test notification will
|
||||
be sent to all of the user's devices registered on the server.
|
||||
|
||||
A mobile client should pass this parameter, to avoid triggering a test
|
||||
notification for other clients.
|
||||
|
||||
See [`POST /mobile_push/register`](/api/register-push-device)
|
||||
for details on push account IDs.
|
||||
type: integer
|
||||
example: 111222
|
||||
responses:
|
||||
"200":
|
||||
description: Success.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/JsonSuccess"
|
||||
"400":
|
||||
description: |
|
||||
Bad request.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/NoActivePushDeviceError"
|
||||
"403":
|
||||
description: |
|
||||
Forbidden.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/PushNotificationAdminActionRequiredError"
|
||||
"502":
|
||||
description: |
|
||||
Bad gateway.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/FailedToConnectBouncerError"
|
||||
- $ref: "#/components/schemas/InternalBouncerServerError"
|
||||
/mobile_push/register:
|
||||
post:
|
||||
operationId: register-push-device
|
||||
@@ -28459,6 +28522,62 @@ components:
|
||||
|
||||
A typical failed JSON response for when the push device token is not recognized
|
||||
by the push notification bouncer:
|
||||
NoActivePushDeviceError:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/CodedError"
|
||||
- example:
|
||||
{
|
||||
"code": "NO_ACTIVE_PUSH_DEVICE",
|
||||
"msg": "No active registered push device",
|
||||
"result": "error",
|
||||
}
|
||||
description: |
|
||||
## No active push device
|
||||
|
||||
A typical failed JSON response for when no registered device is available
|
||||
for the user (and `push_account_id`) to receive a push notification.
|
||||
FailedToConnectBouncerError:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/CodedError"
|
||||
- example:
|
||||
{
|
||||
"code": "FAILED_TO_CONNECT_BOUNCER",
|
||||
"msg": "Network error while connecting to Zulip push notification service.",
|
||||
"result": "error",
|
||||
}
|
||||
description: |
|
||||
## Failed to connect bouncer
|
||||
|
||||
A typical failed JSON response for when a network error occurs
|
||||
while the server attempts to connect to the bouncer server.
|
||||
InternalBouncerServerError:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/CodedError"
|
||||
- example:
|
||||
{
|
||||
"code": "INTERNAL_SERVER_ERROR_ON_BOUNCER",
|
||||
"msg": "Internal server error on Zulip push notification service, retry later.",
|
||||
"result": "error",
|
||||
}
|
||||
description: |
|
||||
## Internal bouncer server error
|
||||
|
||||
A typical failed JSON response for when a 5xx error occurs on the bouncer server.
|
||||
PushNotificationAdminActionRequiredError:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/CodedError"
|
||||
- example:
|
||||
{
|
||||
"code": "ADMIN_ACTION_REQUIRED",
|
||||
"msg": "Push notification configuration issue on server, contact the server administrator or retry later.",
|
||||
"result": "error",
|
||||
}
|
||||
description: |
|
||||
## Admin action required
|
||||
|
||||
A typical failed JSON response for when there is a push notification
|
||||
configuration issue on the server, such as invalid credentials,
|
||||
an expired plan, or an unregistered organization. Admin action is required.
|
||||
Event_types:
|
||||
description: |
|
||||
A JSON-encoded array indicating which types of events you're interested
|
||||
|
||||
@@ -11,7 +11,17 @@ from firebase_admin.messaging import UnregisteredError
|
||||
from analytics.models import RealmCount
|
||||
from zerver.actions.user_groups import check_add_user_group
|
||||
from zerver.lib.avatar import absolute_avatar_url
|
||||
from zerver.lib.push_notifications import handle_push_notification, handle_remove_push_notification
|
||||
from zerver.lib.exceptions import MissingRemoteRealmError
|
||||
from zerver.lib.push_notifications import (
|
||||
PushNotificationsDisallowedByBouncerError,
|
||||
handle_push_notification,
|
||||
handle_remove_push_notification,
|
||||
)
|
||||
from zerver.lib.remote_server import (
|
||||
PushNotificationBouncerError,
|
||||
PushNotificationBouncerRetryLaterError,
|
||||
PushNotificationBouncerServerError,
|
||||
)
|
||||
from zerver.lib.test_classes import E2EEPushNotificationTestCase
|
||||
from zerver.lib.test_helpers import activate_push_notification_service
|
||||
from zerver.lib.timestamp import datetime_to_timestamp
|
||||
@@ -864,3 +874,195 @@ class RequireE2EEPushNotificationsSettingTest(E2EEPushNotificationTestCase):
|
||||
|
||||
self.assertEqual(send_apple.call_args.args[2]["alert"]["body"], "New message")
|
||||
self.assertEqual(send_android.call_args.args[2]["content"], "New message")
|
||||
|
||||
|
||||
class SendTestPushNotificationTest(E2EEPushNotificationTestCase):
|
||||
def test_success_cloud(self) -> None:
|
||||
hamlet = self.example_user("hamlet")
|
||||
unused, registered_device_android = self.register_push_devices_for_notification()
|
||||
|
||||
with (
|
||||
self.mock_fcm() as mock_fcm_messaging,
|
||||
self.mock_apns() as send_notification,
|
||||
self.assertLogs("zilencer.lib.push_notifications", level="INFO"),
|
||||
self.assertLogs("zerver.lib.push_notifications", level="INFO") as zerver_logger,
|
||||
):
|
||||
mock_fcm_messaging.send_each.return_value = self.make_fcm_success_response()
|
||||
send_notification.return_value.is_successful = True
|
||||
|
||||
# Send test notification to all of the registered mobile devices.
|
||||
result = self.api_post(
|
||||
hamlet, "/api/v1/mobile_push/e2ee/test_notification", subdomain="zulip"
|
||||
)
|
||||
self.assert_json_success(result)
|
||||
|
||||
mock_fcm_messaging.send_each.assert_called_once()
|
||||
send_notification.assert_called_once()
|
||||
|
||||
self.assertEqual(
|
||||
"INFO:zerver.lib.push_notifications:"
|
||||
f"Sending E2EE test push notification for user {hamlet.id}",
|
||||
zerver_logger.output[0],
|
||||
)
|
||||
self.assertEqual(
|
||||
"INFO:zerver.lib.push_notifications:"
|
||||
f"Sent E2EE mobile push notifications for user {hamlet.id}: 1 via FCM, 1 via APNs",
|
||||
zerver_logger.output[-1],
|
||||
)
|
||||
|
||||
# Send test notification to a selected mobile device.
|
||||
result = self.api_post(
|
||||
hamlet,
|
||||
"/api/v1/mobile_push/e2ee/test_notification",
|
||||
{"push_account_id": registered_device_android.push_account_id},
|
||||
subdomain="zulip",
|
||||
)
|
||||
self.assert_json_success(result)
|
||||
|
||||
self.assertEqual(
|
||||
"INFO:zerver.lib.push_notifications:"
|
||||
f"Sending E2EE test push notification for user {hamlet.id}",
|
||||
zerver_logger.output[0],
|
||||
)
|
||||
self.assertEqual(
|
||||
"INFO:zerver.lib.push_notifications:"
|
||||
f"Sent E2EE mobile push notifications for user {hamlet.id}: 1 via FCM, 0 via APNs",
|
||||
zerver_logger.output[-1],
|
||||
)
|
||||
|
||||
@responses.activate
|
||||
@override_settings(ZILENCER_ENABLED=False)
|
||||
def test_success_self_hosted(self) -> None:
|
||||
self.add_mock_response()
|
||||
|
||||
hamlet = self.example_user("hamlet")
|
||||
self.register_push_devices_for_notification(is_server_self_hosted=True)
|
||||
|
||||
with (
|
||||
self.mock_fcm() as mock_fcm_messaging,
|
||||
self.mock_apns() as send_notification,
|
||||
mock.patch(
|
||||
"corporate.lib.stripe.RemoteRealmBillingSession.current_count_for_billed_licenses",
|
||||
return_value=10,
|
||||
),
|
||||
self.assertLogs("zilencer.lib.push_notifications", level="INFO"),
|
||||
self.assertLogs("zerver.lib.push_notifications", level="INFO") as zerver_logger,
|
||||
):
|
||||
mock_fcm_messaging.send_each.return_value = self.make_fcm_success_response()
|
||||
send_notification.return_value.is_successful = True
|
||||
|
||||
# Send test notification to all of the registered mobile devices.
|
||||
result = self.api_post(
|
||||
hamlet, "/api/v1/mobile_push/e2ee/test_notification", subdomain="zulip"
|
||||
)
|
||||
self.assert_json_success(result)
|
||||
|
||||
mock_fcm_messaging.send_each.assert_called_once()
|
||||
send_notification.assert_called_once()
|
||||
|
||||
self.assertEqual(
|
||||
"INFO:zerver.lib.push_notifications:"
|
||||
f"Sending E2EE test push notification for user {hamlet.id}",
|
||||
zerver_logger.output[0],
|
||||
)
|
||||
self.assertEqual(
|
||||
"INFO:zerver.lib.push_notifications:"
|
||||
f"Sent E2EE mobile push notifications for user {hamlet.id}: 1 via FCM, 1 via APNs",
|
||||
zerver_logger.output[-1],
|
||||
)
|
||||
|
||||
@responses.activate
|
||||
@override_settings(ZILENCER_ENABLED=False)
|
||||
def test_error_responses(self) -> None:
|
||||
self.add_mock_response()
|
||||
hamlet = self.example_user("hamlet")
|
||||
|
||||
# No registered device to send to.
|
||||
result = self.api_post(
|
||||
hamlet, "/api/v1/mobile_push/e2ee/test_notification", subdomain="zulip"
|
||||
)
|
||||
self.assert_json_error(result, "No active registered push device", 400)
|
||||
|
||||
# Verify errors propagated to the client.
|
||||
registered_device_apple, registered_device_android = (
|
||||
self.register_push_devices_for_notification(is_server_self_hosted=True)
|
||||
)
|
||||
|
||||
def assert_error_response(msg: str, http_status_code: int) -> None:
|
||||
with self.assertLogs("zerver.lib.push_notifications", level="INFO") as zerver_logger:
|
||||
result = self.api_post(
|
||||
hamlet, "/api/v1/mobile_push/e2ee/test_notification", subdomain="zulip"
|
||||
)
|
||||
self.assert_json_error(result, msg, http_status_code)
|
||||
|
||||
self.assertEqual(
|
||||
"INFO:zerver.lib.push_notifications:"
|
||||
f"Sending E2EE test push notification for user {hamlet.id}",
|
||||
zerver_logger.output[0],
|
||||
)
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"zerver.lib.remote_server.send_to_push_bouncer",
|
||||
side_effect=PushNotificationBouncerRetryLaterError("network error"),
|
||||
),
|
||||
self.assertLogs(level="ERROR") as error_logs,
|
||||
):
|
||||
assert_error_response(
|
||||
"Network error while connecting to Zulip push notification service.", 502
|
||||
)
|
||||
self.assertEqual(
|
||||
"ERROR:django.request:Bad Gateway: /api/v1/mobile_push/e2ee/test_notification",
|
||||
error_logs.output[0],
|
||||
)
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"zerver.lib.remote_server.send_to_push_bouncer",
|
||||
side_effect=PushNotificationBouncerServerError("server error"),
|
||||
),
|
||||
self.assertLogs(level="ERROR") as error_logs,
|
||||
):
|
||||
assert_error_response(
|
||||
"Internal server error on Zulip push notification service, retry later.", 502
|
||||
)
|
||||
self.assertEqual(
|
||||
"ERROR:django.request:Bad Gateway: /api/v1/mobile_push/e2ee/test_notification",
|
||||
error_logs.output[0],
|
||||
)
|
||||
|
||||
with mock.patch(
|
||||
"zerver.lib.remote_server.send_to_push_bouncer", side_effect=MissingRemoteRealmError
|
||||
):
|
||||
assert_error_response(
|
||||
"Push notification configuration issue on server, contact the server administrator or retry later.",
|
||||
403,
|
||||
)
|
||||
|
||||
with mock.patch(
|
||||
"zerver.lib.remote_server.send_to_push_bouncer",
|
||||
side_effect=PushNotificationBouncerError,
|
||||
):
|
||||
assert_error_response(
|
||||
"Push notification configuration issue on server, contact the server administrator or retry later.",
|
||||
403,
|
||||
)
|
||||
|
||||
with mock.patch(
|
||||
"zerver.lib.remote_server.send_to_push_bouncer",
|
||||
side_effect=PushNotificationsDisallowedByBouncerError("plan expired"),
|
||||
):
|
||||
assert_error_response(
|
||||
"Push notification configuration issue on server, contact the server administrator or retry later.",
|
||||
403,
|
||||
)
|
||||
|
||||
# Device marked expired on bouncer (not on server).
|
||||
registered_device_apple.delete()
|
||||
registered_device_android.delete()
|
||||
|
||||
with mock.patch(
|
||||
"corporate.lib.stripe.RemoteRealmBillingSession.current_count_for_billed_licenses",
|
||||
return_value=10,
|
||||
):
|
||||
assert_error_response("No active registered push device", 400)
|
||||
|
||||
@@ -21,8 +21,10 @@ from zerver.lib.exceptions import (
|
||||
)
|
||||
from zerver.lib.push_notifications import (
|
||||
InvalidPushDeviceTokenError,
|
||||
NoActivePushDeviceError,
|
||||
add_push_device_token,
|
||||
remove_push_device_token,
|
||||
send_e2ee_test_push_notification,
|
||||
send_test_push_notification,
|
||||
uses_notification_bouncer,
|
||||
validate_token,
|
||||
@@ -110,6 +112,30 @@ def send_test_push_notification_api(
|
||||
return json_success(request)
|
||||
|
||||
|
||||
@human_users_only
|
||||
@typed_endpoint
|
||||
def send_e2ee_test_push_notification_api(
|
||||
request: HttpRequest, user_profile: UserProfile, *, push_account_id: Json[int] | None = None
|
||||
) -> HttpResponse:
|
||||
# We skip push devices with `bouncer_device_id` set to `null` as they are
|
||||
# not yet registered with the bouncer to send mobile push notifications.
|
||||
if push_account_id is not None:
|
||||
# Uses unique index created for 'unique_push_device_user_push_account_id' constraint.
|
||||
push_devices = PushDevice.objects.filter(
|
||||
user=user_profile, push_account_id=push_account_id, bouncer_device_id__isnull=False
|
||||
)
|
||||
else:
|
||||
# Uses 'zerver_pushdevice_user_bouncer_device_id_idx' index.
|
||||
push_devices = PushDevice.objects.filter(user=user_profile, bouncer_device_id__isnull=False)
|
||||
|
||||
if len(push_devices) == 0:
|
||||
raise NoActivePushDeviceError
|
||||
|
||||
send_e2ee_test_push_notification(user_profile, push_devices)
|
||||
|
||||
return json_success(request)
|
||||
|
||||
|
||||
def self_hosting_auth_view_common(
|
||||
request: HttpRequest, user_profile: UserProfile, next_page: str | None = None
|
||||
) -> str:
|
||||
|
||||
@@ -118,6 +118,7 @@ from zerver.views.push_notifications import (
|
||||
self_hosting_auth_not_configured,
|
||||
self_hosting_auth_redirect_endpoint,
|
||||
self_hosting_registration_transfer_challenge_verify,
|
||||
send_e2ee_test_push_notification_api,
|
||||
send_test_push_notification_api,
|
||||
)
|
||||
from zerver.views.reactions import add_reaction, remove_reaction
|
||||
@@ -462,6 +463,7 @@ v1_api_and_json_patterns = [
|
||||
),
|
||||
rest_path("users/me/android_gcm_reg_id", POST=add_android_reg_id, DELETE=remove_android_reg_id),
|
||||
rest_path("mobile_push/test_notification", POST=send_test_push_notification_api),
|
||||
rest_path("mobile_push/e2ee/test_notification", POST=send_e2ee_test_push_notification_api),
|
||||
rest_path("mobile_push/register", POST=register_push_device),
|
||||
# users/*/presence => zerver.views.presence.
|
||||
rest_path(
|
||||
|
||||
Reference in New Issue
Block a user