package quota import ( "context" "testing" "time" "github.com/alicebob/miniredis/v2" "github.com/glebarez/sqlite" "github.com/redis/go-redis/v9" "gorm.io/gorm" "dudu/server/internal/store" "dudu/server/pkg/protocol" ) func setup(t *testing.T) (*Manager, *redis.Client, *gorm.DB) { t.Helper() mr := miniredis.RunT(t) rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()}) db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) if err != nil { t.Fatal(err) } if err := db.AutoMigrate(store.AllModels()...); err != nil { t.Fatal(err) } return New(rdb, db), rdb, db } func TestConsumeTrialFirstThenBalance(t *testing.T) { m, _, db := setup(t) ctx := context.Background() db.Create(&store.User{ID: "u1", BalanceSeconds: 100}) // 第一次扣 60s:全部走试用(180s/天) r, err := m.Consume(ctx, "u1", 60) if err != nil { t.Fatal(err) } if r.TrialPart != 60 || r.BalancePart != 0 { t.Fatalf("want trial=60 balance=0, got %+v", r) } // 再扣 150s:试用只剩 120s,余下 30s 走余额 r, _ = m.Consume(ctx, "u1", 150) if r.TrialPart != 120 || r.BalancePart != 30 || r.BalanceSeconds != 70 { t.Fatalf("want trial=120 balance=30 remain=70, got %+v", r) } // 再扣 100s:试用已尽,余额只剩 70 → 扣穿到 0(不掐断当句,由网关句间判断) r, _ = m.Consume(ctx, "u1", 100) if r.TrialPart != 0 || r.BalancePart != 70 || r.BalanceSeconds != 0 { t.Fatalf("want trial=0 balance=70 remain=0, got %+v", r) } s, _ := m.Get(ctx, "u1") if !s.Exhausted() { t.Fatalf("expect exhausted, got %+v", s) } } func TestPrecheck(t *testing.T) { m, _, db := setup(t) ctx := context.Background() db.Create(&store.User{ID: "u2", BalanceSeconds: 0}) // 余额 0 但有当日试用 → 放行 ok, _, err := m.Precheck(ctx, "u2") if err != nil || !ok { t.Fatalf("expect precheck pass (trial available), ok=%v err=%v", ok, err) } // 用尽试用 → 拒绝 if _, err := m.Consume(ctx, "u2", protocol.TrialDailySeconds); err != nil { t.Fatal(err) } ok, _, _ = m.Precheck(ctx, "u2") if ok { t.Fatal("expect precheck reject after trial exhausted") } } func TestCreditAndSettle(t *testing.T) { m, rdb, db := setup(t) ctx := context.Background() db.Create(&store.User{ID: "u3", BalanceSeconds: 0}) if err := m.CreditPurchase(ctx, "u3", 6000, "o1"); err != nil { t.Fatal(err) } var u store.User db.First(&u, "id = ?", "u3") if u.BalanceSeconds != 6000 { t.Fatalf("want 6000, got %d", u.BalanceSeconds) } var n int64 db.Model(&store.BalanceLedger{}).Where("user_id = ? AND reason = ?", "u3", store.LedgerPurchase).Count(&n) if n != 1 { t.Fatalf("want 1 ledger row, got %d", n) } // 落库会话:30s = 试用 20 + 余额 10 err := m.SettleSession(ctx, store.ASRSession{ ID: "s1", UserID: "u3", AudioSeconds: 30, TrialPart: 20, BalancePart: 10, Provider: "mock", CreatedAt: time.Now(), }) if err != nil { t.Fatal(err) } db.First(&u, "id = ?", "u3") if u.BalanceSeconds != 5990 { t.Fatalf("want 5990 after settle, got %d", u.BalanceSeconds) } var tu store.TrialUsage if err := db.First(&tu, "user_id = ?", "u3").Error; err != nil || tu.UsedSeconds != 20 { t.Fatalf("want trial_usage 20, got %+v err=%v", tu, err) } _ = rdb }