firebase-admin 7.1.0__tar.gz → 7.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/PKG-INFO +1 -1
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/__about__.py +1 -1
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/_auth_utils.py +1 -1
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/_messaging_encoder.py +4 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/_messaging_utils.py +22 -2
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/_utils.py +0 -1
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/credentials.py +1 -1
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/functions.py +96 -17
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin.egg-info/PKG-INFO +1 -1
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/setup.cfg +0 -1
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_functions.py +121 -11
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_messaging.py +16 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/testutils.py +30 -1
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/LICENSE +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/README.md +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/__init__.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/_auth_client.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/_auth_providers.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/_http_client.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/_retry.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/_rfc3339.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/_sseclient.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/_token_gen.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/_user_identifier.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/_user_import.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/_user_mgt.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/app_check.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/auth.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/db.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/exceptions.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/firestore.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/firestore_async.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/instance_id.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/messaging.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/ml.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/project_management.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/remote_config.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/storage.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/tenant_mgt.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin.egg-info/SOURCES.txt +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin.egg-info/dependency_links.txt +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin.egg-info/requires.txt +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin.egg-info/top_level.txt +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/setup.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_app.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_app_check.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_auth_providers.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_credentials.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_db.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_exceptions.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_firestore.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_firestore_async.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_http_client.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_instance_id.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_ml.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_project_management.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_remote_config.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_retry.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_rfc3339.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_sseclient.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_storage.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_tenant_mgt.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_token_gen.py +0 -0
- {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_user_mgt.py +0 -0
|
@@ -207,6 +207,10 @@ class MessageEncoder(json.JSONEncoder):
|
|
|
207
207
|
'fcm_options': cls.encode_android_fcm_options(android.fcm_options),
|
|
208
208
|
'direct_boot_ok': _Validators.check_boolean(
|
|
209
209
|
'AndroidConfig.direct_boot_ok', android.direct_boot_ok),
|
|
210
|
+
'bandwidth_constrained_ok': _Validators.check_boolean(
|
|
211
|
+
'AndroidConfig.bandwidth_constrained_ok', android.bandwidth_constrained_ok),
|
|
212
|
+
'restricted_satellite_ok': _Validators.check_boolean(
|
|
213
|
+
'AndroidConfig.restricted_satellite_ok', android.restricted_satellite_ok),
|
|
210
214
|
}
|
|
211
215
|
result = cls.remove_null_values(result)
|
|
212
216
|
priority = result.get('priority')
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
15
|
"""Types and utilities used by the messaging (FCM) module."""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
import datetime
|
|
18
|
+
from typing import Dict, Optional, Union
|
|
16
19
|
|
|
17
20
|
from firebase_admin import exceptions
|
|
18
21
|
|
|
@@ -51,10 +54,25 @@ class AndroidConfig:
|
|
|
51
54
|
fcm_options: A ``messaging.AndroidFCMOptions`` to be included in the message (optional).
|
|
52
55
|
direct_boot_ok: A boolean indicating whether messages will be allowed to be delivered to
|
|
53
56
|
the app while the device is in direct boot mode (optional).
|
|
57
|
+
bandwidth_constrained_ok: A boolean indicating whether messages will be allowed to be
|
|
58
|
+
delivered to the app while the device is on a bandwidth constrained network (optional).
|
|
59
|
+
restricted_satellite_ok: A boolean indicating whether messages will be allowed to be
|
|
60
|
+
delivered to the app while the device is on a restricted satellite network (optional).
|
|
54
61
|
"""
|
|
55
62
|
|
|
56
|
-
def __init__(
|
|
57
|
-
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
collapse_key: Optional[str] = None,
|
|
66
|
+
priority: Optional[str] = None,
|
|
67
|
+
ttl: Optional[Union[int, float, datetime.timedelta]] = None,
|
|
68
|
+
restricted_package_name: Optional[str] = None,
|
|
69
|
+
data: Optional[Dict[str, str]] = None,
|
|
70
|
+
notification: Optional[AndroidNotification] = None,
|
|
71
|
+
fcm_options: Optional[AndroidFCMOptions] = None,
|
|
72
|
+
direct_boot_ok: Optional[bool] = None,
|
|
73
|
+
bandwidth_constrained_ok: Optional[bool] = None,
|
|
74
|
+
restricted_satellite_ok: Optional[bool] = None
|
|
75
|
+
):
|
|
58
76
|
self.collapse_key = collapse_key
|
|
59
77
|
self.priority = priority
|
|
60
78
|
self.ttl = ttl
|
|
@@ -63,6 +81,8 @@ class AndroidConfig:
|
|
|
63
81
|
self.notification = notification
|
|
64
82
|
self.fcm_options = fcm_options
|
|
65
83
|
self.direct_boot_ok = direct_boot_ok
|
|
84
|
+
self.bandwidth_constrained_ok = bandwidth_constrained_ok
|
|
85
|
+
self.restricted_satellite_ok = restricted_satellite_ok
|
|
66
86
|
|
|
67
87
|
|
|
68
88
|
class AndroidNotification:
|
|
@@ -279,7 +279,6 @@ def handle_httpx_error(error: httpx.HTTPError, message=None, code=None) -> excep
|
|
|
279
279
|
message=f'Failed to establish a connection: {error}',
|
|
280
280
|
cause=error)
|
|
281
281
|
if isinstance(error, httpx.HTTPStatusError):
|
|
282
|
-
print("printing status error", error)
|
|
283
282
|
if not code:
|
|
284
283
|
code = _http_status_to_error_code(error.response.status_code)
|
|
285
284
|
if not message:
|
|
@@ -37,7 +37,7 @@ _scopes = [
|
|
|
37
37
|
AccessTokenInfo = collections.namedtuple('AccessTokenInfo', ['access_token', 'expiry'])
|
|
38
38
|
"""Data included in an OAuth2 access token.
|
|
39
39
|
|
|
40
|
-
Contains the access token string and the expiry time. The
|
|
40
|
+
Contains the access token string and the expiry time. The expiry time is exposed as a
|
|
41
41
|
``datetime`` value.
|
|
42
42
|
"""
|
|
43
43
|
|
|
@@ -18,11 +18,16 @@ from __future__ import annotations
|
|
|
18
18
|
from datetime import datetime, timedelta, timezone
|
|
19
19
|
from urllib import parse
|
|
20
20
|
import re
|
|
21
|
+
import os
|
|
21
22
|
import json
|
|
22
23
|
from base64 import b64encode
|
|
23
24
|
from typing import Any, Optional, Dict
|
|
24
25
|
from dataclasses import dataclass
|
|
26
|
+
|
|
25
27
|
from google.auth.compute_engine import Credentials as ComputeEngineCredentials
|
|
28
|
+
from google.auth.credentials import TokenState
|
|
29
|
+
from google.auth.exceptions import RefreshError
|
|
30
|
+
from google.auth.transport import requests as google_auth_requests
|
|
26
31
|
|
|
27
32
|
import requests
|
|
28
33
|
import firebase_admin
|
|
@@ -45,6 +50,8 @@ _CLOUD_TASKS_API_URL_FORMAT = \
|
|
|
45
50
|
'https://cloudtasks.googleapis.com/v2/' + _CLOUD_TASKS_API_RESOURCE_PATH
|
|
46
51
|
_FIREBASE_FUNCTION_URL_FORMAT = \
|
|
47
52
|
'https://{location_id}-{project_id}.cloudfunctions.net/{resource_id}'
|
|
53
|
+
_EMULATOR_HOST_ENV_VAR = 'CLOUD_TASKS_EMULATOR_HOST'
|
|
54
|
+
_EMULATED_SERVICE_ACCOUNT_DEFAULT = 'emulated-service-acct@email.com'
|
|
48
55
|
|
|
49
56
|
_FUNCTIONS_HEADERS = {
|
|
50
57
|
'X-GOOG-API-FORMAT-VERSION': '2',
|
|
@@ -54,6 +61,17 @@ _FUNCTIONS_HEADERS = {
|
|
|
54
61
|
# Default canonical location ID of the task queue.
|
|
55
62
|
_DEFAULT_LOCATION = 'us-central1'
|
|
56
63
|
|
|
64
|
+
def _get_emulator_host() -> Optional[str]:
|
|
65
|
+
emulator_host = os.environ.get(_EMULATOR_HOST_ENV_VAR)
|
|
66
|
+
if emulator_host:
|
|
67
|
+
if '//' in emulator_host:
|
|
68
|
+
raise ValueError(
|
|
69
|
+
f'Invalid {_EMULATOR_HOST_ENV_VAR}: "{emulator_host}". It must follow format '
|
|
70
|
+
'"host:port".')
|
|
71
|
+
return emulator_host
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
|
|
57
75
|
def _get_functions_service(app) -> _FunctionsService:
|
|
58
76
|
return _utils.get_app_service(app, _FUNCTIONS_ATTRIBUTE, _FunctionsService)
|
|
59
77
|
|
|
@@ -99,13 +117,19 @@ class _FunctionsService:
|
|
|
99
117
|
'projectId option, or use service account credentials. Alternatively, set the '
|
|
100
118
|
'GOOGLE_CLOUD_PROJECT environment variable.')
|
|
101
119
|
|
|
102
|
-
self.
|
|
120
|
+
self._emulator_host = _get_emulator_host()
|
|
121
|
+
if self._emulator_host:
|
|
122
|
+
self._credential = _utils.EmulatorAdminCredentials()
|
|
123
|
+
else:
|
|
124
|
+
self._credential = app.credential.get_credential()
|
|
125
|
+
|
|
103
126
|
self._http_client = _http_client.JsonHttpClient(credential=self._credential)
|
|
104
127
|
|
|
105
128
|
def task_queue(self, function_name: str, extension_id: Optional[str] = None) -> TaskQueue:
|
|
106
129
|
"""Creates a TaskQueue instance."""
|
|
107
130
|
return TaskQueue(
|
|
108
|
-
function_name, extension_id, self._project_id, self._credential, self._http_client
|
|
131
|
+
function_name, extension_id, self._project_id, self._credential, self._http_client,
|
|
132
|
+
self._emulator_host)
|
|
109
133
|
|
|
110
134
|
@classmethod
|
|
111
135
|
def handle_functions_error(cls, error: Any):
|
|
@@ -121,7 +145,8 @@ class TaskQueue:
|
|
|
121
145
|
extension_id: Optional[str],
|
|
122
146
|
project_id,
|
|
123
147
|
credential,
|
|
124
|
-
http_client
|
|
148
|
+
http_client,
|
|
149
|
+
emulator_host: Optional[str] = None
|
|
125
150
|
) -> None:
|
|
126
151
|
|
|
127
152
|
# Validate function_name
|
|
@@ -130,6 +155,7 @@ class TaskQueue:
|
|
|
130
155
|
self._project_id = project_id
|
|
131
156
|
self._credential = credential
|
|
132
157
|
self._http_client = http_client
|
|
158
|
+
self._emulator_host = emulator_host
|
|
133
159
|
self._function_name = function_name
|
|
134
160
|
self._extension_id = extension_id
|
|
135
161
|
# Parse resources from function_name
|
|
@@ -163,16 +189,26 @@ class TaskQueue:
|
|
|
163
189
|
str: The ID of the task relative to this queue.
|
|
164
190
|
"""
|
|
165
191
|
task = self._validate_task_options(task_data, self._resource, opts)
|
|
166
|
-
|
|
192
|
+
emulator_url = self._get_emulator_url(self._resource)
|
|
193
|
+
service_url = emulator_url or self._get_url(self._resource, _CLOUD_TASKS_API_URL_FORMAT)
|
|
167
194
|
task_payload = self._update_task_payload(task, self._resource, self._extension_id)
|
|
168
195
|
try:
|
|
169
196
|
resp = self._http_client.body(
|
|
170
197
|
'post',
|
|
171
198
|
url=service_url,
|
|
172
199
|
headers=_FUNCTIONS_HEADERS,
|
|
173
|
-
json={'task': task_payload.
|
|
200
|
+
json={'task': task_payload.to_api_dict()}
|
|
174
201
|
)
|
|
175
|
-
|
|
202
|
+
if self._is_emulated():
|
|
203
|
+
# Emulator returns a response with format {task: {name: <task_name>}}
|
|
204
|
+
# The task name also has an extra '/' at the start compared to prod
|
|
205
|
+
task_info = resp.get('task') or {}
|
|
206
|
+
task_name = task_info.get('name')
|
|
207
|
+
if task_name:
|
|
208
|
+
task_name = task_name[1:]
|
|
209
|
+
else:
|
|
210
|
+
# Production returns a response with format {name: <task_name>}
|
|
211
|
+
task_name = resp.get('name')
|
|
176
212
|
task_resource = \
|
|
177
213
|
self._parse_resource_name(task_name, f'queues/{self._resource.resource_id}/tasks')
|
|
178
214
|
return task_resource.resource_id
|
|
@@ -193,7 +229,11 @@ class TaskQueue:
|
|
|
193
229
|
ValueError: If the input arguments are invalid.
|
|
194
230
|
"""
|
|
195
231
|
_Validators.check_non_empty_string('task_id', task_id)
|
|
196
|
-
|
|
232
|
+
emulator_url = self._get_emulator_url(self._resource)
|
|
233
|
+
if emulator_url:
|
|
234
|
+
service_url = emulator_url + f'/{task_id}'
|
|
235
|
+
else:
|
|
236
|
+
service_url = self._get_url(self._resource, _CLOUD_TASKS_API_URL_FORMAT + f'/{task_id}')
|
|
197
237
|
try:
|
|
198
238
|
self._http_client.body(
|
|
199
239
|
'delete',
|
|
@@ -231,8 +271,8 @@ class TaskQueue:
|
|
|
231
271
|
"""Validate and create a Task from optional ``TaskOptions``."""
|
|
232
272
|
task_http_request = {
|
|
233
273
|
'url': '',
|
|
234
|
-
'
|
|
235
|
-
'
|
|
274
|
+
'oidcToken': {
|
|
275
|
+
'serviceAccountEmail': ''
|
|
236
276
|
},
|
|
237
277
|
'body': b64encode(json.dumps(data).encode()).decode(),
|
|
238
278
|
'headers': {
|
|
@@ -246,7 +286,7 @@ class TaskQueue:
|
|
|
246
286
|
task.http_request['headers'] = {**task.http_request['headers'], **opts.headers}
|
|
247
287
|
if opts.schedule_time is not None and opts.schedule_delay_seconds is not None:
|
|
248
288
|
raise ValueError(
|
|
249
|
-
'Both
|
|
289
|
+
'Both schedule_delay_seconds and schedule_time cannot be set at the same time.')
|
|
250
290
|
if opts.schedule_time is not None and opts.schedule_delay_seconds is None:
|
|
251
291
|
if not isinstance(opts.schedule_time, datetime):
|
|
252
292
|
raise ValueError('schedule_time should be UTC datetime.')
|
|
@@ -284,22 +324,53 @@ class TaskQueue:
|
|
|
284
324
|
"""Prepares task to be sent with credentials."""
|
|
285
325
|
# Get function url from task or generate from resources
|
|
286
326
|
if not _Validators.is_non_empty_string(task.http_request['url']):
|
|
287
|
-
|
|
288
|
-
|
|
327
|
+
if self._is_emulated():
|
|
328
|
+
task.http_request['url'] = ''
|
|
329
|
+
else:
|
|
330
|
+
task.http_request['url'] = self._get_url(resource, _FIREBASE_FUNCTION_URL_FORMAT)
|
|
331
|
+
|
|
332
|
+
# Refresh the credential to ensure all attributes (e.g. service_account_email, id_token)
|
|
333
|
+
# are populated, preventing cold start errors.
|
|
334
|
+
if self._credential.token_state != TokenState.FRESH:
|
|
335
|
+
try:
|
|
336
|
+
self._credential.refresh(google_auth_requests.Request())
|
|
337
|
+
except RefreshError as err:
|
|
338
|
+
raise ValueError(f'Initial task payload credential refresh failed: {err}') from err
|
|
339
|
+
|
|
340
|
+
# If extension id is provided, it implies that it is being run from a deployed extension.
|
|
289
341
|
# Meaning that it's credential should be a Compute Engine Credential.
|
|
290
342
|
if _Validators.is_non_empty_string(extension_id) and \
|
|
291
343
|
isinstance(self._credential, ComputeEngineCredentials):
|
|
292
|
-
|
|
293
344
|
id_token = self._credential.token
|
|
294
345
|
task.http_request['headers'] = \
|
|
295
|
-
{**task.http_request['headers'], 'Authorization': f'Bearer
|
|
346
|
+
{**task.http_request['headers'], 'Authorization': f'Bearer {id_token}'}
|
|
296
347
|
# Delete oidc token
|
|
297
|
-
del task.http_request['
|
|
348
|
+
del task.http_request['oidcToken']
|
|
298
349
|
else:
|
|
299
|
-
|
|
300
|
-
|
|
350
|
+
try:
|
|
351
|
+
task.http_request['oidcToken'] = \
|
|
352
|
+
{'serviceAccountEmail': self._credential.service_account_email}
|
|
353
|
+
except AttributeError as error:
|
|
354
|
+
if self._is_emulated():
|
|
355
|
+
task.http_request['oidcToken'] = \
|
|
356
|
+
{'serviceAccountEmail': _EMULATED_SERVICE_ACCOUNT_DEFAULT}
|
|
357
|
+
else:
|
|
358
|
+
raise ValueError(
|
|
359
|
+
'Failed to determine service account. Initialize the SDK with service '
|
|
360
|
+
'account credentials or set service account ID as an app option.'
|
|
361
|
+
) from error
|
|
301
362
|
return task
|
|
302
363
|
|
|
364
|
+
def _get_emulator_url(self, resource: Resource):
|
|
365
|
+
if self._emulator_host:
|
|
366
|
+
emulator_url_format = f'http://{self._emulator_host}/' + _CLOUD_TASKS_API_RESOURCE_PATH
|
|
367
|
+
url = self._get_url(resource, emulator_url_format)
|
|
368
|
+
return url
|
|
369
|
+
return None
|
|
370
|
+
|
|
371
|
+
def _is_emulated(self):
|
|
372
|
+
return self._emulator_host is not None
|
|
373
|
+
|
|
303
374
|
|
|
304
375
|
class _Validators:
|
|
305
376
|
"""A collection of data validation utilities."""
|
|
@@ -424,6 +495,14 @@ class Task:
|
|
|
424
495
|
schedule_time: Optional[str] = None
|
|
425
496
|
dispatch_deadline: Optional[str] = None
|
|
426
497
|
|
|
498
|
+
def to_api_dict(self) -> dict:
|
|
499
|
+
"""Converts the Task object to a dictionary suitable for the Cloud Tasks API."""
|
|
500
|
+
return {
|
|
501
|
+
'httpRequest': self.http_request,
|
|
502
|
+
'name': self.name,
|
|
503
|
+
'scheduleTime': self.schedule_time,
|
|
504
|
+
'dispatchDeadline': self.dispatch_deadline,
|
|
505
|
+
}
|
|
427
506
|
|
|
428
507
|
@dataclass
|
|
429
508
|
class Resource:
|
|
@@ -44,13 +44,14 @@ class TestTaskQueue:
|
|
|
44
44
|
def teardown_class(cls):
|
|
45
45
|
testutils.cleanup_apps()
|
|
46
46
|
|
|
47
|
-
def _instrument_functions_service(
|
|
47
|
+
def _instrument_functions_service(
|
|
48
|
+
self, app=None, status=200, payload=_DEFAULT_RESPONSE, mounted_url=_CLOUD_TASKS_URL):
|
|
48
49
|
if not app:
|
|
49
50
|
app = firebase_admin.get_app()
|
|
50
51
|
functions_service = functions._get_functions_service(app)
|
|
51
52
|
recorder = []
|
|
52
53
|
functions_service._http_client.session.mount(
|
|
53
|
-
|
|
54
|
+
mounted_url,
|
|
54
55
|
testutils.MockAdapter(payload, status, recorder))
|
|
55
56
|
return functions_service, recorder
|
|
56
57
|
|
|
@@ -124,6 +125,10 @@ class TestTaskQueue:
|
|
|
124
125
|
assert recorder[0].headers['x-goog-api-client'] == expected_metrics_header
|
|
125
126
|
assert task_id == 'test-task-id'
|
|
126
127
|
|
|
128
|
+
task = json.loads(recorder[0].body.decode())['task']
|
|
129
|
+
assert task['httpRequest']['oidcToken'] == {'serviceAccountEmail': 'mock-email'}
|
|
130
|
+
assert task['httpRequest']['headers'] == {'Content-Type': 'application/json'}
|
|
131
|
+
|
|
127
132
|
def test_task_enqueue_with_extension(self):
|
|
128
133
|
resource_name = (
|
|
129
134
|
'projects/test-project/locations/us-central1/queues/'
|
|
@@ -142,6 +147,59 @@ class TestTaskQueue:
|
|
|
142
147
|
assert recorder[0].headers['x-goog-api-client'] == expected_metrics_header
|
|
143
148
|
assert task_id == 'test-task-id'
|
|
144
149
|
|
|
150
|
+
task = json.loads(recorder[0].body.decode())['task']
|
|
151
|
+
assert task['httpRequest']['oidcToken'] == {'serviceAccountEmail': 'mock-email'}
|
|
152
|
+
assert task['httpRequest']['headers'] == {'Content-Type': 'application/json'}
|
|
153
|
+
|
|
154
|
+
def test_task_enqueue_compute_engine(self):
|
|
155
|
+
app = firebase_admin.initialize_app(
|
|
156
|
+
testutils.MockComputeEngineCredential(),
|
|
157
|
+
options={'projectId': 'test-project'},
|
|
158
|
+
name='test-project-gce')
|
|
159
|
+
_, recorder = self._instrument_functions_service(app)
|
|
160
|
+
queue = functions.task_queue('test-function-name', app=app)
|
|
161
|
+
task_id = queue.enqueue(_DEFAULT_DATA)
|
|
162
|
+
assert len(recorder) == 1
|
|
163
|
+
assert recorder[0].method == 'POST'
|
|
164
|
+
assert recorder[0].url == _DEFAULT_REQUEST_URL
|
|
165
|
+
assert recorder[0].headers['Content-Type'] == 'application/json'
|
|
166
|
+
assert recorder[0].headers['Authorization'] == 'Bearer mock-compute-engine-token'
|
|
167
|
+
expected_metrics_header = _utils.get_metrics_header() + ' mock-gce-cred-metric-tag'
|
|
168
|
+
assert recorder[0].headers['x-goog-api-client'] == expected_metrics_header
|
|
169
|
+
assert task_id == 'test-task-id'
|
|
170
|
+
|
|
171
|
+
task = json.loads(recorder[0].body.decode())['task']
|
|
172
|
+
assert task['httpRequest']['oidcToken'] == {'serviceAccountEmail': 'mock-gce-email'}
|
|
173
|
+
assert task['httpRequest']['headers'] == {'Content-Type': 'application/json'}
|
|
174
|
+
|
|
175
|
+
def test_task_enqueue_with_extension_compute_engine(self):
|
|
176
|
+
resource_name = (
|
|
177
|
+
'projects/test-project/locations/us-central1/queues/'
|
|
178
|
+
'ext-test-extension-id-test-function-name/tasks'
|
|
179
|
+
)
|
|
180
|
+
extension_response = json.dumps({'name': resource_name + '/test-task-id'})
|
|
181
|
+
app = firebase_admin.initialize_app(
|
|
182
|
+
testutils.MockComputeEngineCredential(),
|
|
183
|
+
options={'projectId': 'test-project'},
|
|
184
|
+
name='test-project-gce-extensions')
|
|
185
|
+
_, recorder = self._instrument_functions_service(app, payload=extension_response)
|
|
186
|
+
queue = functions.task_queue('test-function-name', 'test-extension-id', app)
|
|
187
|
+
task_id = queue.enqueue(_DEFAULT_DATA)
|
|
188
|
+
assert len(recorder) == 1
|
|
189
|
+
assert recorder[0].method == 'POST'
|
|
190
|
+
assert recorder[0].url == _CLOUD_TASKS_URL + resource_name
|
|
191
|
+
assert recorder[0].headers['Content-Type'] == 'application/json'
|
|
192
|
+
assert recorder[0].headers['Authorization'] == 'Bearer mock-compute-engine-token'
|
|
193
|
+
expected_metrics_header = _utils.get_metrics_header() + ' mock-gce-cred-metric-tag'
|
|
194
|
+
assert recorder[0].headers['x-goog-api-client'] == expected_metrics_header
|
|
195
|
+
assert task_id == 'test-task-id'
|
|
196
|
+
|
|
197
|
+
task = json.loads(recorder[0].body.decode())['task']
|
|
198
|
+
assert 'oidcToken' not in task['httpRequest']
|
|
199
|
+
assert task['httpRequest']['headers'] == {
|
|
200
|
+
'Content-Type': 'application/json',
|
|
201
|
+
'Authorization': 'Bearer mock-compute-engine-token'}
|
|
202
|
+
|
|
145
203
|
def test_task_delete(self):
|
|
146
204
|
_, recorder = self._instrument_functions_service()
|
|
147
205
|
queue = functions.task_queue('test-function-name')
|
|
@@ -152,6 +210,58 @@ class TestTaskQueue:
|
|
|
152
210
|
expected_metrics_header = _utils.get_metrics_header() + ' mock-cred-metric-tag'
|
|
153
211
|
assert recorder[0].headers['x-goog-api-client'] == expected_metrics_header
|
|
154
212
|
|
|
213
|
+
def test_task_enqueue_with_emulator_host(self, monkeypatch):
|
|
214
|
+
emulator_host = 'localhost:8124'
|
|
215
|
+
emulator_url = f'http://{emulator_host}/'
|
|
216
|
+
request_url = emulator_url + _DEFAULT_TASK_PATH.replace('/tasks/test-task-id', '/tasks')
|
|
217
|
+
|
|
218
|
+
monkeypatch.setenv('CLOUD_TASKS_EMULATOR_HOST', emulator_host)
|
|
219
|
+
app = firebase_admin.initialize_app(
|
|
220
|
+
_utils.EmulatorAdminCredentials(), {'projectId': 'test-project'}, name='emulator-app')
|
|
221
|
+
|
|
222
|
+
expected_task_name = (
|
|
223
|
+
'/projects/test-project/locations/us-central1'
|
|
224
|
+
'/queues/test-function-name/tasks/test-task-id'
|
|
225
|
+
)
|
|
226
|
+
expected_response = json.dumps({'task': {'name': expected_task_name}})
|
|
227
|
+
_, recorder = self._instrument_functions_service(
|
|
228
|
+
app, payload=expected_response, mounted_url=emulator_url)
|
|
229
|
+
|
|
230
|
+
queue = functions.task_queue('test-function-name', app=app)
|
|
231
|
+
task_id = queue.enqueue(_DEFAULT_DATA)
|
|
232
|
+
|
|
233
|
+
assert len(recorder) == 1
|
|
234
|
+
assert recorder[0].method == 'POST'
|
|
235
|
+
assert recorder[0].url == request_url
|
|
236
|
+
assert recorder[0].headers['Content-Type'] == 'application/json'
|
|
237
|
+
|
|
238
|
+
task = json.loads(recorder[0].body.decode())['task']
|
|
239
|
+
assert task['httpRequest']['oidcToken'] == {
|
|
240
|
+
'serviceAccountEmail': 'emulated-service-acct@email.com'
|
|
241
|
+
}
|
|
242
|
+
assert task_id == 'test-task-id'
|
|
243
|
+
|
|
244
|
+
def test_task_enqueue_without_emulator_host_error(self, monkeypatch):
|
|
245
|
+
app = firebase_admin.initialize_app(
|
|
246
|
+
_utils.EmulatorAdminCredentials(),
|
|
247
|
+
{'projectId': 'test-project'}, name='no-emulator-app')
|
|
248
|
+
|
|
249
|
+
_, recorder = self._instrument_functions_service(app)
|
|
250
|
+
monkeypatch.delenv('CLOUD_TASKS_EMULATOR_HOST', raising=False)
|
|
251
|
+
queue = functions.task_queue('test-function-name', app=app)
|
|
252
|
+
with pytest.raises(ValueError) as excinfo:
|
|
253
|
+
queue.enqueue(_DEFAULT_DATA)
|
|
254
|
+
assert "Failed to determine service account" in str(excinfo.value)
|
|
255
|
+
assert len(recorder) == 0
|
|
256
|
+
|
|
257
|
+
def test_get_emulator_url_invalid_format(self, monkeypatch):
|
|
258
|
+
monkeypatch.setenv('CLOUD_TASKS_EMULATOR_HOST', 'http://localhost:8124')
|
|
259
|
+
app = firebase_admin.initialize_app(
|
|
260
|
+
testutils.MockCredential(), {'projectId': 'test-project'}, name='invalid-host-app')
|
|
261
|
+
with pytest.raises(ValueError) as excinfo:
|
|
262
|
+
functions.task_queue('test-function-name', app=app)
|
|
263
|
+
assert 'Invalid CLOUD_TASKS_EMULATOR_HOST' in str(excinfo.value)
|
|
264
|
+
|
|
155
265
|
class TestTaskQueueOptions:
|
|
156
266
|
|
|
157
267
|
_DEFAULT_TASK_OPTS = {'schedule_delay_seconds': None, 'schedule_time': None, \
|
|
@@ -202,13 +312,13 @@ class TestTaskQueueOptions:
|
|
|
202
312
|
assert len(recorder) == 1
|
|
203
313
|
task = json.loads(recorder[0].body.decode())['task']
|
|
204
314
|
|
|
205
|
-
task_schedule_time = datetime.fromisoformat(task['
|
|
315
|
+
task_schedule_time = datetime.fromisoformat(task['scheduleTime'].replace('Z', '+00:00'))
|
|
206
316
|
delta = abs(task_schedule_time - expected_schedule_time)
|
|
207
317
|
assert delta <= timedelta(seconds=1)
|
|
208
318
|
|
|
209
|
-
assert task['
|
|
210
|
-
assert task['
|
|
211
|
-
assert task['
|
|
319
|
+
assert task['dispatchDeadline'] == '200s'
|
|
320
|
+
assert task['httpRequest']['headers']['x-test-header'] == 'test-header-value'
|
|
321
|
+
assert task['httpRequest']['url'] in ['http://google.com', 'https://google.com']
|
|
212
322
|
assert task['name'] == _DEFAULT_TASK_PATH
|
|
213
323
|
|
|
214
324
|
def test_task_options_utc_time(self):
|
|
@@ -230,12 +340,12 @@ class TestTaskQueueOptions:
|
|
|
230
340
|
assert len(recorder) == 1
|
|
231
341
|
task = json.loads(recorder[0].body.decode())['task']
|
|
232
342
|
|
|
233
|
-
task_schedule_time = datetime.fromisoformat(task['
|
|
343
|
+
task_schedule_time = datetime.fromisoformat(task['scheduleTime'].replace('Z', '+00:00'))
|
|
234
344
|
assert task_schedule_time == expected_schedule_time
|
|
235
345
|
|
|
236
|
-
assert task['
|
|
237
|
-
assert task['
|
|
238
|
-
assert task['
|
|
346
|
+
assert task['dispatchDeadline'] == '200s'
|
|
347
|
+
assert task['httpRequest']['headers']['x-test-header'] == 'test-header-value'
|
|
348
|
+
assert task['httpRequest']['url'] in ['http://google.com', 'https://google.com']
|
|
239
349
|
assert task['name'] == _DEFAULT_TASK_PATH
|
|
240
350
|
|
|
241
351
|
def test_schedule_set_twice_error(self):
|
|
@@ -247,7 +357,7 @@ class TestTaskQueueOptions:
|
|
|
247
357
|
queue.enqueue(_DEFAULT_DATA, opts)
|
|
248
358
|
assert len(recorder) == 0
|
|
249
359
|
assert str(excinfo.value) == \
|
|
250
|
-
'Both
|
|
360
|
+
'Both schedule_delay_seconds and schedule_time cannot be set at the same time.'
|
|
251
361
|
|
|
252
362
|
|
|
253
363
|
@pytest.mark.parametrize('schedule_time', [
|
|
@@ -335,6 +335,18 @@ class TestAndroidConfigEncoder:
|
|
|
335
335
|
check_encoding(messaging.Message(
|
|
336
336
|
topic='topic', android=messaging.AndroidConfig(direct_boot_ok=data)))
|
|
337
337
|
|
|
338
|
+
@pytest.mark.parametrize('data', NON_BOOL_ARGS)
|
|
339
|
+
def test_invalid_bandwidth_constrained_ok(self, data):
|
|
340
|
+
with pytest.raises(ValueError):
|
|
341
|
+
check_encoding(messaging.Message(
|
|
342
|
+
topic='topic', android=messaging.AndroidConfig(bandwidth_constrained_ok=data)))
|
|
343
|
+
|
|
344
|
+
@pytest.mark.parametrize('data', NON_BOOL_ARGS)
|
|
345
|
+
def test_invalid_restricted_satellite_ok(self, data):
|
|
346
|
+
with pytest.raises(ValueError):
|
|
347
|
+
check_encoding(messaging.Message(
|
|
348
|
+
topic='topic', android=messaging.AndroidConfig(restricted_satellite_ok=data)))
|
|
349
|
+
|
|
338
350
|
|
|
339
351
|
def test_android_config(self):
|
|
340
352
|
msg = messaging.Message(
|
|
@@ -347,6 +359,8 @@ class TestAndroidConfigEncoder:
|
|
|
347
359
|
data={'k1': 'v1', 'k2': 'v2'},
|
|
348
360
|
fcm_options=messaging.AndroidFCMOptions('analytics_label_v1'),
|
|
349
361
|
direct_boot_ok=True,
|
|
362
|
+
bandwidth_constrained_ok=True,
|
|
363
|
+
restricted_satellite_ok=True,
|
|
350
364
|
)
|
|
351
365
|
)
|
|
352
366
|
expected = {
|
|
@@ -364,6 +378,8 @@ class TestAndroidConfigEncoder:
|
|
|
364
378
|
'analytics_label': 'analytics_label_v1',
|
|
365
379
|
},
|
|
366
380
|
'direct_boot_ok': True,
|
|
381
|
+
'bandwidth_constrained_ok': True,
|
|
382
|
+
'restricted_satellite_ok': True,
|
|
367
383
|
},
|
|
368
384
|
}
|
|
369
385
|
check_encoding(msg, expected)
|
|
@@ -116,12 +116,25 @@ class MockFailedRequest(transport.Request):
|
|
|
116
116
|
# pylint: disable=abstract-method
|
|
117
117
|
class MockGoogleCredential(credentials.Credentials):
|
|
118
118
|
"""A mock Google authentication credential."""
|
|
119
|
+
|
|
120
|
+
def __init__(self):
|
|
121
|
+
super().__init__()
|
|
122
|
+
self.token = None
|
|
123
|
+
self._service_account_email = None
|
|
124
|
+
self._token_state = credentials.TokenState.INVALID
|
|
125
|
+
|
|
119
126
|
def refresh(self, request):
|
|
120
127
|
self.token = 'mock-token'
|
|
128
|
+
self._service_account_email = 'mock-email'
|
|
129
|
+
self._token_state = credentials.TokenState.FRESH
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def token_state(self):
|
|
133
|
+
return self._token_state
|
|
121
134
|
|
|
122
135
|
@property
|
|
123
136
|
def service_account_email(self):
|
|
124
|
-
return
|
|
137
|
+
return self._service_account_email
|
|
125
138
|
|
|
126
139
|
# Simulate x-goog-api-client modification in credential refresh
|
|
127
140
|
def _metric_header_for_usage(self):
|
|
@@ -139,8 +152,24 @@ class MockCredential(firebase_admin.credentials.Base):
|
|
|
139
152
|
|
|
140
153
|
class MockGoogleComputeEngineCredential(compute_engine.Credentials):
|
|
141
154
|
"""A mock Compute Engine credential"""
|
|
155
|
+
|
|
156
|
+
def __init__(self):
|
|
157
|
+
super().__init__()
|
|
158
|
+
self.token = None
|
|
159
|
+
self._service_account_email = None
|
|
160
|
+
self._token_state = credentials.TokenState.INVALID
|
|
161
|
+
|
|
142
162
|
def refresh(self, request):
|
|
143
163
|
self.token = 'mock-compute-engine-token'
|
|
164
|
+
self._service_account_email = 'mock-gce-email'
|
|
165
|
+
self._token_state = credentials.TokenState.FRESH
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def token_state(self):
|
|
169
|
+
return self._token_state
|
|
170
|
+
|
|
171
|
+
def _metric_header_for_usage(self):
|
|
172
|
+
return 'mock-gce-cred-metric-tag'
|
|
144
173
|
|
|
145
174
|
class MockComputeEngineCredential(firebase_admin.credentials.Base):
|
|
146
175
|
"""A mock Firebase credential implementation."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|