package user import ( "errors" "sync" "sync/atomic" "testing" "perms-system-server/internal/consts" 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" "perms-system-server/internal/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // TC-1285 超管重置普通用户密码 func TestResetPassword_SuperAdmin(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() hashed := testutil.HashPassword("OldPass123") userId := insertTestUser(t, ctx, username, hashed) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) logic := NewResetPasswordLogic(ctx, svcCtx) resp, err := logic.ResetPassword(&types.ResetPasswordReq{UserId: userId}) require.NoError(t, err) require.NotNil(t, resp) assert.NotEmpty(t, resp.CredentialsTicket) assert.Greater(t, resp.CredentialsExpiresAt, int64(0)) // 清理 Redis ticket t.Cleanup(func() { svcCtx.Redis.DelCtx(ctx, userCredentialsKeyPrefix+resp.CredentialsTicket) }) } // TC-1287 不能重置超管密码 func TestResetPassword_CannotResetSuperAdmin(t *testing.T) { ctx := ctxhelper.SuperAdminCtxWithUserId(999) svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() hashed := testutil.HashPassword("Pass123456") superUserId := insertTestUserFull(t, ctx, &userModel.SysUser{ Username: username, Password: hashed, IsSuperAdmin: consts.IsSuperAdminYes, Status: consts.StatusEnabled, MustChangePassword: consts.MustChangePasswordNo, }) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", superUserId) }) logic := NewResetPasswordLogic(ctx, svcCtx) _, err := logic.ResetPassword(&types.ResetPasswordReq{UserId: superUserId}) require.Error(t, err) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr)) assert.Equal(t, 403, codeErr.Code()) assert.Contains(t, codeErr.Error(), "超级管理员") } // TC-1288 MEMBER无权重置 func TestResetPassword_MemberForbidden(t *testing.T) { ctx := ctxhelper.MemberCtx("test-product") svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) logic := NewResetPasswordLogic(ctx, svcCtx) _, err := logic.ResetPassword(&types.ResetPasswordReq{UserId: 1}) require.Error(t, err) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr)) assert.Equal(t, 403, codeErr.Code()) } // TC-1289 重置后旧token失效(tokenVersion递增) func TestResetPassword_TokenVersionIncremented(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() hashed := testutil.HashPassword("OldPass123") userId := insertTestUser(t, ctx, username, hashed) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) userBefore, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) logic := NewResetPasswordLogic(ctx, svcCtx) resp, err := logic.ResetPassword(&types.ResetPasswordReq{UserId: userId}) require.NoError(t, err) t.Cleanup(func() { svcCtx.Redis.DelCtx(ctx, userCredentialsKeyPrefix+resp.CredentialsTicket) }) userAfter, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) assert.Equal(t, userBefore.TokenVersion+1, userAfter.TokenVersion) } // TC-1290 重置后mustChangePassword=1 func TestResetPassword_MustChangePassword(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() hashed := testutil.HashPassword("OldPass123") userId := insertTestUserFull(t, ctx, &userModel.SysUser{ Username: username, Password: hashed, IsSuperAdmin: consts.IsSuperAdminNo, Status: consts.StatusEnabled, MustChangePassword: consts.MustChangePasswordNo, }) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) logic := NewResetPasswordLogic(ctx, svcCtx) resp, err := logic.ResetPassword(&types.ResetPasswordReq{UserId: userId}) require.NoError(t, err) t.Cleanup(func() { svcCtx.Redis.DelCtx(ctx, userCredentialsKeyPrefix+resp.CredentialsTicket) }) user, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) assert.Equal(t, int64(consts.MustChangePasswordYes), user.MustChangePassword) } // TC-1291 目标用户不存在 func TestResetPassword_UserNotFound(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) logic := NewResetPasswordLogic(ctx, svcCtx) _, err := logic.ResetPassword(&types.ResetPasswordReq{UserId: 999999999}) require.Error(t, err) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr)) assert.Equal(t, 404, codeErr.Code()) } // TC-1293 重置后用新密码可验证 func TestResetPassword_NewPasswordValid(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() hashed := testutil.HashPassword("OldPass123") userId := insertTestUser(t, ctx, username, hashed) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) logic := NewResetPasswordLogic(ctx, svcCtx) resp, err := logic.ResetPassword(&types.ResetPasswordReq{UserId: userId}) require.NoError(t, err) t.Cleanup(func() { svcCtx.Redis.DelCtx(ctx, userCredentialsKeyPrefix+resp.CredentialsTicket) }) fetchLogic := NewFetchUserCredentialsLogic(ctx, svcCtx) cred, err := fetchLogic.FetchUserCredentials(&types.FetchUserCredentialsReq{Ticket: resp.CredentialsTicket}) require.NoError(t, err) assert.Equal(t, username, cred.Username) assert.NotEmpty(t, cred.Password) msg := util.ValidatePassword(cred.Password) assert.Empty(t, msg) } // TC-1294 正常消费ticket func TestFetchUserCredentials_Success(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() createLogic := NewCreateUserLogic(ctx, svcCtx) createResp, err := createLogic.CreateUser(&types.CreateUserReq{ Username: username, DeptId: 0, }) require.NoError(t, err) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", createResp.Id) }) fetchLogic := NewFetchUserCredentialsLogic(ctx, svcCtx) cred, err := fetchLogic.FetchUserCredentials(&types.FetchUserCredentialsReq{Ticket: createResp.CredentialsTicket}) require.NoError(t, err) assert.Equal(t, username, cred.Username) assert.NotEmpty(t, cred.Password) } // TC-1295 二次消费同一ticket func TestFetchUserCredentials_ConsumedTwice(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() createLogic := NewCreateUserLogic(ctx, svcCtx) createResp, err := createLogic.CreateUser(&types.CreateUserReq{ Username: username, DeptId: 0, }) require.NoError(t, err) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", createResp.Id) }) fetchLogic := NewFetchUserCredentialsLogic(ctx, svcCtx) _, err = fetchLogic.FetchUserCredentials(&types.FetchUserCredentialsReq{Ticket: createResp.CredentialsTicket}) require.NoError(t, err) _, err = fetchLogic.FetchUserCredentials(&types.FetchUserCredentialsReq{Ticket: createResp.CredentialsTicket}) 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-1296 空ticket func TestFetchUserCredentials_EmptyTicket(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) fetchLogic := NewFetchUserCredentialsLogic(ctx, svcCtx) _, err := fetchLogic.FetchUserCredentials(&types.FetchUserCredentialsReq{Ticket: ""}) 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(), "ticket 不能为空") } // TC-1297 非法ticket func TestFetchUserCredentials_InvalidTicket(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) fetchLogic := NewFetchUserCredentialsLogic(ctx, svcCtx) _, err := fetchLogic.FetchUserCredentials(&types.FetchUserCredentialsReq{Ticket: "random_garbage_ticket_value"}) 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-1298 非ADMIN无权消费 func TestFetchUserCredentials_MemberForbidden(t *testing.T) { ctx := ctxhelper.MemberCtx("test-product") svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) fetchLogic := NewFetchUserCredentialsLogic(ctx, svcCtx) _, err := fetchLogic.FetchUserCredentials(&types.FetchUserCredentialsReq{Ticket: "any_ticket"}) require.Error(t, err) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr)) assert.Equal(t, 403, codeErr.Code()) } // TC-1299 并发消费同一ticket func TestFetchUserCredentials_ConcurrentConsume(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() createLogic := NewCreateUserLogic(ctx, svcCtx) createResp, err := createLogic.CreateUser(&types.CreateUserReq{ Username: username, DeptId: 0, }) require.NoError(t, err) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", createResp.Id) }) var successCount atomic.Int32 var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() fetchLogic := NewFetchUserCredentialsLogic(ctx, svcCtx) _, ferr := fetchLogic.FetchUserCredentials(&types.FetchUserCredentialsReq{Ticket: createResp.CredentialsTicket}) if ferr == nil { successCount.Add(1) } }() } wg.Wait() assert.Equal(t, int32(1), successCount.Load()) } // TC-1280 正常创建用户返回ticket func TestCreateUser_TicketFlow(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, Nickname: "测试用户", DeptId: 0, }) require.NoError(t, err) require.NotNil(t, resp) assert.Greater(t, resp.Id, int64(0)) assert.NotEmpty(t, resp.CredentialsTicket) assert.Greater(t, resp.CredentialsExpiresAt, int64(0)) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", resp.Id) }) } // TC-1282 生成密码满足强度要求 func TestCreateUser_PasswordStrength(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() createLogic := NewCreateUserLogic(ctx, svcCtx) createResp, err := createLogic.CreateUser(&types.CreateUserReq{ Username: username, DeptId: 0, }) require.NoError(t, err) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", createResp.Id) }) fetchLogic := NewFetchUserCredentialsLogic(ctx, svcCtx) cred, err := fetchLogic.FetchUserCredentials(&types.FetchUserCredentialsReq{Ticket: createResp.CredentialsTicket}) require.NoError(t, err) msg := util.ValidatePassword(cred.Password) assert.Empty(t, msg, "generated password should pass strength validation") } // TC-1284 创建后mustChangePassword=1 func TestCreateUser_MustChangePassword(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, DeptId: 0, }) require.NoError(t, err) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", resp.Id) svcCtx.Redis.DelCtx(ctx, userCredentialsKeyPrefix+resp.CredentialsTicket) }) user, err := svcCtx.SysUserModel.FindOne(ctx, resp.Id) require.NoError(t, err) assert.Equal(t, int64(consts.MustChangePasswordYes), user.MustChangePassword) }