from typing import Any, AsyncGenerator, Dict, cast from unittest.mock import MagicMock import asynctest import httpx import pytest from fastapi import FastAPI, status from fastapi_users.router import ErrorCode, get_verify_router from fastapi_users.user import get_get_user, get_verify_user from fastapi_users.utils import generate_jwt from tests.conftest import User, UserDB SECRET = "SECRET" LIFETIME = 3600 VERIFY_USER_TOKEN_AUDIENCE = "fastapi-users:verify" JWT_ALGORITHM = "HS256" @pytest.fixture def verify_token(): def _verify_token(user_id=None, email=None, lifetime=LIFETIME): data = {"aud": VERIFY_USER_TOKEN_AUDIENCE} if user_id is not None: data["user_id"] = str(user_id) if email is not None: data["email"] = email return generate_jwt(data, SECRET, lifetime, JWT_ALGORITHM) return _verify_token def after_verification_sync(): return MagicMock(return_value=None) def after_verification_async(): return asynctest.CoroutineMock(return_value=None) @pytest.fixture(params=[after_verification_sync, after_verification_async]) def after_verification(request): return request.param() def after_verification_request_sync(): return MagicMock(return_value=None) def after_verification_request_async(): return asynctest.CoroutineMock(return_value=None) @pytest.fixture( params=[after_verification_request_sync, after_verification_request_async] ) def after_verification_request(request): return request.param() @pytest.fixture @pytest.mark.asyncio async def test_app_client( mock_user_db, after_verification_request, after_verification, get_test_client, ) -> AsyncGenerator[httpx.AsyncClient, None]: verify_user = get_verify_user(mock_user_db) get_user = get_get_user(mock_user_db) verify_router = get_verify_router( verify_user, get_user, User, SECRET, LIFETIME, after_verification_request, after_verification, ) app = FastAPI() app.include_router(verify_router) async for client in get_test_client(app): yield client @pytest.mark.router @pytest.mark.asyncio class TestVerifyTokenRequest: async def test_empty_body( self, test_app_client: httpx.AsyncClient, after_verification_request, ): response = await test_app_client.post("/request-verify-token", json={}) assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY assert after_verification_request.called is False async def test_wrong_email( self, test_app_client: httpx.AsyncClient, after_verification_request, ): json = {"email": "king.arthur"} response = await test_app_client.post("/request-verify-token", json=json) assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY assert after_verification_request.called is False async def test_user_not_exists( self, test_app_client: httpx.AsyncClient, after_verification_request, ): json = {"email": "user@example.com"} response = await test_app_client.post("/request-verify-token", json=json) assert response.status_code == status.HTTP_202_ACCEPTED assert after_verification_request.called is False async def test_user_verified_valid_request( self, test_app_client: httpx.AsyncClient, verified_user: UserDB, after_verification_request, ): input_user = verified_user json = {"email": input_user.email} response = await test_app_client.post("/request-verify-token", json=json) assert after_verification_request.called is False assert response.status_code == status.HTTP_400_BAD_REQUEST data = cast(Dict[str, Any], response.json()) assert data["detail"] == ErrorCode.VERIFY_USER_ALREADY_VERIFIED async def test_user_inactive_valid_request( self, test_app_client: httpx.AsyncClient, inactive_user: UserDB, after_verification_request, ): input_user = inactive_user json = {"email": input_user.email} response = await test_app_client.post("/request-verify-token", json=json) assert after_verification_request.called is False assert response.status_code == status.HTTP_202_ACCEPTED async def test_user_active_valid_request( self, test_app_client: httpx.AsyncClient, user: UserDB, after_verification_request, ): input_user = user json = {"email": input_user.email} response = await test_app_client.post("/request-verify-token", json=json) assert response.status_code == status.HTTP_202_ACCEPTED assert after_verification_request.called is True @pytest.mark.router @pytest.mark.asyncio class TestVerify: async def test_empty_body( self, test_app_client: httpx.AsyncClient, after_verification_request, after_verification, ): response = await test_app_client.post("/verify", json={}) assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY assert after_verification.called is False assert after_verification_request.called is False async def test_invalid_token( self, test_app_client: httpx.AsyncClient, user: UserDB, after_verification_request, after_verification, ): json = {"token": "foo"} response = await test_app_client.post("/verify", json=json) assert response.status_code == status.HTTP_400_BAD_REQUEST data = cast(Dict[str, Any], response.json()) assert data["detail"] == ErrorCode.VERIFY_USER_BAD_TOKEN assert after_verification.called is False assert after_verification_request.called is False async def test_valid_token_missing_user_id( self, test_app_client: httpx.AsyncClient, verify_token, user: UserDB, after_verification_request, after_verification, ): json = {"token": verify_token(None, user.email)} response = await test_app_client.post("/verify", json=json) assert response.status_code == status.HTTP_400_BAD_REQUEST data = cast(Dict[str, Any], response.json()) assert data["detail"] == ErrorCode.VERIFY_USER_BAD_TOKEN assert after_verification.called is False assert after_verification_request.called is False async def test_valid_token_missing_email( self, test_app_client: httpx.AsyncClient, verify_token, user: UserDB, after_verification_request, after_verification, ): json = {"token": verify_token(user.id, None)} response = await test_app_client.post("/verify", json=json) assert response.status_code == status.HTTP_400_BAD_REQUEST data = cast(Dict[str, Any], response.json()) assert data["detail"] == ErrorCode.VERIFY_USER_BAD_TOKEN assert after_verification.called is False assert after_verification_request.called is False async def test_valid_token_invalid_uuid( self, test_app_client: httpx.AsyncClient, verify_token, user: UserDB, after_verification_request, after_verification, ): json = {"token": verify_token("foo", user.email)} response = await test_app_client.post("/verify", json=json) assert response.status_code == status.HTTP_400_BAD_REQUEST data = cast(Dict[str, Any], response.json()) assert data["detail"] == ErrorCode.VERIFY_USER_BAD_TOKEN assert after_verification.called is False assert after_verification_request.called is False async def test_valid_token_invalid_email( self, test_app_client: httpx.AsyncClient, verify_token, user: UserDB, after_verification_request, after_verification, ): json = {"token": verify_token(user.id, "foo")} response = await test_app_client.post("/verify", json=json) assert response.status_code == status.HTTP_400_BAD_REQUEST data = cast(Dict[str, Any], response.json()) assert data["detail"] == ErrorCode.VERIFY_USER_BAD_TOKEN assert after_verification.called is False assert after_verification_request.called is False async def test_valid_token_email_id_mismatch( self, test_app_client: httpx.AsyncClient, verify_token, user: UserDB, inactive_user: UserDB, after_verification_request, after_verification, ): json = {"token": verify_token(user.id, inactive_user.email)} response = await test_app_client.post("/verify", json=json) assert response.status_code == status.HTTP_400_BAD_REQUEST data = cast(Dict[str, Any], response.json()) assert data["detail"] == ErrorCode.VERIFY_USER_BAD_TOKEN assert after_verification.called is False assert after_verification_request.called is False async def test_expired_token( self, test_app_client: httpx.AsyncClient, verify_token, user: UserDB, after_verification_request, after_verification, ): json = {"token": verify_token(user.id, user.email, -1)} response = await test_app_client.post("/verify", json=json) assert response.status_code == status.HTTP_400_BAD_REQUEST data = cast(Dict[str, Any], response.json()) assert data["detail"] == ErrorCode.VERIFY_USER_TOKEN_EXPIRED assert after_verification.called is False assert after_verification_request.called is False async def test_inactive_user( self, test_app_client: httpx.AsyncClient, verify_token, inactive_user: UserDB, after_verification_request, after_verification, ): json = {"token": verify_token(inactive_user.id, inactive_user.email)} response = await test_app_client.post("/verify", json=json) assert response.status_code == status.HTTP_200_OK assert after_verification.called is True assert after_verification_request.called is False data = cast(Dict[str, Any], response.json()) assert data["is_active"] is False async def test_verified_user( self, test_app_client: httpx.AsyncClient, verify_token, verified_user: UserDB, after_verification_request, after_verification, ): json = {"token": verify_token(verified_user.id, verified_user.email)} response = await test_app_client.post("/verify", json=json) assert response.status_code == status.HTTP_400_BAD_REQUEST data = cast(Dict[str, Any], response.json()) assert data["detail"] == ErrorCode.VERIFY_USER_ALREADY_VERIFIED assert after_verification.called is False assert after_verification_request.called is False async def test_active_user( self, test_app_client: httpx.AsyncClient, verify_token, user: UserDB, after_verification_request, after_verification, ): json = {"token": verify_token(user.id, user.email)} response = await test_app_client.post("/verify", json=json) assert response.status_code == status.HTTP_200_OK assert after_verification.called is True assert after_verification_request.called is False data = cast(Dict[str, Any], response.json()) assert data["is_active"] is True