rate_limit_429_cooldown_test.go 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. //go:build unit
  2. package service
  3. import (
  4. "context"
  5. "encoding/json"
  6. "net/http"
  7. "testing"
  8. "time"
  9. "github.com/Wei-Shaw/sub2api/internal/config"
  10. "github.com/stretchr/testify/require"
  11. )
  12. type rateLimit429AccountRepoStub struct {
  13. mockAccountRepoForGemini
  14. rateLimitCalls int
  15. lastRateLimitID int64
  16. lastRateLimitReset time.Time
  17. }
  18. func (r *rateLimit429AccountRepoStub) SetRateLimited(_ context.Context, id int64, resetAt time.Time) error {
  19. r.rateLimitCalls++
  20. r.lastRateLimitID = id
  21. r.lastRateLimitReset = resetAt
  22. return nil
  23. }
  24. func TestGetRateLimit429CooldownSettings_DefaultsWhenNotSet(t *testing.T) {
  25. repo := newMockSettingRepo()
  26. svc := NewSettingService(repo, &config.Config{})
  27. settings, err := svc.GetRateLimit429CooldownSettings(context.Background())
  28. require.NoError(t, err)
  29. require.True(t, settings.Enabled)
  30. require.Equal(t, 5, settings.CooldownSeconds)
  31. }
  32. func TestGetRateLimit429CooldownSettings_ReadsFromDB(t *testing.T) {
  33. repo := newMockSettingRepo()
  34. data, _ := json.Marshal(RateLimit429CooldownSettings{Enabled: false, CooldownSeconds: 12})
  35. repo.data[SettingKeyRateLimit429CooldownSettings] = string(data)
  36. svc := NewSettingService(repo, &config.Config{})
  37. settings, err := svc.GetRateLimit429CooldownSettings(context.Background())
  38. require.NoError(t, err)
  39. require.False(t, settings.Enabled)
  40. require.Equal(t, 12, settings.CooldownSeconds)
  41. }
  42. func TestSetRateLimit429CooldownSettings_EnabledRejectsOutOfRange(t *testing.T) {
  43. svc := NewSettingService(newMockSettingRepo(), &config.Config{})
  44. for _, seconds := range []int{0, -1, 7201, 99999} {
  45. err := svc.SetRateLimit429CooldownSettings(context.Background(), &RateLimit429CooldownSettings{
  46. Enabled: true, CooldownSeconds: seconds,
  47. })
  48. require.Error(t, err, "should reject enabled=true + cooldown_seconds=%d", seconds)
  49. require.Contains(t, err.Error(), "cooldown_seconds must be between 1-7200")
  50. }
  51. }
  52. func TestHandle429_FallbackUsesDBSeconds(t *testing.T) {
  53. accountRepo := &rateLimit429AccountRepoStub{}
  54. settingRepo := newMockSettingRepo()
  55. data, _ := json.Marshal(RateLimit429CooldownSettings{Enabled: true, CooldownSeconds: 12})
  56. settingRepo.data[SettingKeyRateLimit429CooldownSettings] = string(data)
  57. settingSvc := NewSettingService(settingRepo, &config.Config{})
  58. svc := NewRateLimitService(accountRepo, nil, &config.Config{}, nil, nil)
  59. svc.SetSettingService(settingSvc)
  60. account := &Account{ID: 42, Platform: PlatformOpenAI, Type: AccountTypeOAuth}
  61. before := time.Now()
  62. svc.handle429(context.Background(), account, http.Header{}, []byte(`{"error":{"type":"rate_limit_error","message":"slow down"}}`))
  63. after := time.Now()
  64. require.Equal(t, 1, accountRepo.rateLimitCalls)
  65. require.Equal(t, int64(42), accountRepo.lastRateLimitID)
  66. require.True(t, !accountRepo.lastRateLimitReset.Before(before.Add(12*time.Second)) && !accountRepo.lastRateLimitReset.After(after.Add(12*time.Second)))
  67. }
  68. func TestHandle429_FallbackDisabledSkipsLocalMark(t *testing.T) {
  69. accountRepo := &rateLimit429AccountRepoStub{}
  70. settingRepo := newMockSettingRepo()
  71. data, _ := json.Marshal(RateLimit429CooldownSettings{Enabled: false, CooldownSeconds: 12})
  72. settingRepo.data[SettingKeyRateLimit429CooldownSettings] = string(data)
  73. settingSvc := NewSettingService(settingRepo, &config.Config{})
  74. svc := NewRateLimitService(accountRepo, nil, &config.Config{}, nil, nil)
  75. svc.SetSettingService(settingSvc)
  76. account := &Account{ID: 43, Platform: PlatformOpenAI, Type: AccountTypeOAuth}
  77. svc.handle429(context.Background(), account, http.Header{}, []byte(`{"error":{"type":"rate_limit_error","message":"slow down"}}`))
  78. require.Zero(t, accountRepo.rateLimitCalls)
  79. }
  80. // Anthropic 无 reset 头的 429(如 Extra usage required)也应走兜底冷却,
  81. // 否则账号永不冷却,调度器会让每个请求反复撞同一批 429 账号(旋转木马)。
  82. func TestHandle429_AnthropicNoResetTimeUsesFallbackCooldown(t *testing.T) {
  83. accountRepo := &rateLimit429AccountRepoStub{}
  84. settingRepo := newMockSettingRepo()
  85. data, _ := json.Marshal(RateLimit429CooldownSettings{Enabled: true, CooldownSeconds: 12})
  86. settingRepo.data[SettingKeyRateLimit429CooldownSettings] = string(data)
  87. settingSvc := NewSettingService(settingRepo, &config.Config{})
  88. svc := NewRateLimitService(accountRepo, nil, &config.Config{}, nil, nil)
  89. svc.SetSettingService(settingSvc)
  90. account := &Account{ID: 45, Platform: PlatformAnthropic, Type: AccountTypeOAuth}
  91. before := time.Now()
  92. svc.handle429(context.Background(), account, http.Header{}, []byte(`{"error":{"type":"rate_limit_error","message":"Extra usage required"}}`))
  93. after := time.Now()
  94. require.Equal(t, 1, accountRepo.rateLimitCalls)
  95. require.Equal(t, int64(45), accountRepo.lastRateLimitID)
  96. require.True(t, !accountRepo.lastRateLimitReset.Before(before.Add(12*time.Second)) && !accountRepo.lastRateLimitReset.After(after.Add(12*time.Second)))
  97. }
  98. // 管理端关闭兜底冷却时,Anthropic 无 reset 头的 429 保持旧行为:不标记账号。
  99. func TestHandle429_AnthropicNoResetTimeFallbackDisabledSkipsMark(t *testing.T) {
  100. accountRepo := &rateLimit429AccountRepoStub{}
  101. settingRepo := newMockSettingRepo()
  102. data, _ := json.Marshal(RateLimit429CooldownSettings{Enabled: false, CooldownSeconds: 12})
  103. settingRepo.data[SettingKeyRateLimit429CooldownSettings] = string(data)
  104. settingSvc := NewSettingService(settingRepo, &config.Config{})
  105. svc := NewRateLimitService(accountRepo, nil, &config.Config{}, nil, nil)
  106. svc.SetSettingService(settingSvc)
  107. account := &Account{ID: 46, Platform: PlatformAnthropic, Type: AccountTypeOAuth}
  108. svc.handle429(context.Background(), account, http.Header{}, []byte(`{"error":{"type":"rate_limit_error","message":"Extra usage required"}}`))
  109. require.Zero(t, accountRepo.rateLimitCalls)
  110. }
  111. func TestHandle429_FallbackUsesDefaultSecondsWhenSettingServiceMissing(t *testing.T) {
  112. accountRepo := &rateLimit429AccountRepoStub{}
  113. cfg := &config.Config{}
  114. svc := NewRateLimitService(accountRepo, nil, cfg, nil, nil)
  115. account := &Account{ID: 44, Platform: PlatformGemini, Type: AccountTypeAPIKey}
  116. before := time.Now()
  117. svc.handle429(context.Background(), account, http.Header{}, []byte(`{"error":{"message":"slow down"}}`))
  118. after := time.Now()
  119. require.Equal(t, 1, accountRepo.rateLimitCalls)
  120. require.Equal(t, int64(44), accountRepo.lastRateLimitID)
  121. require.True(t, !accountRepo.lastRateLimitReset.Before(before.Add(5*time.Second)) && !accountRepo.lastRateLimitReset.After(after.Add(5*time.Second)))
  122. }