package service import ( "testing" "time" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/setting/system_setting" "github.com/glebarez/sqlite" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/gorm" ) func setupSyncedFundingDB(t *testing.T) *gorm.DB { t.Helper() db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) sqlDB, _ := db.DB() sqlDB.SetMaxOpenConns(1) origDB := model.DB origLogDB := model.LOG_DB model.DB = db model.LOG_DB = db common.UsingSQLite = true common.RedisEnabled = false require.NoError(t, db.AutoMigrate(&model.PendingSyncRecord{}, &model.User{})) t.Cleanup(func() { model.DB = origDB model.LOG_DB = origLogDB sqlDB.Close() }) return db } func TestNewSyncedUserFunding(t *testing.T) { f := NewSyncedUserFunding(100, 200, "req-001", 500000) assert.Equal(t, 100, f.userId) assert.Equal(t, 200, f.remoteUserId) assert.Equal(t, "req-001", f.requestId) assert.Equal(t, 500000, f.syncedQuota) assert.Equal(t, 0, f.consumed) } func TestSyncedUserFunding_Source(t *testing.T) { f := NewSyncedUserFunding(100, 200, "req-001", 500000) assert.Equal(t, BillingSourceSyncedWallet, f.Source()) } func TestPreConsume_Success(t *testing.T) { setupSyncedFundingDB(t) settings := system_setting.GetRegionSyncSettings() origThreshold := settings.MinBalanceThreshold settings.MinBalanceThreshold = 100000 defer func() { settings.MinBalanceThreshold = origThreshold }() // 创建测试用户(AtomicDecreaseSyncedQuota 操作 DB 中的 synced_quota) require.NoError(t, model.DB.Create(&model.User{ Id: 100, Username: "test-synced", Source: common.UserSourceSynced, SyncedQuota: 1000000, }).Error) f := NewSyncedUserFunding(100, 200, "req-pre-success", 1000000) err := f.PreConsume(50000) require.NoError(t, err) assert.Equal(t, 50000, f.consumed) // 验证 PendingSyncRecord 已创建 var record model.PendingSyncRecord require.NoError(t, model.DB.Where("request_id = ?", "req-pre-success").First(&record).Error) assert.Equal(t, 100, record.UserId) assert.Equal(t, 200, record.RemoteUserId) assert.Equal(t, 50000, record.Quota) assert.Equal(t, model.PendingSyncStatusPending, record.Status) } func TestPreConsume_ZeroAmount(t *testing.T) { setupSyncedFundingDB(t) f := NewSyncedUserFunding(100, 200, "req-pre-zero", 1000000) err := f.PreConsume(0) assert.NoError(t, err) assert.Equal(t, 0, f.consumed) } func TestPreConsume_NegativeAmount(t *testing.T) { setupSyncedFundingDB(t) f := NewSyncedUserFunding(100, 200, "req-pre-neg", 1000000) err := f.PreConsume(-100) assert.NoError(t, err) assert.Equal(t, 0, f.consumed) } func TestPreConsume_InsufficientQuota(t *testing.T) { setupSyncedFundingDB(t) settings := system_setting.GetRegionSyncSettings() origThreshold := settings.MinBalanceThreshold settings.MinBalanceThreshold = 100000 defer func() { settings.MinBalanceThreshold = origThreshold }() require.NoError(t, model.DB.Create(&model.User{ Id: 100, Username: "test-insuf", Source: common.UserSourceSynced, SyncedQuota: 149999, }).Error) // syncedQuota=149999, amount=50000, threshold=100000 // 149999 < 50000 + 100000 = 150000 → 不满足 f := NewSyncedUserFunding(100, 200, "req-pre-insuf", 149999) err := f.PreConsume(50000) assert.Error(t, err) assert.Contains(t, err.Error(), "余额不足") assert.Equal(t, 0, f.consumed) } func TestPreConsume_ExactlyAtThreshold(t *testing.T) { setupSyncedFundingDB(t) settings := system_setting.GetRegionSyncSettings() origThreshold := settings.MinBalanceThreshold settings.MinBalanceThreshold = 100000 defer func() { settings.MinBalanceThreshold = origThreshold }() require.NoError(t, model.DB.Create(&model.User{ Id: 100, Username: "test-exact", Source: common.UserSourceSynced, SyncedQuota: 150000, }).Error) // syncedQuota=150000, amount=50000, threshold=100000 // 150000 >= 50000 + 100000 = 150000 → 刚好满足 f := NewSyncedUserFunding(100, 200, "req-pre-exact", 150000) err := f.PreConsume(50000) assert.NoError(t, err) assert.Equal(t, 50000, f.consumed) } func TestSettle_PositiveDelta(t *testing.T) { setupSyncedFundingDB(t) f := NewSyncedUserFunding(100, 200, "req-settle-pos", 500000) f.consumed = 1000 err := f.Settle(500) assert.NoError(t, err) assert.Equal(t, 1500, f.consumed) assert.Equal(t, 499500, f.syncedQuota) // 500000 - 500 } func TestSettle_NegativeDelta(t *testing.T) { setupSyncedFundingDB(t) f := NewSyncedUserFunding(100, 200, "req-settle-neg", 500000) f.consumed = 1000 err := f.Settle(-300) assert.NoError(t, err) assert.Equal(t, 700, f.consumed) assert.Equal(t, 500300, f.syncedQuota) // 500000 - (-300) = 500000 + 300 } func TestSettle_ZeroDelta(t *testing.T) { f := NewSyncedUserFunding(100, 200, "req-settle-zero", 500000) f.consumed = 1000 err := f.Settle(0) assert.NoError(t, err) assert.Equal(t, 1000, f.consumed) // 不变 assert.Equal(t, 500000, f.syncedQuota) // 不变 } func TestSettle_TransitionsPendingToSettled(t *testing.T) { db := setupSyncedFundingDB(t) // 先创建 pending 记录 require.NoError(t, model.CreatePendingSyncRecord(100, 200, "req-settle-trans", 1000, 1000)) f := NewSyncedUserFunding(100, 200, "req-settle-trans", 500000) f.consumed = 1000 err := f.Settle(-300) // 实际消费 700,退还 300 assert.NoError(t, err) assert.Equal(t, 700, f.consumed) // 验证记录状态从 pending 转为 settled,quota 更新为实际消费 var record model.PendingSyncRecord require.NoError(t, db.Where("request_id = ?", "req-settle-trans").First(&record).Error) assert.Equal(t, model.PendingSyncStatusSettled, record.Status) assert.Equal(t, 700, record.Quota) } func TestRefund_Success(t *testing.T) { db := setupSyncedFundingDB(t) // 创建测试用户 require.NoError(t, model.DB.Create(&model.User{ Id: 100, Username: "test-refund", Source: common.UserSourceSynced, SyncedQuota: 500000, }).Error) // 先创建一条 pending 记录 require.NoError(t, model.CreatePendingSyncRecord(100, 200, "req-refund-ok", 5000, 5000)) f := NewSyncedUserFunding(100, 200, "req-refund-ok", 500000) f.consumed = 5000 err := f.Refund() assert.NoError(t, err) // 验证记录已被标记为 synced var record model.PendingSyncRecord require.NoError(t, db.Where("request_id = ?", "req-refund-ok").First(&record).Error) assert.Equal(t, model.PendingSyncStatusSynced, record.Status) assert.NotZero(t, record.SyncedAt) } func TestRefund_NoConsumed(t *testing.T) { setupSyncedFundingDB(t) f := NewSyncedUserFunding(100, 200, "req-refund-nocons", 500000) f.consumed = 0 err := f.Refund() assert.NoError(t, err) } func TestRefund_RecordNotFound(t *testing.T) { setupSyncedFundingDB(t) // 没有 PendingSyncRecord,但 consumed > 0 f := NewSyncedUserFunding(100, 200, "req-refund-notfound", 500000) f.consumed = 5000 err := f.Refund() assert.NoError(t, err) // 应优雅处理,不报错 } func TestRefund_RecordAlreadySynced(t *testing.T) { db := setupSyncedFundingDB(t) // 创建已 synced 的记录 require.NoError(t, db.Create(&model.PendingSyncRecord{ UserId: 100, RemoteUserId: 200, RequestId: "req-refund-synced", Quota: 5000, Status: model.PendingSyncStatusSynced, SyncedAt: time.Now().Unix(), CreatedAt: time.Now().Unix(), }).Error) f := NewSyncedUserFunding(100, 200, "req-refund-synced", 500000) f.consumed = 5000 err := f.Refund() assert.NoError(t, err) // status=pending 不匹配,优雅处理 }