补充兑换码开通会员集成测试场景
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
# 兑换码开通会员集成验证(2026-02-28)
|
||||
|
||||
对应 issue:`#8 [P0][T5] 兑换码开通会员 API 集成测试`
|
||||
|
||||
## 覆盖场景
|
||||
|
||||
1. 成功兑换
|
||||
- 用例:`TestRedeemCodeServiceRedeemSuccessAndRepeat`
|
||||
- 结果:通过
|
||||
- 校验点:创建会员成功、`used_uses` 递增、结果字段可用。
|
||||
|
||||
2. 重复兑换
|
||||
- 用例:`TestRedeemCodeServiceRedeemSuccessAndRepeat`
|
||||
- 结果:通过
|
||||
- 校验点:第二次兑换返回 `ErrRedeemCodeUsedUp`。
|
||||
|
||||
3. 过期码
|
||||
- 用例:`TestRedeemCodeServiceRedeemExpiredCode`
|
||||
- 结果:通过
|
||||
- 校验点:返回 `ErrRedeemCodeExpired`。
|
||||
|
||||
4. 非法码
|
||||
- 用例:`TestRedeemCodeServiceRedeemInvalidCode`
|
||||
- 结果:通过
|
||||
- 校验点:返回 `ErrRedeemCodeInvalid`。
|
||||
|
||||
5. 会员状态更新及时可见
|
||||
- 用例:`TestRedeemCodeServiceRedeemExtendsActiveMembership`
|
||||
- 结果:通过
|
||||
- 校验点:已激活会员再次兑换后 `extended=true`,且 `ends_at` 向后延长。
|
||||
|
||||
## 执行命令
|
||||
|
||||
```bash
|
||||
go test ./internal/membership/service -run TestRedeemCodeServiceRedeem -v
|
||||
go test ./...
|
||||
```
|
||||
@@ -32,6 +32,7 @@ require (
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
@@ -49,4 +50,5 @@ require (
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
gorm.io/driver/sqlite v1.6.0 // indirect
|
||||
)
|
||||
|
||||
@@ -57,6 +57,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
@@ -111,5 +113,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
||||
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
|
||||
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user