From a56dd9014ab872efa26451b6036edb7741e6e9ec Mon Sep 17 00:00:00 2001 From: Lex Alexander Date: Tue, 5 May 2026 10:28:52 -0700 Subject: [PATCH 1/3] feat: Ensure service account file exists before initializing credentials --- pyfcm/baseapi.py | 8 ++++++++ tests/test_fcm.py | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/pyfcm/baseapi.py b/pyfcm/baseapi.py index 8ece016..d4dba03 100644 --- a/pyfcm/baseapi.py +++ b/pyfcm/baseapi.py @@ -8,6 +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 @@ -177,6 +178,13 @@ def _initialize_credentials(self): Initialize credentials and FCM endpoint if not already initialized. """ if self.credentials is None: + + missing_account_file = not path.isfile(self._service_account_file) + + if missing_account_file: + raise InvalidDataError(f"The service account file you passed does not exist at '{path}'. " + "Ensure it does not have any typos and exists." + ) 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..b7dfce3 100644 --- a/tests/test_fcm.py +++ b/tests/test_fcm.py @@ -8,6 +8,14 @@ def test_push_service_without_credentials(): except errors.AuthenticationError: pass +def test_push_service_with_incorrect_service_account_file(): + try: + # figure out why this goddamn test is not running + fcm = FCMNotification(service_account_file='./foo.json', project_id=None, credentials=None) + fcm.notify() + assert False, "Should raise InvalidDataError without correct service account file path" + except errors.InvalidDataError: + pass def test_push_service_directly_passed_credentials(push_service): # We should infer the project ID/endpoint from credentials From 9fea43ea62708a4d3bae53b7eea060f94ee7620a Mon Sep 17 00:00:00 2001 From: Lex Alexander Date: Fri, 15 May 2026 20:25:05 -0700 Subject: [PATCH 2/3] Remove comment and use service account file as path --- pyfcm/baseapi.py | 10 +++------- tests/test_fcm.py | 1 - 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/pyfcm/baseapi.py b/pyfcm/baseapi.py index d4dba03..071d2de 100644 --- a/pyfcm/baseapi.py +++ b/pyfcm/baseapi.py @@ -10,7 +10,6 @@ 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 @@ -178,14 +177,11 @@ def _initialize_credentials(self): Initialize credentials and FCM endpoint if not already initialized. """ if self.credentials is None: - - missing_account_file = not path.isfile(self._service_account_file) - - if missing_account_file: - raise InvalidDataError(f"The service account file you passed does not exist at '{path}'. " + if not path.isfile(self._service_account_file): + raise InvalidDataError(f"The service account file you passed does not exist at '{self._service_account_file}'. " "Ensure it does not have any typos and exists." ) - self.credentials = service_account.Credentials.from_service_account_file( + self.credentials = 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 b7dfce3..19b2708 100644 --- a/tests/test_fcm.py +++ b/tests/test_fcm.py @@ -10,7 +10,6 @@ def test_push_service_without_credentials(): def test_push_service_with_incorrect_service_account_file(): try: - # figure out why this goddamn test is not running fcm = FCMNotification(service_account_file='./foo.json', project_id=None, credentials=None) fcm.notify() assert False, "Should raise InvalidDataError without correct service account file path" From c4fb45f19e29d75dd971221421cee8abf5a717d9 Mon Sep 17 00:00:00 2001 From: Lex Alexander Date: Sat, 16 May 2026 04:25:50 -0700 Subject: [PATCH 3/3] Include regression test from Niccari's branch and bring back service_account --- pyfcm/baseapi.py | 4 ++-- tests/test_fcm.py | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/pyfcm/baseapi.py b/pyfcm/baseapi.py index 071d2de..fab80de 100644 --- a/pyfcm/baseapi.py +++ b/pyfcm/baseapi.py @@ -9,7 +9,7 @@ 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 @@ -181,7 +181,7 @@ def _initialize_credentials(self): raise InvalidDataError(f"The service account file you passed does not exist at '{self._service_account_file}'. " "Ensure it does not have any typos and exists." ) - self.credentials = Credentials.from_service_account_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 19b2708..396ba52 100644 --- a/tests/test_fcm.py +++ b/tests/test_fcm.py @@ -16,6 +16,26 @@ def test_push_service_with_incorrect_service_account_file(): except errors.InvalidDataError: pass +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() + 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