diff --git a/fastapi_users/manager.py b/fastapi_users/manager.py index 85ca0261..65da8113 100644 --- a/fastapi_users/manager.py +++ b/fastapi_users/manager.py @@ -672,7 +672,7 @@ class BaseUserManager(Generic[models.UP, models.ID]): except exceptions.UserNotExists: validated_update_dict["email"] = value validated_update_dict["is_verified"] = False - elif field == "password": + elif field == "password" and value is not None: await self.validate_password(value, user) validated_update_dict["hashed_password"] = self.password_helper.hash( value diff --git a/tests/test_manager.py b/tests/test_manager.py index f8503a47..30fa5a5e 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -585,6 +585,17 @@ class TestUpdateUser: assert user_manager.on_after_update.called is True + async def test_unsafe_update_password_unchanged( + self, user: UserModel, user_manager: UserManagerMock[UserModel] + ): + old_hashed_password = user.hashed_password + user_update = UserUpdate(password=None) + updated_user = await user_manager.update(user_update, user, safe=False) + + assert updated_user.hashed_password == old_hashed_password + + assert user_manager.on_after_update.called is True + async def test_password_update_invalid( self, user: UserModel, user_manager: UserManagerMock[UserModel] ): diff --git a/tests/test_router_users.py b/tests/test_router_users.py index cc4364a9..eda71f37 100644 --- a/tests/test_router_users.py +++ b/tests/test_router_users.py @@ -826,6 +826,33 @@ class TestUpdateUser: updated_user = mock_user_db.update.call_args[0][0] assert updated_user.hashed_password != current_hashed_password + async def test_valid_body_password_unchanged_unverified_superuser( + self, + mocker, + mock_user_db, + test_app_client: Tuple[httpx.AsyncClient, bool], + user: UserModel, + superuser: UserModel, + ): + client, requires_verification = test_app_client + mocker.spy(mock_user_db, "update") + current_hashed_password = user.hashed_password + + json = {"password": None} + response = await client.patch( + f"/{user.id}", + json=json, + headers={"Authorization": f"Bearer {superuser.id}"}, + ) + if requires_verification: + assert response.status_code == status.HTTP_403_FORBIDDEN + else: + 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_password + @pytest.mark.router @pytest.mark.asyncio