274 lines
8.0 KiB
Go
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
|
|
} |