package user import ( "context" "errors" "testing" "time" "perms-system-server/internal/consts" "perms-system-server/internal/loaders" deptModel "perms-system-server/internal/model/dept" userModel "perms-system-server/internal/model/user" "perms-system-server/internal/response" "perms-system-server/internal/svc" "perms-system-server/internal/testutil" "perms-system-server/internal/testutil/ctxhelper" "perms-system-server/internal/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func insertTestDept(t *testing.T, ctx context.Context, svcCtx *svc.ServiceContext) int64 { t.Helper() now := time.Now().Unix() res, err := svcCtx.SysDeptModel.Insert(ctx, &deptModel.SysDept{ Name: testutil.UniqueId(), ParentId: 0, Path: "/", Sort: 0, DeptType: "NORMAL", Status: consts.StatusEnabled, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) id, _ := res.LastInsertId() return id } // TC-0135: 正常更新 func TestUpdateUser_Success(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) deptId := insertTestDept(t, ctx, svcCtx) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_dept`", deptId) }) logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{ Id: userId, Nickname: strPtr("新昵称"), Email: strPtr("new@example.com"), Phone: strPtr("13900139000"), Remark: strPtr("更新备注"), DeptId: int64Ptr(deptId), }) require.NoError(t, err) user, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) assert.Equal(t, "新昵称", user.Nickname) assert.Equal(t, "new@example.com", user.Email) assert.Equal(t, "13900139000", user.Phone) assert.Equal(t, "更新备注", user.Remark) assert.Equal(t, deptId, user.DeptId) } // TC-0136: 不存在 func TestUpdateUser_NotFound(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{ Id: 999999999, Nickname: strPtr("ghost"), }) require.Error(t, err) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr)) assert.Equal(t, 404, codeErr.Code()) assert.Equal(t, "用户不存在", codeErr.Error()) } // TC-0137: 仅传id func TestUpdateUser_OnlyId_NothingChanges(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) before, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) logic := NewUpdateUserLogic(ctx, svcCtx) err = logic.UpdateUser(&types.UpdateUserReq{Id: userId}) require.NoError(t, err) after, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) assert.Equal(t, before.Nickname, after.Nickname) assert.Equal(t, before.Email, after.Email) assert.Equal(t, before.Phone, after.Phone) assert.Equal(t, before.DeptId, after.DeptId) } // TC-0138: 清空nickname func TestUpdateUser_ClearNickname(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{ Id: userId, Nickname: strPtr(""), }) require.NoError(t, err) user, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) assert.Equal(t, "", user.Nickname) } // TC-0139: 清空email func TestUpdateUser_ClearEmail(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{ Id: userId, Email: strPtr(""), }) require.NoError(t, err) user, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) assert.Equal(t, "", user.Email) } // TC-0141: 非法email格式 func TestUpdateUser_InvalidEmail(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{ Id: userId, Email: strPtr("bad-email"), }) require.Error(t, err) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr)) assert.Equal(t, 400, codeErr.Code()) assert.Equal(t, "邮箱格式不正确", codeErr.Error()) } // TC-0145: DeptId设为0(取消部门) func TestUpdateUser_DeptIdZero_Clear(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) deptId := insertTestDept(t, ctx, svcCtx) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_dept`", deptId) }) logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{ Id: userId, DeptId: int64Ptr(deptId), }) require.NoError(t, err) err = logic.UpdateUser(&types.UpdateUserReq{ Id: userId, DeptId: int64Ptr(0), }) require.NoError(t, err) user, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) assert.Equal(t, int64(0), user.DeptId) } // TC-0146: DeptId设为正值 func TestUpdateUser_DeptIdSet(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) deptId := insertTestDept(t, ctx, svcCtx) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_dept`", deptId) }) logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{ Id: userId, DeptId: int64Ptr(deptId), }) require.NoError(t, err) user, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) assert.Equal(t, deptId, user.DeptId) } // TC-0147: DeptId不传(nil) func TestUpdateUser_NilDeptId_Unchanged(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) deptId := insertTestDept(t, ctx, svcCtx) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_dept`", deptId) }) logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{ Id: userId, DeptId: int64Ptr(deptId), }) require.NoError(t, err) err = logic.UpdateUser(&types.UpdateUserReq{ Id: userId, Nickname: strPtr("changed"), }) require.NoError(t, err) user, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) assert.Equal(t, deptId, user.DeptId) assert.Equal(t, "changed", user.Nickname) } // TC-0140: 清空remark func TestUpdateUser_ClearRemark(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{ Id: userId, Remark: strPtr("some remark"), }) require.NoError(t, err) err = logic.UpdateUser(&types.UpdateUserReq{ Id: userId, Remark: strPtr(""), }) require.NoError(t, err) user, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) assert.Equal(t, "", user.Remark) } // TC-0143: 合法phone func TestUpdateUser_ValidInternationalPhone(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{ Id: userId, Phone: strPtr("+8613800138000"), }) require.NoError(t, err) user, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) assert.Equal(t, "+8613800138000", user.Phone) } // TC-0142: 非法phone格式 func TestUpdateUser_InvalidPhone(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{ Id: userId, Phone: strPtr("12345"), }) require.Error(t, err) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr)) assert.Equal(t, 400, codeErr.Code()) assert.Equal(t, "手机号格式不正确", codeErr.Error()) } // TC-0144: 不传email(nil) func TestUpdateUser_NilEmail_Unchanged(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) before, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) originalEmail := before.Email logic := NewUpdateUserLogic(ctx, svcCtx) err = logic.UpdateUser(&types.UpdateUserReq{ Id: userId, Nickname: strPtr("changed-nick"), }) require.NoError(t, err) after, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) assert.Equal(t, originalEmail, after.Email) assert.Equal(t, "changed-nick", after.Nickname) } // TC-0521: MEMBER用户尝试修改其他用户被CheckManageAccess拒绝 func TestUpdateUser_MemberCannotManageOtherUser(t *testing.T) { svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() superCtx := ctxhelper.SuperAdminCtx() targetName := testutil.UniqueId() targetId := insertTestUser(t, superCtx, targetName, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(superCtx, conn, "`sys_user`", targetId) }) ctx := ctxhelper.MemberCtx("test_product") logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{Id: targetId, Nickname: strPtr("hacked")}) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 403, ce.Code()) } // TC-0535: 产品管理员可以修改其管理范围内的用户信息 func TestUpdateUser_ProductAdminCanManageUser(t *testing.T) { svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() superCtx := ctxhelper.SuperAdminCtx() deptId := insertTestDept(t, superCtx, svcCtx) t.Cleanup(func() { testutil.CleanTable(superCtx, conn, "`sys_dept`", deptId) }) targetName := testutil.UniqueId() targetId := insertTestUser(t, superCtx, targetName, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(superCtx, conn, "`sys_user`", targetId) }) productCode := "test_product" mId := insertTestMember(t, svcCtx, productCode, targetId) t.Cleanup(func() { testutil.CleanTable(superCtx, conn, "`sys_product_member`", mId) }) ctx := ctxhelper.AdminCtx(productCode) logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{Id: targetId, Nickname: strPtr("new-nick")}) require.NoError(t, err) user, err := svcCtx.SysUserModel.FindOne(superCtx, targetId) require.NoError(t, err) assert.Equal(t, "new-nick", user.Nickname) } // TC-0536: UpdateUser 昵称超过64字符被拒绝 func TestUpdateUser_NicknameTooLong(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) longNick := string(make([]byte, 65)) for i := range longNick { longNick = longNick[:i] + "a" + longNick[i+1:] } logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{Id: userId, Nickname: &longNick}) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 400, ce.Code()) assert.Contains(t, ce.Error(), "昵称长度不能超过64个字符") } // TC-0537: UpdateUser 部门不存在被拒绝 func TestUpdateUser_DeptNotExists(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{Id: userId, DeptId: int64Ptr(999999999)}) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 400, ce.Code()) assert.Contains(t, ce.Error(), "部门不存在") } // TC-0522: updateUser自己修改DeptId被拒绝 func TestUpdateUser_SelfEditDeptIdRejected(t *testing.T) { ctx := ctxhelper.CustomCtx(&loaders.UserDetails{ UserId: 100, Username: "self_user", Status: consts.StatusEnabled, }) svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{Id: 100, DeptId: int64Ptr(5)}) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 403, ce.Code()) assert.Equal(t, "不允许修改自己的部门和状态", ce.Error()) } // TC-0523: updateUser自己修改Status被拒绝 func TestUpdateUser_SelfEditStatusRejected(t *testing.T) { ctx := ctxhelper.CustomCtx(&loaders.UserDetails{ UserId: 100, Username: "self_user", Status: consts.StatusEnabled, }) svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{Id: 100, Status: 2}) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 403, ce.Code()) assert.Equal(t, "不允许修改自己的部门和状态", ce.Error()) } // TC-0524: updateUser未登录被拒绝 func TestUpdateUser_NotLoggedInRejected(t *testing.T) { ctx := context.Background() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{Id: 1, Nickname: strPtr("hacked")}) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 401, ce.Code()) assert.Equal(t, "未登录", ce.Error()) } // TC-0148: 超管A通过updateUser修改超管B的状态被拒绝(H-2修复验证) func TestUpdateUser_SuperAdminCannotFreezeOtherSuperAdmin(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() now := time.Now().Unix() uRes, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{ Username: testutil.UniqueId(), Password: testutil.HashPassword("pass"), Nickname: "super_b", IsSuperAdmin: consts.IsSuperAdminYes, MustChangePassword: 2, Status: consts.StatusEnabled, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) superBId, _ := uRes.LastInsertId() t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", superBId) }) logic := NewUpdateUserLogic(ctx, svcCtx) err = logic.UpdateUser(&types.UpdateUserReq{ Id: superBId, Status: consts.StatusDisabled, }) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 403, ce.Code()) assert.Equal(t, "不能通过此接口修改其他超级管理员的状态和部门", ce.Error()) user, err := svcCtx.SysUserModel.FindOne(ctx, superBId) require.NoError(t, err) assert.Equal(t, int64(consts.StatusEnabled), user.Status, "超管B的状态不应被修改") }