diff --git a/README.rst b/README.rst index a8b13eab..eebe9d19 100644 --- a/README.rst +++ b/README.rst @@ -115,6 +115,7 @@ For WNS, you need both the ``WNS_PACKAGE_SECURITY_KEY`` and the ``WNS_SECRET_KEY - ``APNS_TOPIC``: The topic of the remote notification, which is typically the bundle ID for your app. If you omit this header and your APNs certificate does not specify multiple topics, the APNs server uses the certificate’s Subject as the default topic. - ``APNS_USE_ALTERNATIVE_PORT``: Use port 2197 for APNS, instead of default port 443. - ``APNS_USE_SANDBOX``: Use 'api.development.push.apple.com', instead of default host 'api.push.apple.com'. Default value depends on ``DEBUG`` setting of your environment: if ``DEBUG`` is True and you use production certificate, you should explicitly set ``APNS_USE_SANDBOX`` to False. +- ``APNS_ERROR_TIMEOUT``: Timeout in seconds for APNS Push requests (Optional, default value is 5) **FCM/GCM settings** diff --git a/push_notifications/apns_async.py b/push_notifications/apns_async.py index 811b0f90..5ca82980 100644 --- a/push_notifications/apns_async.py +++ b/push_notifications/apns_async.py @@ -305,6 +305,7 @@ def apns_send_bulk_message( mutable_content: Optional[bool] = False, category: Optional[str] = None, err_func: Optional[ErrFunc] = None, + timeout: Optional[int] = None, ) -> Dict[str, str]: """ Sends an APNS notification to one or more registration_ids. @@ -326,7 +327,11 @@ def apns_send_bulk_message( Notification Content Extension or UNNotificationCategory configuration. It allows the app to display custom actions with the notification. :param content_available: If True the `content-available` flag will be set to 1, allowing the app to be woken up in the background + :param timeout: Timeout in seconds for each notification send operation """ + if not timeout: + timeout = get_manager().get_apns_error_timeout(application_id) + try: topic = get_manager().get_apns_topic(application_id) results: Dict[str, str] = {} @@ -351,6 +356,7 @@ def apns_send_bulk_message( mutable_content=mutable_content, category=category, err_func=err_func, + timeout=timeout, ) ) @@ -386,6 +392,7 @@ def apns_send_bulk_message( async def _send_bulk_request( registration_ids: list[str], + timeout: int, alert: Union[str, Alert], application_id: Optional[str] = None, creds: Optional[Credentials] = None, @@ -432,16 +439,17 @@ async def _send_bulk_request( for registration_id in registration_ids ] - send_requests = [_send_request(client, request) for request in requests] + send_requests = [_send_request(client, request, timeout) for request in requests] return await asyncio.gather(*send_requests) async def _send_request( apns: APNs, request: NotificationRequest, + timeout: int, ) -> Tuple[str, NotificationResult]: try: - res = await asyncio.wait_for(apns.send_notification(request), timeout=1) + res = await asyncio.wait_for(apns.send_notification(request), timeout=timeout) return request.device_token, res except asyncio.TimeoutError: diff --git a/push_notifications/conf/app.py b/push_notifications/conf/app.py index 38bde083..444cc2b8 100644 --- a/push_notifications/conf/app.py +++ b/push_notifications/conf/app.py @@ -380,6 +380,10 @@ def get_apns_use_alternative_port( def get_apns_topic(self, application_id: Optional[str] = None) -> Optional[str]: return self._get_application_settings(application_id, "APNS", "TOPIC") + + def get_apns_error_timeout(self, application_id: Optional[str] = None) -> int: + return self._get_application_settings(application_id, "APNS", "ERROR_TIMEOUT") + def get_wns_package_security_id(self, application_id: Optional[str] = None) -> str: return self._get_application_settings( application_id, "WNS", "PACKAGE_SECURITY_ID" diff --git a/push_notifications/conf/base.py b/push_notifications/conf/base.py index 3e1c3b66..027943a0 100644 --- a/push_notifications/conf/base.py +++ b/push_notifications/conf/base.py @@ -22,6 +22,9 @@ def get_apns_use_sandbox(self, application_id: Optional[str] = None) -> bool: def get_apns_use_alternative_port(self, application_id: Optional[str] = None) -> bool: raise NotImplementedError + def get_apns_error_timeout(self, application_id: Optional[str] = None) -> int: + raise NotImplementedError + def get_wns_package_security_id(self, application_id: Optional[str] = None) -> str: raise NotImplementedError diff --git a/push_notifications/settings.py b/push_notifications/settings.py index 01cc30f0..fb2fbb22 100644 --- a/push_notifications/settings.py +++ b/push_notifications/settings.py @@ -18,6 +18,7 @@ PUSH_NOTIFICATIONS_SETTINGS.setdefault("APNS_USE_SANDBOX", False) PUSH_NOTIFICATIONS_SETTINGS.setdefault("APNS_USE_ALTERNATIVE_PORT", False) PUSH_NOTIFICATIONS_SETTINGS.setdefault("APNS_TOPIC", None) +PUSH_NOTIFICATIONS_SETTINGS.setdefault("APNS_ERROR_TIMEOUT", 5) # WNS PUSH_NOTIFICATIONS_SETTINGS.setdefault("WNS_PACKAGE_SECURITY_ID", None) diff --git a/tests/test_apns_async_push_payload.py b/tests/test_apns_async_push_payload.py index ee808156..9a2cb5c7 100644 --- a/tests/test_apns_async_push_payload.py +++ b/tests/test_apns_async_push_payload.py @@ -3,7 +3,7 @@ from unittest import mock import pytest -from django.test import TestCase +from django.test import TestCase, override_settings try: @@ -276,3 +276,128 @@ def test_push_payload_with_content_available_not_set(self, mock_apns): req = args[0] assert "content-available" not in req.message["aps"] + + +class APNSErrorTimeoutTests(TestCase): + + @mock.patch("push_notifications.apns_async.asyncio.wait_for") + @mock.patch("push_notifications.apns_async.APNS", autospec=True) + @override_settings( + PUSH_NOTIFICATION_SETTINGS={ + 'APNS_ERROR_TIMEOUT': 15 + } + ) + def test_test_timeout_value_passed_to_wait_for(self, mock_apns, mock_wait_for): + mock_wait_for.return_value = mock.AsyncMock( + return_value=NotificationResult("123", "200") + ) + apns_send_message( + "123", + "Test message", + creds=TokenCredentials( + key="aaa", + key_id="bbb", + team_id="ccc", + ), + ) + + mock_wait_for.assert_called_once() + _, kwargs = mock_wait_for.call_args + assert kwargs["timeout"] == 15 + + @mock.patch("push_notifications.apns_async.asyncio.wait_for") + @mock.patch("push_notifications.apns_async.APNs", autospec=True) + def test_default_timeout_is_5_seconds(self, mock_apns, mock_wait_for): + mock_wait_for.return_value = mock.AsyncMock( + return_value=NotificationResult("123", "200") + ) + + apns_send_message( + "123", + "Test message", + creds=TokenCredentials( + key="aaa", + key_id="bbb", + team_id="ccc", + ), + ) + + mock_wait_for.assert_called_once() + _, kwargs = mock_wait_for.call_args + assert kwargs["timeout"] == 5 + + @mock.patch("push_notifications.apns_async.asyncio.wait_for") + @mock.patch("push_notifications.apns_async.APNs", autospec=True) + @override_settings( + PUSH_NOTIFICATIONS_SETTINGS={ + 'APNS_ERROR_TIMEOUT': 1 + } + ) + def test_short_timeout_value_is_respected(self, mock_apns, mock_wait_for): + mock_wait_for.return_value = mock.AsyncMock( + return_value=NotificationResult("123", "200") + ) + + apns_send_message( + "123", + "Test message", + creds=TokenCredentials( + key="aaa", + key_id="bbb", + team_id="ccc", + ), + ) + + mock_wait_for.assert_called_once() + _, kwargs = mock_wait_for.call_args + assert kwargs["timeout"] == 1 + + @mock.patch("push_notifications.apns_async.asyncio.wait_for") + @mock.patch("push_notifications.apns_async.APNs", autospec=True) + @override_settings( + PUSH_NOTIFICATIONS_SETTINGS={ + 'APNS_ERROR_TIMEOUT': 30 + } + ) + def test_long_timeout_value_is_respected(self, mock_apns, mock_wait_for): + mock_wait_for.return_value = mock.AsyncMock( + return_value=NotificationResult("123", "200") + ) + + apns_send_message( + "123", + "Test message", + creds=TokenCredentials( + key="aaa", + key_id="bbb", + team_id="ccc", + ), + ) + + mock_wait_for.assert_called_once() + _, kwargs = mock_wait_for.call_args + assert kwargs["timeout"] == 30 + + @mock.patch("push_notifications.apns_async.asyncio.wait_for") + @mock.patch("push_notifications.apns_async.APNs", autospec=True) + @override_settings( + PUSH_NOTIFICATIONS_SETTINGS={} + ) + def test_empty_settings_uses_default_timeout(self, mock_apns, mock_wait_for): + mock_wait_for.return_value = mock.AsyncMock( + return_value=NotificationResult("123", "200") + ) + + apns_send_message( + "123", + "Test message", + creds=TokenCredentials( + key="aaa", + key_id="bbb", + team_id="ccc", + ), + ) + + mock_wait_for.assert_called_once() + _, kwargs = mock_wait_for.call_args + assert kwargs["timeout"] == 5