ratelimit_service_clear_test.go 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. //go:build unit
  2. package service
  3. import (
  4. "context"
  5. "errors"
  6. "testing"
  7. "time"
  8. "github.com/Wei-Shaw/sub2api/internal/config"
  9. "github.com/stretchr/testify/require"
  10. )
  11. type rateLimitClearRepoStub struct {
  12. mockAccountRepoForGemini
  13. getByIDAccount *Account
  14. getByIDErr error
  15. getByIDCalls int
  16. clearErrorCalls int
  17. clearRateLimitCalls int
  18. clearAntigravityCalls int
  19. clearModelRateLimitCalls int
  20. clearTempUnschedCalls int
  21. clearErrorErr error
  22. clearRateLimitErr error
  23. clearAntigravityErr error
  24. clearModelRateLimitErr error
  25. clearTempUnschedulableErr error
  26. }
  27. func (r *rateLimitClearRepoStub) GetByID(ctx context.Context, id int64) (*Account, error) {
  28. r.getByIDCalls++
  29. if r.getByIDErr != nil {
  30. return nil, r.getByIDErr
  31. }
  32. return r.getByIDAccount, nil
  33. }
  34. func (r *rateLimitClearRepoStub) ClearError(ctx context.Context, id int64) error {
  35. r.clearErrorCalls++
  36. return r.clearErrorErr
  37. }
  38. func (r *rateLimitClearRepoStub) ClearRateLimit(ctx context.Context, id int64) error {
  39. r.clearRateLimitCalls++
  40. return r.clearRateLimitErr
  41. }
  42. func (r *rateLimitClearRepoStub) ClearAntigravityQuotaScopes(ctx context.Context, id int64) error {
  43. r.clearAntigravityCalls++
  44. return r.clearAntigravityErr
  45. }
  46. func (r *rateLimitClearRepoStub) ClearModelRateLimits(ctx context.Context, id int64) error {
  47. r.clearModelRateLimitCalls++
  48. return r.clearModelRateLimitErr
  49. }
  50. func (r *rateLimitClearRepoStub) ClearTempUnschedulable(ctx context.Context, id int64) error {
  51. r.clearTempUnschedCalls++
  52. return r.clearTempUnschedulableErr
  53. }
  54. type tempUnschedCacheRecorder struct {
  55. deletedIDs []int64
  56. deleteErr error
  57. }
  58. type recoverTokenInvalidatorStub struct {
  59. accounts []*Account
  60. err error
  61. }
  62. func (c *tempUnschedCacheRecorder) SetTempUnsched(ctx context.Context, accountID int64, state *TempUnschedState) error {
  63. return nil
  64. }
  65. func (c *tempUnschedCacheRecorder) GetTempUnsched(ctx context.Context, accountID int64) (*TempUnschedState, error) {
  66. return nil, nil
  67. }
  68. func (c *tempUnschedCacheRecorder) DeleteTempUnsched(ctx context.Context, accountID int64) error {
  69. c.deletedIDs = append(c.deletedIDs, accountID)
  70. return c.deleteErr
  71. }
  72. func (s *recoverTokenInvalidatorStub) InvalidateToken(ctx context.Context, account *Account) error {
  73. s.accounts = append(s.accounts, account)
  74. return s.err
  75. }
  76. func TestRateLimitService_ClearRateLimit_AlsoClearsTempUnschedulable(t *testing.T) {
  77. repo := &rateLimitClearRepoStub{}
  78. cache := &tempUnschedCacheRecorder{}
  79. svc := NewRateLimitService(repo, nil, &config.Config{}, nil, cache)
  80. err := svc.ClearRateLimit(context.Background(), 42)
  81. require.NoError(t, err)
  82. require.Equal(t, 1, repo.clearRateLimitCalls)
  83. require.Equal(t, 1, repo.clearAntigravityCalls)
  84. require.Equal(t, 1, repo.clearModelRateLimitCalls)
  85. require.Equal(t, 1, repo.clearTempUnschedCalls)
  86. require.Equal(t, []int64{42}, cache.deletedIDs)
  87. }
  88. func TestRateLimitService_ClearRateLimit_ClearTempUnschedulableFailed(t *testing.T) {
  89. repo := &rateLimitClearRepoStub{
  90. clearTempUnschedulableErr: errors.New("clear temp unsched failed"),
  91. }
  92. cache := &tempUnschedCacheRecorder{}
  93. svc := NewRateLimitService(repo, nil, &config.Config{}, nil, cache)
  94. err := svc.ClearRateLimit(context.Background(), 7)
  95. require.Error(t, err)
  96. require.Equal(t, 1, repo.clearTempUnschedCalls)
  97. require.Empty(t, cache.deletedIDs)
  98. }
  99. func TestRateLimitService_ClearRateLimit_ClearRateLimitFailed(t *testing.T) {
  100. repo := &rateLimitClearRepoStub{
  101. clearRateLimitErr: errors.New("clear rate limit failed"),
  102. }
  103. cache := &tempUnschedCacheRecorder{}
  104. svc := NewRateLimitService(repo, nil, &config.Config{}, nil, cache)
  105. err := svc.ClearRateLimit(context.Background(), 11)
  106. require.Error(t, err)
  107. require.Equal(t, 1, repo.clearRateLimitCalls)
  108. require.Equal(t, 0, repo.clearAntigravityCalls)
  109. require.Equal(t, 0, repo.clearModelRateLimitCalls)
  110. require.Equal(t, 0, repo.clearTempUnschedCalls)
  111. require.Empty(t, cache.deletedIDs)
  112. }
  113. func TestRateLimitService_ClearRateLimit_ClearAntigravityFailed(t *testing.T) {
  114. repo := &rateLimitClearRepoStub{
  115. clearAntigravityErr: errors.New("clear antigravity failed"),
  116. }
  117. cache := &tempUnschedCacheRecorder{}
  118. svc := NewRateLimitService(repo, nil, &config.Config{}, nil, cache)
  119. err := svc.ClearRateLimit(context.Background(), 12)
  120. require.Error(t, err)
  121. require.Equal(t, 1, repo.clearRateLimitCalls)
  122. require.Equal(t, 1, repo.clearAntigravityCalls)
  123. require.Equal(t, 0, repo.clearModelRateLimitCalls)
  124. require.Equal(t, 0, repo.clearTempUnschedCalls)
  125. require.Empty(t, cache.deletedIDs)
  126. }
  127. func TestRateLimitService_ClearRateLimit_ClearModelRateLimitsFailed(t *testing.T) {
  128. repo := &rateLimitClearRepoStub{
  129. clearModelRateLimitErr: errors.New("clear model rate limits failed"),
  130. }
  131. cache := &tempUnschedCacheRecorder{}
  132. svc := NewRateLimitService(repo, nil, &config.Config{}, nil, cache)
  133. err := svc.ClearRateLimit(context.Background(), 13)
  134. require.Error(t, err)
  135. require.Equal(t, 1, repo.clearRateLimitCalls)
  136. require.Equal(t, 1, repo.clearAntigravityCalls)
  137. require.Equal(t, 1, repo.clearModelRateLimitCalls)
  138. require.Equal(t, 0, repo.clearTempUnschedCalls)
  139. require.Empty(t, cache.deletedIDs)
  140. }
  141. func TestRateLimitService_ClearRateLimit_CacheDeleteFailedShouldNotFail(t *testing.T) {
  142. repo := &rateLimitClearRepoStub{}
  143. cache := &tempUnschedCacheRecorder{
  144. deleteErr: errors.New("cache delete failed"),
  145. }
  146. svc := NewRateLimitService(repo, nil, &config.Config{}, nil, cache)
  147. err := svc.ClearRateLimit(context.Background(), 14)
  148. require.NoError(t, err)
  149. require.Equal(t, 1, repo.clearRateLimitCalls)
  150. require.Equal(t, 1, repo.clearAntigravityCalls)
  151. require.Equal(t, 1, repo.clearModelRateLimitCalls)
  152. require.Equal(t, 1, repo.clearTempUnschedCalls)
  153. require.Equal(t, []int64{14}, cache.deletedIDs)
  154. }
  155. func TestRateLimitService_ClearRateLimit_WithoutTempUnschedCache(t *testing.T) {
  156. repo := &rateLimitClearRepoStub{}
  157. svc := NewRateLimitService(repo, nil, &config.Config{}, nil, nil)
  158. err := svc.ClearRateLimit(context.Background(), 15)
  159. require.NoError(t, err)
  160. require.Equal(t, 1, repo.clearRateLimitCalls)
  161. require.Equal(t, 1, repo.clearAntigravityCalls)
  162. require.Equal(t, 1, repo.clearModelRateLimitCalls)
  163. require.Equal(t, 1, repo.clearTempUnschedCalls)
  164. }
  165. func TestRateLimitService_RecoverAccountAfterSuccessfulTest_PreservesOtherAndConcurrentLimits(t *testing.T) {
  166. now := time.Now()
  167. repo := &rateLimitClearRepoStub{
  168. getByIDAccount: &Account{
  169. ID: 42,
  170. Status: StatusError,
  171. RateLimitedAt: &now,
  172. TempUnschedulableUntil: &now,
  173. Extra: map[string]any{
  174. "model_rate_limits": map[string]any{
  175. "claude-sonnet-4-5": map[string]any{
  176. "rate_limit_reset_at": now.Format(time.RFC3339),
  177. },
  178. },
  179. "antigravity_quota_scopes": map[string]any{"gemini": true},
  180. },
  181. },
  182. }
  183. cache := &tempUnschedCacheRecorder{}
  184. blocker := &runtimeBlockRecorder{}
  185. svc := NewRateLimitService(repo, nil, &config.Config{}, nil, cache)
  186. svc.SetAccountRuntimeBlocker(blocker)
  187. result, err := svc.RecoverAccountAfterSuccessfulTest(context.Background(), 42)
  188. require.NoError(t, err)
  189. require.NotNil(t, result)
  190. require.False(t, result.ClearedError)
  191. require.False(t, result.ClearedRateLimit)
  192. require.Equal(t, 1, repo.getByIDCalls)
  193. require.Zero(t, repo.clearErrorCalls)
  194. require.Zero(t, repo.clearRateLimitCalls)
  195. require.Zero(t, repo.clearAntigravityCalls)
  196. require.Zero(t, repo.clearModelRateLimitCalls)
  197. require.Zero(t, repo.clearTempUnschedCalls)
  198. require.Empty(t, cache.deletedIDs)
  199. require.Empty(t, blocker.clearedIDs)
  200. }
  201. func TestRateLimitService_RecoverAccountAfterSuccessfulTest_NoRecoverableStateIsNoop(t *testing.T) {
  202. repo := &rateLimitClearRepoStub{
  203. getByIDAccount: &Account{
  204. ID: 7,
  205. Status: StatusActive,
  206. Schedulable: true,
  207. Extra: map[string]any{},
  208. },
  209. }
  210. cache := &tempUnschedCacheRecorder{}
  211. svc := NewRateLimitService(repo, nil, &config.Config{}, nil, cache)
  212. result, err := svc.RecoverAccountAfterSuccessfulTest(context.Background(), 7)
  213. require.NoError(t, err)
  214. require.NotNil(t, result)
  215. require.False(t, result.ClearedError)
  216. require.False(t, result.ClearedRateLimit)
  217. require.Equal(t, 1, repo.getByIDCalls)
  218. require.Equal(t, 0, repo.clearErrorCalls)
  219. require.Equal(t, 0, repo.clearRateLimitCalls)
  220. require.Equal(t, 0, repo.clearAntigravityCalls)
  221. require.Equal(t, 0, repo.clearModelRateLimitCalls)
  222. require.Equal(t, 0, repo.clearTempUnschedCalls)
  223. require.Empty(t, cache.deletedIDs)
  224. }
  225. func TestRateLimitService_RecoverAccountAfterSuccessfulTest_DoesNotInvokeRecovery(t *testing.T) {
  226. repo := &rateLimitClearRepoStub{
  227. getByIDAccount: &Account{
  228. ID: 9,
  229. Status: StatusError,
  230. },
  231. clearErrorErr: errors.New("clear error failed"),
  232. }
  233. svc := NewRateLimitService(repo, nil, &config.Config{}, nil, nil)
  234. result, err := svc.RecoverAccountAfterSuccessfulTest(context.Background(), 9)
  235. require.NoError(t, err)
  236. require.NotNil(t, result)
  237. require.Equal(t, 1, repo.getByIDCalls)
  238. require.Zero(t, repo.clearErrorCalls)
  239. require.Equal(t, 0, repo.clearRateLimitCalls)
  240. }
  241. func TestRateLimitService_RecoverAccountState_InvalidatesOAuthTokenOnErrorRecovery(t *testing.T) {
  242. repo := &rateLimitClearRepoStub{
  243. getByIDAccount: &Account{
  244. ID: 21,
  245. Type: AccountTypeOAuth,
  246. Status: StatusError,
  247. },
  248. }
  249. invalidator := &recoverTokenInvalidatorStub{}
  250. svc := NewRateLimitService(repo, nil, &config.Config{}, nil, nil)
  251. svc.SetTokenCacheInvalidator(invalidator)
  252. result, err := svc.RecoverAccountState(context.Background(), 21, AccountRecoveryOptions{
  253. InvalidateToken: true,
  254. })
  255. require.NoError(t, err)
  256. require.NotNil(t, result)
  257. require.True(t, result.ClearedError)
  258. require.False(t, result.ClearedRateLimit)
  259. require.Equal(t, 1, repo.clearErrorCalls)
  260. require.Len(t, invalidator.accounts, 1)
  261. require.Equal(t, int64(21), invalidator.accounts[0].ID)
  262. }