Skip to content

Commit e1fbb83

Browse files
committed
tests
1 parent dc07761 commit e1fbb83

File tree

3 files changed

+119
-9
lines changed

3 files changed

+119
-9
lines changed

services/web/server/src/simcore_service_webserver/errors.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,7 @@
44

55

66
class WebServerBaseError(OsparcErrorMixin, Exception):
7+
msg_template = "Unexpected error in web-server"
8+
79
def __init__(self, **ctx: Any) -> None:
810
super().__init__(**ctx)

services/web/server/src/simcore_service_webserver/exceptions_handlers.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
class WebApiExceptionHandler(Protocol):
1717
def __call__(
18-
self, request: web.Request, exception: BaseException
18+
self, exception: BaseException, request: web.Request
1919
) -> web.HTTPError | BaseException | None:
2020
"""
2121
Callback to process an exception raised during a web request, allowing custom handling.
@@ -24,8 +24,8 @@ def __call__(
2424
into an `web.HTTPError` (i.e.the errors specified in the web-api)
2525
2626
Arguments:
27-
request -- current request
2827
exception -- exception raised in web handler during this request
28+
request -- current request
2929
3030
Returns:
3131
- None: to suppress `exception`
@@ -64,10 +64,6 @@ def create_exception_handlers_decorator(
6464
def _decorator(handler: Handler):
6565
@functools.wraps(handler)
6666
async def _wrapper(request: web.Request) -> web.StreamResponse:
67-
# NOTE: this could als be dynamically extended with other
68-
# customizations of error handlers [(exception_types,exception_handler ), ...]
69-
# then we can use contextlib.ExitStack() to nest them.
70-
# In that case, the order will be important
7167
with _handled_exception_context(
7268
exception_types,
7369
exception_handler,
@@ -99,12 +95,12 @@ def __missing__(self, key):
9995

10096

10197
def _sort_exceptions_by_specificity(
102-
exceptions: list[type[BaseException]],
98+
exceptions: list[type[BaseException]], *, concrete_first: bool = True
10399
) -> list[type[BaseException]]:
104100
return sorted(
105101
exceptions,
106102
key=lambda exc: sum(issubclass(e, exc) for e in exceptions if e is not exc),
107-
reverse=True,
103+
reverse=not concrete_first,
108104
)
109105

110106

@@ -130,8 +126,8 @@ def create__http_error_map_handler(
130126
)
131127

132128
def _handler(
133-
request: web.Request,
134129
exception: BaseException,
130+
request: web.Request,
135131
) -> web.HTTPError | BaseException | None:
136132
if exc_cls := next((_ for _ in included if isinstance(exception, _)), None):
137133
http_error_info = to_http_error_map[exc_cls]
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# pylint: disable=protected-access
2+
# pylint: disable=redefined-outer-name
3+
# pylint: disable=too-many-arguments
4+
# pylint: disable=too-many-statements
5+
# pylint: disable=unused-argument
6+
# pylint: disable=unused-variable
7+
8+
9+
import pytest
10+
from aiohttp import web
11+
from aiohttp.test_utils import make_mocked_request
12+
from servicelib.aiohttp import status
13+
from simcore_service_webserver.errors import WebServerBaseError
14+
from simcore_service_webserver.exceptions_handlers import (
15+
HttpErrorInfo,
16+
_handled_exception_context,
17+
_sort_exceptions_by_specificity,
18+
create__http_error_map_handler,
19+
)
20+
21+
22+
class BasePluginError(WebServerBaseError):
23+
...
24+
25+
26+
class OneError(BasePluginError):
27+
...
28+
29+
30+
class OtherError(BasePluginError):
31+
...
32+
33+
34+
def test_sort_concrete_first():
35+
assert _sort_exceptions_by_specificity([Exception, BasePluginError]) == [
36+
BasePluginError,
37+
Exception,
38+
]
39+
40+
assert _sort_exceptions_by_specificity(
41+
[Exception, BasePluginError], concrete_first=False
42+
) == [
43+
Exception,
44+
BasePluginError,
45+
]
46+
47+
48+
def test_sort_exceptions_by_specificity():
49+
50+
got_exceptions_cls = _sort_exceptions_by_specificity(
51+
[
52+
Exception,
53+
OtherError,
54+
OneError,
55+
BasePluginError,
56+
ValueError,
57+
ArithmeticError,
58+
ZeroDivisionError,
59+
]
60+
)
61+
62+
for from_, exc in enumerate(got_exceptions_cls, start=1):
63+
for exc_after in got_exceptions_cls[from_:]:
64+
assert not issubclass(exc_after, exc), f"{got_exceptions_cls=}"
65+
66+
67+
@pytest.fixture
68+
def fake_request() -> web.Request:
69+
return make_mocked_request("GET", "/foo")
70+
71+
72+
def test_http_error_map_handler_factory(fake_request: web.Request):
73+
74+
exc_handler = create__http_error_map_handler(
75+
{
76+
OneError: HttpErrorInfo(
77+
status.HTTP_400_BAD_REQUEST, "Error One mapped to 400"
78+
)
79+
}
80+
)
81+
82+
# Converts exception in map
83+
got_exc = exc_handler(OneError(), fake_request)
84+
85+
assert isinstance(got_exc, web.HTTPBadRequest)
86+
assert got_exc.reason == "Error One mapped to 400"
87+
88+
# By-passes exceptions not listed
89+
err = RuntimeError()
90+
assert exc_handler(err, fake_request) is err
91+
92+
93+
def test_handled_exception_context(fake_request: web.Request):
94+
def _suppress_handler(exception, request):
95+
assert request == fake_request
96+
assert isinstance(
97+
exception, BasePluginError
98+
), "only BasePluginError exceptions should call this handler"
99+
return None # noqa: RET501, PLR1711
100+
101+
def _rest_handler(exc_cls):
102+
with _handled_exception_context(
103+
BasePluginError, _suppress_handler, request=fake_request
104+
):
105+
raise exc_cls
106+
107+
# checks
108+
_rest_handler(OneError)
109+
_rest_handler(OtherError)
110+
111+
with pytest.raises(ArithmeticError):
112+
_rest_handler(ArithmeticError)

0 commit comments

Comments
 (0)