topfans/backend/services/userService/service/user_service_password_test.go
2026-06-15 14:15:24 +08:00

274 lines
8.0 KiB
Go

package service
import (
"context"
"errors"
"os"
"testing"
"github.com/topfans/backend/pkg/database"
appErrors "github.com/topfans/backend/pkg/errors"
"github.com/topfans/backend/pkg/models"
pb "github.com/topfans/backend/pkg/proto/user"
"github.com/topfans/backend/services/userService/repository"
"gorm.io/gorm"
)
// Test #1: 正常路径 — 有效 token + 正确 old + 有效 new
func TestUpdatePassword_Success(t *testing.T) {
skipIfNoTestEnv(t)
db := setupTestDB(t)
defer cleanupTestDB(t, db)
userRepo := repository.NewUserRepository()
user := createTestUser(t, db, userRepo, "13800001001")
defer deleteTestUser(t, db, userRepo, user.ID)
// 直接保存 verify_token 到 Redis(scene=password)
if err := SaveVerifyToken(context.Background(), "password", user.Mobile, "valid_token", 300); err != nil {
t.Skipf("Redis unavailable: %v", err)
}
svc := &userService{userRepo: userRepo, db: db}
req := &pb.UpdatePasswordRequest{
OldPassword: "password123",
NewPassword: "newpassword456",
VerifyToken: "valid_token",
}
resp, err := svc.UpdatePassword(context.Background(), req, user.ID)
if err != nil {
t.Fatalf("UpdatePassword failed: %v", err)
}
if resp == nil {
t.Fatal("expected non-nil response")
}
// 验证密码已更新
updated, _ := userRepo.GetByID(user.ID)
if !userRepo.VerifyPassword(updated, "newpassword456") {
t.Error("password not updated")
}
// 验证 token 已被 ConsumeVerifyToken 删除(Plan A)
stored, _ := GetVerifyToken(context.Background(), "password", user.Mobile)
if stored != "" {
t.Error("verify_token should be consumed after success")
}
}
// Test #2: verify_token 缺失
func TestUpdatePassword_VerifyTokenEmpty(t *testing.T) {
skipIfNoTestEnv(t)
db := setupTestDB(t)
defer cleanupTestDB(t, db)
userRepo := repository.NewUserRepository()
user := createTestUser(t, db, userRepo, "13800001002")
defer deleteTestUser(t, db, userRepo, user.ID)
svc := &userService{userRepo: userRepo, db: db}
req := &pb.UpdatePasswordRequest{
OldPassword: "password123",
NewPassword: "newpassword456",
VerifyToken: "",
}
_, err := svc.UpdatePassword(context.Background(), req, user.ID)
if !errors.Is(err, appErrors.ErrInvalidVerifyToken) {
t.Errorf("expected ErrInvalidVerifyToken, got %v", err)
}
}
// Test #3: verify_token 错误
func TestUpdatePassword_VerifyTokenWrong(t *testing.T) {
skipIfNoTestEnv(t)
db := setupTestDB(t)
defer cleanupTestDB(t, db)
userRepo := repository.NewUserRepository()
user := createTestUser(t, db, userRepo, "13800001003")
defer deleteTestUser(t, db, userRepo, user.ID)
// 保存的 token 与请求的不同
if err := SaveVerifyToken(context.Background(), "password", user.Mobile, "correct_token", 300); err != nil {
t.Skipf("Redis unavailable: %v", err)
}
svc := &userService{userRepo: userRepo, db: db}
req := &pb.UpdatePasswordRequest{
OldPassword: "password123",
NewPassword: "newpassword456",
VerifyToken: "wrong_token",
}
_, err := svc.UpdatePassword(context.Background(), req, user.ID)
if !errors.Is(err, appErrors.ErrInvalidVerifyToken) {
t.Errorf("expected ErrInvalidVerifyToken, got %v", err)
}
}
// Test #5: 旧密码错误
func TestUpdatePassword_WrongOldPassword(t *testing.T) {
skipIfNoTestEnv(t)
db := setupTestDB(t)
defer cleanupTestDB(t, db)
userRepo := repository.NewUserRepository()
user := createTestUser(t, db, userRepo, "13800001005")
defer deleteTestUser(t, db, userRepo, user.ID)
if err := SaveVerifyToken(context.Background(), "password", user.Mobile, "valid_token", 300); err != nil {
t.Skipf("Redis unavailable: %v", err)
}
svc := &userService{userRepo: userRepo, db: db}
req := &pb.UpdatePasswordRequest{
OldPassword: "wrong_old_password",
NewPassword: "newpassword456",
VerifyToken: "valid_token",
}
_, err := svc.UpdatePassword(context.Background(), req, user.ID)
if !errors.Is(err, appErrors.ErrInvalidOldPassword) {
t.Errorf("expected ErrInvalidOldPassword, got %v", err)
}
// Plan A: 旧密码错时 token 仍保留
stored, _ := GetVerifyToken(context.Background(), "password", user.Mobile)
if stored != "valid_token" {
t.Error("verify_token should be retained after business failure")
}
}
// Test #7: 新旧密码相同
func TestUpdatePassword_SameAsOld(t *testing.T) {
skipIfNoTestEnv(t)
db := setupTestDB(t)
defer cleanupTestDB(t, db)
userRepo := repository.NewUserRepository()
user := createTestUser(t, db, userRepo, "13800001007")
defer deleteTestUser(t, db, userRepo, user.ID)
if err := SaveVerifyToken(context.Background(), "password", user.Mobile, "valid_token", 300); err != nil {
t.Skipf("Redis unavailable: %v", err)
}
svc := &userService{userRepo: userRepo, db: db}
req := &pb.UpdatePasswordRequest{
OldPassword: "password123",
NewPassword: "password123",
VerifyToken: "valid_token",
}
_, err := svc.UpdatePassword(context.Background(), req, user.ID)
if !errors.Is(err, appErrors.ErrSameAsOldPassword) {
t.Errorf("expected ErrSameAsOldPassword, got %v", err)
}
}
// Test #11 (Plan A): 旧密码错误后 token 保留可重试
func TestUpdatePassword_PlanA_RetryAfterWrongPassword(t *testing.T) {
skipIfNoTestEnv(t)
db := setupTestDB(t)
defer cleanupTestDB(t, db)
userRepo := repository.NewUserRepository()
user := createTestUser(t, db, userRepo, "13800001011")
defer deleteTestUser(t, db, userRepo, user.ID)
if err := SaveVerifyToken(context.Background(), "password", user.Mobile, "shared_token", 300); err != nil {
t.Skipf("Redis unavailable: %v", err)
}
svc := &userService{userRepo: userRepo, db: db}
// 第一次:旧密码错
_, err1 := svc.UpdatePassword(context.Background(), &pb.UpdatePasswordRequest{
OldPassword: "wrong_old",
NewPassword: "newpassword456",
VerifyToken: "shared_token",
}, user.ID)
if !errors.Is(err1, appErrors.ErrInvalidOldPassword) {
t.Errorf("first call: expected ErrInvalidOldPassword, got %v", err1)
}
// token 仍在
stored, _ := GetVerifyToken(context.Background(), "password", user.Mobile)
if stored != "shared_token" {
t.Fatal("token should still be in Redis after business failure")
}
// 第二次:用同一 token + 正确旧密码 → 成功(Plan A 的核心收益)
_, err2 := svc.UpdatePassword(context.Background(), &pb.UpdatePasswordRequest{
OldPassword: "password123",
NewPassword: "newpassword456",
VerifyToken: "shared_token",
}, user.ID)
if err2 != nil {
t.Fatalf("retry: expected success, got %v", err2)
}
// token 已被 Consume
stored2, _ := GetVerifyToken(context.Background(), "password", user.Mobile)
if stored2 != "" {
t.Error("token should be consumed after successful retry")
}
}
// ==================== Test helpers ====================
func skipIfNoTestEnv(t *testing.T) {
if os.Getenv("SKIP_DB_TESTS") != "" {
t.Skip("SKIP_DB_TESTS set")
}
}
func setupTestDB(t *testing.T) *gorm.DB {
config := database.Config{
Host: "localhost",
Port: 5432,
User: getEnvOrDefault("TEST_DB_USER", "haihuizhu"),
Password: getEnvOrDefault("TEST_DB_PASSWORD", "admin"),
DBName: getEnvOrDefault("TEST_DB_NAME", "top-fans"),
SSLMode: "disable",
TimeZone: "Asia/Shanghai",
}
if err := database.Init(config); err != nil {
t.Skipf("Skipping: failed to connect to test database: %v", err)
}
return database.GetDB()
}
func cleanupTestDB(_ *testing.T, db *gorm.DB) {
db.Exec("DELETE FROM fan_profiles WHERE user_id IN (SELECT id FROM users WHERE mobile LIKE '13800001%')")
db.Exec("DELETE FROM users WHERE mobile LIKE '13800001%'")
}
func createTestUser(t *testing.T, _ *gorm.DB, repo repository.UserRepository, mobile string) *models.User {
hash, err := repository.HashPassword("password123")
if err != nil {
t.Fatalf("HashPassword: %v", err)
}
user := &models.User{
Mobile: mobile,
PasswordHash: hash,
IsActive: true,
}
if err := repo.Create(user); err != nil {
t.Fatalf("Create user: %v", err)
}
return user
}
func deleteTestUser(_ *testing.T, db *gorm.DB, _ repository.UserRepository, userID int64) {
db.Exec("DELETE FROM users WHERE id = ?", userID)
}
func getEnvOrDefault(key, defaultVal string) string {
if v := os.Getenv(key); v != "" {
return v
}
return defaultVal
}