diff --git a/pyfcm/baseapi.py b/pyfcm/baseapi.py index 8ece016..fb8928c 100644 --- a/pyfcm/baseapi.py +++ b/pyfcm/baseapi.py @@ -8,7 +8,7 @@ import requests from requests.adapters import HTTPAdapter from urllib3 import Retry - +from os import path from google.oauth2 import service_account from google.oauth2.credentials import Credentials import google.auth.transport.requests @@ -177,6 +177,10 @@ def _initialize_credentials(self): Initialize credentials and FCM endpoint if not already initialized. """ if self.credentials is None: + if not path.isfile(self._service_account_file): + raise InvalidDataError( + "The service account file does not exist or is not a regular file." + ) self.credentials = service_account.Credentials.from_service_account_file( self._service_account_file, scopes=["https://www.googleapis.com/auth/firebase.messaging"], diff --git a/tests/test_fcm.py b/tests/test_fcm.py index 1c1284d..ef0c028 100644 --- a/tests/test_fcm.py +++ b/tests/test_fcm.py @@ -1,3 +1,5 @@ +import pytest + from pyfcm import FCMNotification, errors @@ -9,6 +11,54 @@ def test_push_service_without_credentials(): pass +def test_push_service_with_incorrect_service_account_file(tmp_path): + missing_file = tmp_path / "missing.json" + with pytest.raises(errors.InvalidDataError): + fcm = FCMNotification( + service_account_file=missing_file, project_id=None, credentials=None + ) + fcm.notify() + + +def test_push_service_does_not_leak_credentials(): + raw_credentials = '{"private_key":"TOP-SECRET-PRIVATE-KEY"}' + with pytest.raises(errors.InvalidDataError) as exc_info: + fcm = FCMNotification( + service_account_file=raw_credentials, + project_id=None, + credentials=None, + ) + fcm._initialize_credentials() + + error_message = str(exc_info.value) + assert raw_credentials not in error_message + assert "TOP-SECRET-PRIVATE-KEY" not in error_message + + +def test_push_service_with_valid_service_account_file(mocker): + # When the service account file exists, credentials must be built via + # google.oauth2.service_account.Credentials.from_service_account_file. + # google.oauth2.credentials.Credentials does not provide that method. + mocker.patch("pyfcm.baseapi.path.isfile", return_value=True) + mock_from_file = mocker.patch( + "pyfcm.baseapi.service_account.Credentials.from_service_account_file", + return_value="dummy-credentials", + ) + + fcm = FCMNotification( + service_account_file="./service_account.json", + project_id="test", + credentials=None, + ) + fcm._initialize_credentials() + + mock_from_file.assert_called_once_with( + "./service_account.json", + scopes=["https://www.googleapis.com/auth/firebase.messaging"], + ) + assert fcm.credentials == "dummy-credentials" + + def test_push_service_directly_passed_credentials(push_service): # We should infer the project ID/endpoint from credentials # without the need to explcitily pass it