From cf1713b08561629080537309f7394ad1572226e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20KUBLER?= Date: Fri, 23 Jun 2017 16:12:15 +0200 Subject: [PATCH] Added a Unauthorized exception. Also added a few tests related to this new exception. --- sanic/exceptions.py | 28 ++++++++++++++++++++++++++++ tests/test_exceptions.py | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/sanic/exceptions.py b/sanic/exceptions.py index e1136dd1..1cf28a8e 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -198,6 +198,34 @@ class InvalidRangeType(ContentRangeError): pass +@add_status_code(401) +class Unauthorized(SanicException): + """ + Unauthorized exception (401 HTTP status code). + + :param scheme: Name of the authentication scheme to be used. + :param realm: Description of the protected area. (optional) + :param others: A dict containing values to add to the WWW-Authenticate + header that is generated. This is especially useful when dealing with the + Digest scheme. (optional) + """ + pass + + def __init__(self, message, scheme, realm="", others=None): + super().__init__(message) + + adds = "" + + if others is not None: + values = ["{!s}={!r}".format(k, v) for k, v in others.items()] + adds = ', '.join(values) + adds = ', {}'.format(adds) + + self.headers = { + "WWW-Authenticate": "{} realm='{}'{}".format(scheme, realm, adds) + } + + def abort(status_code, message=None): """ Raise an exception based on SanicException. Returns the HTTP response diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index a2b8dc71..4330ef57 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -3,7 +3,8 @@ from bs4 import BeautifulSoup from sanic import Sanic from sanic.response import text -from sanic.exceptions import InvalidUsage, ServerError, NotFound, abort +from sanic.exceptions import InvalidUsage, ServerError, NotFound, Unauthorized +from sanic.exceptions import abort class SanicExceptionTestException(Exception): @@ -26,6 +27,20 @@ def exception_app(): def handler_404(request): raise NotFound("OK") + @app.route('/401/basic') + def handler_401_basic(request): + raise Unauthorized("Unauthorized", "Basic", "Sanic") + + @app.route('/401/digest') + def handler_401_digest(request): + challenge = { + "qop": "auth, auth-int", + "algorithm": "MD5", + "nonce": "abcdef", + "opaque": "zyxwvu", + } + raise Unauthorized("Unauthorized", "Digest", "Sanic", challenge) + @app.route('/invalid') def handler_invalid(request): raise InvalidUsage("OK") @@ -49,8 +64,10 @@ def exception_app(): return app + def test_catch_exception_list(): app = Sanic('exception_list') + @app.exception([SanicExceptionTestException, NotFound]) def exception_list(request, exception): return text("ok") @@ -91,6 +108,24 @@ def test_not_found_exception(exception_app): assert response.status == 404 +def test_unauthorized_exception(exception_app): + """Test the built-in Unauthorized exception""" + request, response = exception_app.test_client.get('/401/basic') + assert response.status == 401 + assert response.headers.get('WWW-Authenticate') is not None + assert response.headers.get('WWW-Authenticate') == "Basic realm='Sanic'" + + request, response = exception_app.test_client.get('/401/digest') + assert response.status == 401 + + auth_header = response.headers.get('WWW-Authenticate') + expected = ("Digest realm='Sanic', qop='auth, auth-int', algorithm='MD5', " + "nonce='abcdef', opaque='zyxwvu'") + + assert auth_header is not None + assert auth_header == expected + + def test_handled_unhandled_exception(exception_app): """Test that an exception not built into sanic is handled""" request, response = exception_app.test_client.get('/divide_by_zero')