From eb681bb732d3f8424f312ea845c001843e557c06 Mon Sep 17 00:00:00 2001 From: jonathanedey Date: Mon, 9 Jun 2025 11:58:50 -0400 Subject: [PATCH] change(fcm): Remove deprecated FCM APIs --- firebase_admin/_gapic_utils.py | 122 ------- firebase_admin/messaging.py | 118 ------- integration/test_messaging.py | 65 ---- requirements.txt | 1 - setup.py | 1 - snippets/messaging/cloud_messaging.py | 24 +- tests/test_exceptions.py | 161 --------- tests/test_messaging.py | 486 +------------------------- 8 files changed, 16 insertions(+), 962 deletions(-) delete mode 100644 firebase_admin/_gapic_utils.py diff --git a/firebase_admin/_gapic_utils.py b/firebase_admin/_gapic_utils.py deleted file mode 100644 index 3c975808c..000000000 --- a/firebase_admin/_gapic_utils.py +++ /dev/null @@ -1,122 +0,0 @@ -# Copyright 2021 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Internal utilities for interacting with Google API client.""" - -import io -import socket - -import googleapiclient -import httplib2 -import requests - -from firebase_admin import exceptions -from firebase_admin import _utils - - -def handle_platform_error_from_googleapiclient(error, handle_func=None): - """Constructs a ``FirebaseError`` from the given googleapiclient error. - - This can be used to handle errors returned by Google Cloud Platform (GCP) APIs. - - Args: - error: An error raised by the googleapiclient while making an HTTP call to a GCP API. - handle_func: A function that can be used to handle platform errors in a custom way. When - specified, this function will be called with three arguments. It has the same - signature as ```_handle_func_googleapiclient``, but may return ``None``. - - Returns: - FirebaseError: A ``FirebaseError`` that can be raised to the user code. - """ - if not isinstance(error, googleapiclient.errors.HttpError): - return handle_googleapiclient_error(error) - - content = error.content.decode() - status_code = error.resp.status - error_dict, message = _utils._parse_platform_error(content, status_code) # pylint: disable=protected-access - http_response = _http_response_from_googleapiclient_error(error) - exc = None - if handle_func: - exc = handle_func(error, message, error_dict, http_response) - - return exc if exc else _handle_func_googleapiclient(error, message, error_dict, http_response) - - -def _handle_func_googleapiclient(error, message, error_dict, http_response): - """Constructs a ``FirebaseError`` from the given GCP error. - - Args: - error: An error raised by the googleapiclient module while making an HTTP call. - message: A message to be included in the resulting ``FirebaseError``. - error_dict: Parsed GCP error response. - http_response: A requests HTTP response object to associate with the exception. - - Returns: - FirebaseError: A ``FirebaseError`` that can be raised to the user code or None. - """ - code = error_dict.get('status') - return handle_googleapiclient_error(error, message, code, http_response) - - -def handle_googleapiclient_error(error, message=None, code=None, http_response=None): - """Constructs a ``FirebaseError`` from the given googleapiclient error. - - This method is agnostic of the remote service that produced the error, whether it is a GCP - service or otherwise. Therefore, this method does not attempt to parse the error response in - any way. - - Args: - error: An error raised by the googleapiclient module while making an HTTP call. - message: A message to be included in the resulting ``FirebaseError`` (optional). If not - specified the string representation of the ``error`` argument is used as the message. - code: A GCP error code that will be used to determine the resulting error type (optional). - If not specified the HTTP status code on the error response is used to determine a - suitable error code. - http_response: A requests HTTP response object to associate with the exception (optional). - If not specified, one will be created from the ``error``. - - Returns: - FirebaseError: A ``FirebaseError`` that can be raised to the user code. - """ - if isinstance(error, socket.timeout) or ( - isinstance(error, socket.error) and 'timed out' in str(error)): - return exceptions.DeadlineExceededError( - message='Timed out while making an API call: {0}'.format(error), - cause=error) - if isinstance(error, httplib2.ServerNotFoundError): - return exceptions.UnavailableError( - message='Failed to establish a connection: {0}'.format(error), - cause=error) - if not isinstance(error, googleapiclient.errors.HttpError): - return exceptions.UnknownError( - message='Unknown error while making a remote service call: {0}'.format(error), - cause=error) - - if not code: - code = _utils._http_status_to_error_code(error.resp.status) # pylint: disable=protected-access - if not message: - message = str(error) - if not http_response: - http_response = _http_response_from_googleapiclient_error(error) - - err_type = _utils._error_code_to_exception_type(code) # pylint: disable=protected-access - return err_type(message=message, cause=error, http_response=http_response) - - -def _http_response_from_googleapiclient_error(error): - """Creates a requests HTTP Response object from the given googleapiclient error.""" - resp = requests.models.Response() - resp.raw = io.BytesIO(error.content) - resp.status_code = error.resp.status - return resp diff --git a/firebase_admin/messaging.py b/firebase_admin/messaging.py index 99dc93a67..0e3a55f49 100644 --- a/firebase_admin/messaging.py +++ b/firebase_admin/messaging.py @@ -18,21 +18,16 @@ from typing import Any, Callable, Dict, List, Optional, cast import concurrent.futures import json -import warnings import asyncio import logging import requests import httpx -from googleapiclient import http -from googleapiclient import _auth - import firebase_admin from firebase_admin import ( _http_client, _messaging_encoder, _messaging_utils, - _gapic_utils, _utils, exceptions, App @@ -72,8 +67,6 @@ 'WebpushNotificationAction', 'send', - 'send_all', - 'send_multicast', 'send_each', 'send_each_async', 'send_each_for_multicast', @@ -246,64 +239,6 @@ def send_each_for_multicast(multicast_message, dry_run=False, app=None): ) for token in multicast_message.tokens] return _get_messaging_service(app).send_each(messages, dry_run) -def send_all(messages, dry_run=False, app=None): - """Sends the given list of messages via Firebase Cloud Messaging as a single batch. - - If the ``dry_run`` mode is enabled, the message will not be actually delivered to the - recipients. Instead, FCM performs all the usual validations and emulates the send operation. - - Args: - messages: A list of ``messaging.Message`` instances. - dry_run: A boolean indicating whether to run the operation in dry run mode (optional). - app: An App instance (optional). - - Returns: - BatchResponse: A ``messaging.BatchResponse`` instance. - - Raises: - FirebaseError: If an error occurs while sending the message to the FCM service. - ValueError: If the input arguments are invalid. - - send_all() is deprecated. Use send_each() instead. - """ - warnings.warn('send_all() is deprecated. Use send_each() instead.', DeprecationWarning) - return _get_messaging_service(app).send_all(messages, dry_run) - -def send_multicast(multicast_message, dry_run=False, app=None): - """Sends the given mutlicast message to all tokens via Firebase Cloud Messaging (FCM). - - If the ``dry_run`` mode is enabled, the message will not be actually delivered to the - recipients. Instead, FCM performs all the usual validations and emulates the send operation. - - Args: - multicast_message: An instance of ``messaging.MulticastMessage``. - dry_run: A boolean indicating whether to run the operation in dry run mode (optional). - app: An App instance (optional). - - Returns: - BatchResponse: A ``messaging.BatchResponse`` instance. - - Raises: - FirebaseError: If an error occurs while sending the message to the FCM service. - ValueError: If the input arguments are invalid. - - send_multicast() is deprecated. Use send_each_for_multicast() instead. - """ - warnings.warn('send_multicast() is deprecated. Use send_each_for_multicast() instead.', - DeprecationWarning) - if not isinstance(multicast_message, MulticastMessage): - raise ValueError('Message must be an instance of messaging.MulticastMessage class.') - messages = [Message( - data=multicast_message.data, - notification=multicast_message.notification, - android=multicast_message.android, - webpush=multicast_message.webpush, - apns=multicast_message.apns, - fcm_options=multicast_message.fcm_options, - token=token - ) for token in multicast_message.tokens] - return _get_messaging_service(app).send_all(messages, dry_run) - def subscribe_to_topic(tokens, topic, app=None): """Subscribes a list of registration tokens to an FCM topic. @@ -472,7 +407,6 @@ def __init__(self, app: App) -> None: self._client = _http_client.JsonHttpClient(credential=self._credential, timeout=timeout) self._async_client = _http_client.HttpxAsyncClient( credential=self._credential, timeout=timeout) - self._build_transport = _auth.authorized_http @classmethod def encode_message(cls, message): @@ -555,45 +489,6 @@ async def send_data(data): message='Unknown error while making remote service calls: {0}'.format(error), cause=error) - - def send_all(self, messages, dry_run=False): - """Sends the given messages to FCM via the batch API.""" - if not isinstance(messages, list): - raise ValueError('messages must be a list of messaging.Message instances.') - if len(messages) > 500: - raise ValueError('messages must not contain more than 500 elements.') - - responses = [] - - def batch_callback(_, response, error): - exception = None - if error: - exception = self._handle_batch_error(error) - send_response = SendResponse(response, exception) - responses.append(send_response) - - batch = http.BatchHttpRequest( - callback=batch_callback, batch_uri=_MessagingService.FCM_BATCH_URL) - transport = self._build_transport(self._credential) - for message in messages: - body = json.dumps(self._message_data(message, dry_run)) - req = http.HttpRequest( - http=transport, - postproc=self._postproc, - uri=self._fcm_url, - method='POST', - body=body, - headers=self._fcm_headers - ) - batch.add(req) - - try: - batch.execute() - except Exception as error: - raise self._handle_batch_error(error) - else: - return BatchResponse(responses) - def make_topic_management_request(self, tokens, topic, operation): """Invokes the IID service for topic management functionality.""" if isinstance(tokens, str): @@ -670,11 +565,6 @@ def _handle_iid_error(self, error): return _utils.handle_requests_error(error, msg) - def _handle_batch_error(self, error): - """Handles errors received from the googleapiclient while making batch requests.""" - return _gapic_utils.handle_platform_error_from_googleapiclient( - error, _MessagingService._build_fcm_error_googleapiclient) - def close(self) -> None: asyncio.run(self._async_client.aclose()) @@ -700,14 +590,6 @@ def _build_fcm_error_httpx( message, cause=error, http_response=error.response) if exc_type else None return exc_type(message, cause=error) if exc_type else None - - @classmethod - def _build_fcm_error_googleapiclient(cls, error, message, error_dict, http_response): - """Parses an error response from the FCM API and creates a FCM-specific exception if - appropriate.""" - exc_type = cls._build_fcm_error(error_dict) - return exc_type(message, cause=error, http_response=http_response) if exc_type else None - @classmethod def _build_fcm_error( cls, diff --git a/integration/test_messaging.py b/integration/test_messaging.py index 296a4d338..804691962 100644 --- a/integration/test_messaging.py +++ b/integration/test_messaging.py @@ -149,71 +149,6 @@ def test_send_each_for_multicast(): assert response.exception is not None assert response.message_id is None -@pytest.mark.skip(reason="Replaced with test_send_each") -def test_send_all(): - messages = [ - messaging.Message( - topic='foo-bar', notification=messaging.Notification('Title', 'Body')), - messaging.Message( - topic='foo-bar', notification=messaging.Notification('Title', 'Body')), - messaging.Message( - token='not-a-token', notification=messaging.Notification('Title', 'Body')), - ] - - batch_response = messaging.send_all(messages, dry_run=True) - - assert batch_response.success_count == 2 - assert batch_response.failure_count == 1 - assert len(batch_response.responses) == 3 - - response = batch_response.responses[0] - assert response.success is True - assert response.exception is None - assert re.match('^projects/.*/messages/.*$', response.message_id) - - response = batch_response.responses[1] - assert response.success is True - assert response.exception is None - assert re.match('^projects/.*/messages/.*$', response.message_id) - - response = batch_response.responses[2] - assert response.success is False - assert isinstance(response.exception, exceptions.InvalidArgumentError) - assert response.message_id is None - -@pytest.mark.skip(reason="Replaced with test_send_each_500") -def test_send_all_500(): - messages = [] - for msg_number in range(500): - topic = 'foo-bar-{0}'.format(msg_number % 10) - messages.append(messaging.Message(topic=topic)) - - batch_response = messaging.send_all(messages, dry_run=True) - - assert batch_response.success_count == 500 - assert batch_response.failure_count == 0 - assert len(batch_response.responses) == 500 - for response in batch_response.responses: - assert response.success is True - assert response.exception is None - assert re.match('^projects/.*/messages/.*$', response.message_id) - -@pytest.mark.skip(reason="Replaced with test_send_each_for_multicast") -def test_send_multicast(): - multicast = messaging.MulticastMessage( - notification=messaging.Notification('Title', 'Body'), - tokens=['not-a-token', 'also-not-a-token']) - - batch_response = messaging.send_multicast(multicast) - - assert batch_response.success_count == 0 - assert batch_response.failure_count == 2 - assert len(batch_response.responses) == 2 - for response in batch_response.responses: - assert response.success is False - assert response.exception is not None - assert response.message_id is None - def test_subscribe(): resp = messaging.subscribe_to_topic(_REGISTRATION_TOKEN, 'mock-topic') assert resp.success_count + resp.failure_count == 1 diff --git a/requirements.txt b/requirements.txt index ba6f2f947..b5642b549 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,6 @@ respx == 0.22.0 cachecontrol >= 0.12.14 google-api-core[grpc] >= 1.22.1, < 3.0.0dev; platform.python_implementation != 'PyPy' -google-api-python-client >= 1.7.8 google-cloud-firestore >= 2.19.0; platform.python_implementation != 'PyPy' google-cloud-storage >= 1.37.1 pyjwt[crypto] >= 2.5.0 diff --git a/setup.py b/setup.py index e92d207aa..b9eb11806 100644 --- a/setup.py +++ b/setup.py @@ -39,7 +39,6 @@ install_requires = [ 'cachecontrol>=0.12.14', 'google-api-core[grpc] >= 1.22.1, < 3.0.0dev; platform.python_implementation != "PyPy"', - 'google-api-python-client >= 1.7.8', 'google-cloud-firestore>=2.19.0; platform.python_implementation != "PyPy"', 'google-cloud-storage>=1.37.1', 'pyjwt[crypto] >= 2.5.0', diff --git a/snippets/messaging/cloud_messaging.py b/snippets/messaging/cloud_messaging.py index bb63db065..18a992dcc 100644 --- a/snippets/messaging/cloud_messaging.py +++ b/snippets/messaging/cloud_messaging.py @@ -222,9 +222,9 @@ def unsubscribe_from_topic(): # [END unsubscribe] -def send_all(): +def send_each(): registration_token = 'YOUR_REGISTRATION_TOKEN' - # [START send_all] + # [START send_each] # Create a list containing up to 500 messages. messages = [ messaging.Message( @@ -238,15 +238,15 @@ def send_all(): ), ] - response = messaging.send_all(messages) + response = messaging.send_each(messages) # See the BatchResponse reference documentation # for the contents of response. print('{0} messages were sent successfully'.format(response.success_count)) - # [END send_all] + # [END send_each] -def send_multicast(): - # [START send_multicast] +def send_each_for_multicast(): + # [START send_each_for_multicast] # Create a list containing up to 500 registration tokens. # These registration tokens come from the client FCM SDKs. registration_tokens = [ @@ -259,15 +259,15 @@ def send_multicast(): data={'score': '850', 'time': '2:45'}, tokens=registration_tokens, ) - response = messaging.send_multicast(message) + response = messaging.send_each_for_multicast(message) # See the BatchResponse reference documentation # for the contents of response. print('{0} messages were sent successfully'.format(response.success_count)) - # [END send_multicast] + # [END send_each_for_multicast] -def send_multicast_and_handle_errors(): - # [START send_multicast_error] +def send_each_for_multicast_and_handle_errors(): + # [START send_each_for_multicast_error] # These registration tokens come from the client FCM SDKs. registration_tokens = [ 'YOUR_REGISTRATION_TOKEN_1', @@ -279,7 +279,7 @@ def send_multicast_and_handle_errors(): data={'score': '850', 'time': '2:45'}, tokens=registration_tokens, ) - response = messaging.send_multicast(message) + response = messaging.send_each_for_multicast(message) if response.failure_count > 0: responses = response.responses failed_tokens = [] @@ -288,4 +288,4 @@ def send_multicast_and_handle_errors(): # The order of responses corresponds to the order of the registration tokens. failed_tokens.append(registration_tokens[idx]) print('List of tokens that caused failures: {0}'.format(failed_tokens)) - # [END send_multicast_error] + # [END send_each_for_multicast_error] diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 4347c838a..fa1276feb 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -14,17 +14,12 @@ import io import json -import socket -import httplib2 -import pytest import requests from requests import models -from googleapiclient import errors from firebase_admin import exceptions from firebase_admin import _utils -from firebase_admin import _gapic_utils _NOT_FOUND_ERROR_DICT = { @@ -178,159 +173,3 @@ def _create_response(self, status=500, payload=None): resp.raw = io.BytesIO(payload.encode()) exc = requests.exceptions.RequestException('Test error', response=resp) return resp, exc - - -class TestGoogleApiClient: - - @pytest.mark.parametrize('error', [ - socket.timeout('Test error'), - socket.error('Read timed out') - ]) - def test_googleapicleint_timeout_error(self, error): - firebase_error = _gapic_utils.handle_googleapiclient_error(error) - assert isinstance(firebase_error, exceptions.DeadlineExceededError) - assert str(firebase_error) == 'Timed out while making an API call: {0}'.format(error) - assert firebase_error.cause is error - assert firebase_error.http_response is None - - def test_googleapiclient_connection_error(self): - error = httplib2.ServerNotFoundError('Test error') - firebase_error = _gapic_utils.handle_googleapiclient_error(error) - assert isinstance(firebase_error, exceptions.UnavailableError) - assert str(firebase_error) == 'Failed to establish a connection: Test error' - assert firebase_error.cause is error - assert firebase_error.http_response is None - - def test_unknown_transport_error(self): - error = socket.error('Test error') - firebase_error = _gapic_utils.handle_googleapiclient_error(error) - assert isinstance(firebase_error, exceptions.UnknownError) - assert str(firebase_error) == 'Unknown error while making a remote service call: Test error' - assert firebase_error.cause is error - assert firebase_error.http_response is None - - def test_http_response(self): - error = self._create_http_error() - firebase_error = _gapic_utils.handle_googleapiclient_error(error) - assert isinstance(firebase_error, exceptions.InternalError) - assert str(firebase_error) == str(error) - assert firebase_error.cause is error - assert firebase_error.http_response.status_code == 500 - assert firebase_error.http_response.content.decode() == 'Body' - - def test_http_response_with_unknown_status(self): - error = self._create_http_error(status=501) - firebase_error = _gapic_utils.handle_googleapiclient_error(error) - assert isinstance(firebase_error, exceptions.UnknownError) - assert str(firebase_error) == str(error) - assert firebase_error.cause is error - assert firebase_error.http_response.status_code == 501 - assert firebase_error.http_response.content.decode() == 'Body' - - def test_http_response_with_message(self): - error = self._create_http_error() - firebase_error = _gapic_utils.handle_googleapiclient_error( - error, message='Explicit error message') - assert isinstance(firebase_error, exceptions.InternalError) - assert str(firebase_error) == 'Explicit error message' - assert firebase_error.cause is error - assert firebase_error.http_response.status_code == 500 - assert firebase_error.http_response.content.decode() == 'Body' - - def test_http_response_with_code(self): - error = self._create_http_error() - firebase_error = _gapic_utils.handle_googleapiclient_error( - error, code=exceptions.UNAVAILABLE) - assert isinstance(firebase_error, exceptions.UnavailableError) - assert str(firebase_error) == str(error) - assert firebase_error.cause is error - assert firebase_error.http_response.status_code == 500 - assert firebase_error.http_response.content.decode() == 'Body' - - def test_http_response_with_message_and_code(self): - error = self._create_http_error() - firebase_error = _gapic_utils.handle_googleapiclient_error( - error, message='Explicit error message', code=exceptions.UNAVAILABLE) - assert isinstance(firebase_error, exceptions.UnavailableError) - assert str(firebase_error) == 'Explicit error message' - assert firebase_error.cause is error - assert firebase_error.http_response.status_code == 500 - assert firebase_error.http_response.content.decode() == 'Body' - - def test_handle_platform_error(self): - error = self._create_http_error(payload=_NOT_FOUND_PAYLOAD) - firebase_error = _gapic_utils.handle_platform_error_from_googleapiclient(error) - assert isinstance(firebase_error, exceptions.NotFoundError) - assert str(firebase_error) == 'test error' - assert firebase_error.cause is error - assert firebase_error.http_response.status_code == 500 - assert firebase_error.http_response.content.decode() == _NOT_FOUND_PAYLOAD - - def test_handle_platform_error_with_no_response(self): - error = socket.error('Test error') - firebase_error = _gapic_utils.handle_platform_error_from_googleapiclient(error) - assert isinstance(firebase_error, exceptions.UnknownError) - assert str(firebase_error) == 'Unknown error while making a remote service call: Test error' - assert firebase_error.cause is error - assert firebase_error.http_response is None - - def test_handle_platform_error_with_no_error_code(self): - error = self._create_http_error(payload='no error code') - firebase_error = _gapic_utils.handle_platform_error_from_googleapiclient(error) - assert isinstance(firebase_error, exceptions.InternalError) - message = 'Unexpected HTTP response with status: 500; body: no error code' - assert str(firebase_error) == message - assert firebase_error.cause is error - assert firebase_error.http_response.status_code == 500 - assert firebase_error.http_response.content.decode() == 'no error code' - - def test_handle_platform_error_with_custom_handler(self): - error = self._create_http_error(payload=_NOT_FOUND_PAYLOAD) - invocations = [] - - def _custom_handler(cause, message, error_dict, http_response): - invocations.append((cause, message, error_dict, http_response)) - return exceptions.InvalidArgumentError('Custom message', cause, http_response) - - firebase_error = _gapic_utils.handle_platform_error_from_googleapiclient( - error, _custom_handler) - - assert isinstance(firebase_error, exceptions.InvalidArgumentError) - assert str(firebase_error) == 'Custom message' - assert firebase_error.cause is error - assert firebase_error.http_response.status_code == 500 - assert firebase_error.http_response.content.decode() == _NOT_FOUND_PAYLOAD - assert len(invocations) == 1 - args = invocations[0] - assert len(args) == 4 - assert args[0] is error - assert args[1] == 'test error' - assert args[2] == _NOT_FOUND_ERROR_DICT - assert args[3] is not None - - def test_handle_platform_error_with_custom_handler_ignore(self): - error = self._create_http_error(payload=_NOT_FOUND_PAYLOAD) - invocations = [] - - def _custom_handler(cause, message, error_dict, http_response): - invocations.append((cause, message, error_dict, http_response)) - - firebase_error = _gapic_utils.handle_platform_error_from_googleapiclient( - error, _custom_handler) - - assert isinstance(firebase_error, exceptions.NotFoundError) - assert str(firebase_error) == 'test error' - assert firebase_error.cause is error - assert firebase_error.http_response.status_code == 500 - assert firebase_error.http_response.content.decode() == _NOT_FOUND_PAYLOAD - assert len(invocations) == 1 - args = invocations[0] - assert len(args) == 4 - assert args[0] is error - assert args[1] == 'test error' - assert args[2] == _NOT_FOUND_ERROR_DICT - assert args[3] is not None - - def _create_http_error(self, status=500, payload='Body'): - resp = httplib2.Response({'status': status}) - return errors.HttpError(resp, payload.encode()) diff --git a/tests/test_messaging.py b/tests/test_messaging.py index 76cee2a33..341fd9e07 100644 --- a/tests/test_messaging.py +++ b/tests/test_messaging.py @@ -20,8 +20,6 @@ import httpx import respx -from googleapiclient import http -from googleapiclient import _helpers import pytest import firebase_admin @@ -1826,17 +1824,7 @@ def test_send_unknown_fcm_error_code(self, status): self._assert_request(recorder[0], 'POST', self._get_url('explicit-project-id'), body) -class _HttpMockException: - - def __init__(self, exc): - self._exc = exc - - def request(self, url, **kwargs): - raise self._exc - - -class TestBatch: - +class TestSendEach(): @classmethod def setup_class(cls): cred = testutils.MockCredential() @@ -1856,40 +1844,6 @@ def _instrument_messaging_service(self, response_dict, app=None): testutils.MockRequestBasedMultiRequestAdapter(response_dict, recorder)) return fcm_service, recorder - def _instrument_batch_messaging_service(self, app=None, status=200, payload='', exc=None): - def build_mock_transport(_): - if exc: - return _HttpMockException(exc) - - if status == 200: - content_type = 'multipart/mixed; boundary=boundary' - else: - content_type = 'application/json' - return http.HttpMockSequence([ - ({'status': str(status), 'content-type': content_type}, payload), - ]) - - if not app: - app = firebase_admin.get_app() - - fcm_service = messaging._get_messaging_service(app) - fcm_service._build_transport = build_mock_transport - return fcm_service - - def _batch_payload(self, payloads): - # payloads should be a list of (status_code, content) tuples - payload = '' - _playload_format = """--boundary\r\nContent-Type: application/http\r\n\ -Content-ID: \r\n\r\nHTTP/1.1 {} Success\r\n\ -Content-Type: application/json; charset=UTF-8\r\n\r\n{}\r\n\r\n""" - for (index, (status_code, content)) in enumerate(payloads): - payload += _playload_format.format(str(index + 1), str(status_code), content) - payload += '--boundary--' - return payload - - -class TestSendEach(TestBatch): - def test_no_project_id(self): def evaluate(): app = firebase_admin.initialize_app(testutils.MockCredential(), name='no_project_id') @@ -1948,12 +1902,6 @@ async def test_send_each_async(self): batch_response = await messaging.send_each_async([msg1, msg2, msg3], dry_run=True) - # try: - # batch_response = await messaging.send_each_async([msg1, msg2], dry_run=True) - # except Exception as error: - # if isinstance(error.cause.__cause__, StopIteration): - # raise Exception('Received more requests than mocks') - assert batch_response.success_count == 3 assert batch_response.failure_count == 0 assert len(batch_response.responses) == 3 @@ -2217,19 +2165,19 @@ def test_send_each_fcm_error_code(self, status, fcm_error_code, exc_type): check_exception(exception, 'test error', status) -class TestSendEachForMulticast(TestBatch): +class TestSendEachForMulticast(TestSendEach): def test_no_project_id(self): def evaluate(): app = firebase_admin.initialize_app(testutils.MockCredential(), name='no_project_id') with pytest.raises(ValueError): - messaging.send_all([messaging.Message(topic='foo')], app=app) + messaging.send_each([messaging.Message(topic='foo')], app=app) testutils.run_without_project_id(evaluate) @pytest.mark.parametrize('msg', NON_LIST_ARGS) def test_invalid_send_each_for_multicast(self, msg): with pytest.raises(ValueError) as excinfo: - messaging.send_multicast(msg) + messaging.send_each_for_multicast(msg) expected = 'Message must be an instance of messaging.MulticastMessage class.' assert str(excinfo.value) == expected @@ -2338,432 +2286,6 @@ def test_send_each_for_multicast_fcm_error_code(self, status): check_exception(exception, 'test error', status) -class TestSendAll(TestBatch): - - def test_no_project_id(self): - def evaluate(): - app = firebase_admin.initialize_app(testutils.MockCredential(), name='no_project_id') - with pytest.raises(ValueError): - messaging.send_all([messaging.Message(topic='foo')], app=app) - testutils.run_without_project_id(evaluate) - - @pytest.mark.parametrize('msg', NON_LIST_ARGS) - def test_invalid_send_all(self, msg): - with pytest.raises(ValueError) as excinfo: - messaging.send_all(msg) - if isinstance(msg, list): - expected = 'Message must be an instance of messaging.Message class.' - assert str(excinfo.value) == expected - else: - expected = 'messages must be a list of messaging.Message instances.' - assert str(excinfo.value) == expected - - def test_invalid_over_500(self): - msg = messaging.Message(topic='foo') - with pytest.raises(ValueError) as excinfo: - messaging.send_all([msg for _ in range(0, 501)]) - expected = 'messages must not contain more than 500 elements.' - assert str(excinfo.value) == expected - - def test_send_all(self): - payload = json.dumps({'name': 'message-id'}) - _ = self._instrument_batch_messaging_service( - payload=self._batch_payload([(200, payload), (200, payload)])) - msg = messaging.Message(topic='foo') - batch_response = messaging.send_all([msg, msg], dry_run=True) - assert batch_response.success_count == 2 - assert batch_response.failure_count == 0 - assert len(batch_response.responses) == 2 - assert [r.message_id for r in batch_response.responses] == ['message-id', 'message-id'] - assert all([r.success for r in batch_response.responses]) - assert not any([r.exception for r in batch_response.responses]) - - def test_send_all_with_positional_param_enforcement(self): - payload = json.dumps({'name': 'message-id'}) - _ = self._instrument_batch_messaging_service( - payload=self._batch_payload([(200, payload), (200, payload)])) - msg = messaging.Message(topic='foo') - - enforcement = _helpers.positional_parameters_enforcement - _helpers.positional_parameters_enforcement = _helpers.POSITIONAL_EXCEPTION - try: - batch_response = messaging.send_all([msg, msg], dry_run=True) - assert batch_response.success_count == 2 - finally: - _helpers.positional_parameters_enforcement = enforcement - - @pytest.mark.parametrize('status', HTTP_ERROR_CODES) - def test_send_all_detailed_error(self, status): - success_payload = json.dumps({'name': 'message-id'}) - error_payload = json.dumps({ - 'error': { - 'status': 'INVALID_ARGUMENT', - 'message': 'test error' - } - }) - _ = self._instrument_batch_messaging_service( - payload=self._batch_payload([(200, success_payload), (status, error_payload)])) - msg = messaging.Message(topic='foo') - batch_response = messaging.send_all([msg, msg]) - assert batch_response.success_count == 1 - assert batch_response.failure_count == 1 - assert len(batch_response.responses) == 2 - success_response = batch_response.responses[0] - assert success_response.message_id == 'message-id' - assert success_response.success is True - assert success_response.exception is None - error_response = batch_response.responses[1] - assert error_response.message_id is None - assert error_response.success is False - exception = error_response.exception - assert isinstance(exception, exceptions.InvalidArgumentError) - check_exception(exception, 'test error', status) - - @pytest.mark.parametrize('status', HTTP_ERROR_CODES) - def test_send_all_canonical_error_code(self, status): - success_payload = json.dumps({'name': 'message-id'}) - error_payload = json.dumps({ - 'error': { - 'status': 'NOT_FOUND', - 'message': 'test error' - } - }) - _ = self._instrument_batch_messaging_service( - payload=self._batch_payload([(200, success_payload), (status, error_payload)])) - msg = messaging.Message(topic='foo') - batch_response = messaging.send_all([msg, msg]) - assert batch_response.success_count == 1 - assert batch_response.failure_count == 1 - assert len(batch_response.responses) == 2 - success_response = batch_response.responses[0] - assert success_response.message_id == 'message-id' - assert success_response.success is True - assert success_response.exception is None - error_response = batch_response.responses[1] - assert error_response.message_id is None - assert error_response.success is False - exception = error_response.exception - assert isinstance(exception, exceptions.NotFoundError) - check_exception(exception, 'test error', status) - - @pytest.mark.parametrize('status', HTTP_ERROR_CODES) - @pytest.mark.parametrize('fcm_error_code, exc_type', FCM_ERROR_CODES.items()) - def test_send_all_fcm_error_code(self, status, fcm_error_code, exc_type): - success_payload = json.dumps({'name': 'message-id'}) - error_payload = json.dumps({ - 'error': { - 'status': 'INVALID_ARGUMENT', - 'message': 'test error', - 'details': [ - { - '@type': 'type.googleapis.com/google.firebase.fcm.v1.FcmError', - 'errorCode': fcm_error_code, - }, - ], - } - }) - _ = self._instrument_batch_messaging_service( - payload=self._batch_payload([(200, success_payload), (status, error_payload)])) - msg = messaging.Message(topic='foo') - batch_response = messaging.send_all([msg, msg]) - assert batch_response.success_count == 1 - assert batch_response.failure_count == 1 - assert len(batch_response.responses) == 2 - success_response = batch_response.responses[0] - assert success_response.message_id == 'message-id' - assert success_response.success is True - assert success_response.exception is None - error_response = batch_response.responses[1] - assert error_response.message_id is None - assert error_response.success is False - exception = error_response.exception - assert isinstance(exception, exc_type) - check_exception(exception, 'test error', status) - - @pytest.mark.parametrize('status, exc_type', HTTP_ERROR_CODES.items()) - def test_send_all_batch_error(self, status, exc_type): - _ = self._instrument_batch_messaging_service(status=status, payload='{}') - msg = messaging.Message(topic='foo') - with pytest.raises(exc_type) as excinfo: - messaging.send_all([msg]) - expected = 'Unexpected HTTP response with status: {0}; body: {{}}'.format(status) - check_exception(excinfo.value, expected, status) - - @pytest.mark.parametrize('status', HTTP_ERROR_CODES) - def test_send_all_batch_detailed_error(self, status): - payload = json.dumps({ - 'error': { - 'status': 'INVALID_ARGUMENT', - 'message': 'test error' - } - }) - _ = self._instrument_batch_messaging_service(status=status, payload=payload) - msg = messaging.Message(topic='foo') - with pytest.raises(exceptions.InvalidArgumentError) as excinfo: - messaging.send_all([msg]) - check_exception(excinfo.value, 'test error', status) - - @pytest.mark.parametrize('status', HTTP_ERROR_CODES) - def test_send_all_batch_canonical_error_code(self, status): - payload = json.dumps({ - 'error': { - 'status': 'NOT_FOUND', - 'message': 'test error' - } - }) - _ = self._instrument_batch_messaging_service(status=status, payload=payload) - msg = messaging.Message(topic='foo') - with pytest.raises(exceptions.NotFoundError) as excinfo: - messaging.send_all([msg]) - check_exception(excinfo.value, 'test error', status) - - @pytest.mark.parametrize('status', HTTP_ERROR_CODES) - def test_send_all_batch_fcm_error_code(self, status): - payload = json.dumps({ - 'error': { - 'status': 'INVALID_ARGUMENT', - 'message': 'test error', - 'details': [ - { - '@type': 'type.googleapis.com/google.firebase.fcm.v1.FcmError', - 'errorCode': 'UNREGISTERED', - }, - ], - } - }) - _ = self._instrument_batch_messaging_service(status=status, payload=payload) - msg = messaging.Message(topic='foo') - with pytest.raises(messaging.UnregisteredError) as excinfo: - messaging.send_all([msg]) - check_exception(excinfo.value, 'test error', status) - - def test_send_all_runtime_exception(self): - exc = BrokenPipeError('Test error') - _ = self._instrument_batch_messaging_service(exc=exc) - msg = messaging.Message(topic='foo') - - with pytest.raises(exceptions.UnknownError) as excinfo: - messaging.send_all([msg]) - - expected = 'Unknown error while making a remote service call: Test error' - assert str(excinfo.value) == expected - assert excinfo.value.cause is exc - assert excinfo.value.http_response is None - - def test_send_transport_init(self): - def track_call_count(build_transport): - def wrapper(credential): - wrapper.calls += 1 - return build_transport(credential) - wrapper.calls = 0 - return wrapper - - payload = json.dumps({'name': 'message-id'}) - fcm_service = self._instrument_batch_messaging_service( - payload=self._batch_payload([(200, payload), (200, payload)])) - build_mock_transport = fcm_service._build_transport - fcm_service._build_transport = track_call_count(build_mock_transport) - msg = messaging.Message(topic='foo') - - batch_response = messaging.send_all([msg, msg], dry_run=True) - assert batch_response.success_count == 2 - assert fcm_service._build_transport.calls == 1 - - batch_response = messaging.send_all([msg, msg], dry_run=True) - assert batch_response.success_count == 2 - assert fcm_service._build_transport.calls == 2 - - -class TestSendMulticast(TestBatch): - - def test_no_project_id(self): - def evaluate(): - app = firebase_admin.initialize_app(testutils.MockCredential(), name='no_project_id') - with pytest.raises(ValueError): - messaging.send_all([messaging.Message(topic='foo')], app=app) - testutils.run_without_project_id(evaluate) - - @pytest.mark.parametrize('msg', NON_LIST_ARGS) - def test_invalid_send_multicast(self, msg): - with pytest.raises(ValueError) as excinfo: - messaging.send_multicast(msg) - expected = 'Message must be an instance of messaging.MulticastMessage class.' - assert str(excinfo.value) == expected - - def test_send_multicast(self): - payload = json.dumps({'name': 'message-id'}) - _ = self._instrument_batch_messaging_service( - payload=self._batch_payload([(200, payload), (200, payload)])) - msg = messaging.MulticastMessage(tokens=['foo', 'foo']) - batch_response = messaging.send_multicast(msg, dry_run=True) - assert batch_response.success_count == 2 - assert batch_response.failure_count == 0 - assert len(batch_response.responses) == 2 - assert [r.message_id for r in batch_response.responses] == ['message-id', 'message-id'] - assert all([r.success for r in batch_response.responses]) - assert not any([r.exception for r in batch_response.responses]) - - @pytest.mark.parametrize('status', HTTP_ERROR_CODES) - def test_send_multicast_detailed_error(self, status): - success_payload = json.dumps({'name': 'message-id'}) - error_payload = json.dumps({ - 'error': { - 'status': 'INVALID_ARGUMENT', - 'message': 'test error' - } - }) - _ = self._instrument_batch_messaging_service( - payload=self._batch_payload([(200, success_payload), (status, error_payload)])) - msg = messaging.MulticastMessage(tokens=['foo', 'foo']) - batch_response = messaging.send_multicast(msg) - assert batch_response.success_count == 1 - assert batch_response.failure_count == 1 - assert len(batch_response.responses) == 2 - success_response = batch_response.responses[0] - assert success_response.message_id == 'message-id' - assert success_response.success is True - assert success_response.exception is None - error_response = batch_response.responses[1] - assert error_response.message_id is None - assert error_response.success is False - assert error_response.exception is not None - exception = error_response.exception - assert isinstance(exception, exceptions.InvalidArgumentError) - check_exception(exception, 'test error', status) - - @pytest.mark.parametrize('status', HTTP_ERROR_CODES) - def test_send_multicast_canonical_error_code(self, status): - success_payload = json.dumps({'name': 'message-id'}) - error_payload = json.dumps({ - 'error': { - 'status': 'NOT_FOUND', - 'message': 'test error' - } - }) - _ = self._instrument_batch_messaging_service( - payload=self._batch_payload([(200, success_payload), (status, error_payload)])) - msg = messaging.MulticastMessage(tokens=['foo', 'foo']) - batch_response = messaging.send_multicast(msg) - assert batch_response.success_count == 1 - assert batch_response.failure_count == 1 - assert len(batch_response.responses) == 2 - success_response = batch_response.responses[0] - assert success_response.message_id == 'message-id' - assert success_response.success is True - assert success_response.exception is None - error_response = batch_response.responses[1] - assert error_response.message_id is None - assert error_response.success is False - assert error_response.exception is not None - exception = error_response.exception - assert isinstance(exception, exceptions.NotFoundError) - check_exception(exception, 'test error', status) - - @pytest.mark.parametrize('status', HTTP_ERROR_CODES) - def test_send_multicast_fcm_error_code(self, status): - success_payload = json.dumps({'name': 'message-id'}) - error_payload = json.dumps({ - 'error': { - 'status': 'INVALID_ARGUMENT', - 'message': 'test error', - 'details': [ - { - '@type': 'type.googleapis.com/google.firebase.fcm.v1.FcmError', - 'errorCode': 'UNREGISTERED', - }, - ], - } - }) - _ = self._instrument_batch_messaging_service( - payload=self._batch_payload([(200, success_payload), (status, error_payload)])) - msg = messaging.MulticastMessage(tokens=['foo', 'foo']) - batch_response = messaging.send_multicast(msg) - assert batch_response.success_count == 1 - assert batch_response.failure_count == 1 - assert len(batch_response.responses) == 2 - success_response = batch_response.responses[0] - assert success_response.message_id == 'message-id' - assert success_response.success is True - assert success_response.exception is None - error_response = batch_response.responses[1] - assert error_response.message_id is None - assert error_response.success is False - assert error_response.exception is not None - exception = error_response.exception - assert isinstance(exception, messaging.UnregisteredError) - check_exception(exception, 'test error', status) - - @pytest.mark.parametrize('status, exc_type', HTTP_ERROR_CODES.items()) - def test_send_multicast_batch_error(self, status, exc_type): - _ = self._instrument_batch_messaging_service(status=status, payload='{}') - msg = messaging.MulticastMessage(tokens=['foo']) - with pytest.raises(exc_type) as excinfo: - messaging.send_multicast(msg) - expected = 'Unexpected HTTP response with status: {0}; body: {{}}'.format(status) - check_exception(excinfo.value, expected, status) - - @pytest.mark.parametrize('status', HTTP_ERROR_CODES) - def test_send_multicast_batch_detailed_error(self, status): - payload = json.dumps({ - 'error': { - 'status': 'INVALID_ARGUMENT', - 'message': 'test error' - } - }) - _ = self._instrument_batch_messaging_service(status=status, payload=payload) - msg = messaging.MulticastMessage(tokens=['foo']) - with pytest.raises(exceptions.InvalidArgumentError) as excinfo: - messaging.send_multicast(msg) - check_exception(excinfo.value, 'test error', status) - - @pytest.mark.parametrize('status', HTTP_ERROR_CODES) - def test_send_multicast_batch_canonical_error_code(self, status): - payload = json.dumps({ - 'error': { - 'status': 'NOT_FOUND', - 'message': 'test error' - } - }) - _ = self._instrument_batch_messaging_service(status=status, payload=payload) - msg = messaging.MulticastMessage(tokens=['foo']) - with pytest.raises(exceptions.NotFoundError) as excinfo: - messaging.send_multicast(msg) - check_exception(excinfo.value, 'test error', status) - - @pytest.mark.parametrize('status', HTTP_ERROR_CODES) - def test_send_multicast_batch_fcm_error_code(self, status): - payload = json.dumps({ - 'error': { - 'status': 'INVALID_ARGUMENT', - 'message': 'test error', - 'details': [ - { - '@type': 'type.googleapis.com/google.firebase.fcm.v1.FcmError', - 'errorCode': 'UNREGISTERED', - }, - ], - } - }) - _ = self._instrument_batch_messaging_service(status=status, payload=payload) - msg = messaging.MulticastMessage(tokens=['foo']) - with pytest.raises(messaging.UnregisteredError) as excinfo: - messaging.send_multicast(msg) - check_exception(excinfo.value, 'test error', status) - - def test_send_multicast_runtime_exception(self): - exc = BrokenPipeError('Test error') - _ = self._instrument_batch_messaging_service(exc=exc) - msg = messaging.MulticastMessage(tokens=['foo']) - - with pytest.raises(exceptions.UnknownError) as excinfo: - messaging.send_multicast(msg) - - expected = 'Unknown error while making a remote service call: Test error' - assert str(excinfo.value) == expected - assert excinfo.value.cause is exc - assert excinfo.value.http_response is None - - class TestTopicManagement: _DEFAULT_RESPONSE = json.dumps({'results': [{}, {'error': 'error_reason'}]})