Skip to content

Commit

Permalink
feat(functions): Added uri task option and additional task queue te…
Browse files Browse the repository at this point in the history
…st 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
  • Loading branch information
jonathanedey authored Feb 15, 2024
1 parent f73b0a7 commit 998ada6
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 29 deletions.
25 changes: 17 additions & 8 deletions firebase_admin/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,11 @@ def _validate_task_options(
', or underscores (_). The maximum length is 500 characters.')
task.name = self._get_url(
resource, _CLOUD_TASKS_API_RESOURCE_PATH + f'/{opts.task_id}')
if opts.uri is not None:
if not _Validators.is_url(opts.uri):
raise ValueError(
'uri must be a valid RFC3986 URI string using the https or http schema.')
task.http_request['url'] = opts.uri
return task

def _update_task_payload(self, task: Task, resource: Resource, extension_id: str) -> Task:
Expand Down Expand Up @@ -327,7 +332,7 @@ def is_url(cls, url: Any):
return False
try:
parsed = parse.urlparse(url)
if not parsed.netloc:
if not parsed.netloc or parsed.scheme not in ['http', 'https']:
return False
return True
except Exception: # pylint: disable=broad-except
Expand Down Expand Up @@ -382,12 +387,16 @@ class TaskOptions:
By default, Content-Type is set to 'application/json'.
The size of the headers must be less than 80KB.
uri: The full URL path that the request will be sent to. Must be a valid RFC3986 https or
http URL.
"""
schedule_delay_seconds: Optional[int] = None
schedule_time: Optional[datetime] = None
dispatch_deadline_seconds: Optional[int] = None
task_id: Optional[str] = None
headers: Optional[Dict[str, str]] = None
uri: Optional[str] = None

@dataclass
class Task:
Expand All @@ -397,10 +406,10 @@ class Task:
https://cloud.google.com/tasks/docs/reference/rest/v2/projects.locations.queues.tasks#resource:-task
Args:
httpRequest:
name:
schedule_time:
dispatch_deadline:
httpRequest: The request to be made by the task worker.
name: The url path to identify the function.
schedule_time: The time when the task is scheduled to be attempted or retried.
dispatch_deadline: The deadline for requests sent to the worker.
"""
http_request: Dict[str, Optional[str | dict]]
name: Optional[str] = None
Expand All @@ -413,9 +422,9 @@ class Resource:
"""Contains the parsed address of a resource.
Args:
resource_id:
project_id:
location_id:
resource_id: The ID of the resource.
project_id: The project ID of the resource.
location_id: The location ID of the resource.
"""
resource_id: str
project_id: Optional[str] = None
Expand Down
49 changes: 29 additions & 20 deletions tests/test_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ def _instrument_functions_service(self, app=None, status=200, payload=_DEFAULT_R
testutils.MockAdapter(payload, status, recorder))
return functions_service, recorder

def test_task_queue_no_project_id(self):
def evaluate():
app = firebase_admin.initialize_app(testutils.MockCredential(), name='no-project-id')
with pytest.raises(ValueError):
functions.task_queue('test-function-name', app=app)
testutils.run_without_project_id(evaluate)

@pytest.mark.parametrize('function_name', [
'projects/test-project/locations/us-central1/functions/test-function-name',
'locations/us-central1/functions/test-function-name',
Expand Down Expand Up @@ -179,14 +186,16 @@ def _instrument_functions_service(self, app=None, status=200, payload=_DEFAULT_R
'schedule_time': None,
'dispatch_deadline_seconds': 200,
'task_id': 'test-task-id',
'headers': {'x-test-header': 'test-header-value'}
'headers': {'x-test-header': 'test-header-value'},
'uri': 'https://google.com'
},
{
'schedule_delay_seconds': None,
'schedule_time': _SCHEDULE_TIME,
'dispatch_deadline_seconds': 200,
'task_id': 'test-task-id',
'headers': {'x-test-header': 'test-header-value'}
'headers': {'x-test-header': 'test-header-value'},
'uri': 'http://google.com'
},
])
def test_task_options(self, task_opts_params):
Expand All @@ -204,6 +213,7 @@ def test_task_options(self, task_opts_params):

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


Expand All @@ -223,6 +233,7 @@ def test_schedule_set_twice_error(self):
str(datetime.utcnow()),
datetime.utcnow().isoformat(),
datetime.utcnow().isoformat() + 'Z',
'', ' '
])
def test_invalid_schedule_time_error(self, schedule_time):
_, recorder = self._instrument_functions_service()
Expand All @@ -235,11 +246,7 @@ def test_invalid_schedule_time_error(self, schedule_time):


@pytest.mark.parametrize('schedule_delay_seconds', [
-1,
'100',
'-1',
-1.23,
1.23
-1, '100', '-1', '', ' ', -1.23, 1.23
])
def test_invalid_schedule_delay_seconds_error(self, schedule_delay_seconds):
_, recorder = self._instrument_functions_service()
Expand All @@ -252,15 +259,7 @@ def test_invalid_schedule_delay_seconds_error(self, schedule_delay_seconds):


@pytest.mark.parametrize('dispatch_deadline_seconds', [
14,
1801,
-15,
-1800,
0,
'100',
'-1',
-1.23,
1.23,
14, 1801, -15, -1800, 0, '100', '-1', '', ' ', -1.23, 1.23,
])
def test_invalid_dispatch_deadline_seconds_error(self, dispatch_deadline_seconds):
_, recorder = self._instrument_functions_service()
Expand All @@ -274,10 +273,7 @@ def test_invalid_dispatch_deadline_seconds_error(self, dispatch_deadline_seconds


@pytest.mark.parametrize('task_id', [
'task/1',
'task.1',
'a'*501,
*non_alphanumeric_chars
'', ' ', 'task/1', 'task.1', 'a'*501, *non_alphanumeric_chars
])
def test_invalid_task_id_error(self, task_id):
_, recorder = self._instrument_functions_service()
Expand All @@ -290,3 +286,16 @@ def test_invalid_task_id_error(self, task_id):
'task_id can contain only letters ([A-Za-z]), numbers ([0-9]), '
'hyphens (-), or underscores (_). The maximum length is 500 characters.'
)

@pytest.mark.parametrize('uri', [
'', ' ', 'a', 'foo', 'image.jpg', [], {}, True, 'google.com', 'www.google.com'
])
def test_invalid_uri_error(self, uri):
_, recorder = self._instrument_functions_service()
opts = functions.TaskOptions(uri=uri)
queue = functions.task_queue('test-function-name')
with pytest.raises(ValueError) as excinfo:
queue.enqueue(_DEFAULT_DATA, opts)
assert len(recorder) == 0
assert str(excinfo.value) == \
'uri must be a valid RFC3986 URI string using the https or http schema.'
15 changes: 14 additions & 1 deletion tests/testutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

import pytest

from google.auth import credentials
from google.auth import credentials, compute_engine
from google.auth import transport
from requests import adapters
from requests import models
Expand Down Expand Up @@ -133,6 +133,19 @@ def __init__(self):
def get_credential(self):
return self._g_credential

class MockGoogleComputeEngineCredential(compute_engine.Credentials):
"""A mock Compute Engine credential"""
def refresh(self, request):
self.token = 'mock-compute-engine-token'

class MockComputeEngineCredential(firebase_admin.credentials.Base):
"""A mock Firebase credential implementation."""

def __init__(self):
self._g_credential = MockGoogleComputeEngineCredential()

def get_credential(self):
return self._g_credential

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

0 comments on commit 998ada6

Please sign in to comment.