| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152 |
- //go:build unit
- package service
- import (
- "context"
- "encoding/json"
- "net/http"
- "testing"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/config"
- "github.com/stretchr/testify/require"
- )
- type rateLimit429AccountRepoStub struct {
- mockAccountRepoForGemini
- rateLimitCalls int
- lastRateLimitID int64
- lastRateLimitReset time.Time
- }
- func (r *rateLimit429AccountRepoStub) SetRateLimited(_ context.Context, id int64, resetAt time.Time) error {
- r.rateLimitCalls++
- r.lastRateLimitID = id
- r.lastRateLimitReset = resetAt
- return nil
- }
- func TestGetRateLimit429CooldownSettings_DefaultsWhenNotSet(t *testing.T) {
- repo := newMockSettingRepo()
- svc := NewSettingService(repo, &config.Config{})
- settings, err := svc.GetRateLimit429CooldownSettings(context.Background())
- require.NoError(t, err)
- require.True(t, settings.Enabled)
- require.Equal(t, 5, settings.CooldownSeconds)
- }
- func TestGetRateLimit429CooldownSettings_ReadsFromDB(t *testing.T) {
- repo := newMockSettingRepo()
- data, _ := json.Marshal(RateLimit429CooldownSettings{Enabled: false, CooldownSeconds: 12})
- repo.data[SettingKeyRateLimit429CooldownSettings] = string(data)
- svc := NewSettingService(repo, &config.Config{})
- settings, err := svc.GetRateLimit429CooldownSettings(context.Background())
- require.NoError(t, err)
- require.False(t, settings.Enabled)
- require.Equal(t, 12, settings.CooldownSeconds)
- }
- func TestSetRateLimit429CooldownSettings_EnabledRejectsOutOfRange(t *testing.T) {
- svc := NewSettingService(newMockSettingRepo(), &config.Config{})
- for _, seconds := range []int{0, -1, 7201, 99999} {
- err := svc.SetRateLimit429CooldownSettings(context.Background(), &RateLimit429CooldownSettings{
- Enabled: true, CooldownSeconds: seconds,
- })
- require.Error(t, err, "should reject enabled=true + cooldown_seconds=%d", seconds)
- require.Contains(t, err.Error(), "cooldown_seconds must be between 1-7200")
- }
- }
- func TestHandle429_FallbackUsesDBSeconds(t *testing.T) {
- accountRepo := &rateLimit429AccountRepoStub{}
- settingRepo := newMockSettingRepo()
- data, _ := json.Marshal(RateLimit429CooldownSettings{Enabled: true, CooldownSeconds: 12})
- settingRepo.data[SettingKeyRateLimit429CooldownSettings] = string(data)
- settingSvc := NewSettingService(settingRepo, &config.Config{})
- svc := NewRateLimitService(accountRepo, nil, &config.Config{}, nil, nil)
- svc.SetSettingService(settingSvc)
- account := &Account{ID: 42, Platform: PlatformOpenAI, Type: AccountTypeOAuth}
- before := time.Now()
- svc.handle429(context.Background(), account, http.Header{}, []byte(`{"error":{"type":"rate_limit_error","message":"slow down"}}`))
- after := time.Now()
- require.Equal(t, 1, accountRepo.rateLimitCalls)
- require.Equal(t, int64(42), accountRepo.lastRateLimitID)
- require.True(t, !accountRepo.lastRateLimitReset.Before(before.Add(12*time.Second)) && !accountRepo.lastRateLimitReset.After(after.Add(12*time.Second)))
- }
- func TestHandle429_FallbackDisabledSkipsLocalMark(t *testing.T) {
- accountRepo := &rateLimit429AccountRepoStub{}
- settingRepo := newMockSettingRepo()
- data, _ := json.Marshal(RateLimit429CooldownSettings{Enabled: false, CooldownSeconds: 12})
- settingRepo.data[SettingKeyRateLimit429CooldownSettings] = string(data)
- settingSvc := NewSettingService(settingRepo, &config.Config{})
- svc := NewRateLimitService(accountRepo, nil, &config.Config{}, nil, nil)
- svc.SetSettingService(settingSvc)
- account := &Account{ID: 43, Platform: PlatformOpenAI, Type: AccountTypeOAuth}
- svc.handle429(context.Background(), account, http.Header{}, []byte(`{"error":{"type":"rate_limit_error","message":"slow down"}}`))
- require.Zero(t, accountRepo.rateLimitCalls)
- }
- // Anthropic 无 reset 头的 429(如 Extra usage required)也应走兜底冷却,
- // 否则账号永不冷却,调度器会让每个请求反复撞同一批 429 账号(旋转木马)。
- func TestHandle429_AnthropicNoResetTimeUsesFallbackCooldown(t *testing.T) {
- accountRepo := &rateLimit429AccountRepoStub{}
- settingRepo := newMockSettingRepo()
- data, _ := json.Marshal(RateLimit429CooldownSettings{Enabled: true, CooldownSeconds: 12})
- settingRepo.data[SettingKeyRateLimit429CooldownSettings] = string(data)
- settingSvc := NewSettingService(settingRepo, &config.Config{})
- svc := NewRateLimitService(accountRepo, nil, &config.Config{}, nil, nil)
- svc.SetSettingService(settingSvc)
- account := &Account{ID: 45, Platform: PlatformAnthropic, Type: AccountTypeOAuth}
- before := time.Now()
- svc.handle429(context.Background(), account, http.Header{}, []byte(`{"error":{"type":"rate_limit_error","message":"Extra usage required"}}`))
- after := time.Now()
- require.Equal(t, 1, accountRepo.rateLimitCalls)
- require.Equal(t, int64(45), accountRepo.lastRateLimitID)
- require.True(t, !accountRepo.lastRateLimitReset.Before(before.Add(12*time.Second)) && !accountRepo.lastRateLimitReset.After(after.Add(12*time.Second)))
- }
- // 管理端关闭兜底冷却时,Anthropic 无 reset 头的 429 保持旧行为:不标记账号。
- func TestHandle429_AnthropicNoResetTimeFallbackDisabledSkipsMark(t *testing.T) {
- accountRepo := &rateLimit429AccountRepoStub{}
- settingRepo := newMockSettingRepo()
- data, _ := json.Marshal(RateLimit429CooldownSettings{Enabled: false, CooldownSeconds: 12})
- settingRepo.data[SettingKeyRateLimit429CooldownSettings] = string(data)
- settingSvc := NewSettingService(settingRepo, &config.Config{})
- svc := NewRateLimitService(accountRepo, nil, &config.Config{}, nil, nil)
- svc.SetSettingService(settingSvc)
- account := &Account{ID: 46, Platform: PlatformAnthropic, Type: AccountTypeOAuth}
- svc.handle429(context.Background(), account, http.Header{}, []byte(`{"error":{"type":"rate_limit_error","message":"Extra usage required"}}`))
- require.Zero(t, accountRepo.rateLimitCalls)
- }
- func TestHandle429_FallbackUsesDefaultSecondsWhenSettingServiceMissing(t *testing.T) {
- accountRepo := &rateLimit429AccountRepoStub{}
- cfg := &config.Config{}
- svc := NewRateLimitService(accountRepo, nil, cfg, nil, nil)
- account := &Account{ID: 44, Platform: PlatformGemini, Type: AccountTypeAPIKey}
- before := time.Now()
- svc.handle429(context.Background(), account, http.Header{}, []byte(`{"error":{"message":"slow down"}}`))
- after := time.Now()
- require.Equal(t, 1, accountRepo.rateLimitCalls)
- require.Equal(t, int64(44), accountRepo.lastRateLimitID)
- require.True(t, !accountRepo.lastRateLimitReset.Before(before.Add(5*time.Second)) && !accountRepo.lastRateLimitReset.After(after.Add(5*time.Second)))
- }
|