-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
create /internal/tasks/ API to call arbitrary Celery tasks remotely a…
…nd check their async results
- Loading branch information
1 parent
622d0c3
commit 8f1c4c8
Showing
5 changed files
with
285 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
"""Collection of tests for getting task results from the internal API.""" | ||
from unittest.mock import patch | ||
|
||
import faker | ||
from django.test import TestCase | ||
from rest_framework.test import APIRequestFactory | ||
|
||
from internal.views import task_get | ||
|
||
_faker = faker.Faker() | ||
|
||
|
||
class TaskGetTest(TestCase): | ||
"""Task get view test case.""" | ||
|
||
def setUp(self): | ||
"""Set up shared test data.""" | ||
self.factory = APIRequestFactory() | ||
AsyncResult_patch = patch("internal.views.AsyncResult") | ||
self.mock_AsyncResult = AsyncResult_patch.start() | ||
self.addCleanup(AsyncResult_patch.stop) | ||
|
||
def test_task_get(self): | ||
"""Test happy path success getting a completed (ready) task.""" | ||
async_result_id = str(_faker.uuid4()) | ||
self.mock_AsyncResult.return_value.ready.return_value = True | ||
expected_returned_value = self.mock_AsyncResult.return_value.get.return_value | ||
|
||
request = self.factory.get(f"/task_get/{async_result_id}/") | ||
response = task_get(request, async_result_id) | ||
|
||
self.mock_AsyncResult.assert_called_once_with(async_result_id) | ||
self.mock_AsyncResult.return_value.get.assert_called_once_with() | ||
|
||
self.assertEqual(response.status_code, 200) | ||
self.assertTrue(response.data["async_result_id"], async_result_id) | ||
self.assertTrue(response.data["ready"]) | ||
self.assertEqual(response.data["result"], expected_returned_value) | ||
|
||
def test_task_get_not_ready(self): | ||
"""Test when task has not completed (is not ready).""" | ||
async_result_id = str(_faker.uuid4()) | ||
self.mock_AsyncResult.return_value.ready.return_value = False | ||
|
||
request = self.factory.get(f"/task_get/{async_result_id}/") | ||
response = task_get(request, async_result_id) | ||
|
||
self.mock_AsyncResult.assert_called_once_with(async_result_id) | ||
self.mock_AsyncResult.return_value.get.assert_not_called() | ||
|
||
self.assertEqual(response.status_code, 200) | ||
self.assertTrue(response.data["async_result_id"], async_result_id) | ||
self.assertFalse(response.data["ready"]) | ||
self.assertIsNone(response.data["result"]) | ||
|
||
def test_task_get_execution_raised_exception(self): | ||
""" | ||
Test when the worker raised an execution when executing the task. | ||
Typically, what happens is the async task reports "ready" and the "get" call | ||
raises the exception that occurred when the worker executed the task. Example: | ||
$ http localhost:8000/internal/tasks/ \ | ||
task_name="api.tasks.enable_account" kwargs:='{"cloud_account_id":"potato"}' | ||
HTTP/1.1 201 Created | ||
{ | ||
"async_result_id": "9fb41f3d-e4ed-4b55-844a-6d7b62732d80" | ||
} | ||
$ http localhost:8000/internal/tasks/9fb41f3d-e4ed-4b55-844a-6d7b62732d80/ | ||
HTTP/1.1 200 OK | ||
{ | ||
"async_result_id": "9fb41f3d-e4ed-4b55-844a-6d7b62732d80", | ||
"error_args": [ | ||
"Field 'id' expected a number but got 'potato'." | ||
], | ||
"error_class": "builtins.ValueError" | ||
} | ||
""" | ||
async_result_id = str(_faker.uuid4()) | ||
self.mock_AsyncResult.return_value.ready.return_value = True | ||
error_message = "Field 'id' expected a number but got 'potato'." | ||
self.mock_AsyncResult.return_value.get.side_effect = ValueError(error_message) | ||
|
||
request = self.factory.get(f"/task_get/{async_result_id}/") | ||
response = task_get(request, async_result_id) | ||
|
||
self.mock_AsyncResult.assert_called_once_with(async_result_id) | ||
self.mock_AsyncResult.return_value.get.assert_called_once_with() | ||
|
||
self.assertEqual(response.status_code, 200) | ||
self.assertTrue(response.data["async_result_id"], async_result_id) | ||
self.assertEqual(response.data["error_args"], [error_message]) | ||
self.assertEqual(response.data["error_class"], "builtins.ValueError") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
"""Collection of tests for running tasks from the internal API.""" | ||
from unittest.mock import patch | ||
|
||
import faker | ||
from django.test import TestCase | ||
from rest_framework.test import APIRequestFactory | ||
|
||
from internal.views import task_run | ||
|
||
_faker = faker.Faker() | ||
|
||
|
||
class TaskRunTest(TestCase): | ||
"""Task run view test case.""" | ||
|
||
def setUp(self): | ||
"""Set up shared test data.""" | ||
self.factory = APIRequestFactory() | ||
celery_app_patch = patch("internal.views.celery_app") | ||
self.mock_celery_app = celery_app_patch.start() | ||
self.addCleanup(celery_app_patch.stop) | ||
|
||
def test_task_run(self): | ||
"""Test happy path success for running an arbitrary task.""" | ||
task_name = _faker.slug() | ||
task_kwargs = {_faker.slug(): _faker.slug(), _faker.slug(): _faker.slug()} | ||
|
||
mock_signature = self.mock_celery_app.signature.return_value | ||
mock_async_result = mock_signature.delay.return_value | ||
|
||
request = self.factory.post( | ||
"/task_run/", | ||
data={"task_name": task_name, "kwargs": task_kwargs}, | ||
format="json", | ||
) | ||
response = task_run(request) | ||
|
||
self.mock_celery_app.signature.assert_called_with(task_name) | ||
mock_signature.delay.assert_called_with(**task_kwargs) | ||
|
||
self.assertEqual(response.status_code, 201) | ||
self.assertEqual(response.data["async_result_id"], mock_async_result.id) | ||
|
||
def test_task_run_bad_post_params(self): | ||
"""Test error is returned and task is not called when bad input is given.""" | ||
task_name = _faker.slug() | ||
request = self.factory.post( | ||
"/task_run/", | ||
data={task_name: task_name, "unrelated_potato_argument": task_name}, | ||
format="json", | ||
) | ||
response = task_run(request) | ||
self.assertEqual(response.status_code, 400) | ||
self.mock_celery_app.signature.assert_not_called() | ||
|
||
def test_task_run_bad_task_kwargs(self): | ||
""" | ||
Test error is returned when task is called when bad kwargs. | ||
Celery's delay raises TypeError if the arguments it is given do not match the | ||
actual function signature of the task being called. Example: | ||
$ http localhost:8000/internal/tasks/ \ | ||
task_name="api.tasks.enable_account" kwargs:='{"potato":1}' | ||
HTTP/1.1 400 Bad Request | ||
{"error":"enable_account() got an unexpected keyword argument 'potato'"} | ||
""" | ||
task_name = _faker.slug() | ||
task_kwargs = {_faker.slug(): _faker.slug(), _faker.slug(): _faker.slug()} | ||
|
||
type_error_message = _faker.sentence() | ||
mock_signature = self.mock_celery_app.signature.return_value | ||
mock_signature.delay.side_effect = TypeError(type_error_message) | ||
|
||
request = self.factory.post( | ||
"/task_run/", | ||
data={"task_name": task_name, "kwargs": task_kwargs}, | ||
format="json", | ||
) | ||
response = task_run(request) | ||
|
||
self.mock_celery_app.signature.assert_called_with(task_name) | ||
mock_signature.delay.assert_called_with(**task_kwargs) | ||
|
||
self.assertEqual(response.status_code, 400) | ||
self.assertEqual(response.data["error"], type_error_message) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters