Skip to content

Commit 998ada6

Browse files
authored
feat(functions): Added uri task option and additional task queue test coverage (#767)
* feat(functions): Add task queue API support (#751) * Draft implementation of task queue * fix lint * Error handling, code review fixes and typos * feat(functions): Add unit and integration tests for task queue api support (#764) * Unit and Integration tests for task queues. * fix: copyright year * fix: remove commented code * feat(functions): Added `uri` task option and additional task queue test coverage * Removed uri and add doc strings * fix removed typo * re-add missing uri changes * fix missing check
1 parent f73b0a7 commit 998ada6

File tree

3 files changed

+60
-29
lines changed

3 files changed

+60
-29
lines changed

firebase_admin/functions.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,11 @@ def _validate_task_options(
272272
', or underscores (_). The maximum length is 500 characters.')
273273
task.name = self._get_url(
274274
resource, _CLOUD_TASKS_API_RESOURCE_PATH + f'/{opts.task_id}')
275+
if opts.uri is not None:
276+
if not _Validators.is_url(opts.uri):
277+
raise ValueError(
278+
'uri must be a valid RFC3986 URI string using the https or http schema.')
279+
task.http_request['url'] = opts.uri
275280
return task
276281

277282
def _update_task_payload(self, task: Task, resource: Resource, extension_id: str) -> Task:
@@ -327,7 +332,7 @@ def is_url(cls, url: Any):
327332
return False
328333
try:
329334
parsed = parse.urlparse(url)
330-
if not parsed.netloc:
335+
if not parsed.netloc or parsed.scheme not in ['http', 'https']:
331336
return False
332337
return True
333338
except Exception: # pylint: disable=broad-except
@@ -382,12 +387,16 @@ class TaskOptions:
382387
By default, Content-Type is set to 'application/json'.
383388
384389
The size of the headers must be less than 80KB.
390+
391+
uri: The full URL path that the request will be sent to. Must be a valid RFC3986 https or
392+
http URL.
385393
"""
386394
schedule_delay_seconds: Optional[int] = None
387395
schedule_time: Optional[datetime] = None
388396
dispatch_deadline_seconds: Optional[int] = None
389397
task_id: Optional[str] = None
390398
headers: Optional[Dict[str, str]] = None
399+
uri: Optional[str] = None
391400

392401
@dataclass
393402
class Task:
@@ -397,10 +406,10 @@ class Task:
397406
https://cloud.google.com/tasks/docs/reference/rest/v2/projects.locations.queues.tasks#resource:-task
398407
399408
Args:
400-
httpRequest:
401-
name:
402-
schedule_time:
403-
dispatch_deadline:
409+
httpRequest: The request to be made by the task worker.
410+
name: The url path to identify the function.
411+
schedule_time: The time when the task is scheduled to be attempted or retried.
412+
dispatch_deadline: The deadline for requests sent to the worker.
404413
"""
405414
http_request: Dict[str, Optional[str | dict]]
406415
name: Optional[str] = None
@@ -413,9 +422,9 @@ class Resource:
413422
"""Contains the parsed address of a resource.
414423
415424
Args:
416-
resource_id:
417-
project_id:
418-
location_id:
425+
resource_id: The ID of the resource.
426+
project_id: The project ID of the resource.
427+
location_id: The location ID of the resource.
419428
"""
420429
resource_id: str
421430
project_id: Optional[str] = None

tests/test_functions.py

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ def _instrument_functions_service(self, app=None, status=200, payload=_DEFAULT_R
5555
testutils.MockAdapter(payload, status, recorder))
5656
return functions_service, recorder
5757

58+
def test_task_queue_no_project_id(self):
59+
def evaluate():
60+
app = firebase_admin.initialize_app(testutils.MockCredential(), name='no-project-id')
61+
with pytest.raises(ValueError):
62+
functions.task_queue('test-function-name', app=app)
63+
testutils.run_without_project_id(evaluate)
64+
5865
@pytest.mark.parametrize('function_name', [
5966
'projects/test-project/locations/us-central1/functions/test-function-name',
6067
'locations/us-central1/functions/test-function-name',
@@ -179,14 +186,16 @@ def _instrument_functions_service(self, app=None, status=200, payload=_DEFAULT_R
179186
'schedule_time': None,
180187
'dispatch_deadline_seconds': 200,
181188
'task_id': 'test-task-id',
182-
'headers': {'x-test-header': 'test-header-value'}
189+
'headers': {'x-test-header': 'test-header-value'},
190+
'uri': 'https://google.com'
183191
},
184192
{
185193
'schedule_delay_seconds': None,
186194
'schedule_time': _SCHEDULE_TIME,
187195
'dispatch_deadline_seconds': 200,
188196
'task_id': 'test-task-id',
189-
'headers': {'x-test-header': 'test-header-value'}
197+
'headers': {'x-test-header': 'test-header-value'},
198+
'uri': 'http://google.com'
190199
},
191200
])
192201
def test_task_options(self, task_opts_params):
@@ -204,6 +213,7 @@ def test_task_options(self, task_opts_params):
204213

205214
assert task['dispatch_deadline'] == '200s'
206215
assert task['http_request']['headers']['x-test-header'] == 'test-header-value'
216+
assert task['http_request']['url'] in ['http://google.com', 'https://google.com']
207217
assert task['name'] == _DEFAULT_TASK_PATH
208218

209219

@@ -223,6 +233,7 @@ def test_schedule_set_twice_error(self):
223233
str(datetime.utcnow()),
224234
datetime.utcnow().isoformat(),
225235
datetime.utcnow().isoformat() + 'Z',
236+
'', ' '
226237
])
227238
def test_invalid_schedule_time_error(self, schedule_time):
228239
_, recorder = self._instrument_functions_service()
@@ -235,11 +246,7 @@ def test_invalid_schedule_time_error(self, schedule_time):
235246

236247

237248
@pytest.mark.parametrize('schedule_delay_seconds', [
238-
-1,
239-
'100',
240-
'-1',
241-
-1.23,
242-
1.23
249+
-1, '100', '-1', '', ' ', -1.23, 1.23
243250
])
244251
def test_invalid_schedule_delay_seconds_error(self, schedule_delay_seconds):
245252
_, recorder = self._instrument_functions_service()
@@ -252,15 +259,7 @@ def test_invalid_schedule_delay_seconds_error(self, schedule_delay_seconds):
252259

253260

254261
@pytest.mark.parametrize('dispatch_deadline_seconds', [
255-
14,
256-
1801,
257-
-15,
258-
-1800,
259-
0,
260-
'100',
261-
'-1',
262-
-1.23,
263-
1.23,
262+
14, 1801, -15, -1800, 0, '100', '-1', '', ' ', -1.23, 1.23,
264263
])
265264
def test_invalid_dispatch_deadline_seconds_error(self, dispatch_deadline_seconds):
266265
_, recorder = self._instrument_functions_service()
@@ -274,10 +273,7 @@ def test_invalid_dispatch_deadline_seconds_error(self, dispatch_deadline_seconds
274273

275274

276275
@pytest.mark.parametrize('task_id', [
277-
'task/1',
278-
'task.1',
279-
'a'*501,
280-
*non_alphanumeric_chars
276+
'', ' ', 'task/1', 'task.1', 'a'*501, *non_alphanumeric_chars
281277
])
282278
def test_invalid_task_id_error(self, task_id):
283279
_, recorder = self._instrument_functions_service()
@@ -290,3 +286,16 @@ def test_invalid_task_id_error(self, task_id):
290286
'task_id can contain only letters ([A-Za-z]), numbers ([0-9]), '
291287
'hyphens (-), or underscores (_). The maximum length is 500 characters.'
292288
)
289+
290+
@pytest.mark.parametrize('uri', [
291+
'', ' ', 'a', 'foo', 'image.jpg', [], {}, True, 'google.com', 'www.google.com'
292+
])
293+
def test_invalid_uri_error(self, uri):
294+
_, recorder = self._instrument_functions_service()
295+
opts = functions.TaskOptions(uri=uri)
296+
queue = functions.task_queue('test-function-name')
297+
with pytest.raises(ValueError) as excinfo:
298+
queue.enqueue(_DEFAULT_DATA, opts)
299+
assert len(recorder) == 0
300+
assert str(excinfo.value) == \
301+
'uri must be a valid RFC3986 URI string using the https or http schema.'

tests/testutils.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
import pytest
2020

21-
from google.auth import credentials
21+
from google.auth import credentials, compute_engine
2222
from google.auth import transport
2323
from requests import adapters
2424
from requests import models
@@ -133,6 +133,19 @@ def __init__(self):
133133
def get_credential(self):
134134
return self._g_credential
135135

136+
class MockGoogleComputeEngineCredential(compute_engine.Credentials):
137+
"""A mock Compute Engine credential"""
138+
def refresh(self, request):
139+
self.token = 'mock-compute-engine-token'
140+
141+
class MockComputeEngineCredential(firebase_admin.credentials.Base):
142+
"""A mock Firebase credential implementation."""
143+
144+
def __init__(self):
145+
self._g_credential = MockGoogleComputeEngineCredential()
146+
147+
def get_credential(self):
148+
return self._g_credential
136149

137150
class MockMultiRequestAdapter(adapters.HTTPAdapter):
138151
"""A mock HTTP adapter that supports multiple responses for the Python requests module."""

0 commit comments

Comments
 (0)