Files
junhong_cmp_fiber/internal/task/auto_purchase_terms_integration_test.go
break d5a35f6360 新增测试代码规范、修复响应体断言缺失与魔法数字
- AGENTS.md 新增测试代码专项规范:常量、fixture、触发器、HTTP断言、层边界说明
- TestUpdateAllocationExpiryBaseHTTP 补全 PATCH 响应体字段断言
- 四个集成测试文件以 testIDMask 局部常量替换 0x7fffffff 魔法数字

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

196 lines
11 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
// testIDMask 用于将纳秒时间戳截断为合法的 uint 主键范围,仅限测试用。
const testIDMask = testIDMask
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() & testIDMask)
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)
}
}
// TestAutoPurchaseConfigChangeDoesNotModifySnapshot 验证自动购包后修改套餐配置不改变已有使用记录的计时快照。
func TestAutoPurchaseConfigChangeDoesNotModifySnapshot(t *testing.T) {
tx := testutil.NewPostgresTransaction(t)
card := createAutoPurchaseCard(t, tx, constants.RealNameStatusVerified)
pkg := createAutoPurchasePackage(t, tx, constants.PackageTypeFormal, constants.PackageExpiryBaseFromPurchase, 30)
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("自动购包创建主套餐失败:%v", err)
}
// 购买后修改套餐的生效条件和时长
if err := tx.Model(pkg).Updates(map[string]any{"expiry_base": constants.PackageExpiryBaseFromActivation, "duration_days": 99}).Error; 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 != constants.PackageExpiryBaseFromPurchase || usage.DurationDaysSnapshot != 30 {
t.Fatalf("套餐配置变更不应影响已购买快照expiryBase=%s days=%d", usage.ExpiryBaseSnapshot, usage.DurationDaysSnapshot)
}
}
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() & testIDMask)
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}
}