test_push_notifications: Use responses module to mock HTTP responses.

This commit is contained in:
akshatdalton 2021-06-11 20:51:27 +00:00 committed by Tim Abbott
parent efbde2ad01
commit 1a76d06add
2 changed files with 132 additions and 112 deletions

View File

@ -42,7 +42,7 @@ def send_to_push_bouncer(
* 400 errors from the push bouncer. Here there are 2 categories: * 400 errors from the push bouncer. Here there are 2 categories:
Our server failed to connect to the push bouncer (should throw) Our server failed to connect to the push bouncer (should throw)
vs. client-side errors like and invalid token. vs. client-side errors like an invalid token.
""" """
url = urllib.parse.urljoin( url = urllib.parse.urljoin(

View File

@ -2,21 +2,24 @@ import base64
import datetime import datetime
import itertools import itertools
import os import os
import re
import uuid import uuid
from contextlib import contextmanager from contextlib import contextmanager
from typing import Any, Dict, Iterator, List, Optional from typing import Any, Dict, Iterator, List, Optional, Tuple
from unittest import mock, skipUnless from unittest import mock, skipUnless
from unittest.mock import call from unittest.mock import call
from urllib import parse
import orjson import orjson
import requests import responses
from django.conf import settings from django.conf import settings
from django.db import transaction from django.db import transaction
from django.db.models import F from django.db.models import F
from django.http import HttpResponse
from django.test import override_settings from django.test import override_settings
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.timezone import now from django.utils.timezone import now
from requests.exceptions import ConnectionError
from requests.models import PreparedRequest
from analytics.lib.counts import CountStat, LoggingCountStat from analytics.lib.counts import CountStat, LoggingCountStat
from analytics.models import InstallationCount, RealmCount from analytics.models import InstallationCount, RealmCount
@ -103,20 +106,27 @@ class BouncerTestCase(ZulipTestCase):
RemoteZulipServer.objects.filter(uuid=self.server_uuid).delete() RemoteZulipServer.objects.filter(uuid=self.server_uuid).delete()
super().tearDown() super().tearDown()
def bounce_request(self, *args: Any, **kwargs: Any) -> HttpResponse: def request_callback(self, request: PreparedRequest) -> Tuple[int, Dict[str, str], bytes]:
"""This method is used to carry out the push notification bouncer assert isinstance(request.body, str) or request.body is None
requests using the Django test browser, rather than python-requests. params: Dict[str, List[str]] = parse.parse_qs(request.body)
""" # In Python 3, the values of the dict from `parse_qs` are
# args[0] is method, args[1] is URL. # in a list, because there might be multiple values.
local_url = args[1].replace(settings.PUSH_NOTIFICATION_BOUNCER_URL, "") # But since we are sending values with no same keys, hence
if args[0] == "POST": # we can safely pick the first value.
result = self.uuid_post(self.server_uuid, local_url, kwargs["data"], subdomain="") data = {k: v[0] for k, v in params.items()}
assert request.url is not None # allow mypy to infer url is present.
local_url = request.url.replace(settings.PUSH_NOTIFICATION_BOUNCER_URL, "")
if request.method == "POST":
result = self.uuid_post(self.server_uuid, local_url, data, subdomain="")
elif request.method == "GET":
result = self.uuid_get(self.server_uuid, local_url, data, subdomain="")
return (result.status_code, result.headers, result.content)
elif args[0] == "GET": def add_mock_response(self) -> None:
result = self.uuid_get(self.server_uuid, local_url, kwargs["data"], subdomain="") # Match any endpoint with the PUSH_NOTIFICATION_BOUNCER_URL.
else: COMPILED_URL = re.compile(settings.PUSH_NOTIFICATION_BOUNCER_URL + ".*")
raise AssertionError("Unsupported method for bounce_request") responses.add_callback(responses.POST, COMPILED_URL, callback=self.request_callback)
return result responses.add_callback(responses.GET, COMPILED_URL, callback=self.request_callback)
def get_generic_payload(self, method: str = "register") -> Dict[str, Any]: def get_generic_payload(self, method: str = "register") -> Dict[str, Any]:
user_id = 10 user_id = 10
@ -295,12 +305,12 @@ class PushBouncerNotificationTest(BouncerTestCase):
self.assert_json_error(result, "Invalid APNS token") self.assert_json_error(result, "Invalid APNS token")
@override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com") @override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com")
@mock.patch("zerver.lib.remote_server.requests.request") @responses.activate
def test_push_bouncer_api(self, mock_request: Any) -> None: def test_push_bouncer_api(self) -> None:
"""This is a variant of the below test_push_api, but using the full """This is a variant of the below test_push_api, but using the full
push notification bouncer flow push notification bouncer flow
""" """
mock_request.side_effect = self.bounce_request self.add_mock_response()
user = self.example_user("cordelia") user = self.example_user("cordelia")
self.login_user(user) self.login_user(user)
server = RemoteZulipServer.objects.get(uuid=self.server_uuid) server = RemoteZulipServer.objects.get(uuid=self.server_uuid)
@ -330,9 +340,9 @@ class PushBouncerNotificationTest(BouncerTestCase):
) )
self.assert_json_error(result, "Token does not exist") self.assert_json_error(result, "Token does not exist")
with mock.patch( URL = settings.PUSH_NOTIFICATION_BOUNCER_URL + "/api/v1/remotes/push/register"
"zerver.lib.remote_server.requests.request", side_effect=requests.ConnectionError with responses.RequestsMock() as resp, self.assertLogs(level="ERROR") as error_log:
), self.assertLogs(level="ERROR") as error_log: resp.add(responses.POST, URL, body=ConnectionError(), status=502)
result = self.client_post(endpoint, {"token": token}, subdomain="zulip") result = self.client_post(endpoint, {"token": token}, subdomain="zulip")
self.assert_json_error( self.assert_json_error(
result, result,
@ -346,9 +356,8 @@ class PushBouncerNotificationTest(BouncerTestCase):
], ],
) )
with mock.patch( with responses.RequestsMock() as resp, self.assertLogs(level="WARNING") as warn_log:
"zerver.lib.remote_server.requests.request", return_value=Result(status=500) resp.add(responses.POST, URL, body=orjson.dumps({"msg": "error"}), status=500)
), self.assertLogs(level="WARNING") as warn_log:
result = self.client_post(endpoint, {"token": token}, subdomain="zulip") result = self.client_post(endpoint, {"token": token}, subdomain="zulip")
self.assert_json_error(result, "Received 500 from push notification bouncer", 502) self.assert_json_error(result, "Received 500 from push notification bouncer", 502)
self.assertEqual( self.assertEqual(
@ -419,38 +428,50 @@ class AnalyticsBouncerTest(BouncerTestCase):
TIME_ZERO = datetime.datetime(1988, 3, 14, tzinfo=datetime.timezone.utc) TIME_ZERO = datetime.datetime(1988, 3, 14, tzinfo=datetime.timezone.utc)
@override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com") @override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com")
@mock.patch("zerver.lib.remote_server.requests.request") @responses.activate
def test_analytics_api(self, mock_request: Any) -> None: def test_analytics_api(self) -> None:
"""This is a variant of the below test_push_api, but using the full """This is a variant of the below test_push_api, but using the full
push notification bouncer flow push notification bouncer flow
""" """
mock_request.side_effect = self.bounce_request ANALYTICS_URL = settings.PUSH_NOTIFICATION_BOUNCER_URL + "/api/v1/remotes/server/analytics"
ANALYTICS_STATUS_URL = ANALYTICS_URL + "/status"
user = self.example_user("hamlet") user = self.example_user("hamlet")
end_time = self.TIME_ZERO end_time = self.TIME_ZERO
with mock.patch( with responses.RequestsMock() as resp, mock.patch(
"zerver.lib.remote_server.requests.request", side_effect=requests.ConnectionError "zerver.lib.remote_server.logging.warning"
), mock.patch("zerver.lib.remote_server.logging.warning") as mock_warning: ) as mock_warning:
resp.add(responses.GET, ANALYTICS_STATUS_URL, body=ConnectionError())
send_analytics_to_remote_server() send_analytics_to_remote_server()
mock_warning.assert_called_once_with( mock_warning.assert_called_once_with(
"ConnectionError while trying to connect to push notification bouncer" "ConnectionError while trying to connect to push notification bouncer"
) )
self.assertTrue(resp.assert_call_count(ANALYTICS_STATUS_URL, 1))
self.add_mock_response()
# Send any existing data over, so that we can start the test with a "clean" slate # Send any existing data over, so that we can start the test with a "clean" slate
audit_log_max_id = RealmAuditLog.objects.all().order_by("id").last().id audit_log_max_id = RealmAuditLog.objects.all().order_by("id").last().id
send_analytics_to_remote_server() send_analytics_to_remote_server()
self.assertEqual(mock_request.call_count, 2) self.assertTrue(responses.assert_call_count(ANALYTICS_STATUS_URL, 1))
remote_audit_log_count = RemoteRealmAuditLog.objects.count() remote_audit_log_count = RemoteRealmAuditLog.objects.count()
self.assertEqual(RemoteRealmCount.objects.count(), 0) self.assertEqual(RemoteRealmCount.objects.count(), 0)
self.assertEqual(RemoteInstallationCount.objects.count(), 0) self.assertEqual(RemoteInstallationCount.objects.count(), 0)
def check_counts( def check_counts(
mock_request_call_count: int, analytics_status_mock_request_call_count: int,
analytics_mock_request_call_count: int,
remote_realm_count: int, remote_realm_count: int,
remote_installation_count: int, remote_installation_count: int,
remote_realm_audit_log: int, remote_realm_audit_log: int,
) -> None: ) -> None:
self.assertEqual(mock_request.call_count, mock_request_call_count) self.assertTrue(
responses.assert_call_count(
ANALYTICS_STATUS_URL, analytics_status_mock_request_call_count
)
)
self.assertTrue(
responses.assert_call_count(ANALYTICS_URL, analytics_mock_request_call_count)
)
self.assertEqual(RemoteRealmCount.objects.count(), remote_realm_count) self.assertEqual(RemoteRealmCount.objects.count(), remote_realm_count)
self.assertEqual(RemoteInstallationCount.objects.count(), remote_installation_count) self.assertEqual(RemoteInstallationCount.objects.count(), remote_installation_count)
self.assertEqual( self.assertEqual(
@ -491,11 +512,11 @@ class AnalyticsBouncerTest(BouncerTestCase):
self.assertEqual(RealmAuditLog.objects.filter(id__gt=audit_log_max_id).count(), 2) self.assertEqual(RealmAuditLog.objects.filter(id__gt=audit_log_max_id).count(), 2)
send_analytics_to_remote_server() send_analytics_to_remote_server()
check_counts(4, 1, 1, 1) check_counts(2, 2, 1, 1, 1)
# Test having no new rows # Test having no new rows
send_analytics_to_remote_server() send_analytics_to_remote_server()
check_counts(5, 1, 1, 1) check_counts(3, 2, 1, 1, 1)
# Test only having new RealmCount rows # Test only having new RealmCount rows
RealmCount.objects.create( RealmCount.objects.create(
@ -511,14 +532,14 @@ class AnalyticsBouncerTest(BouncerTestCase):
value=9, value=9,
) )
send_analytics_to_remote_server() send_analytics_to_remote_server()
check_counts(7, 3, 1, 1) check_counts(4, 3, 3, 1, 1)
# Test only having new InstallationCount rows # Test only having new InstallationCount rows
InstallationCount.objects.create( InstallationCount.objects.create(
property=realm_stat.property, end_time=end_time + datetime.timedelta(days=1), value=6 property=realm_stat.property, end_time=end_time + datetime.timedelta(days=1), value=6
) )
send_analytics_to_remote_server() send_analytics_to_remote_server()
check_counts(9, 3, 2, 1) check_counts(5, 4, 3, 2, 1)
# Test only having new RealmAuditLog rows # Test only having new RealmAuditLog rows
# Non-synced event # Non-synced event
@ -530,7 +551,7 @@ class AnalyticsBouncerTest(BouncerTestCase):
extra_data="data", extra_data="data",
) )
send_analytics_to_remote_server() send_analytics_to_remote_server()
check_counts(10, 3, 2, 1) check_counts(6, 4, 3, 2, 1)
# Synced event # Synced event
RealmAuditLog.objects.create( RealmAuditLog.objects.create(
realm=user.realm, realm=user.realm,
@ -540,7 +561,7 @@ class AnalyticsBouncerTest(BouncerTestCase):
extra_data="data", extra_data="data",
) )
send_analytics_to_remote_server() send_analytics_to_remote_server()
check_counts(12, 3, 2, 2) check_counts(7, 5, 3, 2, 2)
(realm_count_data, installation_count_data, realmauditlog_data) = build_analytics_data( (realm_count_data, installation_count_data, realmauditlog_data) = build_analytics_data(
RealmCount.objects.all(), InstallationCount.objects.all(), RealmAuditLog.objects.all() RealmCount.objects.all(), InstallationCount.objects.all(), RealmAuditLog.objects.all()
@ -583,12 +604,12 @@ class AnalyticsBouncerTest(BouncerTestCase):
) )
@override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com") @override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com")
@mock.patch("zerver.lib.remote_server.requests.request") @responses.activate
def test_analytics_api_invalid(self, mock_request: Any) -> None: def test_analytics_api_invalid(self) -> None:
"""This is a variant of the below test_push_api, but using the full """This is a variant of the below test_push_api, but using the full
push notification bouncer flow push notification bouncer flow
""" """
mock_request.side_effect = self.bounce_request self.add_mock_response()
user = self.example_user("hamlet") user = self.example_user("hamlet")
end_time = self.TIME_ZERO end_time = self.TIME_ZERO
@ -608,9 +629,9 @@ class AnalyticsBouncerTest(BouncerTestCase):
# Servers on Zulip 2.0.6 and earlier only send realm_counts and installation_counts data, # Servers on Zulip 2.0.6 and earlier only send realm_counts and installation_counts data,
# and don't send realmauditlog_rows. Make sure that continues to work. # and don't send realmauditlog_rows. Make sure that continues to work.
@override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com") @override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com")
@mock.patch("zerver.lib.remote_server.requests.request") @responses.activate
def test_old_two_table_format(self, mock_request: Any) -> None: def test_old_two_table_format(self) -> None:
mock_request.side_effect = self.bounce_request self.add_mock_response()
# Send fixture generated with Zulip 2.0 code # Send fixture generated with Zulip 2.0 code
send_to_push_bouncer( send_to_push_bouncer(
"POST", "POST",
@ -621,16 +642,17 @@ class AnalyticsBouncerTest(BouncerTestCase):
"version": '"2.0.6+git"', "version": '"2.0.6+git"',
}, },
) )
self.assertEqual(mock_request.call_count, 1) ANALYTICS_URL = settings.PUSH_NOTIFICATION_BOUNCER_URL + "/api/v1/remotes/server/analytics"
self.assertTrue(responses.assert_call_count(ANALYTICS_URL, 1))
self.assertEqual(RemoteRealmCount.objects.count(), 1) self.assertEqual(RemoteRealmCount.objects.count(), 1)
self.assertEqual(RemoteInstallationCount.objects.count(), 0) self.assertEqual(RemoteInstallationCount.objects.count(), 0)
self.assertEqual(RemoteRealmAuditLog.objects.count(), 0) self.assertEqual(RemoteRealmAuditLog.objects.count(), 0)
# Make sure we aren't sending data we don't mean to, even if we don't store it. # Make sure we aren't sending data we don't mean to, even if we don't store it.
@override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com") @override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com")
@mock.patch("zerver.lib.remote_server.requests.request") @responses.activate
def test_only_sending_intended_realmauditlog_data(self, mock_request: Any) -> None: def test_only_sending_intended_realmauditlog_data(self) -> None:
mock_request.side_effect = self.bounce_request self.add_mock_response()
user = self.example_user("hamlet") user = self.example_user("hamlet")
# Event type in SYNCED_BILLING_EVENTS -- should be included # Event type in SYNCED_BILLING_EVENTS -- should be included
RealmAuditLog.objects.create( RealmAuditLog.objects.create(
@ -672,9 +694,9 @@ class AnalyticsBouncerTest(BouncerTestCase):
send_analytics_to_remote_server() send_analytics_to_remote_server()
@override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com") @override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com")
@mock.patch("zerver.lib.remote_server.requests.request") @responses.activate
def test_realmauditlog_data_mapping(self, mock_request: Any) -> None: def test_realmauditlog_data_mapping(self) -> None:
mock_request.side_effect = self.bounce_request self.add_mock_response()
user = self.example_user("hamlet") user = self.example_user("hamlet")
log_entry = RealmAuditLog.objects.create( log_entry = RealmAuditLog.objects.create(
realm=user.realm, realm=user.realm,
@ -769,21 +791,18 @@ class PushNotificationTest(BouncerTestCase):
class HandlePushNotificationTest(PushNotificationTest): class HandlePushNotificationTest(PushNotificationTest):
DEFAULT_SUBDOMAIN = "" DEFAULT_SUBDOMAIN = ""
def bounce_request(self, *args: Any, **kwargs: Any) -> HttpResponse: def request_callback(self, request: PreparedRequest) -> Tuple[int, Dict[str, str], bytes]:
"""This method is used to carry out the push notification bouncer assert request.url is not None # allow mypy to infer url is present.
requests using the Django test browser, rather than python-requests. local_url = request.url.replace(settings.PUSH_NOTIFICATION_BOUNCER_URL, "")
""" result = self.uuid_post(
# args[0] is method, args[1] is URL. self.server_uuid, local_url, request.body, content_type="application/json"
local_url = args[1].replace(settings.PUSH_NOTIFICATION_BOUNCER_URL, "") )
if args[0] == "POST": return (result.status_code, result.headers, result.content)
result = self.uuid_post(
self.server_uuid, local_url, kwargs["data"], content_type="application/json"
)
else:
raise AssertionError("Unsupported method for bounce_request")
return result
@override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com")
@responses.activate
def test_end_to_end(self) -> None: def test_end_to_end(self) -> None:
self.add_mock_response()
self.setup_apns_tokens() self.setup_apns_tokens()
self.setup_gcm_tokens() self.setup_gcm_tokens()
@ -797,9 +816,7 @@ class HandlePushNotificationTest(PushNotificationTest):
"message_id": message.id, "message_id": message.id,
"trigger": "private_message", "trigger": "private_message",
} }
with self.settings(PUSH_NOTIFICATION_BOUNCER_URL=""), mock.patch( with mock.patch(
"zerver.lib.remote_server.requests.request", side_effect=self.bounce_request
), mock.patch(
"zerver.lib.push_notifications.gcm_client" "zerver.lib.push_notifications.gcm_client"
) as mock_gcm, self.mock_apns() as mock_apns, mock.patch( ) as mock_gcm, self.mock_apns() as mock_apns, mock.patch(
"zerver.lib.push_notifications.logger.info" "zerver.lib.push_notifications.logger.info"
@ -828,7 +845,10 @@ class HandlePushNotificationTest(PushNotificationTest):
message.id, message.id,
) )
@override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com")
@responses.activate
def test_unregistered_client(self) -> None: def test_unregistered_client(self) -> None:
self.add_mock_response()
self.setup_apns_tokens() self.setup_apns_tokens()
self.setup_gcm_tokens() self.setup_gcm_tokens()
@ -842,9 +862,7 @@ class HandlePushNotificationTest(PushNotificationTest):
"message_id": message.id, "message_id": message.id,
"trigger": "private_message", "trigger": "private_message",
} }
with self.settings(PUSH_NOTIFICATION_BOUNCER_URL=""), mock.patch( with mock.patch(
"zerver.lib.remote_server.requests.request", side_effect=self.bounce_request
), mock.patch(
"zerver.lib.push_notifications.gcm_client" "zerver.lib.push_notifications.gcm_client"
) as mock_gcm, self.mock_apns() as mock_apns, mock.patch( ) as mock_gcm, self.mock_apns() as mock_apns, mock.patch(
"zerver.lib.push_notifications.logger.info" "zerver.lib.push_notifications.logger.info"
@ -873,6 +891,8 @@ class HandlePushNotificationTest(PushNotificationTest):
RemotePushDeviceToken.objects.filter(kind=PushDeviceToken.APNS).count(), 0 RemotePushDeviceToken.objects.filter(kind=PushDeviceToken.APNS).count(), 0
) )
@override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com")
@responses.activate
def test_connection_error(self) -> None: def test_connection_error(self) -> None:
self.setup_apns_tokens() self.setup_apns_tokens()
self.setup_gcm_tokens() self.setup_gcm_tokens()
@ -888,11 +908,9 @@ class HandlePushNotificationTest(PushNotificationTest):
"message_id": message.id, "message_id": message.id,
"trigger": "private_message", "trigger": "private_message",
} }
with self.settings(PUSH_NOTIFICATION_BOUNCER_URL=""), mock.patch( URL = settings.PUSH_NOTIFICATION_BOUNCER_URL + "/api/v1/remotes/push/notify"
"zerver.lib.remote_server.requests.request", side_effect=self.bounce_request responses.add(responses.POST, URL, body=ConnectionError())
), mock.patch("zerver.lib.push_notifications.gcm_client") as mock_gcm, mock.patch( with mock.patch("zerver.lib.push_notifications.gcm_client") as mock_gcm:
"zerver.lib.remote_server.requests.request", side_effect=requests.ConnectionError
):
gcm_devices = [ gcm_devices = [
(b64_to_hex(device.token), device.ios_app_id, device.token) (b64_to_hex(device.token), device.ios_app_id, device.token)
for device in RemotePushDeviceToken.objects.filter(kind=PushDeviceToken.GCM) for device in RemotePushDeviceToken.objects.filter(kind=PushDeviceToken.GCM)
@ -1829,52 +1847,55 @@ class TestSendNotificationsToBouncer(ZulipTestCase):
) )
class Result: @override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com")
def __init__(self, status: int = 200, content: bytes = orjson.dumps({"msg": "error"})) -> None:
self.status_code = status
self.content = content
class TestSendToPushBouncer(ZulipTestCase): class TestSendToPushBouncer(ZulipTestCase):
@mock.patch("requests.request", return_value=Result(status=500)) def add_mock_response(
def test_500_error(self, mock_request: mock.MagicMock) -> None: self, body: bytes = orjson.dumps({"msg": "error"}), status: int = 200
) -> None:
URL = settings.PUSH_NOTIFICATION_BOUNCER_URL + "/api/v1/remotes/register"
responses.add(responses.POST, URL, body=body, status=status)
@responses.activate
def test_500_error(self) -> None:
self.add_mock_response(status=500)
with self.assertLogs(level="WARNING") as m: with self.assertLogs(level="WARNING") as m:
with self.assertRaises(PushNotificationBouncerRetryLaterError): with self.assertRaises(PushNotificationBouncerRetryLaterError):
send_to_push_bouncer("register", "register", {"data": "true"}) send_to_push_bouncer("POST", "register", {"data": "true"})
self.assertEqual(m.output, ["WARNING:root:Received 500 from push notification bouncer"]) self.assertEqual(m.output, ["WARNING:root:Received 500 from push notification bouncer"])
@mock.patch("requests.request", return_value=Result(status=400)) @responses.activate
def test_400_error(self, mock_request: mock.MagicMock) -> None: def test_400_error(self) -> None:
self.add_mock_response(status=400)
with self.assertRaises(JsonableError) as exc: with self.assertRaises(JsonableError) as exc:
send_to_push_bouncer("register", "register", {"msg": "true"}) send_to_push_bouncer("POST", "register", {"msg": "true"})
self.assertEqual(exc.exception.msg, "error") self.assertEqual(exc.exception.msg, "error")
@responses.activate
def test_400_error_invalid_server_key(self) -> None: def test_400_error_invalid_server_key(self) -> None:
from zerver.decorator import InvalidZulipServerError from zerver.decorator import InvalidZulipServerError
# This is the exception our decorator uses for an invalid Zulip server # This is the exception our decorator uses for an invalid Zulip server
error_obj = InvalidZulipServerError("testRole") error_obj = InvalidZulipServerError("testRole")
with mock.patch( self.add_mock_response(body=orjson.dumps(error_obj.to_json()), status=400)
"requests.request", with self.assertRaises(PushNotificationBouncerException) as exc:
return_value=Result(status=400, content=orjson.dumps(error_obj.to_json())), send_to_push_bouncer("POST", "register", {"msg": "true"})
):
with self.assertRaises(PushNotificationBouncerException) as exc:
send_to_push_bouncer("register", "register", {"msg": "true"})
self.assertEqual( self.assertEqual(
str(exc.exception), str(exc.exception),
"Push notifications bouncer error: " "Push notifications bouncer error: "
"Zulip server auth failure: testRole is not registered", "Zulip server auth failure: testRole is not registered",
) )
@mock.patch("requests.request", return_value=Result(status=400, content=b"/")) @responses.activate
def test_400_error_when_content_is_not_serializable(self, mock_request: mock.MagicMock) -> None: def test_400_error_when_content_is_not_serializable(self) -> None:
self.add_mock_response(body=b"/", status=400)
with self.assertRaises(orjson.JSONDecodeError): with self.assertRaises(orjson.JSONDecodeError):
send_to_push_bouncer("register", "register", {"msg": "true"}) send_to_push_bouncer("POST", "register", {"msg": "true"})
@mock.patch("requests.request", return_value=Result(status=300, content=b"/")) @responses.activate
def test_300_error(self, mock_request: mock.MagicMock) -> None: def test_300_error(self) -> None:
self.add_mock_response(body=b"/", status=300)
with self.assertRaises(PushNotificationBouncerException) as exc: with self.assertRaises(PushNotificationBouncerException) as exc:
send_to_push_bouncer("register", "register", {"msg": "true"}) send_to_push_bouncer("POST", "register", {"msg": "true"})
self.assertEqual( self.assertEqual(
str(exc.exception), "Push notification bouncer returned unexpected status code 300" str(exc.exception), "Push notification bouncer returned unexpected status code 300"
) )
@ -1892,6 +1913,7 @@ class TestNumPushDevicesForUser(PushNotificationTest):
class TestPushApi(BouncerTestCase): class TestPushApi(BouncerTestCase):
@responses.activate
def test_push_api_error_handling(self) -> None: def test_push_api_error_handling(self) -> None:
user = self.example_user("cordelia") user = self.example_user("cordelia")
self.login_user(user) self.login_user(user)
@ -1922,13 +1944,14 @@ class TestPushApi(BouncerTestCase):
# Use push notification bouncer and try to remove non-existing tokens. # Use push notification bouncer and try to remove non-existing tokens.
with self.settings( with self.settings(
PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com" PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com"
), mock.patch( ), responses.RequestsMock() as resp:
"zerver.lib.remote_server.requests.request", side_effect=self.bounce_request URL = settings.PUSH_NOTIFICATION_BOUNCER_URL + "/api/v1/remotes/push/unregister"
) as remote_server_request: resp.add_callback(responses.POST, URL, callback=self.request_callback)
result = self.client_delete(endpoint, {"token": "abcd1234"}) result = self.client_delete(endpoint, {"token": "abcd1234"})
self.assert_json_error(result, "Token does not exist") self.assert_json_error(result, "Token does not exist")
remote_server_request.assert_called_once() self.assertTrue(resp.assert_call_count(URL, 1))
@responses.activate
def test_push_api_add_and_remove_device_tokens(self) -> None: def test_push_api_add_and_remove_device_tokens(self) -> None:
user = self.example_user("cordelia") user = self.example_user("cordelia")
self.login_user(user) self.login_user(user)
@ -1956,9 +1979,8 @@ class TestPushApi(BouncerTestCase):
self.assert_length(tokens, 1) self.assert_length(tokens, 1)
self.assertEqual(tokens[0].token, token) self.assertEqual(tokens[0].token, token)
with self.settings( with self.settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com"):
PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com" self.add_mock_response()
), mock.patch("zerver.lib.remote_server.requests.request", side_effect=self.bounce_request):
# Enable push notification bouncer and add tokens. # Enable push notification bouncer and add tokens.
for endpoint, token in bouncer_requests: for endpoint, token in bouncer_requests:
# Test that we can push twice. # Test that we can push twice.
@ -1996,9 +2018,7 @@ class TestPushApi(BouncerTestCase):
# Use push notification bouncer and test removing device tokens. # Use push notification bouncer and test removing device tokens.
# Tokens will be removed both locally and remotely. # Tokens will be removed both locally and remotely.
with self.settings( with self.settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com"):
PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com"
), mock.patch("zerver.lib.remote_server.requests.request", side_effect=self.bounce_request):
for endpoint, token in bouncer_requests: for endpoint, token in bouncer_requests:
result = self.client_delete(endpoint, {"token": token}) result = self.client_delete(endpoint, {"token": token})
self.assert_json_success(result) self.assert_json_success(result)