Skip to content

Commit 3158e85

Browse files
committed
Amending ellar exception module refactor and added more test for new exception module implementation
1 parent badc350 commit 3158e85

File tree

13 files changed

+242
-25
lines changed

13 files changed

+242
-25
lines changed

tests/test_application/sample.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,13 @@ def runtime_error(self, request: Request):
9898
)
9999
class ApplicationModule:
100100
@exception_handler(405)
101-
async def method_not_allow_exception(self, request, exec):
101+
async def method_not_allow_exception(self, ctx, exec):
102102
return JSONResponse({"detail": "Custom message"}, status_code=405)
103103

104104
@exception_handler(500)
105-
async def error_500(self, request, exec):
105+
async def error_500(self, ctx, exec):
106106
return JSONResponse({"detail": "Server Error"}, status_code=500)
107107

108108
@exception_handler(HTTPException)
109-
async def http_exception(self, request, exc):
109+
async def http_exception(self, ctx, exc):
110110
return JSONResponse({"detail": exc.detail}, status_code=exc.status_code)

tests/test_application/test_application_functions.py

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os
2+
import typing as t
23

3-
from starlette.responses import JSONResponse
4+
from starlette.responses import JSONResponse, Response
45

56
from ellar.common import Module, get, template_filter, template_global
67
from ellar.compatible import asynccontextmanager
@@ -15,6 +16,11 @@
1516
from ellar.core.connection import Request
1617
from ellar.core.context import IExecutionContext
1718
from ellar.core.events import EventHandler
19+
from ellar.core.exceptions.interfaces import (
20+
IExceptionHandler,
21+
IExceptionMiddlewareService,
22+
)
23+
from ellar.core.exceptions.service import ExceptionMiddlewareService
1824
from ellar.core.modules import ModuleTemplateRef
1925
from ellar.core.staticfiles import StaticFiles
2026
from ellar.core.templating import Environment
@@ -179,7 +185,12 @@ def test_app_staticfiles_route(self, tmpdir):
179185
file.write("<file content>")
180186

181187
config = Config(STATIC_DIRECTORIES=[tmpdir])
182-
app = App(injector=EllarInjector(), config=config)
188+
injector = EllarInjector()
189+
injector.container.register_singleton(
190+
IExceptionMiddlewareService, ExceptionMiddlewareService
191+
)
192+
injector.container.register_instance(config)
193+
app = App(injector=injector, config=config)
183194
client = TestClient(app)
184195

185196
response = client.get("/static/example.txt")
@@ -198,7 +209,12 @@ def test_app_staticfiles_with_different_static_path(self, tmpdir):
198209
config = Config(
199210
STATIC_MOUNT_PATH="/static-modified", STATIC_DIRECTORIES=[tmpdir]
200211
)
201-
app = App(injector=EllarInjector(), config=config)
212+
injector = EllarInjector()
213+
injector.container.register_singleton(
214+
IExceptionMiddlewareService, ExceptionMiddlewareService
215+
)
216+
injector.container.register_instance(config)
217+
app = App(injector=injector, config=config)
202218
client = TestClient(app)
203219

204220
response = client.get("/static-modified/example.txt")
@@ -246,6 +262,10 @@ def test_app_initialization_completion(self):
246262
injector = EllarInjector(
247263
auto_bind=False
248264
) # will raise an exception is service is not registered
265+
injector.container.register_singleton(
266+
IExceptionMiddlewareService, ExceptionMiddlewareService
267+
)
268+
injector.container.register_instance(config)
249269

250270
app = App(config=config, injector=injector)
251271
assert injector.get(Reflector)
@@ -258,16 +278,24 @@ def test_app_exception_handler(self):
258278
class CustomException(Exception):
259279
pass
260280

281+
class CustomExceptionHandler(IExceptionHandler):
282+
exception_type_or_code = CustomException
283+
284+
async def catch(
285+
self, ctx: IExecutionContext, exc: t.Union[t.Any, Exception]
286+
) -> t.Union[Response, t.Any]:
287+
return JSONResponse(dict(detail=str(exc)), status_code=404)
288+
261289
config = Config()
262290
injector = EllarInjector(
263291
auto_bind=False
264292
) # will raise an exception is service is not registered
265-
293+
injector.container.register_singleton(
294+
IExceptionMiddlewareService, ExceptionMiddlewareService
295+
)
296+
injector.container.register_instance(config)
266297
app = App(config=config, injector=injector)
267-
268-
@app.exception_handler(CustomException)
269-
async def exception_custom_exception(request, exc):
270-
return JSONResponse(dict(detail=str(exc)), status_code=404)
298+
app.add_exception_handler(CustomExceptionHandler())
271299

272300
@get("/404")
273301
def raise_custom_exception():

tests/test_common/test_decorators/test_html.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
TEMPLATE_GLOBAL_KEY,
1010
)
1111
from ellar.core.connection import Request
12+
from ellar.core.exceptions import ImproperConfiguration
1213
from ellar.core.response.model import HTMLResponseModel
1314
from ellar.core.templating import TemplateFunctionData
14-
from ellar.exceptions import ImproperConfiguration
1515
from ellar.reflect import reflect
1616

1717

tests/test_common/test_decorators/test_modules.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
from ellar.common import Module
44
from ellar.constants import MODULE_METADATA, MODULE_WATERMARK
5+
from ellar.core.exceptions import ImproperConfiguration
56
from ellar.di import has_binding, is_decorated_with_injectable
6-
from ellar.exceptions import ImproperConfiguration
77
from ellar.reflect import reflect
88

99

tests/test_conf/test_default_conf.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from ellar.core.conf import Config, ConfigDefaultTypesMixin
99
from ellar.core.conf.config import ConfigRuntimeError
1010
from ellar.core.versioning import DefaultAPIVersioning, UrlPathAPIVersioning
11-
from ellar.exceptions import APIException, RequestValidationError
1211

1312

1413
class ConfigTesting(ConfigDefaultTypesMixin):
@@ -46,9 +45,6 @@ def test_default_configurations():
4645

4746
assert config.MIDDLEWARE == []
4847

49-
assert RequestValidationError in config.EXCEPTION_HANDLERS
50-
assert APIException in config.EXCEPTION_HANDLERS
51-
5248
assert callable(config.DEFAULT_NOT_FOUND_HANDLER)
5349
assert config.DEFAULT_LIFESPAN_HANDLER is None
5450

tests/test_exceptions/test_api_exception.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from ellar.common import get
44
from ellar.core import TestClientFactory
5-
from ellar.exceptions import (
5+
from ellar.core.exceptions import (
66
APIException,
77
AuthenticationFailed,
88
MethodNotAllowed,
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import typing as t
2+
3+
import pytest
4+
from pydantic.error_wrappers import ValidationError
5+
from starlette.exceptions import HTTPException
6+
from starlette.responses import JSONResponse, Response
7+
8+
from ellar.common import get
9+
from ellar.core import Config, TestClientFactory
10+
from ellar.core.context import IExecutionContext
11+
from ellar.core.exceptions.callable_exceptions import CallableExceptionHandler
12+
from ellar.core.exceptions.handlers import APIException, APIExceptionHandler
13+
from ellar.core.exceptions.interfaces import IExceptionHandler
14+
from ellar.core.exceptions.service import ExceptionMiddlewareService
15+
from ellar.core.middleware import ExceptionMiddleware
16+
17+
18+
class InvalidExceptionHandler:
19+
pass
20+
21+
22+
class NewException(Exception):
23+
pass
24+
25+
26+
class NewExceptionHandler(IExceptionHandler):
27+
exception_type_or_code = NewException
28+
29+
async def catch(
30+
self, ctx: IExecutionContext, exc: t.Union[t.Any, Exception]
31+
) -> t.Union[Response, t.Any]:
32+
return JSONResponse({"detail": str(exc)}, status_code=400)
33+
34+
35+
class OverrideAPIExceptionHandler(APIExceptionHandler):
36+
async def catch(
37+
self, ctx: IExecutionContext, exc: t.Union[t.Any, Exception]
38+
) -> t.Union[Response, t.Any]:
39+
return JSONResponse({"detail": str(exc)}, status_code=404)
40+
41+
42+
class ServerErrorHandler(IExceptionHandler):
43+
exception_type_or_code = 500
44+
45+
async def catch(
46+
self, ctx: IExecutionContext, exc: t.Union[t.Any, Exception]
47+
) -> t.Union[Response, t.Any]:
48+
return JSONResponse({"detail": "Server Error"}, status_code=500)
49+
50+
51+
def error_500(ctx: IExecutionContext, exc: Exception):
52+
assert isinstance(ctx, IExecutionContext)
53+
return JSONResponse({"detail": "Server Error"}, status_code=500)
54+
55+
56+
def test_invalid_handler_raise_exception():
57+
with pytest.raises(ValidationError) as ex:
58+
Config(EXCEPTION_HANDLERS=[InvalidExceptionHandler])
59+
60+
assert ex.value.errors() == [
61+
{
62+
"loc": ("EXCEPTION_HANDLERS", 0),
63+
"msg": "Expected TExceptionHandler, received: <class 'tests.test_exceptions.test_custom_exceptions.InvalidExceptionHandler'>",
64+
"type": "value_error",
65+
}
66+
]
67+
68+
with pytest.raises(ValidationError) as ex:
69+
Config(EXCEPTION_HANDLERS=[InvalidExceptionHandler()])
70+
71+
assert ex.value.errors() == [
72+
{
73+
"loc": ("EXCEPTION_HANDLERS", 0),
74+
"msg": "Expected TExceptionHandler, received: "
75+
"<class 'tests.test_exceptions.test_custom_exceptions.InvalidExceptionHandler'>",
76+
"type": "value_error",
77+
}
78+
]
79+
80+
81+
def test_invalid_iexception_handler_setup_raise_exception():
82+
with pytest.raises(AssertionError) as ex:
83+
84+
class InvalidExceptionSetup(IExceptionHandler):
85+
def catch(
86+
self, ctx: IExecutionContext, exc: t.Any
87+
) -> t.Union[Response, t.Any]:
88+
pass
89+
90+
assert "exception_type_or_code must be defined" in str(ex.value)
91+
92+
93+
def test_custom_exception_works():
94+
@get()
95+
def homepage():
96+
raise NewException("New Exception")
97+
98+
tm = TestClientFactory.create_test_module()
99+
tm.app.router.append(homepage)
100+
tm.app.config.EXCEPTION_HANDLERS += [NewExceptionHandler()]
101+
tm.app.rebuild_middleware_stack()
102+
103+
client = tm.get_client()
104+
res = client.get("/")
105+
assert res.status_code == 400
106+
assert res.json() == {"detail": "New Exception"}
107+
108+
109+
def test_exception_override_works():
110+
@get()
111+
def homepage():
112+
raise APIException("New APIException")
113+
114+
tm = TestClientFactory.create_test_module()
115+
tm.app.router.append(homepage)
116+
tm.app.config.EXCEPTION_HANDLERS += [OverrideAPIExceptionHandler()]
117+
tm.app.rebuild_middleware_stack()
118+
119+
client = tm.get_client()
120+
res = client.get("/")
121+
assert res.status_code == 404
122+
assert res.json() == {"detail": "New APIException"}
123+
124+
125+
@pytest.mark.parametrize(
126+
"exception_handler",
127+
[
128+
CallableExceptionHandler(
129+
exc_class_or_status_code=500, callable_exception_handler=error_500
130+
),
131+
ServerErrorHandler(),
132+
],
133+
)
134+
def test_500_error_as_a_function(exception_handler):
135+
@get()
136+
def homepage():
137+
raise RuntimeError("Server Error")
138+
139+
tm = TestClientFactory.create_test_module()
140+
tm.app.router.append(homepage)
141+
tm.app.config.EXCEPTION_HANDLERS += [exception_handler]
142+
tm.app.rebuild_middleware_stack()
143+
144+
client = tm.get_client(raise_server_exceptions=False)
145+
res = client.get("/")
146+
assert res.status_code == 500
147+
assert res.json() == {"detail": "Server Error"}
148+
149+
150+
def test_raise_default_http_exception():
151+
@get()
152+
def homepage():
153+
raise HTTPException(detail="Bad Request", status_code=400)
154+
155+
tm = TestClientFactory.create_test_module()
156+
tm.app.router.append(homepage)
157+
client = tm.get_client()
158+
res = client.get("/")
159+
assert res.status_code == 400
160+
assert res.text == "Bad Request"
161+
162+
163+
@pytest.mark.parametrize("status_code", [204, 304])
164+
def test_raise_default_http_exception_for_204_and_304(status_code):
165+
@get()
166+
def homepage():
167+
raise HTTPException(detail="Server Error", status_code=status_code)
168+
169+
tm = TestClientFactory.create_test_module()
170+
tm.app.router.append(homepage)
171+
172+
client = tm.get_client()
173+
res = client.get("/")
174+
assert res.status_code == status_code
175+
assert res.text == ""
176+
177+
178+
def test_debug_after_response_sent(test_client_factory):
179+
async def app(scope, receive, send):
180+
response = Response(b"", status_code=204)
181+
await response(scope, receive, send)
182+
raise RuntimeError("Something went wrong")
183+
184+
app = ExceptionMiddleware(
185+
app,
186+
debug=True,
187+
exception_middleware_service=ExceptionMiddlewareService(config=Config()),
188+
)
189+
client = test_client_factory(app)
190+
with pytest.raises(RuntimeError):
191+
client.get("/")

tests/test_exceptions/test_error_details.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from ellar.exceptions.base import ErrorDetail
1+
from ellar.core.exceptions.base import ErrorDetail
22

33

44
class TestErrorDetail:

tests/test_exceptions/test_validation_exception.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from ellar.common import Ws, get, ws_route
55
from ellar.core import TestClientFactory
6-
from ellar.exceptions.validation import (
6+
from ellar.core.exceptions.validation import (
77
RequestValidationError,
88
WebSocketRequestValidationError,
99
)

tests/test_guard.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from ellar.common import Req, get, guards
55
from ellar.core import AppFactory, TestClient
6+
from ellar.core.exceptions import APIException
67
from ellar.core.guard import (
78
APIKeyCookie,
89
APIKeyHeader,
@@ -11,7 +12,6 @@
1112
HttpBearerAuth,
1213
HttpDigestAuth,
1314
)
14-
from ellar.exceptions import APIException
1515
from ellar.openapi import OpenAPIDocumentBuilder
1616
from ellar.serializer import serialize_object
1717

0 commit comments

Comments
 (0)