Skip to content

Commit aaaf7f7

Browse files
committed
fix: Credentials type should be google.auth.credentials.Credentials
- changed _credentials, fcm_end_point to cached propety, for better type hint - added non project_id unittest for BaseAPI
1 parent fadf75d commit aaaf7f7

File tree

4 files changed

+45
-48
lines changed

4 files changed

+45
-48
lines changed

pyfcm/baseapi.py

Lines changed: 28 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# from __future__ import annotations
22

3+
from functools import cached_property
34
import json
45
import time
56
import threading
@@ -9,7 +10,7 @@
910
from urllib3 import Retry
1011

1112
from google.oauth2 import service_account
12-
from google.oauth2.credentials import Credentials
13+
from google.auth.credentials import Credentials
1314
import google.auth.transport.requests
1415

1516
from pyfcm.errors import (
@@ -41,7 +42,7 @@ def __init__(
4142
Attributes:
4243
service_account_file (str): path to service account JSON file
4344
project_id (str): project ID of Google account
44-
credentials (Credentials): Google oauth2 credentials instance, such as ADC
45+
credentials (Credentials): Google auth credentials instance, such as ADC, service account one
4546
proxy_dict (dict): proxy settings dictionary, use proxy (keys: `http`, `https`)
4647
env (dict): environment settings dictionary, for example "app_engine"
4748
json_encoder (BaseJSONEncoder): JSON encoder
@@ -53,9 +54,8 @@ def __init__(
5354
)
5455

5556
self._service_account_file = service_account_file
56-
self._fcm_end_point = None
5757
self._project_id = project_id
58-
self.credentials = credentials
58+
self._provided_credentials = credentials
5959
self.custom_adapter = adapter
6060
self.thread_local = threading.local()
6161

@@ -76,22 +76,28 @@ def __init__(
7676

7777
self.json_encoder = json_encoder
7878

79-
@property
79+
@cached_property
80+
def _credentials(self) -> Credentials:
81+
if self._provided_credentials is not None:
82+
return self._provided_credentials
83+
84+
credentials = service_account.Credentials.from_service_account_file(
85+
self._service_account_file,
86+
scopes=["https://www.googleapis.com/auth/firebase.messaging"],
87+
)
88+
# Service account credentials has project_id (others are not)
89+
self._project_id = credentials.project_id or self._project_id
90+
self._service_account_file = None
91+
return credentials
92+
93+
@cached_property
8094
def fcm_end_point(self) -> str:
81-
if self._fcm_end_point is not None:
82-
return self._fcm_end_point
83-
if self.credentials is None:
84-
self._initialize_credentials()
85-
# prefer the project ID scoped to the supplied credentials.
86-
# If, for some reason, the credentials do not specify a project id,
87-
# we'll check for an explicitly supplied one, and raise an error otherwise
88-
project_id = getattr(self.credentials, "project_id", None) or self._project_id
89-
if not project_id:
90-
raise AuthenticationError(
91-
"Please provide a project_id either explicitly or through Google credentials."
92-
)
93-
self._fcm_end_point = self.FCM_END_POINT_BASE + f"/{project_id}/messages:send"
94-
return self._fcm_end_point
95+
if self._provided_credentials is None:
96+
# read credentails to resolve project_id if needed
97+
_ = self._credentials
98+
if self._project_id is None:
99+
raise RuntimeError("Please provide a project_id either explicitly or through Google credentials.")
100+
return self.FCM_END_POINT_BASE + f"/{self._project_id}/messages:send"
95101

96102
@property
97103
def requests_session(self):
@@ -171,32 +177,18 @@ def _is_access_token_expired(self, response):
171177

172178
return False
173179

174-
def _initialize_credentials(self):
175-
"""
176-
Initialize credentials and FCM endpoint if not already initialized.
177-
"""
178-
if self.credentials is None:
179-
self.credentials = service_account.Credentials.from_service_account_file(
180-
self._service_account_file,
181-
scopes=["https://www.googleapis.com/auth/firebase.messaging"],
182-
)
183-
self._service_account_file = None
184-
185-
def _get_access_token(self):
180+
def _get_access_token(self) -> str:
186181
"""
187182
Generates access token from credentials.
188183
If token expires then new access token is generated.
189184
Returns:
190185
str: Access token
191186
"""
192-
if self.credentials is None:
193-
self._initialize_credentials()
194-
195187
# get OAuth 2.0 access token
196188
try:
197189
request = google.auth.transport.requests.Request()
198-
self.credentials.refresh(request)
199-
return self.credentials.token
190+
self._credentials.refresh(request)
191+
return self._credentials.token # pyright: ignore[reportReturnType]
200192
except Exception as e:
201193
raise InvalidDataError(e)
202194

tests/conftest.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,20 @@
22
from unittest.mock import AsyncMock
33

44
import pytest
5+
from google.auth.credentials import Credentials
56

67
from pyfcm import FCMNotification
78
from pyfcm.baseapi import BaseAPI
8-
from google.auth.credentials import Credentials
99

1010

1111
class DummyCredentials(Credentials):
12-
def refresh():
12+
def refresh(self, request):
1313
pass
1414

15-
@property
16-
def project_id(self):
17-
return "test"
18-
1915

20-
@pytest.fixture(scope="module")
16+
@pytest.fixture(scope="function")
2117
def push_service():
22-
return FCMNotification(credentials=DummyCredentials())
18+
return FCMNotification(credentials=DummyCredentials(), project_id="test")
2319

2420

2521
@pytest.fixture
@@ -48,6 +44,6 @@ def mock_aiohttp_session(mocker):
4844
return mock_send
4945

5046

51-
@pytest.fixture(scope="module")
47+
@pytest.fixture(scope="function")
5248
def base_api():
53-
return BaseAPI(credentials=DummyCredentials())
49+
return BaseAPI(credentials=DummyCredentials(), project_id="test")

tests/test_baseapi.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
import json
22
import time
33

4+
import pytest
5+
6+
7+
def test_empty_project_id(base_api):
8+
base_api._project_id = None
9+
with pytest.raises(RuntimeError) as e:
10+
base_api.fcm_end_point
11+
assert str(e.value) == "Please provide a project_id either explicitly or through Google credentials."
12+
413

514
def test_json_dumps(base_api):
615
json_string = base_api.json_dumps([{"test": "Test"}, {"test2": "Test2"}])

tests/test_fcm.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ def test_push_service_without_credentials():
1212
def test_push_service_directly_passed_credentials(push_service):
1313
# We should infer the project ID/endpoint from credentials
1414
# without the need to explcitily pass it
15+
push_service._project_id = "abc123"
1516
assert push_service.fcm_end_point == (
16-
"https://fcm.googleapis.com/v1/projects/"
17-
f"{push_service.credentials.project_id}/messages:send"
17+
"https://fcm.googleapis.com/v1/projects/abc123/messages:send"
1818
)
1919

2020

0 commit comments

Comments
 (0)