9 Commits

5 changed files with 65 additions and 23 deletions

View File

@ -343,7 +343,7 @@ to ask the user for the username and password again.
Log In Bookkeeping Log In Bookkeeping
=================# ==================
You can register a callback to run when the user logs in, for ex., You can register a callback to run when the user logs in, for ex.,
logging the log in event, adding the log in counter, etc. logging the log in event, adding the log in counter, etc.
@ -359,10 +359,13 @@ Writing Tests
============= =============
You can write tests with our test client that handles HTTP Digest You can write tests with our test client that handles HTTP Digest
Authentication. Example for a unittest testcase: Authentication.
Example for a unittest_ test case:
:: ::
from flask import Flask
from flask_digest_auth import Client from flask_digest_auth import Client
from flask_testing import TestCase from flask_testing import TestCase
from my_app import create_app from my_app import create_app
@ -385,6 +388,41 @@ Authentication. Example for a unittest testcase:
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
Example for a pytest_ test:
::
import pytest
from flask import Flask
from flask_digest_auth import Client
from my_app import create_app
@pytest.fixture()
def app():
app: Flask = create_app({
"SECRET_KEY": token_urlsafe(32),
"TESTING": True
})
app.test_client_class = Client
yield app
@pytest.fixture()
def client(app):
return app.test_client()
def test_admin(app: Flask, client: Client):
with app.app_context():
response = self.client.get("/admin")
assert response.status_code == 401
response = self.client.get(
"/admin", digest_auth=("my_name", "my_pass"))
assert response.status_code == 200
.. _unittest: https://docs.python.org/3/library/unittest.html
.. _pytest: https://pytest.org
Copyright Copyright
========= =========
@ -402,6 +440,7 @@ Copyright
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
Authors Authors
======= =======

View File

@ -17,7 +17,7 @@
[metadata] [metadata]
name = flask-digest-auth name = flask-digest-auth
version = 0.2.0 version = 0.2.1
author = imacat author = imacat
author_email = imacat@mail.imacat.idv.tw author_email = imacat@mail.imacat.idv.tw
description = The Flask HTTP Digest Authentication project. description = The Flask HTTP Digest Authentication project.

View File

@ -110,7 +110,7 @@ class DigestAuth:
class NoLogInException(Exception): class NoLogInException(Exception):
"""The exception thrown when the user is not authorized.""" """The exception thrown when the user is not authorized."""
def get_logged_in_user() -> t.Optional[t.Any]: def get_logged_in_user() -> t.Any:
"""Returns the currently logged-in user. """Returns the currently logged-in user.
:return: The currently logged-in user. :return: The currently logged-in user.
@ -120,6 +120,7 @@ class DigestAuth:
raise NoLogInException raise NoLogInException
user: t.Optional[t.Any] = self.__get_user(session["user"]) user: t.Optional[t.Any] = self.__get_user(session["user"])
if user is None: if user is None:
del session["user"]
raise NoLogInException raise NoLogInException
return user return user

View File

@ -35,14 +35,15 @@ _PASSWORD: str = "Circle Of Life"
class User: class User:
"""A dummy user""" """A dummy user"""
def __init__(self, username: str, password_hash: str): def __init__(self, username: str, password: str):
"""Constructs a dummy user. """Constructs a dummy user.
:param username: The username. :param username: The username.
:param password_hash: The password hash. :param password: The clear-text password.
""" """
self.username: str = username self.username: str = username
self.password_hash: str = password_hash self.password_hash: str = make_password_hash(
_REALM, username, password)
self.visits: int = 0 self.visits: int = 0
@ -63,9 +64,8 @@ class AuthenticationTestCase(TestCase):
auth: DigestAuth = DigestAuth(realm=_REALM) auth: DigestAuth = DigestAuth(realm=_REALM)
auth.init_app(app) auth.init_app(app)
user_db: t.Dict[str, User] \ self.user: User = User(_USERNAME, _PASSWORD)
= {_USERNAME: User( user_db: t.Dict[str, User] = {_USERNAME: self.user}
_USERNAME, make_password_hash(_REALM, _USERNAME, _PASSWORD))}
@auth.register_get_password @auth.register_get_password
def get_password_hash(username: str) -> t.Optional[str]: def get_password_hash(username: str) -> t.Optional[str]:
@ -141,7 +141,7 @@ class AuthenticationTestCase(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data.decode("UTF-8"), self.assertEqual(response.data.decode("UTF-8"),
f"Hello, {_USERNAME}! #2") f"Hello, {_USERNAME}! #2")
self.assertEqual(g.user.visits, 1) self.assertEqual(self.user.visits, 1)
def test_stale_opaque(self) -> None: def test_stale_opaque(self) -> None:
"""Tests the stale and opaque value. """Tests the stale and opaque value.
@ -218,4 +218,4 @@ class AuthenticationTestCase(TestCase):
response = self.client.get(admin_uri) response = self.client.get(admin_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(g.user.visits, 2) self.assertEqual(self.user.visits, 2)

View File

@ -21,7 +21,6 @@
import typing as t import typing as t
from secrets import token_urlsafe from secrets import token_urlsafe
import flask_login
from flask import Response, Flask, g, redirect, request from flask import Response, Flask, g, redirect, request
from flask_testing import TestCase from flask_testing import TestCase
from werkzeug.datastructures import WWWAuthenticate, Authorization from werkzeug.datastructures import WWWAuthenticate, Authorization
@ -36,14 +35,15 @@ _PASSWORD: str = "Circle Of Life"
class User: class User:
"""A dummy user.""" """A dummy user."""
def __init__(self, username: str, password_hash: str): def __init__(self, username: str, password: str):
"""Constructs a dummy user. """Constructs a dummy user.
:param username: The username. :param username: The username.
:param password_hash: The password hash. :param password: The clear-text password.
""" """
self.username: str = username self.username: str = username
self.password_hash: str = password_hash self.password_hash: str = make_password_hash(
_REALM, username, password)
self.visits: int = 0 self.visits: int = 0
self.is_authenticated: bool = True self.is_authenticated: bool = True
self.is_active: bool = True self.is_active: bool = True
@ -86,9 +86,8 @@ class FlaskLoginTestCase(TestCase):
auth: DigestAuth = DigestAuth(realm=_REALM) auth: DigestAuth = DigestAuth(realm=_REALM)
auth.init_app(app) auth.init_app(app)
user_db: t.Dict[str, User] \ self.user: User = User(_USERNAME, _PASSWORD)
= {_USERNAME: User( user_db: t.Dict[str, User] = {_USERNAME: self.user}
_USERNAME, make_password_hash(_REALM, _USERNAME, _PASSWORD))}
@auth.register_get_password @auth.register_get_password
def get_password_hash(username: str) -> t.Optional[str]: def get_password_hash(username: str) -> t.Optional[str]:
@ -154,7 +153,7 @@ class FlaskLoginTestCase(TestCase):
:return: None. :return: None.
""" """
if not self.has_flask_login: if not self.has_flask_login:
self.skipTest("Skipped testing Flask-Login integration without it.") self.skipTest("Skipped without Flask-Login.")
response: Response = self.client.get(self.app.url_for("admin-1")) response: Response = self.client.get(self.app.url_for("admin-1"))
self.assertEqual(response.status_code, 401) self.assertEqual(response.status_code, 401)
@ -167,7 +166,7 @@ class FlaskLoginTestCase(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data.decode("UTF-8"), self.assertEqual(response.data.decode("UTF-8"),
f"Hello, {_USERNAME}! #2") f"Hello, {_USERNAME}! #2")
self.assertEqual(flask_login.current_user.visits, 1) self.assertEqual(self.user.visits, 1)
def test_stale_opaque(self) -> None: def test_stale_opaque(self) -> None:
"""Tests the stale and opaque value. """Tests the stale and opaque value.
@ -175,7 +174,7 @@ class FlaskLoginTestCase(TestCase):
:return: None. :return: None.
""" """
if not self.has_flask_login: if not self.has_flask_login:
self.skipTest("Skipped testing Flask-Login integration without it.") self.skipTest("Skipped without Flask-Login.")
admin_uri: str = self.app.url_for("admin-1") admin_uri: str = self.app.url_for("admin-1")
response: Response response: Response
@ -222,6 +221,9 @@ class FlaskLoginTestCase(TestCase):
:return: None. :return: None.
""" """
if not self.has_flask_login:
self.skipTest("Skipped without Flask-Login.")
admin_uri: str = self.app.url_for("admin-1") admin_uri: str = self.app.url_for("admin-1")
logout_uri: str = self.app.url_for("logout") logout_uri: str = self.app.url_for("logout")
response: Response response: Response
@ -253,4 +255,4 @@ class FlaskLoginTestCase(TestCase):
response = self.client.get(admin_uri) response = self.client.get(admin_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(flask_login.current_user.visits, 2) self.assertEqual(self.user.visits, 2)