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 1/3] 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') From 9fcdacb62405cc2e1ae63a73a3be74d3fa926083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20KUBLER?= Date: Fri, 23 Jun 2017 16:29:04 +0200 Subject: [PATCH 2/3] Modified the name of an argument. --- sanic/exceptions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sanic/exceptions.py b/sanic/exceptions.py index 1cf28a8e..95e41b4a 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -205,19 +205,19 @@ class Unauthorized(SanicException): :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 + :param challenge: 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): + def __init__(self, message, scheme, realm="", challenge=None): super().__init__(message) adds = "" - if others is not None: - values = ["{!s}={!r}".format(k, v) for k, v in others.items()] + if challenge is not None: + values = ["{!s}={!r}".format(k, v) for k, v in challenge.items()] adds = ', '.join(values) adds = ', {}'.format(adds) From 60aa60f48ea2536ac95ad61276dfa4ab64195a92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20KUBLER?= Date: Fri, 23 Jun 2017 17:16:31 +0200 Subject: [PATCH 3/3] Fixed the test for the new Unauthorized exception. --- tests/test_exceptions.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 4330ef57..dcdecabd 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -119,11 +119,12 @@ def test_unauthorized_exception(exception_app): 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 + assert auth_header.startswith('Digest') + assert "qop='auth, auth-int'" in auth_header + assert "algorithm='MD5'" in auth_header + assert "nonce='abcdef'" in auth_header + assert "opaque='zyxwvu'" in auth_header def test_handled_unhandled_exception(exception_app):