Files
qitongxue-api/internal/logic/promo_test.go
2026-09-29 10:57:01 +08:00

159 lines
5.6 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 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)
}
}