package user import ( "errors" "fmt" "math" "testing" "time" "perms-system-server/internal/consts" "perms-system-server/internal/loaders" deptModel "perms-system-server/internal/model/dept" memberModel "perms-system-server/internal/model/productmember" roleModel "perms-system-server/internal/model/role" 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 insertTestMember(t *testing.T, svcCtx *svc.ServiceContext, productCode string, userId int64) int64 { t.Helper() now := time.Now().Unix() res, err := svcCtx.SysProductMemberModel.Insert(ctxhelper.SuperAdminCtx(), &memberModel.SysProductMember{ ProductCode: productCode, UserId: userId, MemberType: "MEMBER", Status: 1, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) id, _ := res.LastInsertId() return id } func insertTestRole(t *testing.T, svcCtx *svc.ServiceContext, productCode string, status int64) int64 { t.Helper() now := time.Now().Unix() res, err := svcCtx.SysRoleModel.Insert(ctxhelper.SuperAdminCtx(), &roleModel.SysRole{ ProductCode: productCode, Name: "role_" + testutil.UniqueId(), Status: status, PermsLevel: 1, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) id, _ := res.LastInsertId() return id } // TC-0184: 正常绑定 func TestBindRoles_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")) mId := insertTestMember(t, svcCtx, "test_product", userId) r1 := insertTestRole(t, svcCtx, "test_product", 1) r2 := insertTestRole(t, svcCtx, "test_product", 1) t.Cleanup(func() { testutil.CleanTableByField(ctx, conn, "`sys_user_role`", "userId", userId) testutil.CleanTable(ctx, conn, "`sys_product_member`", mId) testutil.CleanTable(ctx, conn, "`sys_user`", userId) testutil.CleanTable(ctx, conn, "`sys_role`", r1, r2) }) logic := NewBindRolesLogic(ctx, svcCtx) err := logic.BindRoles(&types.BindRolesReq{ UserId: userId, RoleIds: []int64{r1, r2}, }) require.NoError(t, err) roleIds, err := svcCtx.SysUserRoleModel.FindRoleIdsByUserId(ctx, userId) require.NoError(t, err) assert.ElementsMatch(t, []int64{r1, r2}, roleIds) } // TC-0185: 用户不存在 func TestBindRoles_UserNotFound(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) logic := NewBindRolesLogic(ctx, svcCtx) err := logic.BindRoles(&types.BindRolesReq{ UserId: 999999999, RoleIds: []int64{1}, }) 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-0186: 清空角色 func TestBindRoles_EmptyRoleIds_ClearsAll(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")) mId := insertTestMember(t, svcCtx, "test_product", userId) r1 := insertTestRole(t, svcCtx, "test_product", 1) t.Cleanup(func() { testutil.CleanTableByField(ctx, conn, "`sys_user_role`", "userId", userId) testutil.CleanTable(ctx, conn, "`sys_product_member`", mId) testutil.CleanTable(ctx, conn, "`sys_user`", userId) testutil.CleanTable(ctx, conn, "`sys_role`", r1) }) logic := NewBindRolesLogic(ctx, svcCtx) err := logic.BindRoles(&types.BindRolesReq{ UserId: userId, RoleIds: []int64{r1}, }) require.NoError(t, err) err = logic.BindRoles(&types.BindRolesReq{ UserId: userId, RoleIds: []int64{}, }) require.NoError(t, err) roleIds, err := svcCtx.SysUserRoleModel.FindRoleIdsByUserId(ctx, userId) require.NoError(t, err) assert.Empty(t, roleIds) } // TC-0184: 正常重新绑定 func TestBindRoles_Rebind(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")) mId := insertTestMember(t, svcCtx, "test_product", userId) r1 := insertTestRole(t, svcCtx, "test_product", 1) r2 := insertTestRole(t, svcCtx, "test_product", 1) r3 := insertTestRole(t, svcCtx, "test_product", 1) t.Cleanup(func() { testutil.CleanTableByField(ctx, conn, "`sys_user_role`", "userId", userId) testutil.CleanTable(ctx, conn, "`sys_product_member`", mId) testutil.CleanTable(ctx, conn, "`sys_user`", userId) testutil.CleanTable(ctx, conn, "`sys_role`", r1, r2, r3) }) logic := NewBindRolesLogic(ctx, svcCtx) err := logic.BindRoles(&types.BindRolesReq{ UserId: userId, RoleIds: []int64{r1, r2}, }) require.NoError(t, err) err = logic.BindRoles(&types.BindRolesReq{ UserId: userId, RoleIds: []int64{r2, r3}, }) require.NoError(t, err) roleIds, err := svcCtx.SysUserRoleModel.FindRoleIdsByUserId(ctx, userId) require.NoError(t, err) assert.ElementsMatch(t, []int64{r2, r3}, roleIds) } // TC-0188: 角色不属于当前产品 func TestBindRoles_RoleBelongsToOtherProduct(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")) mId := insertTestMember(t, svcCtx, "test_product", userId) otherRole := insertTestRole(t, svcCtx, "other_product", 1) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_product_member`", mId) testutil.CleanTable(ctx, conn, "`sys_user`", userId) testutil.CleanTable(ctx, conn, "`sys_role`", otherRole) }) logic := NewBindRolesLogic(ctx, svcCtx) err := logic.BindRoles(&types.BindRolesReq{ UserId: userId, RoleIds: []int64{otherRole}, }) require.Error(t, err) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr)) assert.Equal(t, 400, codeErr.Code()) assert.Contains(t, codeErr.Error(), "其他产品的角色") } // TC-0189: 角色已禁用 func TestBindRoles_RoleDisabled(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")) mId := insertTestMember(t, svcCtx, "test_product", userId) disabledRole := insertTestRole(t, svcCtx, "test_product", 2) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_product_member`", mId) testutil.CleanTable(ctx, conn, "`sys_user`", userId) testutil.CleanTable(ctx, conn, "`sys_role`", disabledRole) }) logic := NewBindRolesLogic(ctx, svcCtx) err := logic.BindRoles(&types.BindRolesReq{ UserId: userId, RoleIds: []int64{disabledRole}, }) require.Error(t, err) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr)) assert.Equal(t, 400, codeErr.Code()) assert.Contains(t, codeErr.Error(), "已禁用的角色") } // TC-0190: 角色不存在 func TestBindRoles_RoleNotExists(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")) mId := insertTestMember(t, svcCtx, "test_product", userId) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_product_member`", mId) testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) logic := NewBindRolesLogic(ctx, svcCtx) err := logic.BindRoles(&types.BindRolesReq{ UserId: userId, RoleIds: []int64{999999999}, }) require.Error(t, err) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr)) assert.Equal(t, 400, codeErr.Code()) assert.Contains(t, codeErr.Error(), "无效的角色ID") } func insertTestRoleWithLevel(t *testing.T, svcCtx *svc.ServiceContext, productCode string, status int64, permsLevel int64) int64 { t.Helper() now := time.Now().Unix() res, err := svcCtx.SysRoleModel.Insert(ctxhelper.SuperAdminCtx(), &roleModel.SysRole{ ProductCode: productCode, Name: "role_" + testutil.UniqueId(), Status: status, PermsLevel: permsLevel, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) id, _ := res.LastInsertId() return id } // setupDeptForCaller 插入一个 dept,同时构造 caller(使用该 dept)与 target(同 dept 下)的环境 // 返回 deptId、deptPath(caller 使用)、cleanup function func setupDeptForCaller(t *testing.T, svcCtx *svc.ServiceContext) (int64, string, func()) { t.Helper() now := time.Now().Unix() superCtx := ctxhelper.SuperAdminCtx() res, err := svcCtx.SysDeptModel.Insert(superCtx, &deptModel.SysDept{ Name: "dept_" + testutil.UniqueId(), ParentId: 0, Path: "/", DeptType: consts.DeptTypeNormal, Status: consts.StatusEnabled, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) deptId, _ := res.LastInsertId() // 先占位再用真实 deptId 构造 path:"/{deptId}/" path := fmt.Sprintf("/%d/", deptId) _, err = svcCtx.SysDeptModel.Insert(superCtx, &deptModel.SysDept{}) // noop — keep linter happy _ = err // 更新 path dept, _ := svcCtx.SysDeptModel.FindOne(superCtx, deptId) dept.Path = path dept.UpdateTime = time.Now().Unix() require.NoError(t, svcCtx.SysDeptModel.Update(superCtx, dept)) conn := testutil.GetTestSqlConn() cleanup := func() { testutil.CleanTable(superCtx, conn, "`sys_dept`", deptId) } return deptId, path, cleanup } // TC-0208: MEMBER 调用者不能分配权限级别高于自身的角色 (audit H-1 修复后 permsLevel 仅对 MEMBER 生效) func TestBindRoles_PermsLevelEscalation_Rejected(t *testing.T) { svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() superCtx := ctxhelper.SuperAdminCtx() deptId, deptPath, cleanupDept := setupDeptForCaller(t, svcCtx) t.Cleanup(cleanupDept) productCode := "test_product" // 目标用户:放进 dept 下,MEMBER 产品成员 username := testutil.UniqueId() targetUserId := insertTestUserFull(t, superCtx, &userModel.SysUser{ Username: username, Password: testutil.HashPassword("pass"), Nickname: "tgt", DeptId: deptId, IsSuperAdmin: 2, MustChangePassword: 2, Status: 1, }) mId := insertTestMember(t, svcCtx, productCode, targetUserId) highLevelRole := insertTestRoleWithLevel(t, svcCtx, productCode, 1, 1) // M-3 修复后 GuardRoleLevelAssignable 走 DB 强一致读取 caller 的 MinPermsLevel, // 因此需要在 DB 里为调用者落地真实的 user + role + user_role 关系链(permsLevel=50)。 callerUserId, callerCleanup := seedCallerWithRoleLevel(t, svcCtx, productCode, 50) t.Cleanup(callerCleanup) t.Cleanup(func() { testutil.CleanTableByField(superCtx, conn, "`sys_user_role`", "userId", targetUserId) testutil.CleanTable(superCtx, conn, "`sys_product_member`", mId) testutil.CleanTable(superCtx, conn, "`sys_user`", targetUserId) testutil.CleanTable(superCtx, conn, "`sys_role`", highLevelRole) }) // MEMBER 调用者与 target 同 dept,DB 中 MinPermsLevel=50,目标角色 permsLevel=1 → 越级 ctx := ctxhelper.CustomCtx(&loaders.UserDetails{ UserId: callerUserId, Username: "member_caller", IsSuperAdmin: false, MemberType: consts.MemberTypeMember, Status: consts.StatusEnabled, ProductCode: productCode, DeptId: deptId, DeptPath: deptPath, MinPermsLevel: 50, }) logic := NewBindRolesLogic(ctx, svcCtx) err := logic.BindRoles(&types.BindRolesReq{ UserId: targetUserId, RoleIds: []int64{highLevelRole}, }) 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(), "不能分配权限级别高于自身的角色") } // TC-0711: ADMIN 调用者(MinPermsLevel=math.MaxInt64)不受 permsLevel 校验约束 (audit H-1 回归) // 修复前:ADMIN 通过 member_type 获得权限,MinPermsLevel 保持 math.MaxInt64, // r.PermsLevel < math.MaxInt64 必然成立 → ADMIN 无法绑定任何角色。 // 修复后:代码显式豁免 ADMIN/DEVELOPER 的 permsLevel 校验。 func TestBindRoles_AdminBypassesPermsLevelCheck(t *testing.T) { svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() superCtx := ctxhelper.SuperAdminCtx() productCode := "test_product" username := testutil.UniqueId() userId := insertTestUser(t, superCtx, username, testutil.HashPassword("pass")) mId := insertTestMember(t, svcCtx, productCode, userId) lowLevelRole := insertTestRoleWithLevel(t, svcCtx, productCode, 1, 1) t.Cleanup(func() { testutil.CleanTableByField(superCtx, conn, "`sys_user_role`", "userId", userId) testutil.CleanTable(superCtx, conn, "`sys_product_member`", mId) testutil.CleanTable(superCtx, conn, "`sys_user`", userId) testutil.CleanTable(superCtx, conn, "`sys_role`", lowLevelRole) }) // 关键:模拟 loader 真实产出——ADMIN 没有自定义角色,MinPermsLevel=math.MaxInt64 ctx := ctxhelper.CustomCtx(&loaders.UserDetails{ UserId: 999997, Username: "admin_caller", IsSuperAdmin: false, MemberType: consts.MemberTypeAdmin, Status: consts.StatusEnabled, ProductCode: productCode, DeptId: 1, DeptPath: "/1/", MinPermsLevel: math.MaxInt64, // 默认 sentinel }) logic := NewBindRolesLogic(ctx, svcCtx) err := logic.BindRoles(&types.BindRolesReq{ UserId: userId, RoleIds: []int64{lowLevelRole}, }) require.NoError(t, err, "ADMIN 调用者应当能绑定任意级别的角色 (audit H-1)") roleIds, err := svcCtx.SysUserRoleModel.FindRoleIdsByUserId(ctx, userId) require.NoError(t, err) assert.Contains(t, roleIds, lowLevelRole) } // TC-0712: DEVELOPER 调用者同样不受 permsLevel 校验约束 (audit H-1 回归) func TestBindRoles_DeveloperBypassesPermsLevelCheck(t *testing.T) { svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() superCtx := ctxhelper.SuperAdminCtx() deptId, deptPath, cleanupDept := setupDeptForCaller(t, svcCtx) t.Cleanup(cleanupDept) productCode := "test_product" username := testutil.UniqueId() userId := insertTestUserFull(t, superCtx, &userModel.SysUser{ Username: username, Password: testutil.HashPassword("pass"), Nickname: "tgt_dev", DeptId: deptId, IsSuperAdmin: 2, MustChangePassword: 2, Status: 1, }) mId := insertTestMember(t, svcCtx, productCode, userId) lowLevelRole := insertTestRoleWithLevel(t, svcCtx, productCode, 1, 1) t.Cleanup(func() { testutil.CleanTableByField(superCtx, conn, "`sys_user_role`", "userId", userId) testutil.CleanTable(superCtx, conn, "`sys_product_member`", mId) testutil.CleanTable(superCtx, conn, "`sys_user`", userId) testutil.CleanTable(superCtx, conn, "`sys_role`", lowLevelRole) }) ctx := ctxhelper.CustomCtx(&loaders.UserDetails{ UserId: 999996, Username: "developer_caller", IsSuperAdmin: false, MemberType: consts.MemberTypeDeveloper, Status: consts.StatusEnabled, ProductCode: productCode, DeptId: deptId, DeptPath: deptPath, MinPermsLevel: math.MaxInt64, }) logic := NewBindRolesLogic(ctx, svcCtx) err := logic.BindRoles(&types.BindRolesReq{ UserId: userId, RoleIds: []int64{lowLevelRole}, }) require.NoError(t, err, "DEVELOPER 调用者应当能绑定任意级别的角色 (audit H-1)") } // TC-0713: MinPermsLevel == math.MaxInt64 的 MEMBER 调用者也必须被豁免 // (sentinel 判定路径:既不是 ADMIN/DEVELOPER,也没有角色,此时 r.PermsLevel