package user import ( "context" "database/sql" "errors" "strings" "sync" "testing" "time" 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" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) 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-0122: 正常创建 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-0123: 用户名已存在(预检) 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: "pass456", }) 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-0125: 非法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: "pass123", 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-0126: 合法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: "pass123", 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-0127: 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: "pass123", 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-0128: 非法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: "pass123", 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-0129: 合法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: "pass123", 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-0130: 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: "pass123", 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-0131: 并发同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-0129: 合法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: "pass123", 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-0133: 密码少于6字符 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: "12345", }) require.Error(t, err) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr)) assert.Equal(t, 400, codeErr.Code()) assert.Equal(t, "密码长度不能少于6个字符", codeErr.Error()) } // TC-0134: 密码超过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-0516: 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: "pass123"}) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 403, ce.Code()) } // TC-0546: 用户名含特殊字符被拒绝 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-0547: 用户名太短(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-0548: 用户名太长(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-0549: 部门不存在被拒绝 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-0550: 昵称超过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-0551: 备注超过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-0124: 带完整可选字段 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-0124完整字段", 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-0124完整字段", user.Remark) assert.Equal(t, deptId, user.DeptId) assert.Equal(t, int64(1), user.Status) assert.Equal(t, int64(2), user.IsSuperAdmin) }