grok_quota_fetcher_test.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. //go:build unit
  2. package service
  3. import (
  4. "net/http"
  5. "testing"
  6. "time"
  7. "github.com/Wei-Shaw/sub2api/internal/pkg/xai"
  8. "github.com/stretchr/testify/require"
  9. )
  10. func grokInt64PtrForTest(v int64) *int64 { return &v }
  11. func grokIntPtrForTest(v int) *int { return &v }
  12. func TestGrokQuotaFetcherBuildUsageInfoUnknownUntilFirstSnapshot(t *testing.T) {
  13. t.Parallel()
  14. usage := NewGrokQuotaFetcher().BuildUsageInfo(&Account{Platform: PlatformGrok, Type: AccountTypeOAuth})
  15. require.Equal(t, "passive", usage.Source)
  16. require.Equal(t, xai.GrokFreeRolling24hTokenLimit, usage.GrokFreeTokenLimit)
  17. require.Equal(t, "quota_unknown", usage.ErrorCode)
  18. require.Contains(t, usage.Error, "unknown until billing is probed")
  19. }
  20. func TestGrokQuotaFetcherDoesNotTreatGrok45ResponsesWindowAsHeavy(t *testing.T) {
  21. t.Parallel()
  22. // 8300 / 53M is the grok-4.5 Responses rate-limit window, not a plan fingerprint.
  23. reqLimit, tokLimit := int64(8300), int64(53_000_000)
  24. fresh := time.Now().UTC().Format(time.RFC3339)
  25. account := &Account{
  26. Platform: PlatformGrok,
  27. Type: AccountTypeOAuth,
  28. Credentials: map[string]any{
  29. "subscription_tier": "SuperGrokPro",
  30. },
  31. Extra: map[string]any{
  32. grokQuotaSnapshotExtraKey: &xai.QuotaSnapshot{
  33. Requests: &xai.QuotaWindow{Limit: &reqLimit},
  34. Tokens: &xai.QuotaWindow{Limit: &tokLimit},
  35. LastHeadersSeenAt: fresh,
  36. HeadersObserved: true,
  37. UpdatedAt: fresh,
  38. },
  39. },
  40. }
  41. usage := NewGrokQuotaFetcher().BuildUsageInfo(account)
  42. require.Equal(t, "supergrok", usage.SubscriptionTier)
  43. }
  44. func TestGrokQuotaFetcherUsesGrok45ResponsesWindowAsHeavy(t *testing.T) {
  45. t.Parallel()
  46. reqLimit, tokLimit := int64(8300), int64(53_000_000)
  47. fresh := time.Now().UTC().Format(time.RFC3339)
  48. account := &Account{
  49. Platform: PlatformGrok,
  50. Type: AccountTypeOAuth,
  51. Credentials: map[string]any{
  52. "subscription_tier": "SuperGrokPro",
  53. },
  54. Extra: map[string]any{
  55. grokQuotaSnapshotExtraKey: &xai.QuotaSnapshot{
  56. Model: "grok-4.5",
  57. Requests: &xai.QuotaWindow{Limit: &reqLimit},
  58. Tokens: &xai.QuotaWindow{Limit: &tokLimit},
  59. LastHeadersSeenAt: fresh,
  60. HeadersObserved: true,
  61. UpdatedAt: fresh,
  62. },
  63. },
  64. }
  65. usage := NewGrokQuotaFetcher().BuildUsageInfo(account)
  66. require.Equal(t, "supergrok_heavy", usage.SubscriptionTier)
  67. }
  68. func TestGrokQuotaFetcherJWTBeatsAmbiguousSuperGrokProQuota(t *testing.T) {
  69. t.Parallel()
  70. heavyReq := int64(8300)
  71. fresh := time.Now().UTC().Format(time.RFC3339)
  72. account := &Account{
  73. Platform: PlatformGrok,
  74. Type: AccountTypeOAuth,
  75. Credentials: map[string]any{
  76. "access_token": makeGrokOAuthJWT(map[string]any{"tier": 1}),
  77. "subscription_tier": "SuperGrokPro",
  78. },
  79. Extra: map[string]any{
  80. grokQuotaSnapshotExtraKey: &xai.QuotaSnapshot{
  81. Requests: &xai.QuotaWindow{Limit: &heavyReq},
  82. LastHeadersSeenAt: fresh,
  83. HeadersObserved: true,
  84. UpdatedAt: fresh,
  85. },
  86. },
  87. }
  88. usage := NewGrokQuotaFetcher().BuildUsageInfo(account)
  89. require.Equal(t, "supergrok", usage.SubscriptionTier)
  90. }
  91. func TestGrokQuotaFetcherPrefersLiveJWTTierOverStaleBillingPlan(t *testing.T) {
  92. t.Parallel()
  93. account := &Account{
  94. Platform: PlatformGrok,
  95. Type: AccountTypeOAuth,
  96. Credentials: map[string]any{
  97. "access_token": makeGrokOAuthJWT(map[string]any{"tier": 0}),
  98. "subscription_tier": "supergrok_heavy",
  99. },
  100. Extra: map[string]any{
  101. grokBillingExtraKey: &xai.BillingSummary{
  102. Plan: "SuperGrok Heavy",
  103. StatusCode: http.StatusOK,
  104. UpdatedAt: "2030-01-01T00:00:00Z",
  105. },
  106. },
  107. }
  108. usage := NewGrokQuotaFetcher().BuildUsageInfo(account)
  109. require.Equal(t, "free", usage.SubscriptionTier)
  110. require.Equal(t, "free", usage.SubscriptionTierRaw)
  111. }
  112. func TestGrokQuotaFetcherUsesCredentialTierWhenBillingHasNoPlan(t *testing.T) {
  113. t.Parallel()
  114. account := &Account{
  115. Platform: PlatformGrok,
  116. Type: AccountTypeOAuth,
  117. Credentials: map[string]any{
  118. "subscription_tier": " FREE ",
  119. "entitlement_status": " active ",
  120. },
  121. Extra: map[string]any{
  122. grokBillingExtraKey: &xai.BillingSummary{
  123. PeriodType: "weekly",
  124. StatusCode: http.StatusOK,
  125. UpdatedAt: "2030-01-01T00:00:00Z",
  126. },
  127. },
  128. }
  129. usage := NewGrokQuotaFetcher().BuildUsageInfo(account)
  130. require.NotNil(t, usage.GrokBilling)
  131. require.Equal(t, "free", usage.SubscriptionTier)
  132. require.Equal(t, "FREE", usage.SubscriptionTierRaw)
  133. require.Equal(t, "active", usage.GrokEntitlementStatus)
  134. }
  135. func TestGrokQuotaFetcherBuildUsageInfoFromSnapshot(t *testing.T) {
  136. t.Parallel()
  137. updatedAt := "2030-01-01T00:00:00Z"
  138. account := &Account{
  139. Platform: PlatformGrok,
  140. Type: AccountTypeOAuth,
  141. Extra: map[string]any{
  142. grokQuotaSnapshotExtraKey: &xai.QuotaSnapshot{
  143. Requests: &xai.QuotaWindow{
  144. Limit: grokInt64PtrForTest(100),
  145. Remaining: grokInt64PtrForTest(12),
  146. ResetAt: updatedAt,
  147. },
  148. Tokens: &xai.QuotaWindow{
  149. Limit: grokInt64PtrForTest(1000),
  150. Remaining: grokInt64PtrForTest(900),
  151. },
  152. RetryAfterSeconds: grokIntPtrForTest(30),
  153. SubscriptionTier: "supergrok",
  154. EntitlementStatus: "active",
  155. StatusCode: http.StatusTooManyRequests,
  156. LastProbeAt: updatedAt,
  157. LastHeadersSeenAt: updatedAt,
  158. UpdatedAt: updatedAt,
  159. },
  160. },
  161. }
  162. usage := NewGrokQuotaFetcher().BuildUsageInfo(account)
  163. require.Equal(t, "passive", usage.Source)
  164. require.Equal(t, "rate_limited", usage.ErrorCode)
  165. require.Equal(t, "observed", usage.GrokQuotaSnapshotState)
  166. require.Equal(t, "supergrok", usage.SubscriptionTier)
  167. require.Equal(t, "active", usage.GrokEntitlementStatus)
  168. require.Equal(t, int64(100), *usage.GrokRequestQuota.Limit)
  169. require.Equal(t, int64(12), *usage.GrokRequestQuota.Remaining)
  170. require.Equal(t, 30, *usage.GrokRetryAfterSeconds)
  171. require.NotNil(t, usage.UpdatedAt)
  172. require.Equal(t, updatedAt, usage.GrokLastQuotaProbeAt)
  173. require.Equal(t, updatedAt, usage.GrokLastHeadersSeenAt)
  174. require.Equal(t, http.StatusTooManyRequests, usage.GrokLastStatusCode)
  175. require.True(t, usage.UpdatedAt.Equal(time.Date(2030, 1, 1, 0, 0, 0, 0, time.UTC)))
  176. }
  177. func TestGrokQuotaFetcherSnapshotErrorOverridesSuccessfulBillingStatus(t *testing.T) {
  178. t.Parallel()
  179. updatedAt := "2030-01-01T00:00:00Z"
  180. account := &Account{
  181. Platform: PlatformGrok,
  182. Type: AccountTypeOAuth,
  183. Extra: map[string]any{
  184. grokBillingExtraKey: &xai.BillingSummary{
  185. PeriodType: "weekly",
  186. StatusCode: http.StatusOK,
  187. UpdatedAt: updatedAt,
  188. },
  189. grokQuotaSnapshotExtraKey: &xai.QuotaSnapshot{
  190. StatusCode: http.StatusTooManyRequests,
  191. UpdatedAt: updatedAt,
  192. },
  193. },
  194. }
  195. usage := NewGrokQuotaFetcher().BuildUsageInfo(account)
  196. require.Equal(t, "rate_limited", usage.ErrorCode)
  197. require.Equal(t, http.StatusTooManyRequests, usage.GrokLastStatusCode)
  198. }
  199. func TestGrokQuotaFetcherNewerSuccessfulActiveProbeClearsBillingForbidden(t *testing.T) {
  200. t.Parallel()
  201. billingAt := "2030-01-01T00:00:00Z"
  202. probeAt := "2030-01-01T00:05:00Z"
  203. account := &Account{
  204. Platform: PlatformGrok,
  205. Type: AccountTypeOAuth,
  206. Credentials: map[string]any{
  207. "entitlement_status": "forbidden",
  208. },
  209. Extra: map[string]any{
  210. grokBillingExtraKey: &xai.BillingSummary{
  211. StatusCode: http.StatusForbidden,
  212. UpdatedAt: billingAt,
  213. },
  214. grokQuotaSnapshotExtraKey: &xai.QuotaSnapshot{
  215. StatusCode: http.StatusOK,
  216. ObservationSource: "active_probe",
  217. LastProbeAt: probeAt,
  218. UpdatedAt: probeAt,
  219. },
  220. },
  221. }
  222. usage := NewGrokQuotaFetcher().BuildUsageInfo(account)
  223. require.False(t, usage.IsForbidden)
  224. require.Empty(t, usage.ForbiddenType)
  225. require.Empty(t, usage.ErrorCode)
  226. require.Empty(t, usage.GrokEntitlementStatus)
  227. require.Equal(t, http.StatusOK, usage.GrokLastStatusCode)
  228. require.Equal(t, probeAt, usage.GrokLastQuotaProbeAt)
  229. require.NotNil(t, usage.UpdatedAt)
  230. require.True(t, usage.UpdatedAt.Equal(time.Date(2030, 1, 1, 0, 5, 0, 0, time.UTC)))
  231. }
  232. func TestGrokQuotaFetcherSameSecondSuccessfulActiveProbeClearsBillingForbidden(t *testing.T) {
  233. t.Parallel()
  234. observedAt := "2030-01-01T00:05:00Z"
  235. account := &Account{
  236. Platform: PlatformGrok,
  237. Type: AccountTypeOAuth,
  238. Extra: map[string]any{
  239. grokBillingExtraKey: &xai.BillingSummary{
  240. StatusCode: http.StatusForbidden,
  241. UpdatedAt: observedAt,
  242. },
  243. grokQuotaSnapshotExtraKey: &xai.QuotaSnapshot{
  244. StatusCode: http.StatusOK,
  245. ObservationSource: "active_probe",
  246. LastProbeAt: observedAt,
  247. UpdatedAt: observedAt,
  248. },
  249. },
  250. }
  251. usage := NewGrokQuotaFetcher().BuildUsageInfo(account)
  252. require.False(t, usage.IsForbidden)
  253. require.Empty(t, usage.ForbiddenType)
  254. require.Empty(t, usage.ErrorCode)
  255. require.Equal(t, http.StatusOK, usage.GrokLastStatusCode)
  256. }
  257. func TestGrokQuotaFetcherDoesNotClearBillingForbiddenWithoutNewerSuccessfulActiveProbe(t *testing.T) {
  258. t.Parallel()
  259. tests := []struct {
  260. name string
  261. snapshot xai.QuotaSnapshot
  262. }{
  263. {
  264. name: "older active probe",
  265. snapshot: xai.QuotaSnapshot{
  266. StatusCode: http.StatusOK,
  267. ObservationSource: "active_probe",
  268. LastProbeAt: "2030-01-01T00:04:59Z",
  269. UpdatedAt: "2030-01-01T00:04:59Z",
  270. },
  271. },
  272. {
  273. name: "newer passive response",
  274. snapshot: xai.QuotaSnapshot{
  275. StatusCode: http.StatusOK,
  276. ObservationSource: "upstream_response",
  277. UpdatedAt: "2030-01-01T00:05:01Z",
  278. },
  279. },
  280. {
  281. name: "newer failed active probe",
  282. snapshot: xai.QuotaSnapshot{
  283. StatusCode: http.StatusTooManyRequests,
  284. ObservationSource: "active_probe",
  285. LastProbeAt: "2030-01-01T00:05:01Z",
  286. UpdatedAt: "2030-01-01T00:05:01Z",
  287. },
  288. },
  289. }
  290. for _, tt := range tests {
  291. t.Run(tt.name, func(t *testing.T) {
  292. t.Parallel()
  293. account := &Account{
  294. Platform: PlatformGrok,
  295. Type: AccountTypeOAuth,
  296. Extra: map[string]any{
  297. grokBillingExtraKey: &xai.BillingSummary{
  298. StatusCode: http.StatusForbidden,
  299. UpdatedAt: "2030-01-01T00:05:00Z",
  300. },
  301. grokQuotaSnapshotExtraKey: tt.snapshot,
  302. },
  303. }
  304. usage := NewGrokQuotaFetcher().BuildUsageInfo(account)
  305. require.True(t, usage.IsForbidden)
  306. require.Equal(t, "forbidden", usage.ForbiddenType)
  307. require.Equal(t, "forbidden", usage.ErrorCode)
  308. })
  309. }
  310. }
  311. func TestGrokQuotaFetcherBuildUsageInfoFromNoHeadersProbe(t *testing.T) {
  312. t.Parallel()
  313. probedAt := "2030-01-01T00:00:00Z"
  314. account := &Account{
  315. Platform: PlatformGrok,
  316. Type: AccountTypeOAuth,
  317. Extra: map[string]any{
  318. grokQuotaSnapshotExtraKey: xai.QuotaSnapshot{
  319. StatusCode: http.StatusOK,
  320. HeadersObserved: false,
  321. ObservationSource: "active_probe",
  322. LastProbeAt: probedAt,
  323. UpdatedAt: probedAt,
  324. },
  325. },
  326. }
  327. usage := NewGrokQuotaFetcher().BuildUsageInfo(account)
  328. require.Equal(t, "quota_unknown", usage.ErrorCode)
  329. require.Equal(t, "no_headers", usage.GrokQuotaSnapshotState)
  330. require.Contains(t, usage.Error, "No xAI quota headers observed")
  331. require.Equal(t, probedAt, usage.GrokLastQuotaProbeAt)
  332. require.Empty(t, usage.GrokLastHeadersSeenAt)
  333. require.Equal(t, http.StatusOK, usage.GrokLastStatusCode)
  334. require.Nil(t, usage.GrokRequestQuota)
  335. require.Nil(t, usage.GrokTokenQuota)
  336. }
  337. func TestGrokQuotaFetcherClassifiesForbiddenAndReauth(t *testing.T) {
  338. t.Parallel()
  339. tests := []struct {
  340. name string
  341. statusCode int
  342. wantReauth bool
  343. wantForbid bool
  344. wantCode string
  345. wantEntitle string
  346. }{
  347. {name: "reauth", statusCode: http.StatusUnauthorized, wantReauth: true, wantCode: "unauthenticated"},
  348. {name: "forbidden", statusCode: http.StatusForbidden, wantForbid: true, wantCode: "forbidden", wantEntitle: "forbidden"},
  349. }
  350. for _, tt := range tests {
  351. t.Run(tt.name, func(t *testing.T) {
  352. t.Parallel()
  353. account := &Account{
  354. Platform: PlatformGrok,
  355. Type: AccountTypeOAuth,
  356. Extra: map[string]any{
  357. grokQuotaSnapshotExtraKey: xai.QuotaSnapshot{
  358. StatusCode: tt.statusCode,
  359. HeadersObserved: true,
  360. UpdatedAt: time.Now().UTC().Format(time.RFC3339),
  361. },
  362. },
  363. }
  364. usage := NewGrokQuotaFetcher().BuildUsageInfo(account)
  365. require.Equal(t, tt.wantReauth, usage.NeedsReauth)
  366. require.Equal(t, tt.wantForbid, usage.IsForbidden)
  367. require.Equal(t, tt.wantCode, usage.ErrorCode)
  368. require.Equal(t, tt.wantEntitle, usage.GrokEntitlementStatus)
  369. })
  370. }
  371. }