Files
dudu/server/internal/quota/quota_test.go
T
wangjia 40760aa884
ci / server (push) Failing after 14s
ci / design-tokens (push) Failing after 11s
dudu MVP:五端语音输入法初始提交
- server:Go 网关(WS 流式识别中继/计费配额/微信登录支付 mock/反馈/埋点),gummy provider 已真实联调
- desktop:Tauri 2(全局快捷键 push-to-talk/浮层/托盘/设置/登录购买/反馈/首启引导)
- android:Compose 主 App + IME(键盘内录音直传)
- ios:App + 键盘扩展(1A spike 实证键盘内不可录音,走 deep link 听写)
- design/design-pipeline:设计系统 + token 导出 iOS/Android 主题
- doc:前后端设计文档(HTML);web:官网宣传页;todo:任务看板

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 00:38:37 +08:00

120 lines
3.2 KiB
Go

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
}