mirror of
				https://github.com/fastapi-users/fastapi-users.git
				synced 2025-11-04 06:37:51 +08:00 
			
		
		
		
	Fix #561: Update a user with an email already existing in DB raises an error
This commit is contained in:
		@ -10,6 +10,7 @@ class ErrorCode:
 | 
				
			|||||||
    VERIFY_USER_BAD_TOKEN = "VERIFY_USER_BAD_TOKEN"
 | 
					    VERIFY_USER_BAD_TOKEN = "VERIFY_USER_BAD_TOKEN"
 | 
				
			||||||
    VERIFY_USER_ALREADY_VERIFIED = "VERIFY_USER_ALREADY_VERIFIED"
 | 
					    VERIFY_USER_ALREADY_VERIFIED = "VERIFY_USER_ALREADY_VERIFIED"
 | 
				
			||||||
    VERIFY_USER_TOKEN_EXPIRED = "VERIFY_USER_TOKEN_EXPIRED"
 | 
					    VERIFY_USER_TOKEN_EXPIRED = "VERIFY_USER_TOKEN_EXPIRED"
 | 
				
			||||||
 | 
					    UPDATE_USER_EMAIL_ALREADY_EXISTS = "UPDATE_USER_EMAIL_ALREADY_EXISTS"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async def run_handler(handler: Callable, *args, **kwargs):
 | 
					async def run_handler(handler: Callable, *args, **kwargs):
 | 
				
			||||||
 | 
				
			|||||||
@ -7,7 +7,7 @@ from fastapi_users import models
 | 
				
			|||||||
from fastapi_users.authentication import Authenticator
 | 
					from fastapi_users.authentication import Authenticator
 | 
				
			||||||
from fastapi_users.db import BaseUserDatabase
 | 
					from fastapi_users.db import BaseUserDatabase
 | 
				
			||||||
from fastapi_users.password import get_password_hash
 | 
					from fastapi_users.password import get_password_hash
 | 
				
			||||||
from fastapi_users.router.common import run_handler
 | 
					from fastapi_users.router.common import ErrorCode, run_handler
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_users_router(
 | 
					def get_users_router(
 | 
				
			||||||
@ -35,6 +35,20 @@ def get_users_router(
 | 
				
			|||||||
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
 | 
					            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
 | 
				
			||||||
        return user
 | 
					        return user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def _check_unique_email(
 | 
				
			||||||
 | 
					        updated_user: user_update_model,  # type: ignore
 | 
				
			||||||
 | 
					    ) -> None:
 | 
				
			||||||
 | 
					        updated_user = cast(
 | 
				
			||||||
 | 
					            models.BaseUserUpdate, updated_user
 | 
				
			||||||
 | 
					        )  # Prevent mypy complain
 | 
				
			||||||
 | 
					        if updated_user.email:
 | 
				
			||||||
 | 
					            user = await user_db.get_by_email(updated_user.email)
 | 
				
			||||||
 | 
					            if user is not None:
 | 
				
			||||||
 | 
					                raise HTTPException(
 | 
				
			||||||
 | 
					                    status.HTTP_400_BAD_REQUEST,
 | 
				
			||||||
 | 
					                    detail=ErrorCode.UPDATE_USER_EMAIL_ALREADY_EXISTS,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async def _update_user(
 | 
					    async def _update_user(
 | 
				
			||||||
        user: models.BaseUserDB, update_dict: Dict[str, Any], request: Request
 | 
					        user: models.BaseUserDB, update_dict: Dict[str, Any], request: Request
 | 
				
			||||||
    ):
 | 
					    ):
 | 
				
			||||||
@ -55,7 +69,11 @@ def get_users_router(
 | 
				
			|||||||
    ):
 | 
					    ):
 | 
				
			||||||
        return user
 | 
					        return user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @router.patch("/me", response_model=user_model)
 | 
					    @router.patch(
 | 
				
			||||||
 | 
					        "/me",
 | 
				
			||||||
 | 
					        response_model=user_model,
 | 
				
			||||||
 | 
					        dependencies=[Depends(get_current_active_user), Depends(_check_unique_email)],
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    async def update_me(
 | 
					    async def update_me(
 | 
				
			||||||
        request: Request,
 | 
					        request: Request,
 | 
				
			||||||
        updated_user: user_update_model,  # type: ignore
 | 
					        updated_user: user_update_model,  # type: ignore
 | 
				
			||||||
@ -81,7 +99,7 @@ def get_users_router(
 | 
				
			|||||||
    @router.patch(
 | 
					    @router.patch(
 | 
				
			||||||
        "/{id:uuid}",
 | 
					        "/{id:uuid}",
 | 
				
			||||||
        response_model=user_model,
 | 
					        response_model=user_model,
 | 
				
			||||||
        dependencies=[Depends(get_current_superuser)],
 | 
					        dependencies=[Depends(get_current_superuser), Depends(_check_unique_email)],
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    async def update_user(
 | 
					    async def update_user(
 | 
				
			||||||
        id: UUID4, updated_user: user_update_model, request: Request  # type: ignore
 | 
					        id: UUID4, updated_user: user_update_model, request: Request  # type: ignore
 | 
				
			||||||
 | 
				
			|||||||
@ -7,7 +7,7 @@ import pytest
 | 
				
			|||||||
from fastapi import FastAPI, Request, status
 | 
					from fastapi import FastAPI, Request, status
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from fastapi_users.authentication import Authenticator
 | 
					from fastapi_users.authentication import Authenticator
 | 
				
			||||||
from fastapi_users.router import get_users_router
 | 
					from fastapi_users.router import ErrorCode, get_users_router
 | 
				
			||||||
from tests.conftest import MockAuthentication, User, UserDB, UserUpdate
 | 
					from tests.conftest import MockAuthentication, User, UserDB, UserUpdate
 | 
				
			||||||
 | 
					
 | 
				
			||||||
SECRET = "SECRET"
 | 
					SECRET = "SECRET"
 | 
				
			||||||
@ -144,6 +144,28 @@ class TestUpdateMe:
 | 
				
			|||||||
        assert response.status_code == status.HTTP_401_UNAUTHORIZED
 | 
					        assert response.status_code == status.HTTP_401_UNAUTHORIZED
 | 
				
			||||||
        assert after_update.called is False
 | 
					        assert after_update.called is False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def test_existing_email(
 | 
				
			||||||
 | 
					        self,
 | 
				
			||||||
 | 
					        test_app_client: Tuple[httpx.AsyncClient, bool],
 | 
				
			||||||
 | 
					        user: UserDB,
 | 
				
			||||||
 | 
					        verified_user: UserDB,
 | 
				
			||||||
 | 
					        after_update,
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
 | 
					        client, requires_verification = test_app_client
 | 
				
			||||||
 | 
					        response = await client.patch(
 | 
				
			||||||
 | 
					            "/me",
 | 
				
			||||||
 | 
					            json={"email": verified_user.email},
 | 
				
			||||||
 | 
					            headers={"Authorization": f"Bearer {user.id}"},
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        if requires_verification:
 | 
				
			||||||
 | 
					            assert response.status_code == status.HTTP_401_UNAUTHORIZED
 | 
				
			||||||
 | 
					            assert after_update.called is False
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            assert response.status_code == status.HTTP_400_BAD_REQUEST
 | 
				
			||||||
 | 
					            data = cast(Dict[str, Any], response.json())
 | 
				
			||||||
 | 
					            assert data["detail"] == ErrorCode.UPDATE_USER_EMAIL_ALREADY_EXISTS
 | 
				
			||||||
 | 
					            assert after_update.called is False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async def test_empty_body(
 | 
					    async def test_empty_body(
 | 
				
			||||||
        self,
 | 
					        self,
 | 
				
			||||||
        test_app_client: Tuple[httpx.AsyncClient, bool],
 | 
					        test_app_client: Tuple[httpx.AsyncClient, bool],
 | 
				
			||||||
@ -685,6 +707,25 @@ class TestUpdateUser:
 | 
				
			|||||||
            data = cast(Dict[str, Any], response.json())
 | 
					            data = cast(Dict[str, Any], response.json())
 | 
				
			||||||
            assert data["email"] == "king.arthur@tintagel.bt"
 | 
					            assert data["email"] == "king.arthur@tintagel.bt"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def test_existing_email_verified_superuser(
 | 
				
			||||||
 | 
					        self,
 | 
				
			||||||
 | 
					        test_app_client: Tuple[httpx.AsyncClient, bool],
 | 
				
			||||||
 | 
					        user: UserDB,
 | 
				
			||||||
 | 
					        verified_user: UserDB,
 | 
				
			||||||
 | 
					        verified_superuser: UserDB,
 | 
				
			||||||
 | 
					        after_update,
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
 | 
					        client, _ = test_app_client
 | 
				
			||||||
 | 
					        response = await client.patch(
 | 
				
			||||||
 | 
					            f"/{user.id}",
 | 
				
			||||||
 | 
					            json={"email": verified_user.email},
 | 
				
			||||||
 | 
					            headers={"Authorization": f"Bearer {verified_superuser.id}"},
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        assert response.status_code == status.HTTP_400_BAD_REQUEST
 | 
				
			||||||
 | 
					        data = cast(Dict[str, Any], response.json())
 | 
				
			||||||
 | 
					        assert data["detail"] == ErrorCode.UPDATE_USER_EMAIL_ALREADY_EXISTS
 | 
				
			||||||
 | 
					        assert after_update.called is False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async def test_valid_body_verified_superuser(
 | 
					    async def test_valid_body_verified_superuser(
 | 
				
			||||||
        self,
 | 
					        self,
 | 
				
			||||||
        test_app_client: Tuple[httpx.AsyncClient, bool],
 | 
					        test_app_client: Tuple[httpx.AsyncClient, bool],
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user