package logic import ( "testing" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/os/gtime" "tool-api/internal/consts" "tool-api/internal/model/entity" ) // ============================================================================ // 优惠码单测(T04.8) // // 覆盖纯函数:码校验分类、适用范围匹配、互斥(同时传码与券 → 4010)、限流计数。 // 全部不依赖 DB,保证 `go test ./...` 在任何环境可跑。 // ============================================================================ // codeOf 取 gerror 的业务码(无 code 返回 -1)。 func codeOf(err error) int { if err == nil { return 0 } if c := gerror.Code(err); c != nil { return c.Code() } return -1 } func TestNormalizePromoCode(t *testing.T) { cases := map[string]string{ " abc123 ": "ABC123", "XYZ": "XYZ", "": "", } for in, want := range cases { if got := normalizePromoCode(in); got != want { t.Errorf("normalizePromoCode(%q) = %q, want %q", in, got, want) } } } func TestScopeMatches(t *testing.T) { keys := []string{"vip-month", "quota_100"} cases := []struct { name string scope int orderType int targetKey string want bool }{ {"全部恒真", consts.PromoScopeAll, consts.OrderTypeMember, "vip-month", true}, {"全部恒真-次数包", consts.PromoScopeAll, consts.OrderTypeQuota, "quota_100", true}, {"指定套餐命中", consts.PromoScopePlan, consts.OrderTypeMember, "vip-month", true}, {"指定套餐-订单类型不符", consts.PromoScopePlan, consts.OrderTypeQuota, "vip-month", false}, {"指定套餐-key 不匹配", consts.PromoScopePlan, consts.OrderTypeMember, "vip-year", false}, {"指定次数包命中", consts.PromoScopePack, consts.OrderTypeQuota, "quota_100", true}, {"指定次数包-订单类型不符", consts.PromoScopePack, consts.OrderTypeMember, "quota_100", false}, {"未知 scope fail-closed", 9, consts.OrderTypeMember, "vip-month", false}, } for _, c := range cases { if got := scopeMatches(c.scope, keys, c.orderType, c.targetKey); got != c.want { t.Errorf("%s: scopeMatches=%v, want %v", c.name, got, c.want) } } // scopeKeys 为空 → 指定 scope 下无任何适用对象(fail-closed) if scopeMatches(consts.PromoScopePlan, nil, consts.OrderTypeMember, "vip-month") { t.Error("scopeKeys 为空时 scope=指定套餐 应判为不匹配") } } func TestPromoPriceError(t *testing.T) { now := gtime.Now() base := func() *entity.PromoCodes { return &entity.PromoCodes{ Id: 1, Code: "OK", ProductId: "admin_1", PriceCents: 1, Scope: consts.PromoScopeAll, MaxUses: 0, UsedCount: 0, PerUserLimit: 1, Status: 1, } } cases := []struct { name string mutate func(*entity.PromoCodes) orderType int targetKey string usedByUser int wantCode int // 0 = 通过 }{ {"正常可用", func(p *entity.PromoCodes) {}, consts.OrderTypeMember, "vip-month", 0, 0}, {"nil", nil, consts.OrderTypeMember, "vip-month", 0, consts.CodePromoInvalid}, {"已停用", func(p *entity.PromoCodes) { p.Status = 0 }, consts.OrderTypeMember, "vip-month", 0, consts.CodePromoInvalid}, {"未生效", func(p *entity.PromoCodes) { p.ValidFrom = gtime.New("2999-01-01 00:00:00") }, consts.OrderTypeMember, "vip-month", 0, consts.CodePromoInvalid}, {"已过期", func(p *entity.PromoCodes) { p.ValidTo = gtime.New("2000-01-01 00:00:00") }, consts.OrderTypeMember, "vip-month", 0, consts.CodePromoInvalid}, {"达总上限", func(p *entity.PromoCodes) { p.MaxUses = 3; p.UsedCount = 3 }, consts.OrderTypeMember, "vip-month", 0, consts.CodePromoInvalid}, {"超单人限用", func(p *entity.PromoCodes) { p.PerUserLimit = 1 }, consts.OrderTypeMember, "vip-month", 1, consts.CodePromoInvalid}, {"不适用-类型", func(p *entity.PromoCodes) { p.Scope = consts.PromoScopePlan p.ScopeKeys = `["vip-month"]` }, consts.OrderTypeQuota, "quota_100", 0, consts.CodePromoInvalid}, {"适用-指定套餐", func(p *entity.PromoCodes) { p.Scope = consts.PromoScopePlan p.ScopeKeys = `["vip-month"]` }, consts.OrderTypeMember, "vip-month", 0, 0}, } for _, c := range cases { p := base() if c.mutate == nil { p = nil } else { c.mutate(p) } err := promoPriceError(p, now, c.orderType, c.targetKey, c.usedByUser) if got := codeOf(err); got != c.wantCode { t.Errorf("%s: code=%d (err=%v), want %d", c.name, got, err, c.wantCode) } } } // TestVoucherMutualExclusion 互斥:promo_code 与 user_coupon_id 同时传 → 4010。 func TestVoucherMutualExclusion(t *testing.T) { if err := voucherConflictErr("ABC", 0); err != nil { t.Errorf("仅传优惠码不应报错,got %v", err) } if err := voucherConflictErr("", 99); err != nil { t.Errorf("仅传优惠券不应报错,got %v", err) } if err := voucherConflictErr("", 0); err != nil { t.Errorf("都不传不应报错,got %v", err) } err := voucherConflictErr("ABC", 99) if codeOf(err) != consts.CodePromoInvalid { t.Errorf("同时传码与券应返回 4010,got code=%d err=%v", codeOf(err), err) } } // TestPromoFailWindow 限流窗口:错误计数→清空→再计数。 func TestPromoFailWindow(t *testing.T) { const uid = int64(987654321) clearPromoFailures(uid) if n := promoFailCount(uid); n != 0 { t.Fatalf("初始应为 0,got %d", n) } recordPromoFailure(uid) recordPromoFailure(uid) if n := promoFailCount(uid); n != 2 { t.Fatalf("记两次后应为 2,got %d", n) } clearPromoFailures(uid) if n := promoFailCount(uid); n != 0 { t.Fatalf("清空后应为 0,got %d", n) } // 非正 userId 不计数(避免匿名/边界写入) recordPromoFailure(0) if n := promoFailCount(0); n != 0 { t.Fatalf("userId<=0 不应计数,got %d", n) } }