mirror of
https://github.com/fastapi-users/fastapi-users.git
synced 2025-11-02 12:21:53 +08:00
Revamp authentication routes structure (#201)
* Fix #68: use makefun to generate dynamic dependencies * Remove every Starlette imports * Split every routers and remove event handlers * Make users router optional * Pass after_update handler to get_users_router * Update documentation * Remove test file * Write migration doc for splitted routers
This commit is contained in:
@ -1,17 +1,14 @@
|
||||
import asyncio
|
||||
from typing import Any, List, Mapping, Optional, Tuple
|
||||
from typing import List, Optional
|
||||
|
||||
import http.cookies
|
||||
import httpx
|
||||
import pytest
|
||||
from asgi_lifespan import LifespanManager
|
||||
from fastapi import Depends, FastAPI
|
||||
from fastapi import Depends, Response, FastAPI
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from httpx_oauth.oauth2 import OAuth2
|
||||
from pydantic import UUID4
|
||||
from starlette.applications import ASGIApp
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
|
||||
from fastapi_users import models
|
||||
from fastapi_users.authentication import Authenticator, BaseAuthentication
|
||||
@ -229,16 +226,15 @@ def mock_user_db_oauth(
|
||||
return MockUserDatabase(UserDBOAuth)
|
||||
|
||||
|
||||
class MockAuthentication(BaseAuthentication):
|
||||
class MockAuthentication(BaseAuthentication[str]):
|
||||
def __init__(self, name: str = "mock"):
|
||||
super().__init__(name)
|
||||
super().__init__(name, logout=True)
|
||||
self.scheme = OAuth2PasswordBearer("/users/login", auto_error=False)
|
||||
|
||||
async def __call__(self, request: Request, user_db: BaseUserDatabase):
|
||||
token = await self.scheme.__call__(request)
|
||||
if token is not None:
|
||||
async def __call__(self, credentials: Optional[str], user_db: BaseUserDatabase):
|
||||
if credentials is not None:
|
||||
try:
|
||||
token_uuid = UUID4(token)
|
||||
token_uuid = UUID4(credentials)
|
||||
return await user_db.get(token_uuid)
|
||||
except ValueError:
|
||||
return None
|
||||
@ -247,41 +243,15 @@ class MockAuthentication(BaseAuthentication):
|
||||
async def get_login_response(self, user: BaseUserDB, response: Response):
|
||||
return {"token": user.id}
|
||||
|
||||
async def get_logout_response(self, user: BaseUserDB, response: Response):
|
||||
return None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_authentication():
|
||||
return MockAuthentication()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def request_builder():
|
||||
def _request_builder(
|
||||
headers: Mapping[str, Any] = None, cookies: Mapping[str, str] = None
|
||||
) -> Request:
|
||||
encoded_headers: List[Tuple[bytes, bytes]] = []
|
||||
|
||||
if headers is not None:
|
||||
encoded_headers += [
|
||||
(key.lower().encode("latin-1"), headers[key].encode("latin-1"))
|
||||
for key in headers
|
||||
]
|
||||
|
||||
if cookies is not None:
|
||||
for key in cookies:
|
||||
cookie = http.cookies.SimpleCookie() # type: http.cookies.BaseCookie
|
||||
cookie[key] = cookies[key]
|
||||
cookie_val = cookie.output(header="").strip()
|
||||
encoded_headers.append((b"cookie", cookie_val.encode("latin-1")))
|
||||
|
||||
scope = {
|
||||
"type": "http",
|
||||
"headers": encoded_headers,
|
||||
}
|
||||
return Request(scope)
|
||||
|
||||
return _request_builder
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def get_test_client():
|
||||
async def _get_test_client(app: ASGIApp) -> httpx.AsyncClient:
|
||||
@ -304,7 +274,7 @@ def get_test_auth_client(mock_user_db, get_test_client):
|
||||
authenticator = Authenticator(backends, mock_user_db)
|
||||
|
||||
@app.get("/test-current-user")
|
||||
def test_current_user(user: UserDB = Depends(authenticator.get_current_user),):
|
||||
def test_current_user(user: UserDB = Depends(authenticator.get_current_user)):
|
||||
return user
|
||||
|
||||
@app.get("/test-current-active-user")
|
||||
|
||||
@ -1,49 +1,63 @@
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
from starlette import status
|
||||
from starlette.requests import Request
|
||||
from fastapi import Request, status
|
||||
from fastapi.security.base import SecurityBase
|
||||
|
||||
from fastapi_users.authentication import BaseAuthentication
|
||||
from fastapi_users.authentication import (
|
||||
BaseAuthentication,
|
||||
DuplicateBackendNamesError,
|
||||
)
|
||||
from fastapi_users.db import BaseUserDatabase
|
||||
from fastapi_users.models import BaseUserDB
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def auth_backend_none():
|
||||
class BackendNone(BaseAuthentication):
|
||||
async def __call__(
|
||||
self, request: Request, user_db: BaseUserDatabase
|
||||
) -> Optional[BaseUserDB]:
|
||||
return None
|
||||
|
||||
return BackendNone()
|
||||
class MockSecurityScheme(SecurityBase):
|
||||
def __call__(self, request: Request) -> Optional[str]:
|
||||
return "mock"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def auth_backend_user(user):
|
||||
class BackendUser(BaseAuthentication):
|
||||
async def __call__(
|
||||
self, request: Request, user_db: BaseUserDatabase
|
||||
) -> Optional[BaseUserDB]:
|
||||
return user
|
||||
class BackendNone(BaseAuthentication[str]):
|
||||
def __init__(self, name="none"):
|
||||
super().__init__(name, logout=False)
|
||||
self.scheme = MockSecurityScheme()
|
||||
|
||||
return BackendUser()
|
||||
async def __call__(
|
||||
self, credentials: Optional[str], user_db: BaseUserDatabase
|
||||
) -> Optional[BaseUserDB]:
|
||||
return None
|
||||
|
||||
|
||||
class BackendUser(BaseAuthentication[str]):
|
||||
def __init__(self, user: BaseUserDB, name="user"):
|
||||
super().__init__(name, logout=False)
|
||||
self.scheme = MockSecurityScheme()
|
||||
self.user = user
|
||||
|
||||
async def __call__(
|
||||
self, credentials: Optional[str], user_db: BaseUserDatabase
|
||||
) -> Optional[BaseUserDB]:
|
||||
return self.user
|
||||
|
||||
|
||||
@pytest.mark.authentication
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticator(
|
||||
get_test_auth_client, auth_backend_none, auth_backend_user
|
||||
):
|
||||
client = await get_test_auth_client([auth_backend_none, auth_backend_user])
|
||||
async def test_authenticator(get_test_auth_client, user):
|
||||
client = await get_test_auth_client([BackendNone(), BackendUser(user)])
|
||||
response = await client.get("/test-current-user")
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
|
||||
@pytest.mark.authentication
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticator_none(get_test_auth_client, auth_backend_none):
|
||||
client = await get_test_auth_client([auth_backend_none, auth_backend_none])
|
||||
async def test_authenticator_none(get_test_auth_client):
|
||||
client = await get_test_auth_client([BackendNone(), BackendNone(name="none-bis")])
|
||||
response = await client.get("/test-current-user")
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
@pytest.mark.authentication
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticators_with_same_name(get_test_auth_client):
|
||||
with pytest.raises(DuplicateBackendNamesError):
|
||||
await get_test_auth_client([BackendNone(), BackendNone()])
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import pytest
|
||||
from starlette.responses import Response
|
||||
from fastapi import Response
|
||||
|
||||
from fastapi_users.authentication import BaseAuthentication
|
||||
|
||||
@ -12,12 +12,9 @@ def base_authentication():
|
||||
@pytest.mark.authentication
|
||||
class TestAuthenticate:
|
||||
@pytest.mark.asyncio
|
||||
async def test_not_implemented(
|
||||
self, base_authentication, mock_user_db, request_builder
|
||||
):
|
||||
request = request_builder({})
|
||||
async def test_not_implemented(self, base_authentication, mock_user_db):
|
||||
with pytest.raises(NotImplementedError):
|
||||
await base_authentication(request, mock_user_db)
|
||||
await base_authentication(None, mock_user_db)
|
||||
|
||||
|
||||
@pytest.mark.authentication
|
||||
|
||||
@ -2,7 +2,7 @@ import re
|
||||
|
||||
import jwt
|
||||
import pytest
|
||||
from starlette.responses import Response
|
||||
from fastapi import Response
|
||||
|
||||
from fastapi_users.authentication.cookie import CookieAuthentication
|
||||
from fastapi_users.utils import JWT_ALGORITHM, generate_jwt
|
||||
@ -28,10 +28,10 @@ cookie_authentication_httponly = CookieAuthentication(
|
||||
|
||||
@pytest.fixture
|
||||
def token():
|
||||
def _token(user=None, lifetime=LIFETIME):
|
||||
def _token(user_id=None, lifetime=LIFETIME):
|
||||
data = {"aud": "fastapi-users:auth"}
|
||||
if user is not None:
|
||||
data["user_id"] = str(user.id)
|
||||
if user_id is not None:
|
||||
data["user_id"] = str(user_id)
|
||||
return generate_jwt(data, lifetime, SECRET, JWT_ALGORITHM)
|
||||
|
||||
return _token
|
||||
@ -45,35 +45,28 @@ def test_default_name():
|
||||
@pytest.mark.authentication
|
||||
class TestAuthenticate:
|
||||
@pytest.mark.asyncio
|
||||
async def test_missing_token(self, mock_user_db, request_builder):
|
||||
request = request_builder()
|
||||
authenticated_user = await cookie_authentication(request, mock_user_db)
|
||||
async def test_missing_token(self, mock_user_db):
|
||||
authenticated_user = await cookie_authentication(None, mock_user_db)
|
||||
assert authenticated_user is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invalid_token(self, mock_user_db, request_builder):
|
||||
cookies = {}
|
||||
cookies[COOKIE_NAME] = "foo"
|
||||
request = request_builder(cookies=cookies)
|
||||
authenticated_user = await cookie_authentication(request, mock_user_db)
|
||||
async def test_invalid_token(self, mock_user_db):
|
||||
authenticated_user = await cookie_authentication("foo", mock_user_db)
|
||||
assert authenticated_user is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_valid_token_missing_user_payload(
|
||||
self, mock_user_db, request_builder, token
|
||||
):
|
||||
cookies = {}
|
||||
cookies[COOKIE_NAME] = token()
|
||||
request = request_builder(cookies=cookies)
|
||||
authenticated_user = await cookie_authentication(request, mock_user_db)
|
||||
async def test_valid_token_missing_user_payload(self, mock_user_db, token):
|
||||
authenticated_user = await cookie_authentication(token(), mock_user_db)
|
||||
assert authenticated_user is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_valid_token(self, mock_user_db, request_builder, token, user):
|
||||
cookies = {}
|
||||
cookies[COOKIE_NAME] = token(user)
|
||||
request = request_builder(cookies=cookies)
|
||||
authenticated_user = await cookie_authentication(request, mock_user_db)
|
||||
async def test_valid_token_invalid_uuid(self, mock_user_db, token):
|
||||
authenticated_user = await cookie_authentication(token("foo"), mock_user_db)
|
||||
assert authenticated_user is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_valid_token(self, mock_user_db, token, user):
|
||||
authenticated_user = await cookie_authentication(token(user.id), mock_user_db)
|
||||
assert authenticated_user.id == user.id
|
||||
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import jwt
|
||||
import pytest
|
||||
from starlette.responses import Response
|
||||
from fastapi import Response
|
||||
|
||||
from fastapi_users.authentication.jwt import JWTAuthentication
|
||||
from fastapi_users.utils import JWT_ALGORITHM, generate_jwt
|
||||
@ -34,43 +34,32 @@ def test_default_name(jwt_authentication):
|
||||
@pytest.mark.authentication
|
||||
class TestAuthenticate:
|
||||
@pytest.mark.asyncio
|
||||
async def test_missing_token(
|
||||
self, jwt_authentication, mock_user_db, request_builder
|
||||
):
|
||||
request = request_builder(headers={})
|
||||
authenticated_user = await jwt_authentication(request, mock_user_db)
|
||||
async def test_missing_token(self, jwt_authentication, mock_user_db):
|
||||
authenticated_user = await jwt_authentication(None, mock_user_db)
|
||||
assert authenticated_user is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invalid_token(
|
||||
self, jwt_authentication, mock_user_db, request_builder
|
||||
):
|
||||
request = request_builder(headers={"Authorization": "Bearer foo"})
|
||||
authenticated_user = await jwt_authentication(request, mock_user_db)
|
||||
async def test_invalid_token(self, jwt_authentication, mock_user_db):
|
||||
authenticated_user = await jwt_authentication("foo", mock_user_db)
|
||||
assert authenticated_user is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_valid_token_missing_user_payload(
|
||||
self, jwt_authentication, mock_user_db, request_builder, token
|
||||
self, jwt_authentication, mock_user_db, token
|
||||
):
|
||||
request = request_builder(headers={"Authorization": f"Bearer {token()}"})
|
||||
authenticated_user = await jwt_authentication(request, mock_user_db)
|
||||
authenticated_user = await jwt_authentication(token(), mock_user_db)
|
||||
assert authenticated_user is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_valid_token_invalid_uuid(
|
||||
self, jwt_authentication, mock_user_db, request_builder, token
|
||||
self, jwt_authentication, mock_user_db, token
|
||||
):
|
||||
request = request_builder(headers={"Authorization": f"Bearer {token('foo')}"})
|
||||
authenticated_user = await jwt_authentication(request, mock_user_db)
|
||||
authenticated_user = await jwt_authentication(token("foo"), mock_user_db)
|
||||
assert authenticated_user is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_valid_token(
|
||||
self, jwt_authentication, mock_user_db, request_builder, token, user
|
||||
):
|
||||
request = request_builder(headers={"Authorization": f"Bearer {token(user.id)}"})
|
||||
authenticated_user = await jwt_authentication(request, mock_user_db)
|
||||
async def test_valid_token(self, jwt_authentication, mock_user_db, token, user):
|
||||
authenticated_user = await jwt_authentication(token(user.id), mock_user_db)
|
||||
assert authenticated_user.id == user.id
|
||||
|
||||
|
||||
|
||||
@ -1,58 +1,26 @@
|
||||
import pytest
|
||||
import httpx
|
||||
from fastapi import Depends, FastAPI
|
||||
from httpx_oauth.oauth2 import OAuth2
|
||||
from starlette import status
|
||||
from fastapi import Depends, FastAPI, status
|
||||
|
||||
from fastapi_users import FastAPIUsers
|
||||
from fastapi_users.router import Event, EventHandlersRouter
|
||||
from tests.conftest import User, UserCreate, UserUpdate, UserDB
|
||||
|
||||
|
||||
def sync_event_handler():
|
||||
return None
|
||||
|
||||
|
||||
async def async_event_handler():
|
||||
return None
|
||||
|
||||
|
||||
@pytest.fixture(params=[sync_event_handler, async_event_handler])
|
||||
def fastapi_users(
|
||||
request, mock_user_db, mock_authentication, oauth_client
|
||||
) -> FastAPIUsers:
|
||||
fastapi_users = FastAPIUsers(
|
||||
mock_user_db,
|
||||
[mock_authentication],
|
||||
User,
|
||||
UserCreate,
|
||||
UserUpdate,
|
||||
UserDB,
|
||||
"SECRET",
|
||||
)
|
||||
|
||||
fastapi_users.get_oauth_router(oauth_client, "SECRET")
|
||||
|
||||
@fastapi_users.on_after_register()
|
||||
def on_after_register():
|
||||
return request.param()
|
||||
|
||||
@fastapi_users.on_after_forgot_password()
|
||||
def on_after_forgot_password():
|
||||
return request.param()
|
||||
|
||||
@fastapi_users.on_after_update()
|
||||
def on_after_update():
|
||||
return request.param()
|
||||
|
||||
return fastapi_users
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@pytest.mark.asyncio
|
||||
async def test_app_client(fastapi_users, get_test_client) -> httpx.AsyncClient:
|
||||
async def test_app_client(
|
||||
mock_user_db, mock_authentication, oauth_client, get_test_client
|
||||
) -> httpx.AsyncClient:
|
||||
fastapi_users = FastAPIUsers(
|
||||
mock_user_db, [mock_authentication], User, UserCreate, UserUpdate, UserDB,
|
||||
)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(fastapi_users.router, prefix="/users")
|
||||
app.include_router(fastapi_users.get_register_router())
|
||||
app.include_router(fastapi_users.get_reset_password_router("SECRET"))
|
||||
app.include_router(fastapi_users.get_auth_router(mock_authentication))
|
||||
app.include_router(fastapi_users.get_oauth_router(oauth_client, "SECRET"))
|
||||
app.include_router(fastapi_users.get_users_router(), prefix="/users")
|
||||
|
||||
@app.get("/current-user")
|
||||
def current_user(user=Depends(fastapi_users.get_current_user)):
|
||||
@ -69,35 +37,51 @@ async def test_app_client(fastapi_users, get_test_client) -> httpx.AsyncClient:
|
||||
return await get_test_client(app)
|
||||
|
||||
|
||||
@pytest.mark.fastapi_users
|
||||
class TestFastAPIUsers:
|
||||
def test_event_handlers(self, fastapi_users):
|
||||
event_handlers = fastapi_users.router.event_handlers
|
||||
assert len(event_handlers[Event.ON_AFTER_REGISTER]) == 1
|
||||
assert len(event_handlers[Event.ON_AFTER_FORGOT_PASSWORD]) == 1
|
||||
|
||||
|
||||
@pytest.mark.fastapi_users
|
||||
@pytest.mark.asyncio
|
||||
class TestRouter:
|
||||
class TestRoutes:
|
||||
async def test_routes_exist(self, test_app_client: httpx.AsyncClient):
|
||||
response = await test_app_client.post("/users/register")
|
||||
assert response.status_code != status.HTTP_404_NOT_FOUND
|
||||
response = await test_app_client.post("/register")
|
||||
assert response.status_code not in (
|
||||
status.HTTP_404_NOT_FOUND,
|
||||
status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
)
|
||||
|
||||
response = await test_app_client.post("/users/login")
|
||||
assert response.status_code != status.HTTP_404_NOT_FOUND
|
||||
response = await test_app_client.post("/forgot-password")
|
||||
assert response.status_code not in (
|
||||
status.HTTP_404_NOT_FOUND,
|
||||
status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
)
|
||||
|
||||
response = await test_app_client.post("/users/forgot-password")
|
||||
assert response.status_code != status.HTTP_404_NOT_FOUND
|
||||
response = await test_app_client.post("/reset-password")
|
||||
assert response.status_code not in (
|
||||
status.HTTP_404_NOT_FOUND,
|
||||
status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
)
|
||||
|
||||
response = await test_app_client.post("/users/reset-password")
|
||||
assert response.status_code != status.HTTP_404_NOT_FOUND
|
||||
response = await test_app_client.post("/login")
|
||||
assert response.status_code not in (
|
||||
status.HTTP_404_NOT_FOUND,
|
||||
status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
)
|
||||
|
||||
response = await test_app_client.post("/logout")
|
||||
assert response.status_code not in (
|
||||
status.HTTP_404_NOT_FOUND,
|
||||
status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
)
|
||||
|
||||
response = await test_app_client.get("/users/aaa")
|
||||
assert response.status_code != status.HTTP_404_NOT_FOUND
|
||||
assert response.status_code not in (
|
||||
status.HTTP_404_NOT_FOUND,
|
||||
status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
)
|
||||
|
||||
response = await test_app_client.patch("/users/aaa")
|
||||
assert response.status_code != status.HTTP_404_NOT_FOUND
|
||||
assert response.status_code not in (
|
||||
status.HTTP_404_NOT_FOUND,
|
||||
status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.fastapi_users
|
||||
@ -177,21 +161,3 @@ class TestGetCurrentSuperuser:
|
||||
"/current-superuser", headers={"Authorization": f"Bearer {superuser.id}"}
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
|
||||
@pytest.mark.fastapi_users
|
||||
def test_get_oauth_router(mocker, fastapi_users: FastAPIUsers, oauth_client: OAuth2):
|
||||
# Check that existing OAuth router declared
|
||||
# before the handlers decorators is correctly binded
|
||||
existing_oauth_router = fastapi_users.oauth_routers[0]
|
||||
event_handlers = existing_oauth_router.event_handlers
|
||||
assert len(event_handlers[Event.ON_AFTER_REGISTER]) == 1
|
||||
assert len(event_handlers[Event.ON_AFTER_FORGOT_PASSWORD]) == 1
|
||||
|
||||
# Check that OAuth router declared
|
||||
# after the handlers decorators is correctly binded
|
||||
oauth_router = fastapi_users.get_oauth_router(oauth_client, "SECRET")
|
||||
assert isinstance(oauth_router, EventHandlersRouter)
|
||||
event_handlers = oauth_router.event_handlers
|
||||
assert len(event_handlers[Event.ON_AFTER_REGISTER]) == 1
|
||||
assert len(event_handlers[Event.ON_AFTER_FORGOT_PASSWORD]) == 1
|
||||
|
||||
96
tests/test_router_auth.py
Normal file
96
tests/test_router_auth.py
Normal file
@ -0,0 +1,96 @@
|
||||
from typing import cast, Dict, Any
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from fastapi import FastAPI, status
|
||||
|
||||
from fastapi_users.authentication import Authenticator
|
||||
from fastapi_users.router import ErrorCode, get_auth_router
|
||||
from tests.conftest import MockAuthentication, UserDB
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@pytest.mark.asyncio
|
||||
async def test_app_client(
|
||||
mock_user_db, mock_authentication, get_test_client
|
||||
) -> httpx.AsyncClient:
|
||||
mock_authentication_bis = MockAuthentication(name="mock-bis")
|
||||
authenticator = Authenticator(
|
||||
[mock_authentication, mock_authentication_bis], mock_user_db
|
||||
)
|
||||
|
||||
mock_auth_router = get_auth_router(mock_authentication, mock_user_db, authenticator)
|
||||
mock_bis_auth_router = get_auth_router(
|
||||
mock_authentication_bis, mock_user_db, authenticator
|
||||
)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(mock_auth_router, prefix="/mock")
|
||||
app.include_router(mock_bis_auth_router, prefix="/mock-bis")
|
||||
|
||||
return await get_test_client(app)
|
||||
|
||||
|
||||
@pytest.mark.router
|
||||
@pytest.mark.parametrize("path", ["/mock/login", "/mock-bis/login"])
|
||||
@pytest.mark.asyncio
|
||||
class TestLogin:
|
||||
async def test_empty_body(self, path, test_app_client: httpx.AsyncClient):
|
||||
response = await test_app_client.post(path, data={})
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
async def test_missing_username(self, path, test_app_client: httpx.AsyncClient):
|
||||
data = {"password": "guinevere"}
|
||||
response = await test_app_client.post(path, data=data)
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
async def test_missing_password(self, path, test_app_client: httpx.AsyncClient):
|
||||
data = {"username": "king.arthur@camelot.bt"}
|
||||
response = await test_app_client.post(path, data=data)
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
async def test_not_existing_user(self, path, test_app_client: httpx.AsyncClient):
|
||||
data = {"username": "lancelot@camelot.bt", "password": "guinevere"}
|
||||
response = await test_app_client.post(path, data=data)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["detail"] == ErrorCode.LOGIN_BAD_CREDENTIALS
|
||||
|
||||
async def test_wrong_password(self, path, test_app_client: httpx.AsyncClient):
|
||||
data = {"username": "king.arthur@camelot.bt", "password": "percival"}
|
||||
response = await test_app_client.post(path, data=data)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["detail"] == ErrorCode.LOGIN_BAD_CREDENTIALS
|
||||
|
||||
async def test_valid_credentials(
|
||||
self, path, test_app_client: httpx.AsyncClient, user: UserDB
|
||||
):
|
||||
data = {"username": "king.arthur@camelot.bt", "password": "guinevere"}
|
||||
response = await test_app_client.post(path, data=data)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == {"token": str(user.id)}
|
||||
|
||||
async def test_inactive_user(self, path, test_app_client: httpx.AsyncClient):
|
||||
data = {"username": "percival@camelot.bt", "password": "angharad"}
|
||||
response = await test_app_client.post(path, data=data)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["detail"] == ErrorCode.LOGIN_BAD_CREDENTIALS
|
||||
|
||||
|
||||
@pytest.mark.router
|
||||
@pytest.mark.parametrize("path", ["/mock/logout", "/mock-bis/logout"])
|
||||
@pytest.mark.asyncio
|
||||
class TestLogout:
|
||||
async def test_missing_token(self, path, test_app_client: httpx.AsyncClient):
|
||||
response = await test_app_client.post(path)
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
async def test_valid_credentials(
|
||||
self, mocker, path, test_app_client: httpx.AsyncClient, user: UserDB
|
||||
):
|
||||
response = await test_app_client.post(
|
||||
path, headers={"Authorization": f"Bearer {user.id}"}
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
@ -4,12 +4,10 @@ from typing import Dict, Any, cast
|
||||
import asynctest
|
||||
import httpx
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from starlette import status
|
||||
from starlette.requests import Request
|
||||
from fastapi import FastAPI, status, Request
|
||||
|
||||
from fastapi_users.authentication import Authenticator
|
||||
from fastapi_users.router.common import ErrorCode, Event
|
||||
from fastapi_users.router.common import ErrorCode
|
||||
from fastapi_users.router.oauth import generate_state_token, get_oauth_router
|
||||
from tests.conftest import MockAuthentication, UserDB
|
||||
|
||||
@ -17,16 +15,16 @@ from tests.conftest import MockAuthentication, UserDB
|
||||
SECRET = "SECRET"
|
||||
|
||||
|
||||
def event_handler_sync():
|
||||
def after_register_sync():
|
||||
return MagicMock(return_value=None)
|
||||
|
||||
|
||||
def event_handler_async():
|
||||
def after_register_async():
|
||||
return asynctest.CoroutineMock(return_value=None)
|
||||
|
||||
|
||||
@pytest.fixture(params=[event_handler_sync, event_handler_async])
|
||||
def event_handler(request):
|
||||
@pytest.fixture(params=[after_register_sync, after_register_async])
|
||||
def after_register(request):
|
||||
return request.param()
|
||||
|
||||
|
||||
@ -35,7 +33,7 @@ def get_test_app_client(
|
||||
mock_user_db_oauth,
|
||||
mock_authentication,
|
||||
oauth_client,
|
||||
event_handler,
|
||||
after_register,
|
||||
get_test_client,
|
||||
):
|
||||
async def _get_test_app_client(redirect_url: str = None) -> httpx.AsyncClient:
|
||||
@ -51,10 +49,9 @@ def get_test_app_client(
|
||||
authenticator,
|
||||
SECRET,
|
||||
redirect_url,
|
||||
after_register,
|
||||
)
|
||||
|
||||
oauth_router.add_event_handler(Event.ON_AFTER_REGISTER, event_handler)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(oauth_router)
|
||||
|
||||
@ -151,7 +148,7 @@ class TestCallback:
|
||||
test_app_client: httpx.AsyncClient,
|
||||
oauth_client,
|
||||
user_oauth,
|
||||
event_handler,
|
||||
after_register,
|
||||
):
|
||||
with asynctest.patch.object(
|
||||
oauth_client, "get_access_token"
|
||||
@ -171,7 +168,7 @@ class TestCallback:
|
||||
get_id_email_mock.assert_awaited_once_with("TOKEN")
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
assert event_handler.called is False
|
||||
assert after_register.called is False
|
||||
|
||||
async def test_existing_user_with_oauth(
|
||||
self,
|
||||
@ -179,7 +176,7 @@ class TestCallback:
|
||||
test_app_client: httpx.AsyncClient,
|
||||
oauth_client,
|
||||
user_oauth,
|
||||
event_handler,
|
||||
after_register,
|
||||
):
|
||||
state_jwt = generate_state_token({"authentication_backend": "mock"}, "SECRET")
|
||||
with asynctest.patch.object(
|
||||
@ -206,7 +203,7 @@ class TestCallback:
|
||||
|
||||
assert data["token"] == str(user_oauth.id)
|
||||
|
||||
assert event_handler.called is False
|
||||
assert after_register.called is False
|
||||
|
||||
async def test_existing_user_without_oauth(
|
||||
self,
|
||||
@ -214,7 +211,7 @@ class TestCallback:
|
||||
test_app_client: httpx.AsyncClient,
|
||||
oauth_client,
|
||||
superuser_oauth,
|
||||
event_handler,
|
||||
after_register,
|
||||
):
|
||||
state_jwt = generate_state_token({"authentication_backend": "mock"}, "SECRET")
|
||||
with asynctest.patch.object(
|
||||
@ -244,14 +241,14 @@ class TestCallback:
|
||||
|
||||
assert data["token"] == str(superuser_oauth.id)
|
||||
|
||||
assert event_handler.called is False
|
||||
assert after_register.called is False
|
||||
|
||||
async def test_unknown_user(
|
||||
self,
|
||||
mock_user_db_oauth,
|
||||
test_app_client: httpx.AsyncClient,
|
||||
oauth_client,
|
||||
event_handler,
|
||||
after_register,
|
||||
):
|
||||
state_jwt = generate_state_token({"authentication_backend": "mock"}, "SECRET")
|
||||
with asynctest.patch.object(
|
||||
@ -281,10 +278,10 @@ class TestCallback:
|
||||
|
||||
assert "token" in data
|
||||
|
||||
assert event_handler.called is True
|
||||
actual_user = event_handler.call_args[0][0]
|
||||
assert after_register.called is True
|
||||
actual_user = after_register.call_args[0][0]
|
||||
assert str(actual_user.id) == data["token"]
|
||||
request = event_handler.call_args[0][1]
|
||||
request = after_register.call_args[0][1]
|
||||
assert isinstance(request, Request)
|
||||
|
||||
async def test_inactive_user(
|
||||
@ -293,7 +290,7 @@ class TestCallback:
|
||||
test_app_client: httpx.AsyncClient,
|
||||
oauth_client,
|
||||
inactive_user_oauth,
|
||||
event_handler,
|
||||
after_register,
|
||||
):
|
||||
state_jwt = generate_state_token({"authentication_backend": "mock"}, "SECRET")
|
||||
with asynctest.patch.object(
|
||||
@ -318,7 +315,7 @@ class TestCallback:
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["detail"] == ErrorCode.LOGIN_BAD_CREDENTIALS
|
||||
|
||||
assert event_handler.called is False
|
||||
assert after_register.called is False
|
||||
|
||||
async def test_redirect_url_router(
|
||||
self,
|
||||
|
||||
122
tests/test_router_register.py
Normal file
122
tests/test_router_register.py
Normal file
@ -0,0 +1,122 @@
|
||||
from typing import cast, Dict, Any
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import asynctest
|
||||
import httpx
|
||||
import pytest
|
||||
from fastapi import FastAPI, status, Request
|
||||
|
||||
from fastapi_users.router import ErrorCode, get_register_router
|
||||
from tests.conftest import User, UserCreate, UserDB
|
||||
|
||||
SECRET = "SECRET"
|
||||
LIFETIME = 3600
|
||||
|
||||
|
||||
def after_register_sync():
|
||||
return MagicMock(return_value=None)
|
||||
|
||||
|
||||
def after_register_async():
|
||||
return asynctest.CoroutineMock(return_value=None)
|
||||
|
||||
|
||||
@pytest.fixture(params=[after_register_sync, after_register_async])
|
||||
def after_register(request):
|
||||
return request.param()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@pytest.mark.asyncio
|
||||
async def test_app_client(
|
||||
mock_user_db, mock_authentication, after_register, get_test_client
|
||||
) -> httpx.AsyncClient:
|
||||
register_router = get_register_router(
|
||||
mock_user_db, User, UserCreate, UserDB, after_register,
|
||||
)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(register_router)
|
||||
|
||||
return await get_test_client(app)
|
||||
|
||||
|
||||
@pytest.mark.router
|
||||
@pytest.mark.asyncio
|
||||
class TestRegister:
|
||||
async def test_empty_body(self, test_app_client: httpx.AsyncClient, after_register):
|
||||
response = await test_app_client.post("/register", json={})
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
assert after_register.called is False
|
||||
|
||||
async def test_missing_password(
|
||||
self, test_app_client: httpx.AsyncClient, after_register
|
||||
):
|
||||
json = {"email": "king.arthur@camelot.bt"}
|
||||
response = await test_app_client.post("/register", json=json)
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
assert after_register.called is False
|
||||
|
||||
async def test_wrong_email(
|
||||
self, test_app_client: httpx.AsyncClient, after_register
|
||||
):
|
||||
json = {"email": "king.arthur", "password": "guinevere"}
|
||||
response = await test_app_client.post("/register", json=json)
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
assert after_register.called is False
|
||||
|
||||
async def test_existing_user(
|
||||
self, test_app_client: httpx.AsyncClient, after_register
|
||||
):
|
||||
json = {"email": "king.arthur@camelot.bt", "password": "guinevere"}
|
||||
response = await test_app_client.post("/register", json=json)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["detail"] == ErrorCode.REGISTER_USER_ALREADY_EXISTS
|
||||
assert after_register.called is False
|
||||
|
||||
async def test_valid_body(self, test_app_client: httpx.AsyncClient, after_register):
|
||||
json = {"email": "lancelot@camelot.bt", "password": "guinevere"}
|
||||
response = await test_app_client.post("/register", json=json)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert after_register.called is True
|
||||
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert "hashed_password" not in data
|
||||
assert "password" not in data
|
||||
assert data["id"] is not None
|
||||
|
||||
actual_user = after_register.call_args[0][0]
|
||||
assert str(actual_user.id) == data["id"]
|
||||
request = after_register.call_args[0][1]
|
||||
assert isinstance(request, Request)
|
||||
|
||||
async def test_valid_body_is_superuser(
|
||||
self, test_app_client: httpx.AsyncClient, after_register
|
||||
):
|
||||
json = {
|
||||
"email": "lancelot@camelot.bt",
|
||||
"password": "guinevere",
|
||||
"is_superuser": True,
|
||||
}
|
||||
response = await test_app_client.post("/register", json=json)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert after_register.called is True
|
||||
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["is_superuser"] is False
|
||||
|
||||
async def test_valid_body_is_active(
|
||||
self, test_app_client: httpx.AsyncClient, after_register
|
||||
):
|
||||
json = {
|
||||
"email": "lancelot@camelot.bt",
|
||||
"password": "guinevere",
|
||||
"is_active": False,
|
||||
}
|
||||
response = await test_app_client.post("/register", json=json)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert after_register.called is True
|
||||
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["is_active"] is True
|
||||
198
tests/test_router_reset.py
Normal file
198
tests/test_router_reset.py
Normal file
@ -0,0 +1,198 @@
|
||||
from typing import cast, Dict, Any
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import asynctest
|
||||
import httpx
|
||||
import jwt
|
||||
import pytest
|
||||
from fastapi import FastAPI, status, Request
|
||||
|
||||
from fastapi_users.router import ErrorCode, get_reset_password_router
|
||||
from fastapi_users.utils import JWT_ALGORITHM, generate_jwt
|
||||
from tests.conftest import UserDB
|
||||
|
||||
SECRET = "SECRET"
|
||||
LIFETIME = 3600
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def forgot_password_token():
|
||||
def _forgot_password_token(user_id=None, lifetime=LIFETIME):
|
||||
data = {"aud": "fastapi-users:reset"}
|
||||
if user_id is not None:
|
||||
data["user_id"] = str(user_id)
|
||||
return generate_jwt(data, lifetime, SECRET, JWT_ALGORITHM)
|
||||
|
||||
return _forgot_password_token
|
||||
|
||||
|
||||
def after_forgot_password_sync():
|
||||
return MagicMock(return_value=None)
|
||||
|
||||
|
||||
def after_forgot_password_async():
|
||||
return asynctest.CoroutineMock(return_value=None)
|
||||
|
||||
|
||||
@pytest.fixture(params=[after_forgot_password_sync, after_forgot_password_async])
|
||||
def after_forgot_password(request):
|
||||
return request.param()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@pytest.mark.asyncio
|
||||
async def test_app_client(
|
||||
mock_user_db, mock_authentication, after_forgot_password, get_test_client
|
||||
) -> httpx.AsyncClient:
|
||||
reset_router = get_reset_password_router(
|
||||
mock_user_db, SECRET, LIFETIME, after_forgot_password
|
||||
)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(reset_router)
|
||||
|
||||
return await get_test_client(app)
|
||||
|
||||
|
||||
@pytest.mark.router
|
||||
@pytest.mark.asyncio
|
||||
class TestForgotPassword:
|
||||
async def test_empty_body(
|
||||
self, test_app_client: httpx.AsyncClient, after_forgot_password
|
||||
):
|
||||
response = await test_app_client.post("/forgot-password", json={})
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
assert after_forgot_password.called is False
|
||||
|
||||
async def test_not_existing_user(
|
||||
self, test_app_client: httpx.AsyncClient, after_forgot_password
|
||||
):
|
||||
json = {"email": "lancelot@camelot.bt"}
|
||||
response = await test_app_client.post("/forgot-password", json=json)
|
||||
assert response.status_code == status.HTTP_202_ACCEPTED
|
||||
assert after_forgot_password.called is False
|
||||
|
||||
async def test_inactive_user(
|
||||
self, test_app_client: httpx.AsyncClient, after_forgot_password
|
||||
):
|
||||
json = {"email": "percival@camelot.bt"}
|
||||
response = await test_app_client.post("/forgot-password", json=json)
|
||||
assert response.status_code == status.HTTP_202_ACCEPTED
|
||||
assert after_forgot_password.called is False
|
||||
|
||||
async def test_existing_user(
|
||||
self, test_app_client: httpx.AsyncClient, after_forgot_password, user
|
||||
):
|
||||
json = {"email": "king.arthur@camelot.bt"}
|
||||
response = await test_app_client.post("/forgot-password", json=json)
|
||||
assert response.status_code == status.HTTP_202_ACCEPTED
|
||||
assert after_forgot_password.called is True
|
||||
|
||||
actual_user = after_forgot_password.call_args[0][0]
|
||||
assert actual_user.id == user.id
|
||||
actual_token = after_forgot_password.call_args[0][1]
|
||||
decoded_token = jwt.decode(
|
||||
actual_token,
|
||||
SECRET,
|
||||
audience="fastapi-users:reset",
|
||||
algorithms=[JWT_ALGORITHM],
|
||||
)
|
||||
assert decoded_token["user_id"] == str(user.id)
|
||||
request = after_forgot_password.call_args[0][2]
|
||||
assert isinstance(request, Request)
|
||||
|
||||
|
||||
@pytest.mark.router
|
||||
@pytest.mark.asyncio
|
||||
class TestResetPassword:
|
||||
async def test_empty_body(self, test_app_client: httpx.AsyncClient):
|
||||
response = await test_app_client.post("/reset-password", json={})
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
async def test_missing_token(self, test_app_client: httpx.AsyncClient):
|
||||
json = {"password": "guinevere"}
|
||||
response = await test_app_client.post("/reset-password", json=json)
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
async def test_missing_password(self, test_app_client: httpx.AsyncClient):
|
||||
json = {"token": "foo"}
|
||||
response = await test_app_client.post("/reset-password", json=json)
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
async def test_invalid_token(self, test_app_client: httpx.AsyncClient):
|
||||
json = {"token": "foo", "password": "guinevere"}
|
||||
response = await test_app_client.post("/reset-password", json=json)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["detail"] == ErrorCode.RESET_PASSWORD_BAD_TOKEN
|
||||
|
||||
async def test_valid_token_missing_user_id_payload(
|
||||
self,
|
||||
mocker,
|
||||
mock_user_db,
|
||||
test_app_client: httpx.AsyncClient,
|
||||
forgot_password_token,
|
||||
):
|
||||
mocker.spy(mock_user_db, "update")
|
||||
|
||||
json = {"token": forgot_password_token(), "password": "holygrail"}
|
||||
response = await test_app_client.post("/reset-password", json=json)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["detail"] == ErrorCode.RESET_PASSWORD_BAD_TOKEN
|
||||
assert mock_user_db.update.called is False
|
||||
|
||||
async def test_valid_token_invalid_uuid(
|
||||
self,
|
||||
mocker,
|
||||
mock_user_db,
|
||||
test_app_client: httpx.AsyncClient,
|
||||
forgot_password_token,
|
||||
):
|
||||
mocker.spy(mock_user_db, "update")
|
||||
|
||||
json = {"token": forgot_password_token("foo"), "password": "holygrail"}
|
||||
response = await test_app_client.post("/reset-password", json=json)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["detail"] == ErrorCode.RESET_PASSWORD_BAD_TOKEN
|
||||
assert mock_user_db.update.called is False
|
||||
|
||||
async def test_inactive_user(
|
||||
self,
|
||||
mocker,
|
||||
mock_user_db,
|
||||
test_app_client: httpx.AsyncClient,
|
||||
forgot_password_token,
|
||||
inactive_user: UserDB,
|
||||
):
|
||||
mocker.spy(mock_user_db, "update")
|
||||
|
||||
json = {
|
||||
"token": forgot_password_token(inactive_user.id),
|
||||
"password": "holygrail",
|
||||
}
|
||||
response = await test_app_client.post("/reset-password", json=json)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["detail"] == ErrorCode.RESET_PASSWORD_BAD_TOKEN
|
||||
assert mock_user_db.update.called is False
|
||||
|
||||
async def test_existing_user(
|
||||
self,
|
||||
mocker,
|
||||
mock_user_db,
|
||||
test_app_client: httpx.AsyncClient,
|
||||
forgot_password_token,
|
||||
user: UserDB,
|
||||
):
|
||||
mocker.spy(mock_user_db, "update")
|
||||
current_hashed_passord = user.hashed_password
|
||||
|
||||
json = {"token": forgot_password_token(user.id), "password": "holygrail"}
|
||||
response = await 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 != current_hashed_passord
|
||||
@ -3,365 +3,50 @@ from unittest.mock import MagicMock
|
||||
|
||||
import asynctest
|
||||
import httpx
|
||||
import jwt
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from starlette import status
|
||||
from starlette.requests import Request
|
||||
from fastapi import FastAPI, status, Request
|
||||
|
||||
from fastapi_users.authentication import Authenticator
|
||||
from fastapi_users.router import ErrorCode, Event, get_user_router
|
||||
from fastapi_users.utils import JWT_ALGORITHM, generate_jwt
|
||||
from tests.conftest import MockAuthentication, User, UserCreate, UserUpdate, UserDB
|
||||
from fastapi_users.router import get_users_router
|
||||
from tests.conftest import MockAuthentication, User, UserUpdate, UserDB
|
||||
|
||||
SECRET = "SECRET"
|
||||
LIFETIME = 3600
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def forgot_password_token():
|
||||
def _forgot_password_token(user_id=None, lifetime=LIFETIME):
|
||||
data = {"aud": "fastapi-users:reset"}
|
||||
if user_id is not None:
|
||||
data["user_id"] = str(user_id)
|
||||
return generate_jwt(data, lifetime, SECRET, JWT_ALGORITHM)
|
||||
|
||||
return _forgot_password_token
|
||||
|
||||
|
||||
def event_handler_sync():
|
||||
def after_update_sync():
|
||||
return MagicMock(return_value=None)
|
||||
|
||||
|
||||
def event_handler_async():
|
||||
def after_update_async():
|
||||
return asynctest.CoroutineMock(return_value=None)
|
||||
|
||||
|
||||
@pytest.fixture(params=[event_handler_sync, event_handler_async])
|
||||
def event_handler(request):
|
||||
@pytest.fixture(params=[after_update_sync, after_update_async])
|
||||
def after_update(request):
|
||||
return request.param()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@pytest.mark.asyncio
|
||||
async def test_app_client(
|
||||
mock_user_db, mock_authentication, event_handler, get_test_client
|
||||
mock_user_db, mock_authentication, after_update, get_test_client
|
||||
) -> httpx.AsyncClient:
|
||||
mock_authentication_bis = MockAuthentication(name="mock-bis")
|
||||
authenticator = Authenticator(
|
||||
[mock_authentication, mock_authentication_bis], mock_user_db
|
||||
)
|
||||
|
||||
user_router = get_user_router(
|
||||
mock_user_db,
|
||||
User,
|
||||
UserCreate,
|
||||
UserUpdate,
|
||||
UserDB,
|
||||
authenticator,
|
||||
SECRET,
|
||||
LIFETIME,
|
||||
user_router = get_users_router(
|
||||
mock_user_db, User, UserUpdate, UserDB, authenticator, after_update,
|
||||
)
|
||||
|
||||
user_router.add_event_handler(Event.ON_AFTER_REGISTER, event_handler)
|
||||
user_router.add_event_handler(Event.ON_AFTER_FORGOT_PASSWORD, event_handler)
|
||||
user_router.add_event_handler(Event.ON_AFTER_UPDATE, event_handler)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(user_router)
|
||||
|
||||
return await get_test_client(app)
|
||||
|
||||
|
||||
@pytest.mark.router
|
||||
@pytest.mark.asyncio
|
||||
class TestRegister:
|
||||
async def test_empty_body(self, test_app_client: httpx.AsyncClient, event_handler):
|
||||
response = await test_app_client.post("/register", json={})
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
assert event_handler.called is False
|
||||
|
||||
async def test_missing_password(
|
||||
self, test_app_client: httpx.AsyncClient, event_handler
|
||||
):
|
||||
json = {"email": "king.arthur@camelot.bt"}
|
||||
response = await test_app_client.post("/register", json=json)
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
assert event_handler.called is False
|
||||
|
||||
async def test_wrong_email(self, test_app_client: httpx.AsyncClient, event_handler):
|
||||
json = {"email": "king.arthur", "password": "guinevere"}
|
||||
response = await test_app_client.post("/register", json=json)
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
assert event_handler.called is False
|
||||
|
||||
async def test_existing_user(
|
||||
self, test_app_client: httpx.AsyncClient, event_handler
|
||||
):
|
||||
json = {"email": "king.arthur@camelot.bt", "password": "guinevere"}
|
||||
response = await test_app_client.post("/register", json=json)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["detail"] == ErrorCode.REGISTER_USER_ALREADY_EXISTS
|
||||
assert event_handler.called is False
|
||||
|
||||
async def test_valid_body(self, test_app_client: httpx.AsyncClient, event_handler):
|
||||
json = {"email": "lancelot@camelot.bt", "password": "guinevere"}
|
||||
response = await test_app_client.post("/register", json=json)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert event_handler.called is True
|
||||
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert "hashed_password" not in data
|
||||
assert "password" not in data
|
||||
assert data["id"] is not None
|
||||
|
||||
actual_user = event_handler.call_args[0][0]
|
||||
assert str(actual_user.id) == data["id"]
|
||||
request = event_handler.call_args[0][1]
|
||||
assert isinstance(request, Request)
|
||||
|
||||
async def test_valid_body_is_superuser(
|
||||
self, test_app_client: httpx.AsyncClient, event_handler
|
||||
):
|
||||
json = {
|
||||
"email": "lancelot@camelot.bt",
|
||||
"password": "guinevere",
|
||||
"is_superuser": True,
|
||||
}
|
||||
response = await test_app_client.post("/register", json=json)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert event_handler.called is True
|
||||
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["is_superuser"] is False
|
||||
|
||||
async def test_valid_body_is_active(
|
||||
self, test_app_client: httpx.AsyncClient, event_handler
|
||||
):
|
||||
json = {
|
||||
"email": "lancelot@camelot.bt",
|
||||
"password": "guinevere",
|
||||
"is_active": False,
|
||||
}
|
||||
response = await test_app_client.post("/register", json=json)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert event_handler.called is True
|
||||
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["is_active"] is True
|
||||
|
||||
|
||||
@pytest.mark.router
|
||||
@pytest.mark.parametrize("path", ["/login/mock", "/login/mock-bis"])
|
||||
@pytest.mark.asyncio
|
||||
class TestLogin:
|
||||
async def test_empty_body(self, path, test_app_client: httpx.AsyncClient):
|
||||
response = await test_app_client.post(path, data={})
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
async def test_missing_username(self, path, test_app_client: httpx.AsyncClient):
|
||||
data = {"password": "guinevere"}
|
||||
response = await test_app_client.post(path, data=data)
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
async def test_missing_password(self, path, test_app_client: httpx.AsyncClient):
|
||||
data = {"username": "king.arthur@camelot.bt"}
|
||||
response = await test_app_client.post(path, data=data)
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
async def test_not_existing_user(self, path, test_app_client: httpx.AsyncClient):
|
||||
data = {"username": "lancelot@camelot.bt", "password": "guinevere"}
|
||||
response = await test_app_client.post(path, data=data)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["detail"] == ErrorCode.LOGIN_BAD_CREDENTIALS
|
||||
|
||||
async def test_wrong_password(self, path, test_app_client: httpx.AsyncClient):
|
||||
data = {"username": "king.arthur@camelot.bt", "password": "percival"}
|
||||
response = await test_app_client.post(path, data=data)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["detail"] == ErrorCode.LOGIN_BAD_CREDENTIALS
|
||||
|
||||
async def test_valid_credentials(
|
||||
self, path, test_app_client: httpx.AsyncClient, user: UserDB
|
||||
):
|
||||
data = {"username": "king.arthur@camelot.bt", "password": "guinevere"}
|
||||
response = await test_app_client.post(path, data=data)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == {"token": str(user.id)}
|
||||
|
||||
async def test_inactive_user(self, path, test_app_client: httpx.AsyncClient):
|
||||
data = {"username": "percival@camelot.bt", "password": "angharad"}
|
||||
response = await test_app_client.post(path, data=data)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["detail"] == ErrorCode.LOGIN_BAD_CREDENTIALS
|
||||
|
||||
|
||||
@pytest.mark.router
|
||||
@pytest.mark.parametrize("path", ["/logout/mock", "/logout/mock-bis"])
|
||||
@pytest.mark.asyncio
|
||||
class TestLogout:
|
||||
async def test_missing_token(self, path, test_app_client: httpx.AsyncClient):
|
||||
response = await test_app_client.post(path)
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
async def test_unimplemented_logout(
|
||||
self, mocker, path, test_app_client: httpx.AsyncClient, user: UserDB
|
||||
):
|
||||
get_logout_response_spy = mocker.spy(MockAuthentication, "get_logout_response")
|
||||
response = await test_app_client.post(
|
||||
path, headers={"Authorization": f"Bearer {user.id}"}
|
||||
)
|
||||
assert response.status_code == status.HTTP_202_ACCEPTED
|
||||
|
||||
get_logout_response_spy.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.router
|
||||
@pytest.mark.asyncio
|
||||
class TestForgotPassword:
|
||||
async def test_empty_body(self, test_app_client: httpx.AsyncClient, event_handler):
|
||||
response = await test_app_client.post("/forgot-password", json={})
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
assert event_handler.called is False
|
||||
|
||||
async def test_not_existing_user(
|
||||
self, test_app_client: httpx.AsyncClient, event_handler
|
||||
):
|
||||
json = {"email": "lancelot@camelot.bt"}
|
||||
response = await test_app_client.post("/forgot-password", json=json)
|
||||
assert response.status_code == status.HTTP_202_ACCEPTED
|
||||
assert event_handler.called is False
|
||||
|
||||
async def test_inactive_user(
|
||||
self, test_app_client: httpx.AsyncClient, event_handler
|
||||
):
|
||||
json = {"email": "percival@camelot.bt"}
|
||||
response = await test_app_client.post("/forgot-password", json=json)
|
||||
assert response.status_code == status.HTTP_202_ACCEPTED
|
||||
assert event_handler.called is False
|
||||
|
||||
async def test_existing_user(
|
||||
self, test_app_client: httpx.AsyncClient, event_handler, user
|
||||
):
|
||||
json = {"email": "king.arthur@camelot.bt"}
|
||||
response = await test_app_client.post("/forgot-password", json=json)
|
||||
assert response.status_code == status.HTTP_202_ACCEPTED
|
||||
assert event_handler.called is True
|
||||
|
||||
actual_user = event_handler.call_args[0][0]
|
||||
assert actual_user.id == user.id
|
||||
actual_token = event_handler.call_args[0][1]
|
||||
decoded_token = jwt.decode(
|
||||
actual_token,
|
||||
SECRET,
|
||||
audience="fastapi-users:reset",
|
||||
algorithms=[JWT_ALGORITHM],
|
||||
)
|
||||
assert decoded_token["user_id"] == str(user.id)
|
||||
request = event_handler.call_args[0][2]
|
||||
assert isinstance(request, Request)
|
||||
|
||||
|
||||
@pytest.mark.router
|
||||
@pytest.mark.asyncio
|
||||
class TestResetPassword:
|
||||
async def test_empty_body(self, test_app_client: httpx.AsyncClient):
|
||||
response = await test_app_client.post("/reset-password", json={})
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
async def test_missing_token(self, test_app_client: httpx.AsyncClient):
|
||||
json = {"password": "guinevere"}
|
||||
response = await test_app_client.post("/reset-password", json=json)
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
async def test_missing_password(self, test_app_client: httpx.AsyncClient):
|
||||
json = {"token": "foo"}
|
||||
response = await test_app_client.post("/reset-password", json=json)
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
async def test_invalid_token(self, test_app_client: httpx.AsyncClient):
|
||||
json = {"token": "foo", "password": "guinevere"}
|
||||
response = await test_app_client.post("/reset-password", json=json)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["detail"] == ErrorCode.RESET_PASSWORD_BAD_TOKEN
|
||||
|
||||
async def test_valid_token_missing_user_id_payload(
|
||||
self,
|
||||
mocker,
|
||||
mock_user_db,
|
||||
test_app_client: httpx.AsyncClient,
|
||||
forgot_password_token,
|
||||
):
|
||||
mocker.spy(mock_user_db, "update")
|
||||
|
||||
json = {"token": forgot_password_token(), "password": "holygrail"}
|
||||
response = await test_app_client.post("/reset-password", json=json)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["detail"] == ErrorCode.RESET_PASSWORD_BAD_TOKEN
|
||||
assert mock_user_db.update.called is False
|
||||
|
||||
async def test_valid_token_invalid_uuid(
|
||||
self,
|
||||
mocker,
|
||||
mock_user_db,
|
||||
test_app_client: httpx.AsyncClient,
|
||||
forgot_password_token,
|
||||
):
|
||||
mocker.spy(mock_user_db, "update")
|
||||
|
||||
json = {"token": forgot_password_token("foo"), "password": "holygrail"}
|
||||
response = await test_app_client.post("/reset-password", json=json)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["detail"] == ErrorCode.RESET_PASSWORD_BAD_TOKEN
|
||||
assert mock_user_db.update.called is False
|
||||
|
||||
async def test_inactive_user(
|
||||
self,
|
||||
mocker,
|
||||
mock_user_db,
|
||||
test_app_client: httpx.AsyncClient,
|
||||
forgot_password_token,
|
||||
inactive_user: UserDB,
|
||||
):
|
||||
mocker.spy(mock_user_db, "update")
|
||||
|
||||
json = {
|
||||
"token": forgot_password_token(inactive_user.id),
|
||||
"password": "holygrail",
|
||||
}
|
||||
response = await test_app_client.post("/reset-password", json=json)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["detail"] == ErrorCode.RESET_PASSWORD_BAD_TOKEN
|
||||
assert mock_user_db.update.called is False
|
||||
|
||||
async def test_existing_user(
|
||||
self,
|
||||
mocker,
|
||||
mock_user_db,
|
||||
test_app_client: httpx.AsyncClient,
|
||||
forgot_password_token,
|
||||
user: UserDB,
|
||||
):
|
||||
mocker.spy(mock_user_db, "update")
|
||||
current_hashed_passord = user.hashed_password
|
||||
|
||||
json = {"token": forgot_password_token(user.id), "password": "holygrail"}
|
||||
response = await 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 != current_hashed_passord
|
||||
|
||||
|
||||
@pytest.mark.router
|
||||
@pytest.mark.asyncio
|
||||
class TestMe:
|
||||
@ -392,23 +77,23 @@ class TestMe:
|
||||
@pytest.mark.asyncio
|
||||
class TestUpdateMe:
|
||||
async def test_missing_token(
|
||||
self, test_app_client: httpx.AsyncClient, event_handler
|
||||
self, test_app_client: httpx.AsyncClient, after_update
|
||||
):
|
||||
response = await test_app_client.patch("/me")
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
assert event_handler.called is False
|
||||
assert after_update.called is False
|
||||
|
||||
async def test_inactive_user(
|
||||
self, test_app_client: httpx.AsyncClient, inactive_user: UserDB, event_handler
|
||||
self, test_app_client: httpx.AsyncClient, inactive_user: UserDB, after_update
|
||||
):
|
||||
response = await test_app_client.patch(
|
||||
"/me", headers={"Authorization": f"Bearer {inactive_user.id}"}
|
||||
)
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
assert event_handler.called is False
|
||||
assert after_update.called is False
|
||||
|
||||
async def test_empty_body(
|
||||
self, test_app_client: httpx.AsyncClient, user: UserDB, event_handler
|
||||
self, test_app_client: httpx.AsyncClient, user: UserDB, after_update
|
||||
):
|
||||
response = await test_app_client.patch(
|
||||
"/me", json={}, headers={"Authorization": f"Bearer {user.id}"}
|
||||
@ -418,16 +103,16 @@ class TestUpdateMe:
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["email"] == user.email
|
||||
|
||||
assert event_handler.called is True
|
||||
actual_user = event_handler.call_args[0][0]
|
||||
assert after_update.called is True
|
||||
actual_user = after_update.call_args[0][0]
|
||||
assert actual_user.id == user.id
|
||||
updated_fields = event_handler.call_args[0][1]
|
||||
updated_fields = after_update.call_args[0][1]
|
||||
assert updated_fields == {}
|
||||
request = event_handler.call_args[0][2]
|
||||
request = after_update.call_args[0][2]
|
||||
assert isinstance(request, Request)
|
||||
|
||||
async def test_valid_body(
|
||||
self, test_app_client: httpx.AsyncClient, user: UserDB, event_handler
|
||||
self, test_app_client: httpx.AsyncClient, user: UserDB, after_update
|
||||
):
|
||||
json = {"email": "king.arthur@tintagel.bt"}
|
||||
response = await test_app_client.patch(
|
||||
@ -438,16 +123,16 @@ class TestUpdateMe:
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["email"] == "king.arthur@tintagel.bt"
|
||||
|
||||
assert event_handler.called is True
|
||||
actual_user = event_handler.call_args[0][0]
|
||||
assert after_update.called is True
|
||||
actual_user = after_update.call_args[0][0]
|
||||
assert actual_user.id == user.id
|
||||
updated_fields = event_handler.call_args[0][1]
|
||||
updated_fields = after_update.call_args[0][1]
|
||||
assert updated_fields == {"email": "king.arthur@tintagel.bt"}
|
||||
request = event_handler.call_args[0][2]
|
||||
request = after_update.call_args[0][2]
|
||||
assert isinstance(request, Request)
|
||||
|
||||
async def test_valid_body_is_superuser(
|
||||
self, test_app_client: httpx.AsyncClient, user: UserDB, event_handler
|
||||
self, test_app_client: httpx.AsyncClient, user: UserDB, after_update
|
||||
):
|
||||
json = {"is_superuser": True}
|
||||
response = await test_app_client.patch(
|
||||
@ -458,16 +143,16 @@ class TestUpdateMe:
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["is_superuser"] is False
|
||||
|
||||
assert event_handler.called is True
|
||||
actual_user = event_handler.call_args[0][0]
|
||||
assert after_update.called is True
|
||||
actual_user = after_update.call_args[0][0]
|
||||
assert actual_user.id == user.id
|
||||
updated_fields = event_handler.call_args[0][1]
|
||||
updated_fields = after_update.call_args[0][1]
|
||||
assert updated_fields == {}
|
||||
request = event_handler.call_args[0][2]
|
||||
request = after_update.call_args[0][2]
|
||||
assert isinstance(request, Request)
|
||||
|
||||
async def test_valid_body_is_active(
|
||||
self, test_app_client: httpx.AsyncClient, user: UserDB, event_handler
|
||||
self, test_app_client: httpx.AsyncClient, user: UserDB, after_update
|
||||
):
|
||||
json = {"is_active": False}
|
||||
response = await test_app_client.patch(
|
||||
@ -478,12 +163,12 @@ class TestUpdateMe:
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["is_active"] is True
|
||||
|
||||
assert event_handler.called is True
|
||||
actual_user = event_handler.call_args[0][0]
|
||||
assert after_update.called is True
|
||||
actual_user = after_update.call_args[0][0]
|
||||
assert actual_user.id == user.id
|
||||
updated_fields = event_handler.call_args[0][1]
|
||||
updated_fields = after_update.call_args[0][1]
|
||||
assert updated_fields == {}
|
||||
request = event_handler.call_args[0][2]
|
||||
request = after_update.call_args[0][2]
|
||||
assert isinstance(request, Request)
|
||||
|
||||
async def test_valid_body_password(
|
||||
@ -492,7 +177,7 @@ class TestUpdateMe:
|
||||
mock_user_db,
|
||||
test_app_client: httpx.AsyncClient,
|
||||
user: UserDB,
|
||||
event_handler,
|
||||
after_update,
|
||||
):
|
||||
mocker.spy(mock_user_db, "update")
|
||||
current_hashed_passord = user.hashed_password
|
||||
@ -507,12 +192,12 @@ class TestUpdateMe:
|
||||
updated_user = mock_user_db.update.call_args[0][0]
|
||||
assert updated_user.hashed_password != current_hashed_passord
|
||||
|
||||
assert event_handler.called is True
|
||||
actual_user = event_handler.call_args[0][0]
|
||||
assert after_update.called is True
|
||||
actual_user = after_update.call_args[0][0]
|
||||
assert actual_user.id == user.id
|
||||
updated_fields = event_handler.call_args[0][1]
|
||||
updated_fields = after_update.call_args[0][1]
|
||||
assert updated_fields == {"password": "merlin"}
|
||||
request = event_handler.call_args[0][2]
|
||||
request = after_update.call_args[0][2]
|
||||
assert isinstance(request, Request)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user