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