package user import ( "context" "errors" "math" "testing" "perms-system-server/internal/consts" "perms-system-server/internal/loaders" "perms-system-server/internal/middleware" "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" ) // --------------------------------------------------------------------------- // 覆盖目标:审计 H-4 修复 —— "把用户移出部门树(deptId=0)" 的破坏组织结构操作, // 仅能由 SuperAdmin 或产品 ADMIN 执行。DEVELOPER / MEMBER 执行必须 403,并且 DB 不得发生变更。 // 修复前这是横向越权点:下级把上级拉出部门树后,后续 checkDeptHierarchy 对该目标彻底失效。 // --------------------------------------------------------------------------- // TC-0814: H-4 —— DEVELOPER 调用者执行 deptId=0 的 UpdateUser 必须 403,且 target.DeptId 不动。 func TestUpdateUser_DeveloperCannotMoveTargetOutOfDept(t *testing.T) { bootstrap := context.Background() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() callerDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "h4_caller_dev", "/700/") targetDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "h4_target_dev", "/700/1/") targetId := insertTestUserWithDept(t, bootstrap, "h4_dev", 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) }) devCtx := middleware.WithUserDetails(context.Background(), &loaders.UserDetails{ UserId: 88881, Username: "h4_dev", IsSuperAdmin: false, MemberType: consts.MemberTypeDeveloper, Status: consts.StatusEnabled, ProductCode: "test_product", DeptId: callerDeptId, DeptPath: "/700/", MinPermsLevel: math.MaxInt64, }) zero := int64(0) err := NewUpdateUserLogic(devCtx, svcCtx).UpdateUser(&types.UpdateUserReq{ Id: targetId, DeptId: &zero, }) require.Error(t, err, "H-4:DEVELOPER 不得把目标移出部门树") var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 403, ce.Code()) assert.Contains(t, ce.Error(), "仅超级管理员或产品管理员可将用户移出部门") u, err := svcCtx.SysUserModel.FindOne(bootstrap, targetId) require.NoError(t, err) assert.Equal(t, targetDeptId, u.DeptId, "被拒绝的请求对 DB 零副作用") } // TC-0815: H-4 —— MEMBER 调用者同理被拒(即便是修改自身的其他字段也不能顺手把自己移出部门)。 // 用户修改自身时,路由层 if caller.UserId == req.Id 分支只拦 DeptId != nil/Status != 0; // 但修改他人为 deptId=0 的分支仍必须 403,以防任何下级调用者漂白组织结构。 func TestUpdateUser_MemberCannotMoveOtherOutOfDept(t *testing.T) { bootstrap := context.Background() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() callerDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "h4_member_caller", "/800/") targetDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "h4_member_target", "/800/1/") targetId := insertTestUserWithDept(t, bootstrap, "h4_mem", 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) }) memberCtx := middleware.WithUserDetails(context.Background(), &loaders.UserDetails{ UserId: 88882, Username: "h4_mem", IsSuperAdmin: false, MemberType: consts.MemberTypeMember, Status: consts.StatusEnabled, ProductCode: "test_product", DeptId: callerDeptId, DeptPath: "/800/", MinPermsLevel: 10, }) zero := int64(0) err := NewUpdateUserLogic(memberCtx, svcCtx).UpdateUser(&types.UpdateUserReq{ Id: targetId, DeptId: &zero, }) require.Error(t, err, "H-4:MEMBER 更不得移出他人") var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 403, ce.Code()) u, err := svcCtx.SysUserModel.FindOne(bootstrap, targetId) require.NoError(t, err) assert.Equal(t, targetDeptId, u.DeptId) } // TC-0816: H-4 —— 产品 ADMIN 有权将他人移出部门(功能不应被修复路径误伤)。 func TestUpdateUser_ProductAdminCanMoveTargetOutOfDept(t *testing.T) { bootstrap := context.Background() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() adminDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "h4_admin", "/900/") targetDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "h4_admin_target", "/900/1/") targetId := insertTestUserWithDept(t, bootstrap, "h4_admin_tgt", 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`", adminDeptId, targetDeptId) }) adminCtx := middleware.WithUserDetails(context.Background(), &loaders.UserDetails{ UserId: 88883, Username: "h4_admin", IsSuperAdmin: false, MemberType: consts.MemberTypeAdmin, Status: consts.StatusEnabled, ProductCode: "test_product", DeptId: adminDeptId, DeptPath: "/900/", MinPermsLevel: math.MaxInt64, }) zero := int64(0) require.NoError(t, NewUpdateUserLogic(adminCtx, svcCtx).UpdateUser(&types.UpdateUserReq{ Id: targetId, DeptId: &zero, }), "产品 ADMIN 必须仍能执行 deptId=0 的合法运维操作") u, err := svcCtx.SysUserModel.FindOne(bootstrap, targetId) require.NoError(t, err) assert.Equal(t, int64(0), u.DeptId, "ADMIN 的合法 deptId=0 操作必须落盘") } // TC-0817: H-4 —— SuperAdmin 有权将他人移出部门(豁免路径)。 func TestUpdateUser_SuperAdminCanMoveTargetOutOfDept(t *testing.T) { bootstrap := context.Background() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() targetDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "h4_sa_target", "/950/") targetId := insertTestUserWithDept(t, bootstrap, "h4_sa_tgt", 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`", targetDeptId) }) zero := int64(0) require.NoError(t, NewUpdateUserLogic(ctxhelper.SuperAdminCtx(), svcCtx).UpdateUser(&types.UpdateUserReq{ Id: targetId, DeptId: &zero, }), "SuperAdmin 的 deptId=0 操作是合法的顶层运维") u, err := svcCtx.SysUserModel.FindOne(bootstrap, targetId) require.NoError(t, err) assert.Equal(t, int64(0), u.DeptId) }