admin_service_overages_test.go 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. //go:build unit
  2. package service
  3. import (
  4. "context"
  5. "testing"
  6. "time"
  7. "github.com/stretchr/testify/require"
  8. )
  9. type updateAccountOveragesRepoStub struct {
  10. mockAccountRepoForGemini
  11. account *Account
  12. updateCalls int
  13. }
  14. func (r *updateAccountOveragesRepoStub) GetByID(ctx context.Context, id int64) (*Account, error) {
  15. return r.account, nil
  16. }
  17. func (r *updateAccountOveragesRepoStub) Update(ctx context.Context, account *Account) error {
  18. r.updateCalls++
  19. r.account = account
  20. return nil
  21. }
  22. func TestUpdateAccount_DisableOveragesClearsAICreditsKey(t *testing.T) {
  23. accountID := int64(101)
  24. repo := &updateAccountOveragesRepoStub{
  25. account: &Account{
  26. ID: accountID,
  27. Platform: PlatformAntigravity,
  28. Type: AccountTypeOAuth,
  29. Status: StatusActive,
  30. Extra: map[string]any{
  31. "allow_overages": true,
  32. "mixed_scheduling": true,
  33. modelRateLimitsKey: map[string]any{
  34. "claude-sonnet-4-5": map[string]any{
  35. "rate_limited_at": "2026-03-15T00:00:00Z",
  36. "rate_limit_reset_at": "2099-03-15T00:00:00Z",
  37. },
  38. creditsExhaustedKey: map[string]any{
  39. "rate_limited_at": "2026-03-15T00:00:00Z",
  40. "rate_limit_reset_at": time.Now().Add(5 * time.Hour).UTC().Format(time.RFC3339),
  41. },
  42. },
  43. },
  44. },
  45. }
  46. svc := &adminServiceImpl{accountRepo: repo}
  47. updated, err := svc.UpdateAccount(context.Background(), accountID, &UpdateAccountInput{
  48. Extra: map[string]any{
  49. "mixed_scheduling": true,
  50. modelRateLimitsKey: map[string]any{
  51. "claude-sonnet-4-5": map[string]any{
  52. "rate_limited_at": "2026-03-15T00:00:00Z",
  53. "rate_limit_reset_at": "2099-03-15T00:00:00Z",
  54. },
  55. creditsExhaustedKey: map[string]any{
  56. "rate_limited_at": "2026-03-15T00:00:00Z",
  57. "rate_limit_reset_at": time.Now().Add(5 * time.Hour).UTC().Format(time.RFC3339),
  58. },
  59. },
  60. },
  61. })
  62. require.NoError(t, err)
  63. require.NotNil(t, updated)
  64. require.Equal(t, 1, repo.updateCalls)
  65. require.False(t, updated.IsOveragesEnabled())
  66. // 关闭 overages 后,AICredits key 应被清除
  67. rawLimits, ok := repo.account.Extra[modelRateLimitsKey].(map[string]any)
  68. if ok {
  69. _, exists := rawLimits[creditsExhaustedKey]
  70. require.False(t, exists, "关闭 overages 时应清除 AICredits 限流 key")
  71. }
  72. // 普通模型限流应保留
  73. require.True(t, ok)
  74. _, exists := rawLimits["claude-sonnet-4-5"]
  75. require.True(t, exists, "普通模型限流应保留")
  76. }
  77. func TestUpdateAccount_EnableOveragesClearsModelRateLimitsBeforePersist(t *testing.T) {
  78. accountID := int64(102)
  79. repo := &updateAccountOveragesRepoStub{
  80. account: &Account{
  81. ID: accountID,
  82. Platform: PlatformAntigravity,
  83. Type: AccountTypeOAuth,
  84. Status: StatusActive,
  85. Extra: map[string]any{
  86. "mixed_scheduling": true,
  87. modelRateLimitsKey: map[string]any{
  88. "claude-sonnet-4-5": map[string]any{
  89. "rate_limited_at": "2026-03-15T00:00:00Z",
  90. "rate_limit_reset_at": "2099-03-15T00:00:00Z",
  91. },
  92. },
  93. },
  94. },
  95. }
  96. svc := &adminServiceImpl{accountRepo: repo}
  97. updated, err := svc.UpdateAccount(context.Background(), accountID, &UpdateAccountInput{
  98. Extra: map[string]any{
  99. "mixed_scheduling": true,
  100. "allow_overages": true,
  101. },
  102. })
  103. require.NoError(t, err)
  104. require.NotNil(t, updated)
  105. require.Equal(t, 1, repo.updateCalls)
  106. require.True(t, updated.IsOveragesEnabled())
  107. _, exists := repo.account.Extra[modelRateLimitsKey]
  108. require.False(t, exists, "开启 overages 时应在持久化前清掉旧模型限流")
  109. }
  110. func TestUpdateAccount_EmptyExtraPayloadCanClearQuotaLimits(t *testing.T) {
  111. accountID := int64(103)
  112. repo := &updateAccountOveragesRepoStub{
  113. account: &Account{
  114. ID: accountID,
  115. Platform: PlatformAnthropic,
  116. Type: AccountTypeAPIKey,
  117. Status: StatusActive,
  118. Extra: map[string]any{
  119. "quota_limit": 100.0,
  120. "quota_daily_limit": 10.0,
  121. "quota_weekly_limit": 40.0,
  122. },
  123. },
  124. }
  125. svc := &adminServiceImpl{accountRepo: repo}
  126. updated, err := svc.UpdateAccount(context.Background(), accountID, &UpdateAccountInput{
  127. // 显式空对象:语义是“清空 extra 中的可配置键”(例如关闭配额限制)
  128. Extra: map[string]any{},
  129. })
  130. require.NoError(t, err)
  131. require.NotNil(t, updated)
  132. require.Equal(t, 1, repo.updateCalls)
  133. require.NotNil(t, repo.account.Extra)
  134. require.NotContains(t, repo.account.Extra, "quota_limit")
  135. require.NotContains(t, repo.account.Extra, "quota_daily_limit")
  136. require.NotContains(t, repo.account.Extra, "quota_weekly_limit")
  137. require.Len(t, repo.account.Extra, 0)
  138. }
  139. func TestUpdateAccount_FixedWeeklyResetClearsLegacyRollingUsage(t *testing.T) {
  140. now := time.Now().UTC()
  141. daysSinceMonday := (int(now.Weekday()) + 6) % 7
  142. currentWeekStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC).AddDate(0, 0, -daysSinceMonday)
  143. legacyRollingStart := currentWeekStart.Add(-24 * time.Hour)
  144. accountID := int64(104)
  145. repo := &updateAccountOveragesRepoStub{
  146. account: &Account{
  147. ID: accountID,
  148. Platform: PlatformAnthropic,
  149. Type: AccountTypeAPIKey,
  150. Status: StatusActive,
  151. Extra: map[string]any{
  152. "quota_weekly_limit": 40.0,
  153. "quota_weekly_used": 12.5,
  154. "quota_weekly_start": legacyRollingStart.Format(time.RFC3339),
  155. },
  156. },
  157. }
  158. svc := &adminServiceImpl{accountRepo: repo}
  159. updated, err := svc.UpdateAccount(context.Background(), accountID, &UpdateAccountInput{
  160. Extra: map[string]any{
  161. "quota_weekly_limit": 40.0,
  162. "quota_weekly_reset_mode": "fixed",
  163. "quota_weekly_reset_day": float64(1),
  164. "quota_weekly_reset_hour": float64(0),
  165. "quota_reset_timezone": "UTC",
  166. },
  167. })
  168. require.NoError(t, err)
  169. require.NotNil(t, updated)
  170. require.Equal(t, 1, repo.updateCalls)
  171. require.InDelta(t, 0.0, updated.GetQuotaWeeklyUsed(), 1e-9)
  172. require.Equal(t, currentWeekStart.Format(time.RFC3339), updated.Extra["quota_weekly_start"])
  173. require.False(t, updated.IsWeeklyQuotaPeriodExpired())
  174. }