package user import ( "context" "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" "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" ) // --------------------------------------------------------------------------- // 覆盖目标:审计 M-N4 修复 —— CreateUser 必须做 caller.DeptPath → newDept.Path 前缀校验, // 并且目标部门必须处于 Enabled(审计 L-N2),非超管 DeptId=0 必须拒绝, // 避免 Product ADMIN 为"非自己管辖的部门"预埋 admin_* / ops_* 关键用户名并借 AddMember // 的协同路径挂进产品。 // --------------------------------------------------------------------------- func callerAdminCtx(callerUserId, deptId int64, deptPath string) context.Context { return middleware.WithUserDetails(context.Background(), &loaders.UserDetails{ UserId: callerUserId, Username: "prod_admin_caller", IsSuperAdmin: false, MemberType: consts.MemberTypeAdmin, Status: consts.StatusEnabled, ProductCode: "test_product", DeptId: deptId, DeptPath: deptPath, MinPermsLevel: math.MaxInt64, }) } // TC-0994: M-N4 —— 产品 ADMIN 为非自己管辖部门创建用户必须 403。 func TestCreateUser_MN4_AdminCannotCreateOutsideDeptSubtree(t *testing.T) { bootstrap := context.Background() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() callerDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "mn4_caller", "/100/") outsideDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "mn4_outside", "/999/") t.Cleanup(func() { testutil.CleanTable(bootstrap, conn, "`sys_dept`", callerDeptId, outsideDeptId) }) adminCtx := callerAdminCtx(777771, callerDeptId, "/100/") _, err := NewCreateUserLogic(adminCtx, svcCtx).CreateUser(&types.CreateUserReq{ Username: "mn4_seed_" + testutil.UniqueId(), Password: "Pass123456", DeptId: outsideDeptId, }) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 403, ce.Code(), "M-N4:产品 ADMIN 跨部门树创建用户必须 403,防止占用关键用户名等 AddMember 合谋挂进产品") assert.Contains(t, ce.Error(), "无权在非自己管辖的部门下创建用户") } // TC-0995: M-N4 正向 —— 产品 ADMIN 在自己子树下的部门创建用户放行。 func TestCreateUser_MN4_AdminCanCreateInsideDeptSubtree(t *testing.T) { bootstrap := context.Background() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() callerDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "mn4_ok_caller", "/200/") childDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "mn4_ok_child", "/200/1/") t.Cleanup(func() { testutil.CleanTable(bootstrap, conn, "`sys_dept`", callerDeptId, childDeptId) }) adminCtx := callerAdminCtx(777772, callerDeptId, "/200/") username := "mn4ok_" + testutil.UniqueId() resp, err := NewCreateUserLogic(adminCtx, svcCtx).CreateUser(&types.CreateUserReq{ Username: username, Password: "Pass123456", DeptId: childDeptId, }) require.NoError(t, err) require.NotNil(t, resp) t.Cleanup(func() { testutil.CleanTable(bootstrap, conn, "`sys_user`", resp.Id) }) user, err := svcCtx.SysUserModel.FindOne(bootstrap, resp.Id) require.NoError(t, err) assert.Equal(t, username, user.Username) assert.Equal(t, childDeptId, user.DeptId, "用户必须真实落在指定子部门") } // TC-0996: M-N4 —— SuperAdmin 可跨一切部门(含 DeptId=0),继续允许创建系统级账号。 func TestCreateUser_MN4_SuperAdminCanCreateAnywhere(t *testing.T) { bootstrap := context.Background() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() randomDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "mn4_sa", "/1000/") t.Cleanup(func() { testutil.CleanTable(bootstrap, conn, "`sys_dept`", randomDeptId) }) // A) 超管创建在任意部门 usernameDept := "mn4sa_dept_" + testutil.UniqueId() resp, err := NewCreateUserLogic(ctxhelper.SuperAdminCtx(), svcCtx).CreateUser(&types.CreateUserReq{ Username: usernameDept, Password: "Pass123456", DeptId: randomDeptId, }) require.NoError(t, err) require.NotNil(t, resp) t.Cleanup(func() { testutil.CleanTable(bootstrap, conn, "`sys_user`", resp.Id) }) // B) 超管创建 DeptId=0 的系统级账号(历史跨组织账号语义保留) usernameZero := "mn4sa_zero_" + testutil.UniqueId() respZero, err := NewCreateUserLogic(ctxhelper.SuperAdminCtx(), svcCtx).CreateUser(&types.CreateUserReq{ Username: usernameZero, Password: "Pass123456", DeptId: 0, }) require.NoError(t, err) require.NotNil(t, respZero) t.Cleanup(func() { testutil.CleanTable(bootstrap, conn, "`sys_user`", respZero.Id) }) } // TC-0997: M-N4 —— 非超管 caller 的 DeptPath 为空时必须 403,不得在部门树外开口。 func TestCreateUser_MN4_EmptyCallerDeptPathRejected(t *testing.T) { bootstrap := context.Background() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() dstDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "mn4_empty", "/500/") t.Cleanup(func() { testutil.CleanTable(bootstrap, conn, "`sys_dept`", dstDeptId) }) // caller 是产品 ADMIN 但历史账号 DeptPath=="" —— 必须 403 "未归属任何部门" adminCtx := callerAdminCtx(777773, 0, "") _, err := NewCreateUserLogic(adminCtx, svcCtx).CreateUser(&types.CreateUserReq{ Username: "mn4empty_" + testutil.UniqueId(), Password: "Pass123456", DeptId: dstDeptId, }) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 403, ce.Code(), "M-N4:caller.DeptPath=='' 属于 legacy 账号,fail-close 不允许创建用户") assert.Contains(t, ce.Error(), "您未归属任何部门") } // TC-0998: M-N4 —— 非超管 caller 传 DeptId=0 必须 400(禁止非超管创建"无部门"账号)。 func TestCreateUser_MN4_NonSuperAdminMustSpecifyDept(t *testing.T) { bootstrap := context.Background() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() callerDeptId := insertTestDeptForScope(t, bootstrap, svcCtx, "mn4_mustspec", "/300/") t.Cleanup(func() { testutil.CleanTable(bootstrap, conn, "`sys_dept`", callerDeptId) }) adminCtx := callerAdminCtx(777774, callerDeptId, "/300/") _, err := NewCreateUserLogic(adminCtx, svcCtx).CreateUser(&types.CreateUserReq{ Username: "mn4must_" + testutil.UniqueId(), Password: "Pass123456", DeptId: 0, }) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 400, ce.Code(), "M-N4:非超管 CreateUser 必须显式指定部门,禁止在部门树外创建账号") assert.Contains(t, ce.Error(), "必须指定部门") } // TC-0999: L-N2 —— 目标部门已停用时 CreateUser 必须 400 "目标部门已停用",与 UpdateDept 闭环。 func TestCreateUser_LN2_TargetDeptDisabled(t *testing.T) { bootstrap := context.Background() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() now := time.Now().Unix() disRes, err := svcCtx.SysDeptModel.Insert(bootstrap, &deptModel.SysDept{ ParentId: 0, Name: "ln2_dis_" + testutil.UniqueId(), Path: "/900/", Sort: 0, DeptType: "NORMAL", Remark: "", Status: 2, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) disabledId, _ := disRes.LastInsertId() t.Cleanup(func() { testutil.CleanTable(bootstrap, conn, "`sys_dept`", disabledId) }) // 超管也必须被拒绝:L-N2 的规则针对"所有调用方",防止 disabled 部门被意外重新填人 _, err = NewCreateUserLogic(ctxhelper.SuperAdminCtx(), svcCtx).CreateUser(&types.CreateUserReq{ Username: "ln2_" + testutil.UniqueId(), Password: "Pass123456", DeptId: disabledId, }) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 400, ce.Code()) assert.Equal(t, "目标部门已停用", ce.Error(), "L-N2:目标部门 status!=Enabled 必须拒绝,与 UpdateDept 禁用语义闭环") }