40760aa884
- 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>
120 lines
3.2 KiB
Go
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
|
|
}
|