| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905 |
- package user
- import (
- "context"
- "database/sql"
- "errors"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- "math"
- "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/testutil/ctxhelper"
- "perms-system-server/internal/types"
- "strings"
- "sync"
- "testing"
- "time"
- )
- func insertTestUser(t *testing.T, ctx context.Context, username, password string) int64 {
- t.Helper()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- now := time.Now().Unix()
- res, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{
- Username: username,
- Password: password,
- Nickname: "test",
- Avatar: sql.NullString{},
- Email: username + "@test.com",
- Phone: "13800000000",
- Remark: "",
- DeptId: 0,
- IsSuperAdmin: 2,
- MustChangePassword: 2,
- Status: 1,
- CreateTime: now,
- UpdateTime: now,
- })
- require.NoError(t, err)
- id, _ := res.LastInsertId()
- return id
- }
- func insertTestUserFull(t *testing.T, ctx context.Context, u *userModel.SysUser) int64 {
- t.Helper()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- now := time.Now().Unix()
- if u.CreateTime == 0 {
- u.CreateTime = now
- }
- if u.UpdateTime == 0 {
- u.UpdateTime = now
- }
- res, err := svcCtx.SysUserModel.Insert(ctx, u)
- require.NoError(t, err)
- id, _ := res.LastInsertId()
- return id
- }
- func strPtr(s string) *string { return &s }
- func int64Ptr(i int64) *int64 { return &i }
- // TC-0134: 正常创建
- func TestCreateUser_Success(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- username := testutil.UniqueId()
- logic := NewCreateUserLogic(ctx, svcCtx)
- resp, err := logic.CreateUser(&types.CreateUserReq{
- Username: username,
- Password: "Pass123456",
- Nickname: "测试用户",
- Email: username + "@test.com",
- Phone: "13800138000",
- Remark: "集成测试",
- DeptId: 0,
- })
- require.NoError(t, err)
- require.NotNil(t, resp)
- assert.Greater(t, resp.Id, int64(0))
- t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", resp.Id) })
- user, err := svcCtx.SysUserModel.FindOne(ctx, resp.Id)
- require.NoError(t, err)
- assert.Equal(t, username, user.Username)
- assert.Equal(t, "测试用户", user.Nickname)
- assert.Equal(t, int64(1), user.Status)
- assert.Equal(t, int64(2), user.IsSuperAdmin)
- }
- // TC-0135: 用户名已存在(预检)
- func TestCreateUser_UsernameExists(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- username := testutil.UniqueId()
- hashed := testutil.HashPassword("pass123")
- userId := insertTestUser(t, ctx, username, hashed)
- t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) })
- logic := NewCreateUserLogic(ctx, svcCtx)
- _, err := logic.CreateUser(&types.CreateUserReq{
- Username: username,
- Password: "Pass456789",
- })
- require.Error(t, err)
- var codeErr *response.CodeError
- require.True(t, errors.As(err, &codeErr))
- assert.Equal(t, 409, codeErr.Code())
- assert.Equal(t, "用户名已存在", codeErr.Error())
- }
- // TC-0137: 非法email格式
- func TestCreateUser_InvalidEmail(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- logic := NewCreateUserLogic(ctx, svcCtx)
- _, err := logic.CreateUser(&types.CreateUserReq{
- Username: testutil.UniqueId(),
- Password: "Pass123456",
- Email: "not-an-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-0138: 合法email
- func TestCreateUser_ValidEmail(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- username := testutil.UniqueId()
- logic := NewCreateUserLogic(ctx, svcCtx)
- resp, err := logic.CreateUser(&types.CreateUserReq{
- Username: username,
- Password: "Pass123456",
- Email: username + "@example.com",
- })
- require.NoError(t, err)
- require.NotNil(t, resp)
- t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", resp.Id) })
- user, err := svcCtx.SysUserModel.FindOne(ctx, resp.Id)
- require.NoError(t, err)
- assert.Equal(t, username+"@example.com", user.Email)
- }
- // TC-0139: email为空(可选)
- func TestCreateUser_EmptyEmailSkipsValidation(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- username := testutil.UniqueId()
- logic := NewCreateUserLogic(ctx, svcCtx)
- resp, err := logic.CreateUser(&types.CreateUserReq{
- Username: username,
- Password: "Pass123456",
- Email: "",
- })
- require.NoError(t, err)
- require.NotNil(t, resp)
- t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", resp.Id) })
- user, err := svcCtx.SysUserModel.FindOne(ctx, resp.Id)
- require.NoError(t, err)
- assert.Equal(t, "", user.Email)
- }
- // TC-0140: 非法phone格式
- func TestCreateUser_InvalidPhone(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- logic := NewCreateUserLogic(ctx, svcCtx)
- _, err := logic.CreateUser(&types.CreateUserReq{
- Username: testutil.UniqueId(),
- Password: "Pass123456",
- Phone: "abc",
- })
- 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-0141: 合法phone(国际)
- func TestCreateUser_ValidPhone(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- username := testutil.UniqueId()
- logic := NewCreateUserLogic(ctx, svcCtx)
- resp, err := logic.CreateUser(&types.CreateUserReq{
- Username: username,
- Password: "Pass123456",
- Phone: "13900139000",
- })
- require.NoError(t, err)
- require.NotNil(t, resp)
- t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", resp.Id) })
- user, err := svcCtx.SysUserModel.FindOne(ctx, resp.Id)
- require.NoError(t, err)
- assert.Equal(t, "13900139000", user.Phone)
- }
- // TC-0142: phone为空(可选)
- func TestCreateUser_EmptyPhoneSkipsValidation(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- username := testutil.UniqueId()
- logic := NewCreateUserLogic(ctx, svcCtx)
- resp, err := logic.CreateUser(&types.CreateUserReq{
- Username: username,
- Password: "Pass123456",
- Phone: "",
- })
- require.NoError(t, err)
- require.NotNil(t, resp)
- t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", resp.Id) })
- user, err := svcCtx.SysUserModel.FindOne(ctx, resp.Id)
- require.NoError(t, err)
- assert.Equal(t, "", user.Phone)
- }
- // TC-0143: 并发同username(TOCTOU)
- func TestCreateUser_ConcurrentSameUsername(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- username := testutil.UniqueId()
- t.Cleanup(func() {
- testutil.CleanTableByField(ctx, conn, "`sys_user`", "username", username)
- })
- var wg sync.WaitGroup
- results := make(chan error, 2)
- for i := 0; i < 2; i++ {
- wg.Add(1)
- go func() {
- defer wg.Done()
- logic := NewCreateUserLogic(ctx, svcCtx)
- _, err := logic.CreateUser(&types.CreateUserReq{
- Username: username,
- Password: "Pass123456",
- Nickname: "并发测试用户",
- })
- results <- err
- }()
- }
- wg.Wait()
- close(results)
- var errs []error
- for err := range results {
- errs = append(errs, err)
- }
- require.Len(t, errs, 2)
- successCount := 0
- failCount := 0
- for _, err := range errs {
- if err == nil {
- successCount++
- } else {
- failCount++
- var codeErr *response.CodeError
- require.True(t, errors.As(err, &codeErr), "error should be CodeError, got: %v", err)
- assert.Equal(t, 409, codeErr.Code())
- assert.Equal(t, "用户名已存在", codeErr.Error())
- }
- }
- assert.Equal(t, 1, successCount)
- assert.Equal(t, 1, failCount)
- }
- // TC-0141: 合法phone(国际)
- func TestCreateUser_ValidInternationalPhone(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- username := testutil.UniqueId()
- logic := NewCreateUserLogic(ctx, svcCtx)
- resp, err := logic.CreateUser(&types.CreateUserReq{
- Username: username,
- Password: "Pass123456",
- Phone: "+8613800138000",
- })
- require.NoError(t, err)
- require.NotNil(t, resp)
- t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", resp.Id) })
- user, err := svcCtx.SysUserModel.FindOne(ctx, resp.Id)
- require.NoError(t, err)
- assert.Equal(t, "+8613800138000", user.Phone)
- }
- // TC-0145: 密码少于8字符
- func TestCreateUser_PasswordTooShort(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- logic := NewCreateUserLogic(ctx, svcCtx)
- _, err := logic.CreateUser(&types.CreateUserReq{
- Username: testutil.UniqueId(),
- Password: "Pas1234",
- })
- require.Error(t, err)
- var codeErr *response.CodeError
- require.True(t, errors.As(err, &codeErr))
- assert.Equal(t, 400, codeErr.Code())
- assert.Equal(t, "密码长度不能少于8个字符", codeErr.Error())
- }
- // TC-0146: 密码缺少大写字母
- func TestCreateUser_PasswordNoUppercase(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- logic := NewCreateUserLogic(ctx, svcCtx)
- longPwd := "A" + strings.Repeat("a", 71) + "1"
- _, err := logic.CreateUser(&types.CreateUserReq{
- Username: testutil.UniqueId(),
- Password: longPwd,
- })
- require.Error(t, err)
- var codeErr *response.CodeError
- require.True(t, errors.As(err, &codeErr))
- assert.Equal(t, 400, codeErr.Code())
- assert.Equal(t, "密码长度不能超过72个字符", codeErr.Error())
- }
- // TC-0147: 密码缺少小写字母
- func TestCreateUser_PasswordNoLowercase(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- logic := NewCreateUserLogic(ctx, svcCtx)
- _, err := logic.CreateUser(&types.CreateUserReq{
- Username: testutil.UniqueId(),
- Password: "PASS123456",
- })
- 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-0148: 密码缺少数字
- func TestCreateUser_PasswordNoDigit(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- logic := NewCreateUserLogic(ctx, svcCtx)
- _, err := logic.CreateUser(&types.CreateUserReq{
- Username: testutil.UniqueId(),
- Password: "Passpasspass",
- })
- 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-0149: 密码超过72字符
- func TestCreateUser_PasswordTooLong(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- longPwd := strings.Repeat("a", 73)
- logic := NewCreateUserLogic(ctx, svcCtx)
- _, err := logic.CreateUser(&types.CreateUserReq{
- Username: testutil.UniqueId(),
- Password: longPwd,
- })
- require.Error(t, err)
- var codeErr *response.CodeError
- require.True(t, errors.As(err, &codeErr))
- assert.Equal(t, 400, codeErr.Code())
- assert.Equal(t, "密码长度不能超过72个字符", codeErr.Error())
- }
- // TC-0537: createUser非管理员拒绝
- func TestCreateUser_MemberRejected(t *testing.T) {
- ctx := ctxhelper.MemberCtx("test_product")
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- logic := NewCreateUserLogic(ctx, svcCtx)
- _, err := logic.CreateUser(&types.CreateUserReq{Username: "test", Password: "Pass123456"})
- require.Error(t, err)
- var ce *response.CodeError
- require.True(t, errors.As(err, &ce))
- assert.Equal(t, 403, ce.Code())
- }
- // TC-0150: 用户名含特殊字符被拒绝
- func TestCreateUser_UsernameInvalidChars(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- logic := NewCreateUserLogic(ctx, svcCtx)
- _, err := logic.CreateUser(&types.CreateUserReq{
- Username: "user@name!",
- Password: "Pass123456",
- })
- require.Error(t, err)
- var codeErr *response.CodeError
- require.True(t, errors.As(err, &codeErr))
- assert.Equal(t, 400, codeErr.Code())
- assert.Equal(t, "用户名只能包含字母、数字和下划线,长度2-64个字符", codeErr.Error())
- }
- // TC-0151: 用户名太短(1字符)被拒绝
- func TestCreateUser_UsernameTooShort(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- logic := NewCreateUserLogic(ctx, svcCtx)
- _, err := logic.CreateUser(&types.CreateUserReq{
- Username: "a",
- Password: "Pass123456",
- })
- require.Error(t, err)
- var codeErr *response.CodeError
- require.True(t, errors.As(err, &codeErr))
- assert.Equal(t, 400, codeErr.Code())
- assert.Equal(t, "用户名只能包含字母、数字和下划线,长度2-64个字符", codeErr.Error())
- }
- // TC-0152: 用户名太长(65字符)被拒绝
- func TestCreateUser_UsernameTooLong(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- logic := NewCreateUserLogic(ctx, svcCtx)
- _, err := logic.CreateUser(&types.CreateUserReq{
- Username: strings.Repeat("a", 65),
- Password: "Pass123456",
- })
- require.Error(t, err)
- var codeErr *response.CodeError
- require.True(t, errors.As(err, &codeErr))
- assert.Equal(t, 400, codeErr.Code())
- assert.Equal(t, "用户名只能包含字母、数字和下划线,长度2-64个字符", codeErr.Error())
- }
- // TC-0153: 部门不存在被拒绝
- func TestCreateUser_DeptNotExists(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- logic := NewCreateUserLogic(ctx, svcCtx)
- _, err := logic.CreateUser(&types.CreateUserReq{
- Username: testutil.UniqueId(),
- Password: "Pass123456",
- DeptId: 999999999,
- })
- 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-0154: 昵称超过64字符被拒绝
- func TestCreateUser_NicknameTooLong(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- logic := NewCreateUserLogic(ctx, svcCtx)
- _, err := logic.CreateUser(&types.CreateUserReq{
- Username: testutil.UniqueId(),
- Password: "Pass123456",
- Nickname: strings.Repeat("n", 65),
- })
- require.Error(t, err)
- var codeErr *response.CodeError
- require.True(t, errors.As(err, &codeErr))
- assert.Equal(t, 400, codeErr.Code())
- assert.Equal(t, "昵称长度不能超过64个字符", codeErr.Error())
- }
- // TC-0155: 备注超过255字符被拒绝
- func TestCreateUser_RemarkTooLong(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- logic := NewCreateUserLogic(ctx, svcCtx)
- _, err := logic.CreateUser(&types.CreateUserReq{
- Username: testutil.UniqueId(),
- Password: "Pass123456",
- Remark: strings.Repeat("r", 256),
- })
- require.Error(t, err)
- var codeErr *response.CodeError
- require.True(t, errors.As(err, &codeErr))
- assert.Equal(t, 400, codeErr.Code())
- assert.Equal(t, "备注长度不能超过255个字符", codeErr.Error())
- }
- // TC-0136: 带完整可选字段
- func TestCreateUser_AllOptionalFields(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- now := time.Now().Unix()
- deptRes, err := svcCtx.SysDeptModel.Insert(ctx, &deptModel.SysDept{
- ParentId: 0,
- Name: "tc0103_dept_" + testutil.UniqueId(),
- Path: "/",
- Sort: 1,
- DeptType: "NORMAL",
- Remark: "",
- Status: 1,
- CreateTime: now,
- UpdateTime: now,
- })
- require.NoError(t, err)
- deptId, err := deptRes.LastInsertId()
- require.NoError(t, err)
- t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_dept`", deptId) })
- username := testutil.UniqueId()
- logic := NewCreateUserLogic(ctx, svcCtx)
- resp, err := logic.CreateUser(&types.CreateUserReq{
- Username: username,
- Password: "Pass123456",
- Nickname: "全字段用户",
- Email: username + "@example.com",
- Phone: "13900001111",
- Remark: "TC-0136完整字段",
- DeptId: deptId,
- })
- require.NoError(t, err)
- require.NotNil(t, resp)
- assert.Greater(t, resp.Id, int64(0))
- t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", resp.Id) })
- user, err := svcCtx.SysUserModel.FindOne(ctx, resp.Id)
- require.NoError(t, err)
- assert.Equal(t, username, user.Username)
- assert.Equal(t, "全字段用户", user.Nickname)
- assert.Equal(t, username+"@example.com", user.Email)
- assert.Equal(t, "13900001111", user.Phone)
- assert.Equal(t, "TC-0136完整字段", user.Remark)
- assert.Equal(t, deptId, user.DeptId)
- assert.Equal(t, int64(1), user.Status)
- assert.Equal(t, int64(2), user.IsSuperAdmin)
- }
- 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: 产品 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(),
- "产品 ADMIN 跨部门树创建用户必须 403,防止占用关键用户名等 AddMember 合谋挂进产品")
- assert.Contains(t, ce.Error(), "无权在非自己管辖的部门下创建用户")
- }
- // TC-0995: 正向 —— 产品 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: 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: 非超管 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(),
- "caller.DeptPath=='' 属于 legacy 账号,fail-close 不允许创建用户")
- assert.Contains(t, ce.Error(), "您未归属任何部门")
- }
- // TC-0998: 非超管 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(),
- "非超管 CreateUser 必须显式指定部门,禁止在部门树外创建账号")
- assert.Contains(t, ce.Error(), "必须指定部门")
- }
- // TC-0999: 目标部门已停用时 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)
- })
- // 超管也必须被拒绝: 的规则针对"所有调用方",防止 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(),
- "目标部门 status!=Enabled 必须拒绝,与 UpdateDept 禁用语义闭环")
- }
- // TC-1100: DeptId 负值(-1 / MinInt64)在所有类型 caller 面前都必须被 400 拒绝,
- // 禁止构造 "sys_user.deptId = 负数" 的僵尸账号(FindOne 永远 NotFound,部门树永远查不到)。
- func TestCreateUser_NegativeDeptIdRejected(t *testing.T) {
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- cases := []struct {
- name string
- ctx context.Context
- deptId int64
- wantMsg string
- }{
- {"super_admin_negative_one", ctxhelper.SuperAdminCtx(), -1, "部门ID必须为非负整数"},
- {"super_admin_min_int64", ctxhelper.SuperAdminCtx(), math.MinInt64, "部门ID必须为非负整数"},
- }
- for _, tc := range cases {
- tc := tc
- t.Run(tc.name, func(t *testing.T) {
- _, err := NewCreateUserLogic(tc.ctx, svcCtx).CreateUser(&types.CreateUserReq{
- Username: "neg_dept_" + testutil.UniqueId(),
- Password: "Pass123456",
- DeptId: tc.deptId,
- })
- require.Error(t, err, "负值 DeptId 必须被拒绝")
- var ce *response.CodeError
- require.True(t, errors.As(err, &ce))
- assert.Equal(t, 400, ce.Code(),
- "必须 400 而不是 5xx / 404:让调用方知道是入参不合法,不是 DB/权限问题")
- assert.Equal(t, tc.wantMsg, ce.Error())
- })
- }
- }
- func TestCreateUser_DefaultsMustChangePasswordToYes(t *testing.T) {
- ctx := ctxhelper.SuperAdminCtx()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- username := "lcp_" + testutil.UniqueId()
- resp, err := NewCreateUserLogic(ctx, svcCtx).CreateUser(&types.CreateUserReq{
- Username: username,
- Password: "InitPass@123",
- Nickname: "初始口令校验",
- })
- require.NoError(t, err)
- require.NotNil(t, resp)
- t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", resp.Id) })
- u, err := svcCtx.SysUserModel.FindOne(ctx, resp.Id)
- require.NoError(t, err)
- assert.Equal(t, int64(consts.MustChangePasswordYes), u.MustChangePassword,
- "管理员代填初始密码的用户必须被强制下次登录改密,落盘为 Yes")
- }
- // TC-1122: H-R14-1 —— 非超管调用方在 CreateUser 中指定 DEV 部门必须 403。
- //
- // 与 UpdateUser 的 TC-1124 对偶:CreateUser 侧通过"预埋 admin_* 键位账号"也能构成跨产品
- // 升级链路(若 ADMIN 能在 DEV 部门直接创建账号,新账号一旦被 AddMember 进别的产品,立刻
- // 在新产品内全权),因此收敛口径必须与 UpdateUser 完全一致。
- //
- // 注意:这里 DeptPath 特意让 adminPath 恰好是 devDept 的祖先,排除"被 DeptPath 前缀校验
- // 顺手拦掉"的干扰——本用例必须靠 DeptType==DEV 这条显式护栏拦住,才能证明独立生效。
- func TestCreateUser_H_R14_1_AdminCannotCreateInDevDept(t *testing.T) {
- bootstrap := context.Background()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- adminDeptId := insertTestDeptWithType(t, bootstrap, svcCtx, "h_r14_1_c_admin", "/9100/", consts.DeptTypeNormal)
- devDeptId := insertTestDeptWithType(t, bootstrap, svcCtx, "h_r14_1_c_dev", "/9100/1/", consts.DeptTypeDev)
- t.Cleanup(func() {
- testutil.CleanTable(bootstrap, conn, "`sys_dept`", adminDeptId, devDeptId)
- })
- adminCtx := callerAdminCtx(888881, adminDeptId, "/9100/")
- _, err := NewCreateUserLogic(adminCtx, svcCtx).CreateUser(&types.CreateUserReq{
- Username: "h_r14_1_c_" + testutil.UniqueId(),
- Password: "Pass123456",
- DeptId: devDeptId,
- })
- require.Error(t, err, "ADMIN 在 DEV 部门创建用户必须被拒绝")
- var ce *response.CodeError
- require.True(t, errors.As(err, &ce))
- assert.Equal(t, 403, ce.Code(),
- "H-R14-1 创建侧必须 403,防止'先创建后挂载'的跨产品权限升级链路")
- assert.Contains(t, ce.Error(), "仅超级管理员可将用户调入研发部门",
- "错误文案须与 UpdateUser 一致,方便 SRE/审计从单一关键字聚合所有 DEV 护栏告警")
- // 关键:403 发生在 Insert 之前,sys_user 表不得被污染。由于入参 username 每次 UniqueId,
- // 这里不额外查 DB 来验证——断言 403 + req.Username 随机 + t.Cleanup 里也没 LastInsertId
- // 可删,就隐式证明了"零 DB 副作用"。
- }
- // TC-1123: H-R14-1 对偶正向 —— SuperAdmin 在 DEV 部门下创建用户必须成功。
- // 确保护栏只拦非超管;不得误伤 SuperAdmin 的合法运维动作。
- func TestCreateUser_H_R14_1_SuperAdminCanCreateInDevDept(t *testing.T) {
- bootstrap := context.Background()
- svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
- conn := testutil.GetTestSqlConn()
- devDeptId := insertTestDeptWithType(t, bootstrap, svcCtx, "h_r14_1_c_su_dev", "/9200/", consts.DeptTypeDev)
- t.Cleanup(func() {
- testutil.CleanTable(bootstrap, conn, "`sys_dept`", devDeptId)
- })
- superCtx := ctxhelper.SuperAdminCtx()
- username := "h_r14_1_c_su_" + testutil.UniqueId()
- resp, err := NewCreateUserLogic(superCtx, svcCtx).CreateUser(&types.CreateUserReq{
- Username: username,
- Password: "Pass123456",
- DeptId: devDeptId,
- })
- require.NoError(t, err, "SuperAdmin 在 DEV 部门创建用户必须允许")
- require.NotNil(t, resp)
- t.Cleanup(func() { testutil.CleanTable(bootstrap, conn, "`sys_user`", resp.Id) })
- u, err := svcCtx.SysUserModel.FindOne(bootstrap, resp.Id)
- require.NoError(t, err)
- assert.Equal(t, devDeptId, u.DeptId,
- "SuperAdmin 路径必须真的把 DeptId 写到 DEV,证明护栏未误伤 SuperAdmin 的合法运维链路")
- }
|