Close #3: forgot/reset password routes

This commit is contained in:
François Voron
2019-10-13 12:05:10 +02:00
parent ef1f60d7e5
commit 49daeff869
9 changed files with 305 additions and 37 deletions

View File

@@ -68,6 +68,9 @@ class MockUserDatabase(BaseUserDatabase):
async def create(self, user: BaseUserDB) -> BaseUserDB:
return user
async def update(self, user: BaseUserDB) -> BaseUserDB:
return user
@pytest.fixture
def mock_user_db() -> MockUserDatabase:

View File

@@ -5,11 +5,11 @@ from starlette import status
from starlette.responses import Response
from starlette.testclient import TestClient
from fastapi_users.authentication.jwt import JWTAuthentication, generate_jwt
from fastapi_users.authentication.jwt import JWTAuthentication
from fastapi_users.models import BaseUserDB
from fastapi_users.utils import JWT_ALGORITHM, generate_jwt
SECRET = "SECRET"
ALGORITHM = "HS256"
LIFETIME = 3600
@@ -21,8 +21,8 @@ def jwt_authentication():
@pytest.fixture
def token():
def _token(user, lifetime=LIFETIME):
data = {"user_id": user.id}
return generate_jwt(data, lifetime, SECRET, ALGORITHM)
data = {"user_id": user.id, "aud": "fastapi-users:auth"}
return generate_jwt(data, lifetime, SECRET, JWT_ALGORITHM)
return _token
@@ -47,7 +47,9 @@ async def test_get_login_response(jwt_authentication, user):
assert "token" in login_response
token = login_response["token"]
decoded = jwt.decode(token, SECRET, algorithms=[ALGORITHM])
decoded = jwt.decode(
token, SECRET, audience="fastapi-users:auth", algorithms=[JWT_ALGORITHM]
)
assert decoded["user_id"] == user.id

View File

@@ -73,10 +73,7 @@ async def test_queries(sqlalchemy_user_db):
# Exception when inserting non-nullable fields
with pytest.raises(sqlite3.IntegrityError):
wrong_user = BaseUserDB(
id="222",
hashed_password="aaa"
)
wrong_user = BaseUserDB(id="222", hashed_password="aaa")
await sqlalchemy_user_db.create(wrong_user)
# Unknown user

View File

@@ -6,13 +6,20 @@ from starlette.testclient import TestClient
from fastapi_users import FastAPIUsers
from fastapi_users.models import BaseUser, BaseUserDB
SECRET = "SECRET"
@pytest.fixture
def fastapi_users(mock_user_db, mock_authentication) -> FastAPIUsers:
class User(BaseUser):
pass
return FastAPIUsers(mock_user_db, mock_authentication, User)
def on_after_forgot_password(user, token):
pass
return FastAPIUsers(
mock_user_db, mock_authentication, User, on_after_forgot_password, SECRET
)
@pytest.fixture
@@ -43,6 +50,12 @@ class TestRouter:
response = test_app_client.post("/login")
assert response.status_code != status.HTTP_404_NOT_FOUND
response = test_app_client.post("/forgot-password")
assert response.status_code != status.HTTP_404_NOT_FOUND
response = test_app_client.post("/reset-password")
assert response.status_code != status.HTTP_404_NOT_FOUND
class TestGetCurrentUser:
def test_missing_token(self, test_app_client: TestClient):

View File

@@ -1,3 +1,7 @@
import asyncio
from unittest.mock import MagicMock
import jwt
import pytest
from fastapi import FastAPI
from starlette import status
@@ -5,19 +9,65 @@ from starlette.testclient import TestClient
from fastapi_users.models import BaseUser, BaseUserDB
from fastapi_users.router import get_user_router
from fastapi_users.utils import JWT_ALGORITHM, generate_jwt
SECRET = "SECRET"
LIFETIME = 3600
@pytest.fixture
def test_app_client(mock_user_db, mock_authentication) -> TestClient:
class User(BaseUser):
pass
def forgot_password_token():
def _forgot_password_token(user_id, lifetime=LIFETIME):
data = {"user_id": user_id, "aud": "fastapi-users:reset"}
return generate_jwt(data, lifetime, SECRET, JWT_ALGORITHM)
userRouter = get_user_router(mock_user_db, User, mock_authentication)
return _forgot_password_token
app = FastAPI()
app.include_router(userRouter)
return TestClient(app)
@pytest.fixture()
def on_after_forgot_password_sync():
on_after_forgot_password_mock = MagicMock(return_value=None)
return on_after_forgot_password_mock
@pytest.fixture()
def on_after_forgot_password_async():
on_after_forgot_password_mock = MagicMock(return_value=asyncio.Future())
on_after_forgot_password_mock.return_value.set_result(None)
return on_after_forgot_password_mock
@pytest.fixture
def get_test_app_client(mock_user_db, mock_authentication):
def _get_test_app_client(on_after_forgot_password) -> TestClient:
class User(BaseUser):
pass
userRouter = get_user_router(
mock_user_db,
User,
mock_authentication,
on_after_forgot_password,
SECRET,
LIFETIME,
)
app = FastAPI()
app.include_router(userRouter)
return TestClient(app)
return _get_test_app_client
@pytest.fixture
def test_app_client(get_test_app_client, on_after_forgot_password_sync):
return get_test_app_client(on_after_forgot_password_sync)
@pytest.fixture
def test_app_client_async(get_test_app_client, on_after_forgot_password_async):
return get_test_app_client(on_after_forgot_password_async)
class TestRegister:
@@ -81,3 +131,124 @@ class TestLogin:
data = {"username": "percival@camelot.bt", "password": "angharad"}
response = test_app_client.post("/login", data=data)
assert response.status_code == status.HTTP_400_BAD_REQUEST
class TestForgotPassword:
def test_empty_body(
self, test_app_client: TestClient, on_after_forgot_password_sync
):
response = test_app_client.post("/forgot-password", json={})
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
assert on_after_forgot_password_sync.called is False
def test_not_existing_user(
self, test_app_client: TestClient, on_after_forgot_password_sync
):
json = {"email": "lancelot@camelot.bt"}
response = test_app_client.post("/forgot-password", json=json)
assert response.status_code == status.HTTP_202_ACCEPTED
assert on_after_forgot_password_sync.called is False
def test_inactive_user(
self, test_app_client: TestClient, on_after_forgot_password_sync
):
json = {"email": "percival@camelot.bt"}
response = test_app_client.post("/forgot-password", json=json)
assert response.status_code == status.HTTP_202_ACCEPTED
assert on_after_forgot_password_sync.called is False
def test_existing_user_sync_hook(
self, test_app_client: TestClient, on_after_forgot_password_sync, user
):
json = {"email": "king.arthur@camelot.bt"}
response = test_app_client.post("/forgot-password", json=json)
assert response.status_code == status.HTTP_202_ACCEPTED
assert on_after_forgot_password_sync.called is True
actual_user = on_after_forgot_password_sync.call_args[0][0]
assert actual_user.id == user.id
actual_token = on_after_forgot_password_sync.call_args[0][1]
decoded_token = jwt.decode(
actual_token,
SECRET,
audience="fastapi-users:reset",
algorithms=[JWT_ALGORITHM],
)
assert decoded_token["user_id"] == user.id
def test_existing_user_async_hook(
self, test_app_client_async: TestClient, on_after_forgot_password_async, user
):
json = {"email": "king.arthur@camelot.bt"}
response = test_app_client_async.post("/forgot-password", json=json)
assert response.status_code == status.HTTP_202_ACCEPTED
assert on_after_forgot_password_async.called is True
actual_user = on_after_forgot_password_async.call_args[0][0]
assert actual_user.id == user.id
actual_token = on_after_forgot_password_async.call_args[0][1]
decoded_token = jwt.decode(
actual_token,
SECRET,
audience="fastapi-users:reset",
algorithms=[JWT_ALGORITHM],
)
assert decoded_token["user_id"] == user.id
class TestResetPassword:
def test_empty_body(self, test_app_client: TestClient):
response = test_app_client.post("/reset-password", json={})
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
def test_missing_token(self, test_app_client: TestClient):
json = {"password": "guinevere"}
response = test_app_client.post("/reset-password", json=json)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
def test_missing_password(self, test_app_client: TestClient):
json = {"token": "foo"}
response = test_app_client.post("/reset-password", json=json)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
def test_invalid_token(self, test_app_client: TestClient):
json = {"token": "foo", "password": "guinevere"}
response = test_app_client.post("/reset-password", json=json)
print(response.json(), response.status_code)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_inactive_user(
self,
mocker,
mock_user_db,
test_app_client: TestClient,
forgot_password_token,
inactive_user: BaseUserDB,
):
mocker.spy(mock_user_db, "update")
json = {
"token": forgot_password_token(inactive_user.id),
"password": "holygrail",
}
response = test_app_client.post("/reset-password", json=json)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert mock_user_db.update.called is False
def test_existing_user(
self,
mocker,
mock_user_db,
test_app_client: TestClient,
forgot_password_token,
user: BaseUserDB,
):
mocker.spy(mock_user_db, "update")
json = {"token": forgot_password_token(user.id), "password": "holygrail"}
response = test_app_client.post("/reset-password", json=json)
assert response.status_code == status.HTTP_200_OK
assert mock_user_db.update.called is True
updated_user = mock_user_db.update.call_args[0][0]
assert updated_user.hashed_password != user.hashed_password