diff --git a/CHANGELOG b/CHANGELOG index b9fce3f48a..061b18ba1b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Pass app and request to wsgi error handlers * Add lazy_gettext * Add format_timedelta to Report * Add partial TO_CHAR support for date and datetime on SQLite diff --git a/trytond/tests/test_wsgi.py b/trytond/tests/test_wsgi.py new file mode 100644 index 0000000000..4d899c2bc6 --- /dev/null +++ b/trytond/tests/test_wsgi.py @@ -0,0 +1,118 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. + +import unittest +from unittest.mock import Mock, sentinel + +from werkzeug.wrappers import Response +from werkzeug.test import Client +from trytond.wsgi import TrytondWSGI +from trytond.exceptions import TrytonException + + +class WSGIAppTestCase(unittest.TestCase): + 'Test WSGI Application' + + class TestException(TrytonException): + pass + + def test_one_error_handler_called(self): + """Test an error handler is called + with the app, the request and the exception.""" + app = TrytondWSGI() + spy = Mock() + + @app.error_handler + def _handler(*args, **kwargs): + spy(*args, **kwargs) + + exception = self.TestException('foo') + + @app.route('/willfail') + def _route(request): + sentinel.request = request + raise exception + + client = Client(app) + (response, status, headers) = client.get('/willfail') + + spy.assert_called_once_with(app, sentinel.request, exception) + + def test_many_error_handlers_called(self): + "Test many error handlers are called" + app = TrytondWSGI() + spy1 = Mock() + spy2 = Mock() + + @app.error_handler + def _handler1(*args, **kwargs): + spy1(*args, **kwargs) + + @app.error_handler + def _handler2(*args, **kwargs): + spy2(*args, **kwargs) + + exception = self.TestException('foo') + + @app.route('/willfail') + def _route(request): + sentinel.request = request + raise exception + + client = Client(app) + (response, status, headers) = client.get('/willfail') + + spy1.assert_called_once_with(app, sentinel.request, exception) + spy2.assert_called_once_with(app, sentinel.request, exception) + + def test_class_error_handler_called(self): + "Test class error handlers can be used" + app = TrytondWSGI() + spy = Mock() + + class ErrorHandler(): + def __call__(self, *args, **kwargs): + spy(*args, **kwargs) + + app.error_handler(ErrorHandler()) + + exception = self.TestException('foo') + + @app.route('/willfail') + def _route(request): + sentinel.request = request + raise exception + + client = Client(app) + (response, status, headers) = client.get('/willfail') + + spy.assert_called_once_with(app, sentinel.request, exception) + + def test_error_handlers_last_response(self): + "Test last handler response is used" + app = TrytondWSGI() + + @app.error_handler + def _handler1(*args, **kwargs): + return Response(b'bar') + + @app.error_handler + def _handler2(*args, **kwargs): + return Response(b'baz', status=418) + + @app.route('/willfail') + def _route(request): + raise self.TestException('foo') + + client = Client(app) + (response, status, headers) = client.get('/willfail') + + self.assertEqual(next(response), b'baz') + self.assertEqual(status, "418 I'M A TEAPOT") + + +def suite(): + suite_ = unittest.TestSuite() + suite_.addTests( + unittest.TestLoader().loadTestsFromTestCase(WSGIAppTestCase)) + return suite_ diff --git a/trytond/wsgi.py b/trytond/wsgi.py index 8c1e9e4c5c..c876fcb85e 100644 --- a/trytond/wsgi.py +++ b/trytond/wsgi.py @@ -65,6 +65,10 @@ class TrytondWSGI(object): return func return decorator + def error_handler(self, handler): + self.error_handlers.append(handler) + return handler + @wrapt.decorator def auth_required(self, wrapped, instance, args, kwargs): request = args[0] @@ -111,7 +115,7 @@ class TrytondWSGI(object): e.__format_traceback__ = tb_s response = e for error_handler in self.error_handlers: - rv = error_handler(e) + rv = error_handler(self, request, e) if isinstance(rv, Response): response = rv return response