216 lines
5.8 KiB
Python
216 lines
5.8 KiB
Python
import threading
|
|
from types import SimpleNamespace
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
from aiohttp import web
|
|
|
|
from core.routes.helpers import create_route_helpers, register_service_route
|
|
|
|
|
|
class _HashCancelled(Exception):
|
|
pass
|
|
|
|
|
|
class _Routes:
|
|
def __init__(self):
|
|
self.handlers = {}
|
|
|
|
def post(self, path):
|
|
def register(handler):
|
|
self.handlers[('POST', path)] = handler
|
|
return handler
|
|
|
|
return register
|
|
|
|
def get(self, path):
|
|
def register(handler):
|
|
self.handlers[('GET', path)] = handler
|
|
return handler
|
|
|
|
return register
|
|
|
|
|
|
def _build_helpers():
|
|
logger = MagicMock()
|
|
helpers = create_route_helpers(
|
|
web=web,
|
|
logger=logger,
|
|
load_settings=MagicMock(return_value={"download_backend": "python"}),
|
|
hash_calculation_cancelled=_HashCancelled,
|
|
)
|
|
return helpers, logger
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_register_service_route_delegates_to_the_operation():
|
|
routes = _Routes()
|
|
requests = []
|
|
|
|
async def operation(request):
|
|
requests.append(request)
|
|
return 'ok'
|
|
|
|
register_service_route(
|
|
{
|
|
'json_api_endpoint': lambda _error_prefix: lambda func: func,
|
|
'routes': routes,
|
|
},
|
|
path='/model_resolver/example',
|
|
error_prefix='example',
|
|
operation=operation,
|
|
)
|
|
|
|
request = object()
|
|
response = await routes.handlers[('POST', '/model_resolver/example')](request)
|
|
|
|
assert response == 'ok'
|
|
assert requests == [request]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_register_service_route_supports_multiple_methods_and_error_flags():
|
|
routes = _Routes()
|
|
endpoint_options = []
|
|
|
|
def endpoint(error_prefix, return_success_on_error=False):
|
|
endpoint_options.append((error_prefix, return_success_on_error))
|
|
return lambda func: func
|
|
|
|
async def operation(_request):
|
|
return 'ok'
|
|
|
|
register_service_route(
|
|
{
|
|
'json_api_endpoint': endpoint,
|
|
'routes': routes,
|
|
},
|
|
path='/model_resolver/example',
|
|
methods=('get', 'post'),
|
|
error_prefix='example',
|
|
operation=operation,
|
|
return_success_on_error=True,
|
|
)
|
|
|
|
assert set(routes.handlers) == {
|
|
('GET', '/model_resolver/example'),
|
|
('POST', '/model_resolver/example'),
|
|
}
|
|
assert endpoint_options == [('example', True)]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_json_api_endpoint_returns_500_for_unexpected_exception():
|
|
(json_api_endpoint, *_), logger = _build_helpers()
|
|
|
|
@json_api_endpoint("test")
|
|
async def handler(request):
|
|
raise RuntimeError("boom")
|
|
|
|
response = await handler(SimpleNamespace())
|
|
|
|
assert response.status == 500
|
|
assert response.text == '{"error": "boom"}'
|
|
logger.error.assert_called_once()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_json_api_endpoint_preserves_http_exception_status():
|
|
(json_api_endpoint, *_), logger = _build_helpers()
|
|
|
|
@json_api_endpoint("test")
|
|
async def handler(request):
|
|
raise web.HTTPBadRequest(reason="invalid payload")
|
|
|
|
response = await handler(SimpleNamespace())
|
|
|
|
assert response.status == 400
|
|
assert response.text == '{"error": "invalid payload"}'
|
|
logger.error.assert_called_once()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_json_api_endpoint_can_include_failure_success_flag():
|
|
(json_api_endpoint, *_), _ = _build_helpers()
|
|
|
|
@json_api_endpoint("test", return_success_on_error=True)
|
|
async def handler(request):
|
|
raise RuntimeError("boom")
|
|
|
|
response = await handler(SimpleNamespace())
|
|
|
|
assert response.status == 500
|
|
assert response.text == '{"error": "boom", "success": false}'
|
|
|
|
|
|
def test_progress_helpers_validate_ids_and_wrap_results():
|
|
(
|
|
_,
|
|
get_progress_response,
|
|
cancel_progress_response,
|
|
*_rest,
|
|
), _ = _build_helpers()
|
|
tracker = MagicMock()
|
|
tracker.get.return_value = {"status": "running"}
|
|
tracker.mark_cancelled.return_value = False
|
|
request = SimpleNamespace(match_info={"progress_id": "job-1"})
|
|
|
|
progress = get_progress_response(
|
|
tracker,
|
|
request,
|
|
found_wrapper=lambda payload: {"exists": True, **payload},
|
|
)
|
|
cancelled = cancel_progress_response(tracker, request)
|
|
|
|
assert progress.text == '{"exists": true, "status": "running"}'
|
|
assert cancelled.text == (
|
|
'{"success": true, "cancelled": false, "progress_id": "job-1"}'
|
|
)
|
|
tracker.cleanup.assert_called_once_with()
|
|
tracker.mark_cancelled.assert_called_once_with("job-1", "Cancelled")
|
|
|
|
|
|
@pytest.mark.parametrize("helper_index", [1, 2])
|
|
def test_progress_helpers_reject_blank_ids(helper_index):
|
|
(
|
|
_,
|
|
get_progress_response,
|
|
cancel_progress_response,
|
|
*_rest,
|
|
), _ = _build_helpers()
|
|
tracker = MagicMock()
|
|
request = SimpleNamespace(match_info={"analysis_id": " "})
|
|
helper = (get_progress_response, cancel_progress_response)[helper_index - 1]
|
|
|
|
response = helper(tracker, request, param_name="analysis_id")
|
|
|
|
assert response.status == 400
|
|
assert response.text == '{"error": "analysis_id is required"}'
|
|
tracker.cleanup.assert_not_called()
|
|
tracker.get.assert_not_called()
|
|
tracker.mark_cancelled.assert_not_called()
|
|
|
|
|
|
def test_background_helper_reports_unexpected_task_errors():
|
|
(_, _, _, run_in_background_thread, *_rest), logger = _build_helpers()
|
|
tracker = MagicMock()
|
|
tracker.is_cancelled.return_value = False
|
|
on_error = MagicMock()
|
|
error_reported = threading.Event()
|
|
|
|
def report_error(error):
|
|
on_error(error)
|
|
error_reported.set()
|
|
|
|
run_in_background_thread(
|
|
tracker,
|
|
"job-1",
|
|
lambda: (_ for _ in ()).throw(RuntimeError("boom")),
|
|
MagicMock(),
|
|
on_error=report_error,
|
|
)
|
|
|
|
assert error_reported.wait(timeout=2)
|
|
on_error.assert_called_once()
|
|
logger.exception.assert_called_once()
|