| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309 |
- //go:build unit
- package service
- import (
- "context"
- "errors"
- "testing"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/config"
- "github.com/stretchr/testify/require"
- )
- type rateLimitClearRepoStub struct {
- mockAccountRepoForGemini
- getByIDAccount *Account
- getByIDErr error
- getByIDCalls int
- clearErrorCalls int
- clearRateLimitCalls int
- clearAntigravityCalls int
- clearModelRateLimitCalls int
- clearTempUnschedCalls int
- clearErrorErr error
- clearRateLimitErr error
- clearAntigravityErr error
- clearModelRateLimitErr error
- clearTempUnschedulableErr error
- }
- func (r *rateLimitClearRepoStub) GetByID(ctx context.Context, id int64) (*Account, error) {
- r.getByIDCalls++
- if r.getByIDErr != nil {
- return nil, r.getByIDErr
- }
- return r.getByIDAccount, nil
- }
- func (r *rateLimitClearRepoStub) ClearError(ctx context.Context, id int64) error {
- r.clearErrorCalls++
- return r.clearErrorErr
- }
- func (r *rateLimitClearRepoStub) ClearRateLimit(ctx context.Context, id int64) error {
- r.clearRateLimitCalls++
- return r.clearRateLimitErr
- }
- func (r *rateLimitClearRepoStub) ClearAntigravityQuotaScopes(ctx context.Context, id int64) error {
- r.clearAntigravityCalls++
- return r.clearAntigravityErr
- }
- func (r *rateLimitClearRepoStub) ClearModelRateLimits(ctx context.Context, id int64) error {
- r.clearModelRateLimitCalls++
- return r.clearModelRateLimitErr
- }
- func (r *rateLimitClearRepoStub) ClearTempUnschedulable(ctx context.Context, id int64) error {
- r.clearTempUnschedCalls++
- return r.clearTempUnschedulableErr
- }
- type tempUnschedCacheRecorder struct {
- deletedIDs []int64
- deleteErr error
- }
- type recoverTokenInvalidatorStub struct {
- accounts []*Account
- err error
- }
- func (c *tempUnschedCacheRecorder) SetTempUnsched(ctx context.Context, accountID int64, state *TempUnschedState) error {
- return nil
- }
- func (c *tempUnschedCacheRecorder) GetTempUnsched(ctx context.Context, accountID int64) (*TempUnschedState, error) {
- return nil, nil
- }
- func (c *tempUnschedCacheRecorder) DeleteTempUnsched(ctx context.Context, accountID int64) error {
- c.deletedIDs = append(c.deletedIDs, accountID)
- return c.deleteErr
- }
- func (s *recoverTokenInvalidatorStub) InvalidateToken(ctx context.Context, account *Account) error {
- s.accounts = append(s.accounts, account)
- return s.err
- }
- func TestRateLimitService_ClearRateLimit_AlsoClearsTempUnschedulable(t *testing.T) {
- repo := &rateLimitClearRepoStub{}
- cache := &tempUnschedCacheRecorder{}
- svc := NewRateLimitService(repo, nil, &config.Config{}, nil, cache)
- err := svc.ClearRateLimit(context.Background(), 42)
- require.NoError(t, err)
- require.Equal(t, 1, repo.clearRateLimitCalls)
- require.Equal(t, 1, repo.clearAntigravityCalls)
- require.Equal(t, 1, repo.clearModelRateLimitCalls)
- require.Equal(t, 1, repo.clearTempUnschedCalls)
- require.Equal(t, []int64{42}, cache.deletedIDs)
- }
- func TestRateLimitService_ClearRateLimit_ClearTempUnschedulableFailed(t *testing.T) {
- repo := &rateLimitClearRepoStub{
- clearTempUnschedulableErr: errors.New("clear temp unsched failed"),
- }
- cache := &tempUnschedCacheRecorder{}
- svc := NewRateLimitService(repo, nil, &config.Config{}, nil, cache)
- err := svc.ClearRateLimit(context.Background(), 7)
- require.Error(t, err)
- require.Equal(t, 1, repo.clearTempUnschedCalls)
- require.Empty(t, cache.deletedIDs)
- }
- func TestRateLimitService_ClearRateLimit_ClearRateLimitFailed(t *testing.T) {
- repo := &rateLimitClearRepoStub{
- clearRateLimitErr: errors.New("clear rate limit failed"),
- }
- cache := &tempUnschedCacheRecorder{}
- svc := NewRateLimitService(repo, nil, &config.Config{}, nil, cache)
- err := svc.ClearRateLimit(context.Background(), 11)
- require.Error(t, err)
- require.Equal(t, 1, repo.clearRateLimitCalls)
- require.Equal(t, 0, repo.clearAntigravityCalls)
- require.Equal(t, 0, repo.clearModelRateLimitCalls)
- require.Equal(t, 0, repo.clearTempUnschedCalls)
- require.Empty(t, cache.deletedIDs)
- }
- func TestRateLimitService_ClearRateLimit_ClearAntigravityFailed(t *testing.T) {
- repo := &rateLimitClearRepoStub{
- clearAntigravityErr: errors.New("clear antigravity failed"),
- }
- cache := &tempUnschedCacheRecorder{}
- svc := NewRateLimitService(repo, nil, &config.Config{}, nil, cache)
- err := svc.ClearRateLimit(context.Background(), 12)
- require.Error(t, err)
- require.Equal(t, 1, repo.clearRateLimitCalls)
- require.Equal(t, 1, repo.clearAntigravityCalls)
- require.Equal(t, 0, repo.clearModelRateLimitCalls)
- require.Equal(t, 0, repo.clearTempUnschedCalls)
- require.Empty(t, cache.deletedIDs)
- }
- func TestRateLimitService_ClearRateLimit_ClearModelRateLimitsFailed(t *testing.T) {
- repo := &rateLimitClearRepoStub{
- clearModelRateLimitErr: errors.New("clear model rate limits failed"),
- }
- cache := &tempUnschedCacheRecorder{}
- svc := NewRateLimitService(repo, nil, &config.Config{}, nil, cache)
- err := svc.ClearRateLimit(context.Background(), 13)
- require.Error(t, err)
- require.Equal(t, 1, repo.clearRateLimitCalls)
- require.Equal(t, 1, repo.clearAntigravityCalls)
- require.Equal(t, 1, repo.clearModelRateLimitCalls)
- require.Equal(t, 0, repo.clearTempUnschedCalls)
- require.Empty(t, cache.deletedIDs)
- }
- func TestRateLimitService_ClearRateLimit_CacheDeleteFailedShouldNotFail(t *testing.T) {
- repo := &rateLimitClearRepoStub{}
- cache := &tempUnschedCacheRecorder{
- deleteErr: errors.New("cache delete failed"),
- }
- svc := NewRateLimitService(repo, nil, &config.Config{}, nil, cache)
- err := svc.ClearRateLimit(context.Background(), 14)
- require.NoError(t, err)
- require.Equal(t, 1, repo.clearRateLimitCalls)
- require.Equal(t, 1, repo.clearAntigravityCalls)
- require.Equal(t, 1, repo.clearModelRateLimitCalls)
- require.Equal(t, 1, repo.clearTempUnschedCalls)
- require.Equal(t, []int64{14}, cache.deletedIDs)
- }
- func TestRateLimitService_ClearRateLimit_WithoutTempUnschedCache(t *testing.T) {
- repo := &rateLimitClearRepoStub{}
- svc := NewRateLimitService(repo, nil, &config.Config{}, nil, nil)
- err := svc.ClearRateLimit(context.Background(), 15)
- require.NoError(t, err)
- require.Equal(t, 1, repo.clearRateLimitCalls)
- require.Equal(t, 1, repo.clearAntigravityCalls)
- require.Equal(t, 1, repo.clearModelRateLimitCalls)
- require.Equal(t, 1, repo.clearTempUnschedCalls)
- }
- func TestRateLimitService_RecoverAccountAfterSuccessfulTest_PreservesOtherAndConcurrentLimits(t *testing.T) {
- now := time.Now()
- repo := &rateLimitClearRepoStub{
- getByIDAccount: &Account{
- ID: 42,
- Status: StatusError,
- RateLimitedAt: &now,
- TempUnschedulableUntil: &now,
- Extra: map[string]any{
- "model_rate_limits": map[string]any{
- "claude-sonnet-4-5": map[string]any{
- "rate_limit_reset_at": now.Format(time.RFC3339),
- },
- },
- "antigravity_quota_scopes": map[string]any{"gemini": true},
- },
- },
- }
- cache := &tempUnschedCacheRecorder{}
- blocker := &runtimeBlockRecorder{}
- svc := NewRateLimitService(repo, nil, &config.Config{}, nil, cache)
- svc.SetAccountRuntimeBlocker(blocker)
- result, err := svc.RecoverAccountAfterSuccessfulTest(context.Background(), 42)
- require.NoError(t, err)
- require.NotNil(t, result)
- require.False(t, result.ClearedError)
- require.False(t, result.ClearedRateLimit)
- require.Equal(t, 1, repo.getByIDCalls)
- require.Zero(t, repo.clearErrorCalls)
- require.Zero(t, repo.clearRateLimitCalls)
- require.Zero(t, repo.clearAntigravityCalls)
- require.Zero(t, repo.clearModelRateLimitCalls)
- require.Zero(t, repo.clearTempUnschedCalls)
- require.Empty(t, cache.deletedIDs)
- require.Empty(t, blocker.clearedIDs)
- }
- func TestRateLimitService_RecoverAccountAfterSuccessfulTest_NoRecoverableStateIsNoop(t *testing.T) {
- repo := &rateLimitClearRepoStub{
- getByIDAccount: &Account{
- ID: 7,
- Status: StatusActive,
- Schedulable: true,
- Extra: map[string]any{},
- },
- }
- cache := &tempUnschedCacheRecorder{}
- svc := NewRateLimitService(repo, nil, &config.Config{}, nil, cache)
- result, err := svc.RecoverAccountAfterSuccessfulTest(context.Background(), 7)
- require.NoError(t, err)
- require.NotNil(t, result)
- require.False(t, result.ClearedError)
- require.False(t, result.ClearedRateLimit)
- require.Equal(t, 1, repo.getByIDCalls)
- require.Equal(t, 0, repo.clearErrorCalls)
- require.Equal(t, 0, repo.clearRateLimitCalls)
- require.Equal(t, 0, repo.clearAntigravityCalls)
- require.Equal(t, 0, repo.clearModelRateLimitCalls)
- require.Equal(t, 0, repo.clearTempUnschedCalls)
- require.Empty(t, cache.deletedIDs)
- }
- func TestRateLimitService_RecoverAccountAfterSuccessfulTest_DoesNotInvokeRecovery(t *testing.T) {
- repo := &rateLimitClearRepoStub{
- getByIDAccount: &Account{
- ID: 9,
- Status: StatusError,
- },
- clearErrorErr: errors.New("clear error failed"),
- }
- svc := NewRateLimitService(repo, nil, &config.Config{}, nil, nil)
- result, err := svc.RecoverAccountAfterSuccessfulTest(context.Background(), 9)
- require.NoError(t, err)
- require.NotNil(t, result)
- require.Equal(t, 1, repo.getByIDCalls)
- require.Zero(t, repo.clearErrorCalls)
- require.Equal(t, 0, repo.clearRateLimitCalls)
- }
- func TestRateLimitService_RecoverAccountState_InvalidatesOAuthTokenOnErrorRecovery(t *testing.T) {
- repo := &rateLimitClearRepoStub{
- getByIDAccount: &Account{
- ID: 21,
- Type: AccountTypeOAuth,
- Status: StatusError,
- },
- }
- invalidator := &recoverTokenInvalidatorStub{}
- svc := NewRateLimitService(repo, nil, &config.Config{}, nil, nil)
- svc.SetTokenCacheInvalidator(invalidator)
- result, err := svc.RecoverAccountState(context.Background(), 21, AccountRecoveryOptions{
- InvalidateToken: true,
- })
- require.NoError(t, err)
- require.NotNil(t, result)
- require.True(t, result.ClearedError)
- require.False(t, result.ClearedRateLimit)
- require.Equal(t, 1, repo.clearErrorCalls)
- require.Len(t, invalidator.accounts, 1)
- require.Equal(t, int64(21), invalidator.accounts[0].ID)
- }
|