package user import ( "context" "database/sql" "errors" "math" "testing" "time" "perms-system-server/internal/consts" "perms-system-server/internal/loaders" "perms-system-server/internal/middleware" 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/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func insertTestDeptForScope(t *testing.T, ctx context.Context, svcCtx *svc.ServiceContext, tag, path string) int64 { t.Helper() now := time.Now().Unix() res, err := svcCtx.SysDeptModel.Insert(ctx, &deptModel.SysDept{ ParentId: 0, Name: tag + "_" + testutil.UniqueId(), Path: path, Sort: 0, DeptType: "NORMAL", Remark: "", Status: 1, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) id, _ := res.LastInsertId() return id } func insertTestUserWithDept(t *testing.T, ctx context.Context, tag string, deptId int64) int64 { t.Helper() now := time.Now().Unix() return insertTestUserFull(t, ctx, &userModel.SysUser{ Username: "ddu_" + tag + "_" + testutil.UniqueId(), Password: testutil.HashPassword("pw"), Nickname: "n", Avatar: sql.NullString{}, Email: "x@t.com", Phone: "13800000000", DeptId: deptId, IsSuperAdmin: consts.IsSuperAdminNo, MustChangePassword: 2, Status: consts.StatusEnabled, CreateTime: now, UpdateTime: now, }) } // TC-0746: L-F 修复回归 —— DEVELOPER 调用者不得将目标用户的 deptId 调到 // 自己 DeptPath 子树之外的部门。UpdateUser 必须在 req.DeptId 变更时做 Path 前缀校验。 func TestUpdateUser_DeveloperCannotMoveTargetOutsideSubtree(t *testing.T) { bootstrap := context.Background() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() callerDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "caller", "/100/") targetDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "target", "/100/200/") outsideDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "outside", "/999/") targetId := insertTestUserWithDept(t, bootstrap, "lf_out", targetDeptId) mId := insertTestMember(t, svcCtx, "test_product", targetId) t.Cleanup(func() { testutil.CleanTable(bootstrap, conn, "`sys_product_member`", mId) testutil.CleanTable(bootstrap, conn, "`sys_user`", targetId) testutil.CleanTable(bootstrap, conn, "`sys_dept`", callerDeptId, targetDeptId, outsideDeptId) }) devCtx := middleware.WithUserDetails(context.Background(), &loaders.UserDetails{ UserId: 55555, Username: "lf_dev", IsSuperAdmin: false, MemberType: consts.MemberTypeDeveloper, Status: consts.StatusEnabled, ProductCode: "test_product", DeptId: callerDeptId, DeptPath: "/100/", MinPermsLevel: math.MaxInt64, }) newDept := outsideDeptId err := NewUpdateUserLogic(devCtx, svcCtx).UpdateUser(&types.UpdateUserReq{ Id: targetId, DeptId: &newDept, }) require.Error(t, err, "调入外部部门应被拒绝") var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 403, ce.Code()) assert.Contains(t, ce.Error(), "无权将用户调入") user, err := svcCtx.SysUserModel.FindOne(bootstrap, targetId) require.NoError(t, err) assert.Equal(t, targetDeptId, user.DeptId, "被拒绝的请求必须不改动 DB") } // TC-0747: L-F 正向回归 —— DEVELOPER 将目标用户调入自己子树下的部门应允许。 func TestUpdateUser_DeveloperCanMoveTargetWithinSubtree(t *testing.T) { bootstrap := context.Background() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() callerDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "caller_in", "/200/") srcDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "src_in", "/200/1/") dstDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "dst_in", "/200/2/") targetId := insertTestUserWithDept(t, bootstrap, "lf_in", srcDeptId) mId := insertTestMember(t, svcCtx, "test_product", targetId) t.Cleanup(func() { testutil.CleanTable(bootstrap, conn, "`sys_product_member`", mId) testutil.CleanTable(bootstrap, conn, "`sys_user`", targetId) testutil.CleanTable(bootstrap, conn, "`sys_dept`", callerDeptId, srcDeptId, dstDeptId) }) devCtx := middleware.WithUserDetails(context.Background(), &loaders.UserDetails{ UserId: 66666, Username: "lf_dev_ok", IsSuperAdmin: false, MemberType: consts.MemberTypeDeveloper, Status: consts.StatusEnabled, ProductCode: "test_product", DeptId: callerDeptId, DeptPath: "/200/", MinPermsLevel: math.MaxInt64, }) newDept := dstDeptId require.NoError(t, NewUpdateUserLogic(devCtx, svcCtx).UpdateUser(&types.UpdateUserReq{ Id: targetId, DeptId: &newDept, }), "caller DeptPath 的前缀子部门必须允许调入") user, err := svcCtx.SysUserModel.FindOne(bootstrap, targetId) require.NoError(t, err) assert.Equal(t, dstDeptId, user.DeptId) } // TC-0748: L-F —— 产品 ADMIN 调用者被豁免 DeptPath 前缀校验(可跨部门转移)。 func TestUpdateUser_ProductAdminExemptFromSubtreeCheck(t *testing.T) { bootstrap := context.Background() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() adminDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "admin_home", "/300/") targetHomeDept := insertTestDeptForScope(t, bootstrap, svcCtx, "target_home", "/400/") anywhereDept := insertTestDeptForScope(t, bootstrap, svcCtx, "anywhere", "/500/") targetId := insertTestUserWithDept(t, bootstrap, "lf_admin", targetHomeDept) mId := insertTestMember(t, svcCtx, "test_product", targetId) t.Cleanup(func() { testutil.CleanTable(bootstrap, conn, "`sys_product_member`", mId) testutil.CleanTable(bootstrap, conn, "`sys_user`", targetId) testutil.CleanTable(bootstrap, conn, "`sys_dept`", adminDeptId, targetHomeDept, anywhereDept) }) adminCtx := middleware.WithUserDetails(context.Background(), &loaders.UserDetails{ UserId: 77777, Username: "lf_admin", IsSuperAdmin: false, MemberType: consts.MemberTypeAdmin, Status: consts.StatusEnabled, ProductCode: "test_product", DeptId: adminDeptId, DeptPath: "/300/", MinPermsLevel: math.MaxInt64, }) newDept := anywhereDept require.NoError(t, NewUpdateUserLogic(adminCtx, svcCtx).UpdateUser(&types.UpdateUserReq{ Id: targetId, DeptId: &newDept, }), "产品 ADMIN 在 UpdateUser 的 DeptPath 前缀校验中被豁免") }