Files
junhong_cmp_fiber/internal/task/auto_purchase_terms_integration_test.go
break 147f3eb775 修复 UR#55 代码审查问题:事务隔离、错误包装、重复辅助函数
- resolvePackageTerms 接受 tx 参数并使用事务绑定的 Store,确保快照与写入同库
- auto_purchase.go 错误包装改用 pkgerrors.Wrap 而非标准库 errors
- shop_package_batch_allocation 去除 fmt 依赖改用 strconv 拼接
- shop_series_grant 提取 effectiveBase 局部变量避免重复调用
- 测试辅助函数统一迁移至 testutil.StringPointer,删除各文件本地重复定义
- 新增 testutil.NewRedisClient 和 testutil.StringPointer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-23 11:09:08 +09:00

169 lines
9.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package task
import (
"context"
"strconv"
"testing"
"time"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
"github.com/break/junhong_cmp_fiber/internal/testutil"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"go.uber.org/zap"
"gorm.io/gorm"
)
// TestAutoPurchasePersistsTermsSnapshotsAndRealnameDecision 验证自动购包复用快照并遵守实名起算规则。
func TestAutoPurchasePersistsTermsSnapshotsAndRealnameDecision(t *testing.T) {
testCases := []struct {
name string
defaultBase string
override *string
realnameStatus int
expectedBase string
expectedStatus int
expectedPending bool
}{
{name: "跟随默认等待实名", defaultBase: constants.PackageExpiryBaseFromActivation, realnameStatus: constants.RealNameStatusNotVerified, expectedBase: constants.PackageExpiryBaseFromActivation, expectedStatus: constants.PackageUsageStatusPending, expectedPending: true},
{name: "覆盖购买即生效", defaultBase: constants.PackageExpiryBaseFromActivation, override: testutil.StringPointer(constants.PackageExpiryBaseFromPurchase), realnameStatus: constants.RealNameStatusNotVerified, expectedBase: constants.PackageExpiryBaseFromPurchase, expectedStatus: constants.PackageUsageStatusActive},
{name: "覆盖实名但已实名", defaultBase: constants.PackageExpiryBaseFromPurchase, override: testutil.StringPointer(constants.PackageExpiryBaseFromActivation), realnameStatus: constants.RealNameStatusVerified, expectedBase: constants.PackageExpiryBaseFromActivation, expectedStatus: constants.PackageUsageStatusActive},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
tx := testutil.NewPostgresTransaction(t)
card := createAutoPurchaseCard(t, tx, testCase.realnameStatus)
pkg := createAutoPurchasePackage(t, tx, constants.PackageTypeFormal, testCase.defaultBase, 31)
shopID := uint(time.Now().UnixNano() & 0x7fffffff)
if testCase.override != nil {
allocation := &model.ShopPackageAllocation{ShopID: shopID, PackageID: pkg.ID, CostPrice: 1, RetailPrice: 2, Status: constants.StatusEnabled, ShelfStatus: 1, ExpiryBaseOverride: testCase.override}
if err := tx.Create(allocation).Error; err != nil {
t.Fatalf("创建套餐覆盖配置失败:%v", err)
}
}
handler := &AutoPurchaseHandler{db: tx, shopPackageAllocationStore: postgres.NewShopPackageAllocationStore(tx), logger: zap.NewNop()}
order := newAutoPurchaseOrder(card.ID, &shopID)
if err := handler.activateMainPackage(context.Background(), tx, order, pkg, constants.AssetWalletResourceTypeIotCard, card.ID, time.Now()); err != nil {
t.Fatalf("自动购包创建主套餐失败:%v", err)
}
var usage model.PackageUsage
if err := tx.Where("order_id = ? AND package_id = ?", order.ID, pkg.ID).First(&usage).Error; err != nil {
t.Fatalf("查询自动购包记录失败:%v", err)
}
if usage.ExpiryBaseSnapshot != testCase.expectedBase || usage.CalendarTypeSnapshot != constants.PackageCalendarTypeByDay || usage.DurationDaysSnapshot != 31 || usage.Status != testCase.expectedStatus || usage.PendingRealnameActivation != testCase.expectedPending {
t.Fatalf("自动购包快照或实名决策错误:%+v", usage)
}
})
}
}
// TestAutoPurchaseDefaultFromPurchaseActivatesImmediatelyWithoutRealname 验证套餐默认为 from_purchase 时自动购包不需要实名即激活。
func TestAutoPurchaseDefaultFromPurchaseActivatesImmediatelyWithoutRealname(t *testing.T) {
tx := testutil.NewPostgresTransaction(t)
// 卡未实名,但套餐默认 from_purchase应立即激活
card := createAutoPurchaseCard(t, tx, constants.RealNameStatusNotVerified)
pkg := createAutoPurchasePackage(t, tx, constants.PackageTypeFormal, constants.PackageExpiryBaseFromPurchase, 14)
handler := &AutoPurchaseHandler{db: tx, shopPackageAllocationStore: postgres.NewShopPackageAllocationStore(tx), logger: zap.NewNop()}
order := newAutoPurchaseOrder(card.ID, nil)
if err := handler.activateMainPackage(context.Background(), tx, order, pkg, constants.AssetWalletResourceTypeIotCard, card.ID, time.Now()); err != nil {
t.Fatalf("from_purchase 默认值自动购包失败:%v", err)
}
var usage model.PackageUsage
if err := tx.Where("order_id = ? AND package_id = ?", order.ID, pkg.ID).First(&usage).Error; err != nil {
t.Fatalf("查询自动购包记录失败:%v", err)
}
if usage.ExpiryBaseSnapshot != constants.PackageExpiryBaseFromPurchase || usage.DurationDaysSnapshot != 14 {
t.Fatalf("from_purchase 默认值快照错误:%+v", usage)
}
if usage.Status != constants.PackageUsageStatusActive || usage.PendingRealnameActivation {
t.Fatalf("from_purchase 默认值未实名仍应立即激活status=%d pendingRealname=%v", usage.Status, usage.PendingRealnameActivation)
}
}
// TestAutoPurchaseMainAndAddonAreIdempotentByBusinessFact 验证主套餐、加油包和重复消费依赖数据库事实去重。
func TestAutoPurchaseMainAndAddonAreIdempotentByBusinessFact(t *testing.T) {
tx := testutil.NewPostgresTransaction(t)
card := createAutoPurchaseCard(t, tx, constants.RealNameStatusVerified)
formal := createAutoPurchasePackage(t, tx, constants.PackageTypeFormal, constants.PackageExpiryBaseFromPurchase, 30)
addon := createAutoPurchasePackage(t, tx, constants.PackageTypeAddon, constants.PackageExpiryBaseFromActivation, 7)
handler := &AutoPurchaseHandler{db: tx, shopPackageAllocationStore: postgres.NewShopPackageAllocationStore(tx), logger: zap.NewNop()}
formalOrder := newAutoPurchaseOrder(card.ID, nil)
addonOrder := newAutoPurchaseOrder(card.ID, nil)
now := time.Now()
if err := handler.activatePackages(context.Background(), tx, formalOrder, []*model.Package{formal}, now); err != nil {
t.Fatalf("首次自动创建主套餐失败:%v", err)
}
if err := handler.activatePackages(context.Background(), tx, formalOrder, []*model.Package{formal}, now); err != nil {
t.Fatalf("重复消费应按业务事实幂等:%v", err)
}
if err := handler.activatePackages(context.Background(), tx, addonOrder, []*model.Package{addon}, now.Add(time.Minute)); err != nil {
t.Fatalf("自动创建加油包失败:%v", err)
}
var usages []model.PackageUsage
if err := tx.Where("order_id IN ?", []uint{formalOrder.ID, addonOrder.ID}).Order("order_id").Find(&usages).Error; err != nil {
t.Fatalf("查询自动购包记录失败:%v", err)
}
if len(usages) != 2 {
t.Fatalf("重复消费只应形成一组记录,实际 %d", len(usages))
}
for _, usage := range usages {
if usage.ExpiryBaseSnapshot == "" || usage.CalendarTypeSnapshot == "" {
t.Fatalf("自动购包不得产生空快照:%+v", usage)
}
}
if usages[1].MasterUsageID == nil {
t.Fatalf("加油包必须关联主套餐:%+v", usages[1])
}
}
// TestAutoPurchaseTransactionRollbackOnInvalidAddonTerms 验证同一事务中后续套餐失败时不留下部分使用记录。
func TestAutoPurchaseTransactionRollbackOnInvalidAddonTerms(t *testing.T) {
tx := testutil.NewPostgresTransaction(t)
card := createAutoPurchaseCard(t, tx, constants.RealNameStatusVerified)
formal := createAutoPurchasePackage(t, tx, constants.PackageTypeFormal, constants.PackageExpiryBaseFromPurchase, 30)
invalidAddon := createAutoPurchasePackage(t, tx, constants.PackageTypeAddon, "invalid", 7)
handler := &AutoPurchaseHandler{db: tx, shopPackageAllocationStore: postgres.NewShopPackageAllocationStore(tx), logger: zap.NewNop()}
order := newAutoPurchaseOrder(card.ID, nil)
err := tx.Transaction(func(inner *gorm.DB) error {
return handler.activatePackages(context.Background(), inner, order, []*model.Package{formal, invalidAddon}, time.Now())
})
if err == nil {
t.Fatal("非法加油包计时条款必须使业务事务失败")
}
var count int64
if err := tx.Model(&model.PackageUsage{}).Where("order_id = ?", order.ID).Count(&count).Error; err != nil || count != 0 {
t.Fatalf("事务失败不应留下部分主套餐count=%d err=%v", count, err)
}
}
func createAutoPurchaseCard(t *testing.T, tx *gorm.DB, realnameStatus int) *model.IotCard {
t.Helper()
suffix := strconv.FormatInt(time.Now().UnixNano()%100000000000000000, 10)
iccid := "88" + suffix
if len(iccid) < 19 {
iccid += "0000000000000000000"[:19-len(iccid)]
}
iccid = iccid[:19]
card := &model.IotCard{ICCID: iccid, ICCID19: iccid, CarrierID: 1, RealNameStatus: realnameStatus, AssetStatus: constants.AssetStatusInStock, Generation: 1}
if err := tx.Create(card).Error; err != nil {
t.Fatalf("创建自动购包测试卡失败:%v", err)
}
return card
}
func createAutoPurchasePackage(t *testing.T, tx *gorm.DB, packageType, expiryBase string, durationDays int) *model.Package {
t.Helper()
suffix := strconv.FormatInt(time.Now().UnixNano(), 10)
pkg := &model.Package{PackageCode: "UR55-AUTO-" + suffix, PackageName: "UR55自动购包测试套餐", PackageType: packageType, DurationMonths: 1, DurationDays: durationDays, CalendarType: constants.PackageCalendarTypeByDay, ExpiryBase: expiryBase, Status: constants.StatusEnabled, ShelfStatus: 1, DataResetCycle: "monthly"}
if err := tx.Create(pkg).Error; err != nil {
t.Fatalf("创建自动购包测试套餐失败:%v", err)
}
return pkg
}
func newAutoPurchaseOrder(cardID uint, sellerShopID *uint) *model.Order {
orderID := uint(time.Now().UnixNano() & 0x7fffffff)
return &model.Order{Model: gorm.Model{ID: orderID}, OrderNo: "UR55-AUTO-" + strconv.FormatUint(uint64(orderID), 10), OrderType: model.OrderTypeSingleCard, IotCardID: &cardID, SellerShopID: sellerShopID, TotalAmount: 100, Generation: 1}
}