package service import ( "context" "errors" "testing" "time" membershipmodel "wx_service/internal/membership/model" usermodel "wx_service/internal/model" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" ) func setupRedeemTestDB(t *testing.T) *gorm.DB { t.Helper() db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { t.Fatalf("open sqlite: %v", err) } if err := db.AutoMigrate( &membershipmodel.MembershipRedeemCode{}, &membershipmodel.MembershipRedemption{}, &usermodel.UserMembership{}, ); err != nil { t.Fatalf("auto migrate: %v", err) } return db } func seedRedeemCode(t *testing.T, db *gorm.DB, plainCode string, durationDays int, expiresAt *time.Time, maxUses int, status string) membershipmodel.MembershipRedeemCode { t.Helper() code := normalizeCode(plainCode) record := membershipmodel.MembershipRedeemCode{ CodeHash: hashCode(code), CodeSuffix: suffixOf(code, 6), Plan: "month", DurationDays: durationDays, ExpiresAt: expiresAt, MaxUses: maxUses, UsedUses: 0, Status: status, } if err := db.Create(&record).Error; err != nil { t.Fatalf("seed redeem code: %v", err) } return record } func TestRedeemCodeServiceRedeemSuccessAndRepeat(t *testing.T) { t.Parallel() db := setupRedeemTestDB(t) svc := NewRedeemCodeService(db, "") user := &usermodel.User{ID: 1, MiniProgramID: 10} code := "TEST-CODE-001" seedRedeemCode(t, db, code, 30, nil, 1, "active") res, err := svc.Redeem(context.Background(), user, code, "127.0.0.1", "ut") if err != nil { t.Fatalf("redeem success: %v", err) } if res == nil || res.Extended { t.Fatalf("first redeem should create membership and extended=false") } var membership usermodel.UserMembership if err := db.Where("mini_program_id = ? AND user_id = ?", user.MiniProgramID, user.ID).First(&membership).Error; err != nil { t.Fatalf("query membership: %v", err) } if membership.Status != "active" { t.Fatalf("membership status=%s, want=active", membership.Status) } var storedCode membershipmodel.MembershipRedeemCode if err := db.Where("code_hash = ?", hashCode(normalizeCode(code))).First(&storedCode).Error; err != nil { t.Fatalf("query redeem code: %v", err) } if storedCode.UsedUses != 1 { t.Fatalf("used_uses=%d, want=1", storedCode.UsedUses) } _, err = svc.Redeem(context.Background(), user, code, "127.0.0.1", "ut") if !errors.Is(err, ErrRedeemCodeUsedUp) { t.Fatalf("repeat redeem err=%v, want ErrRedeemCodeUsedUp", err) } } func TestRedeemCodeServiceRedeemExpiredCode(t *testing.T) { t.Parallel() db := setupRedeemTestDB(t) svc := NewRedeemCodeService(db, "") user := &usermodel.User{ID: 2, MiniProgramID: 10} expired := time.Now().Add(-1 * time.Hour) code := "TEST-CODE-EXPIRED" seedRedeemCode(t, db, code, 30, &expired, 1, "active") _, err := svc.Redeem(context.Background(), user, code, "127.0.0.1", "ut") if !errors.Is(err, ErrRedeemCodeExpired) { t.Fatalf("redeem expired err=%v, want ErrRedeemCodeExpired", err) } } func TestRedeemCodeServiceRedeemInvalidCode(t *testing.T) { t.Parallel() db := setupRedeemTestDB(t) svc := NewRedeemCodeService(db, "") user := &usermodel.User{ID: 3, MiniProgramID: 10} _, err := svc.Redeem(context.Background(), user, "NOT-FOUND-CODE", "127.0.0.1", "ut") if !errors.Is(err, ErrRedeemCodeInvalid) { t.Fatalf("redeem invalid err=%v, want ErrRedeemCodeInvalid", err) } } func TestRedeemCodeServiceRedeemExtendsActiveMembership(t *testing.T) { t.Parallel() db := setupRedeemTestDB(t) svc := NewRedeemCodeService(db, "") user := &usermodel.User{ID: 4, MiniProgramID: 10} firstCode := "TEST-CODE-A" secondCode := "TEST-CODE-B" seedRedeemCode(t, db, firstCode, 10, nil, 1, "active") seedRedeemCode(t, db, secondCode, 15, nil, 1, "active") firstRes, err := svc.Redeem(context.Background(), user, firstCode, "127.0.0.1", "ut") if err != nil { t.Fatalf("first redeem: %v", err) } secondRes, err := svc.Redeem(context.Background(), user, secondCode, "127.0.0.1", "ut") if err != nil { t.Fatalf("second redeem: %v", err) } if secondRes == nil || !secondRes.Extended { t.Fatalf("second redeem should extend existing membership") } if !secondRes.EndsAt.After(firstRes.EndsAt) { t.Fatalf("second ends_at=%s should be after first ends_at=%s", secondRes.EndsAt, firstRes.EndsAt) } }