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.
Files changed (64) hide show
  1. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/PKG-INFO +1 -1
  2. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/__about__.py +1 -1
  3. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/_auth_utils.py +1 -1
  4. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/_messaging_encoder.py +4 -0
  5. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/_messaging_utils.py +22 -2
  6. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/_utils.py +0 -1
  7. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/credentials.py +1 -1
  8. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/functions.py +96 -17
  9. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin.egg-info/PKG-INFO +1 -1
  10. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/setup.cfg +0 -1
  11. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_functions.py +121 -11
  12. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_messaging.py +16 -0
  13. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/testutils.py +30 -1
  14. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/LICENSE +0 -0
  15. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/README.md +0 -0
  16. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/__init__.py +0 -0
  17. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/_auth_client.py +0 -0
  18. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/_auth_providers.py +0 -0
  19. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/_http_client.py +0 -0
  20. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/_retry.py +0 -0
  21. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/_rfc3339.py +0 -0
  22. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/_sseclient.py +0 -0
  23. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/_token_gen.py +0 -0
  24. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/_user_identifier.py +0 -0
  25. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/_user_import.py +0 -0
  26. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/_user_mgt.py +0 -0
  27. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/app_check.py +0 -0
  28. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/auth.py +0 -0
  29. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/db.py +0 -0
  30. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/exceptions.py +0 -0
  31. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/firestore.py +0 -0
  32. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/firestore_async.py +0 -0
  33. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/instance_id.py +0 -0
  34. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/messaging.py +0 -0
  35. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/ml.py +0 -0
  36. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/project_management.py +0 -0
  37. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/remote_config.py +0 -0
  38. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/storage.py +0 -0
  39. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin/tenant_mgt.py +0 -0
  40. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin.egg-info/SOURCES.txt +0 -0
  41. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin.egg-info/dependency_links.txt +0 -0
  42. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin.egg-info/requires.txt +0 -0
  43. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/firebase_admin.egg-info/top_level.txt +0 -0
  44. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/setup.py +0 -0
  45. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_app.py +0 -0
  46. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_app_check.py +0 -0
  47. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_auth_providers.py +0 -0
  48. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_credentials.py +0 -0
  49. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_db.py +0 -0
  50. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_exceptions.py +0 -0
  51. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_firestore.py +0 -0
  52. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_firestore_async.py +0 -0
  53. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_http_client.py +0 -0
  54. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_instance_id.py +0 -0
  55. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_ml.py +0 -0
  56. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_project_management.py +0 -0
  57. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_remote_config.py +0 -0
  58. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_retry.py +0 -0
  59. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_rfc3339.py +0 -0
  60. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_sseclient.py +0 -0
  61. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_storage.py +0 -0
  62. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_tenant_mgt.py +0 -0
  63. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_token_gen.py +0 -0
  64. {firebase_admin-7.1.0 → firebase_admin-7.3.0}/tests/test_user_mgt.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firebase_admin
3
- Version: 7.1.0
3
+ Version: 7.3.0
4
4
  Summary: Firebase Admin Python SDK
5
5
  Home-page: https://firebase.google.com/docs/admin/setup/
6
6
  Author: Firebase
@@ -14,7 +14,7 @@
14
14
 
15
15
  """About information (version, etc) for Firebase Admin SDK."""
16
16
 
17
- __version__ = '7.1.0'
17
+ __version__ = '7.3.0'
18
18
  __title__ = 'firebase_admin'
19
19
  __author__ = 'Firebase'
20
20
  __license__ = 'Apache License 2.0'
@@ -479,7 +479,7 @@ def _parse_error_body(response):
479
479
  separator = code.find(':')
480
480
  if separator != -1:
481
481
  custom_message = code[separator + 1:].strip()
482
- code = code[:separator]
482
+ code = code[:separator].strip()
483
483
 
484
484
  return code, custom_message
485
485
 
@@ -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__(self, collapse_key=None, priority=None, ttl=None, restricted_package_name=None,
57
- data=None, notification=None, fcm_options=None, direct_boot_ok=None):
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 expirty time is exposed as a
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._credential = app.credential.get_credential()
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
- service_url = self._get_url(self._resource, _CLOUD_TASKS_API_URL_FORMAT)
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.__dict__}
200
+ json={'task': task_payload.to_api_dict()}
174
201
  )
175
- task_name = resp.get('name', None)
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
- service_url = self._get_url(self._resource, _CLOUD_TASKS_API_URL_FORMAT + f'/{task_id}')
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
- 'oidc_token': {
235
- 'service_account_email': ''
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 sechdule_delay_seconds and schedule_time cannot be set at the same time.')
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
- task.http_request['url'] = self._get_url(resource, _FIREBASE_FUNCTION_URL_FORMAT)
288
- # If extension id is provided, it emplies that it is being run from a deployed extension.
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 ${id_token}'}
346
+ {**task.http_request['headers'], 'Authorization': f'Bearer {id_token}'}
296
347
  # Delete oidc token
297
- del task.http_request['oidc_token']
348
+ del task.http_request['oidcToken']
298
349
  else:
299
- task.http_request['oidc_token'] = \
300
- {'service_account_email': self._credential.service_account_email}
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firebase_admin
3
- Version: 7.1.0
3
+ Version: 7.3.0
4
4
  Summary: Firebase Admin Python SDK
5
5
  Home-page: https://firebase.google.com/docs/admin/setup/
6
6
  Author: Firebase
@@ -1,7 +1,6 @@
1
1
  [tool:pytest]
2
2
  testpaths = tests
3
3
  asyncio_default_test_loop_scope = class
4
- asyncio_default_fixture_loop_scope = None
5
4
 
6
5
  [egg_info]
7
6
  tag_build =
@@ -44,13 +44,14 @@ class TestTaskQueue:
44
44
  def teardown_class(cls):
45
45
  testutils.cleanup_apps()
46
46
 
47
- def _instrument_functions_service(self, app=None, status=200, payload=_DEFAULT_RESPONSE):
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
- _CLOUD_TASKS_URL,
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['schedule_time'].replace('Z', '+00:00'))
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['dispatch_deadline'] == '200s'
210
- assert task['http_request']['headers']['x-test-header'] == 'test-header-value'
211
- assert task['http_request']['url'] in ['http://google.com', 'https://google.com']
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['schedule_time'].replace('Z', '+00:00'))
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['dispatch_deadline'] == '200s'
237
- assert task['http_request']['headers']['x-test-header'] == 'test-header-value'
238
- assert task['http_request']['url'] in ['http://google.com', 'https://google.com']
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 sechdule_delay_seconds and schedule_time cannot be set at the same time.'
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 'mock-email'
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