From 0a66849e9763e81cc59d392e972c8be9b5264800 Mon Sep 17 00:00:00 2001 From: insistence <3055204202@qq.com> Date: Fri, 21 Aug 2026 15:57:00 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E4=B8=AA=E4=BA=BA=E4=BF=A1=E6=81=AF=E6=8E=A5=E5=8F=A3=E5=8F=AF?= =?UTF-8?q?=E8=83=BD=E4=BC=9A=E5=AF=BC=E8=87=B4=E8=B6=8A=E6=9D=83=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/user_controller.py | 10 +- .../module_admin/entity/vo/user_vo.py | 13 ++ .../controller/test_user_controller.py | 118 ++++++++++++++++++ .../service/test_user_controller.py | 45 ------- 4 files changed, 138 insertions(+), 48 deletions(-) create mode 100644 ruoyi-fastapi-backend/tests/module_admin/controller/test_user_controller.py delete mode 100644 ruoyi-fastapi-backend/tests/module_admin/service/test_user_controller.py diff --git a/ruoyi-fastapi-backend/module_admin/controller/user_controller.py b/ruoyi-fastapi-backend/module_admin/controller/user_controller.py index 8f2c0c8..068ce9f 100644 --- a/ruoyi-fastapi-backend/module_admin/controller/user_controller.py +++ b/ruoyi-fastapi-backend/module_admin/controller/user_controller.py @@ -33,8 +33,8 @@ EditUserModel, ResetPasswordModel, ResetUserModel, + UpdateUserProfileModel, UserDetailModel, - UserInfoModel, UserModel, UserPageQueryModel, UserProfileModel, @@ -368,12 +368,16 @@ async def change_system_user_profile_avatar( @Log(title='个人信息', business_type=BusinessType.UPDATE) async def change_system_user_profile_info( request: Request, - user_info: UserInfoModel, + user_info: UpdateUserProfileModel, query_db: Annotated[AsyncSession, DBSessionDependency()], current_user: Annotated[CurrentUserModel, CurrentUserDependency()], ) -> Response: edit_user = EditUserModel( - **user_info.model_dump(exclude_unset=True, by_alias=True, exclude={'role_ids', 'post_ids'}), + **user_info.model_dump( + exclude_unset=True, + by_alias=True, + include={'nick_name', 'email', 'phonenumber', 'sex'}, + ), userId=current_user.user.user_id, userName=current_user.user.user_name, updateBy=current_user.user.user_name, diff --git a/ruoyi-fastapi-backend/module_admin/entity/vo/user_vo.py b/ruoyi-fastapi-backend/module_admin/entity/vo/user_vo.py index aa73439..26ddb8f 100644 --- a/ruoyi-fastapi-backend/module_admin/entity/vo/user_vo.py +++ b/ruoyi-fastapi-backend/module_admin/entity/vo/user_vo.py @@ -128,6 +128,19 @@ class UserInfoModel(UserModel): role: list[RoleModel | None] | None = Field(default=[], description='角色信息') +class UpdateUserProfileModel(BaseModel): + """ + 修改用户个人资料模型 + """ + + model_config = ConfigDict(alias_generator=to_camel, extra='forbid') + + nick_name: str | None = Field(default=None, max_length=30, description='用户昵称') + email: str | None = Field(default=None, max_length=50, description='用户邮箱') + phonenumber: str | None = Field(default=None, max_length=11, description='手机号码') + sex: Literal['0', '1', '2'] | None = Field(default=None, description='用户性别(0男 1女 2未知)') + + class CurrentUserModel(BaseModel): model_config = ConfigDict(alias_generator=to_camel) diff --git a/ruoyi-fastapi-backend/tests/module_admin/controller/test_user_controller.py b/ruoyi-fastapi-backend/tests/module_admin/controller/test_user_controller.py new file mode 100644 index 0000000..94ac54b --- /dev/null +++ b/ruoyi-fastapi-backend/tests/module_admin/controller/test_user_controller.py @@ -0,0 +1,118 @@ +import inspect +from types import SimpleNamespace +from unittest.mock import AsyncMock, patch + +import pytest +from pydantic import ValidationError + +from common.enums import PasswordCharacterType +from module_admin.controller.user_controller import change_system_user_profile_info, reset_system_user_pwd +from module_admin.entity.vo.user_vo import EditUserModel, UpdateUserProfileModel +from module_admin.service.user_service import UserService +from utils.pwd_util import PwdUtil + + +@pytest.mark.asyncio +async def test_admin_reset_password_uses_front_end_default_rule() -> None: + redis = SimpleNamespace() + request = SimpleNamespace(app=SimpleNamespace(state=SimpleNamespace(redis=redis))) + reset_user = EditUserModel(userId=2, password='abcdef') + current_user = SimpleNamespace(user=SimpleNamespace(admin=True, user_name='admin')) + expected = object() + + with ( + patch.object(UserService, 'check_user_allowed_services', new_callable=AsyncMock), + patch.object(UserService, 'validate_password_services', new_callable=AsyncMock) as validate_password, + patch.object( + UserService, + 'edit_user_services', + new=AsyncMock(return_value=SimpleNamespace(message='重置成功')), + ), + patch.object(PwdUtil, 'get_password_hash', return_value='hashed-password'), + patch( + 'module_admin.controller.user_controller.ResponseUtil.success', + return_value=expected, + ), + ): + result = await inspect.unwrap(reset_system_user_pwd)( + request, + reset_user, + object(), + current_user, + object(), + ) + + assert result is expected + validate_password.assert_awaited_once_with(redis, 'abcdef', PasswordCharacterType.DEFAULT) + + +@pytest.mark.parametrize( + 'management_field', + [ + {'deptId': 200}, + {'password': 'attacker-controlled'}, + {'status': '1'}, + {'delFlag': '2'}, + {'userType': '99'}, + {'createBy': 'attacker'}, + ], +) +def test_update_user_profile_model_rejects_management_fields(management_field: dict) -> None: + with pytest.raises(ValidationError): + UpdateUserProfileModel(nickName='普通用户', **management_field) + + +@pytest.mark.asyncio +async def test_update_user_profile_only_forwards_editable_fields() -> None: + request = SimpleNamespace() + user_info = UpdateUserProfileModel( + nickName='新昵称', + email='user@example.com', + phonenumber='13800138000', + sex='2', + ) + current_user = SimpleNamespace( + user=SimpleNamespace( + user_id=2, + user_name='user', + role_ids='2', + post_ids='3', + role=[], + ) + ) + expected = object() + + with ( + patch.object( + UserService, + 'edit_user_services', + new=AsyncMock(return_value=SimpleNamespace(message='更新成功')), + ) as edit_user_services, + patch( + 'module_admin.controller.user_controller.ResponseUtil.success', + return_value=expected, + ), + ): + result = await inspect.unwrap(change_system_user_profile_info)( + request, + user_info, + object(), + current_user, + ) + + assert result is expected + edit_user = edit_user_services.await_args.args[1] + update_fields = edit_user.model_dump(exclude_unset=True) + assert update_fields['nick_name'] == '新昵称' + assert update_fields['email'] == 'user@example.com' + assert update_fields['phonenumber'] == '13800138000' + assert update_fields['sex'] == '2' + assert not { + 'dept_id', + 'password', + 'status', + 'del_flag', + 'user_type', + 'create_by', + 'create_time', + }.intersection(update_fields) diff --git a/ruoyi-fastapi-backend/tests/module_admin/service/test_user_controller.py b/ruoyi-fastapi-backend/tests/module_admin/service/test_user_controller.py deleted file mode 100644 index b45441e..0000000 --- a/ruoyi-fastapi-backend/tests/module_admin/service/test_user_controller.py +++ /dev/null @@ -1,45 +0,0 @@ -import inspect -from types import SimpleNamespace -from unittest.mock import AsyncMock, patch - -import pytest - -from common.enums import PasswordCharacterType -from module_admin.controller.user_controller import reset_system_user_pwd -from module_admin.entity.vo.user_vo import EditUserModel -from module_admin.service.user_service import UserService -from utils.pwd_util import PwdUtil - - -@pytest.mark.asyncio -async def test_admin_reset_password_uses_front_end_default_rule() -> None: - redis = SimpleNamespace() - request = SimpleNamespace(app=SimpleNamespace(state=SimpleNamespace(redis=redis))) - reset_user = EditUserModel(userId=2, password='abcdef') - current_user = SimpleNamespace(user=SimpleNamespace(admin=True, user_name='admin')) - expected = object() - - with ( - patch.object(UserService, 'check_user_allowed_services', new_callable=AsyncMock), - patch.object(UserService, 'validate_password_services', new_callable=AsyncMock) as validate_password, - patch.object( - UserService, - 'edit_user_services', - new=AsyncMock(return_value=SimpleNamespace(message='重置成功')), - ), - patch.object(PwdUtil, 'get_password_hash', return_value='hashed-password'), - patch( - 'module_admin.controller.user_controller.ResponseUtil.success', - return_value=expected, - ), - ): - result = await inspect.unwrap(reset_system_user_pwd)( - request, - reset_user, - object(), - current_user, - object(), - ) - - assert result is expected - validate_password.assert_awaited_once_with(redis, 'abcdef', PasswordCharacterType.DEFAULT)