account_quota_reset_test.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. //go:build unit
  2. package service
  3. import (
  4. "testing"
  5. "time"
  6. "github.com/stretchr/testify/assert"
  7. "github.com/stretchr/testify/require"
  8. )
  9. // ---------------------------------------------------------------------------
  10. // nextFixedDailyReset
  11. // ---------------------------------------------------------------------------
  12. func TestNextFixedDailyReset_BeforeResetHour(t *testing.T) {
  13. tz := time.UTC
  14. // 2026-03-14 06:00 UTC, reset hour = 9
  15. after := time.Date(2026, 3, 14, 6, 0, 0, 0, tz)
  16. got := nextFixedDailyReset(9, tz, after)
  17. want := time.Date(2026, 3, 14, 9, 0, 0, 0, tz)
  18. assert.Equal(t, want, got)
  19. }
  20. func TestNextFixedDailyReset_AtResetHour(t *testing.T) {
  21. tz := time.UTC
  22. // Exactly at reset hour → should return tomorrow
  23. after := time.Date(2026, 3, 14, 9, 0, 0, 0, tz)
  24. got := nextFixedDailyReset(9, tz, after)
  25. want := time.Date(2026, 3, 15, 9, 0, 0, 0, tz)
  26. assert.Equal(t, want, got)
  27. }
  28. func TestNextFixedDailyReset_AfterResetHour(t *testing.T) {
  29. tz := time.UTC
  30. // After reset hour → should return tomorrow
  31. after := time.Date(2026, 3, 14, 15, 30, 0, 0, tz)
  32. got := nextFixedDailyReset(9, tz, after)
  33. want := time.Date(2026, 3, 15, 9, 0, 0, 0, tz)
  34. assert.Equal(t, want, got)
  35. }
  36. func TestNextFixedDailyReset_MidnightReset(t *testing.T) {
  37. tz := time.UTC
  38. // Reset at hour 0 (midnight), currently 23:59
  39. after := time.Date(2026, 3, 14, 23, 59, 0, 0, tz)
  40. got := nextFixedDailyReset(0, tz, after)
  41. want := time.Date(2026, 3, 15, 0, 0, 0, 0, tz)
  42. assert.Equal(t, want, got)
  43. }
  44. func TestNextFixedDailyReset_NonUTCTimezone(t *testing.T) {
  45. tz, err := time.LoadLocation("Asia/Shanghai")
  46. require.NoError(t, err)
  47. // 2026-03-14 07:00 UTC = 2026-03-14 15:00 CST, reset hour = 9 (CST)
  48. after := time.Date(2026, 3, 14, 7, 0, 0, 0, time.UTC)
  49. got := nextFixedDailyReset(9, tz, after)
  50. // Already past 9:00 CST today → tomorrow 9:00 CST = 2026-03-15 01:00 UTC
  51. want := time.Date(2026, 3, 15, 9, 0, 0, 0, tz)
  52. assert.Equal(t, want, got)
  53. }
  54. // ---------------------------------------------------------------------------
  55. // lastFixedDailyReset
  56. // ---------------------------------------------------------------------------
  57. func TestLastFixedDailyReset_BeforeResetHour(t *testing.T) {
  58. tz := time.UTC
  59. now := time.Date(2026, 3, 14, 6, 0, 0, 0, tz)
  60. got := lastFixedDailyReset(9, tz, now)
  61. // Before today's 9:00 → yesterday 9:00
  62. want := time.Date(2026, 3, 13, 9, 0, 0, 0, tz)
  63. assert.Equal(t, want, got)
  64. }
  65. func TestLastFixedDailyReset_AtResetHour(t *testing.T) {
  66. tz := time.UTC
  67. now := time.Date(2026, 3, 14, 9, 0, 0, 0, tz)
  68. got := lastFixedDailyReset(9, tz, now)
  69. // At exactly 9:00 → today 9:00
  70. want := time.Date(2026, 3, 14, 9, 0, 0, 0, tz)
  71. assert.Equal(t, want, got)
  72. }
  73. func TestLastFixedDailyReset_AfterResetHour(t *testing.T) {
  74. tz := time.UTC
  75. now := time.Date(2026, 3, 14, 15, 0, 0, 0, tz)
  76. got := lastFixedDailyReset(9, tz, now)
  77. // After 9:00 → today 9:00
  78. want := time.Date(2026, 3, 14, 9, 0, 0, 0, tz)
  79. assert.Equal(t, want, got)
  80. }
  81. // ---------------------------------------------------------------------------
  82. // nextFixedWeeklyReset
  83. // ---------------------------------------------------------------------------
  84. func TestNextFixedWeeklyReset_TargetDayAhead(t *testing.T) {
  85. tz := time.UTC
  86. // 2026-03-14 is Saturday (day=6), target = Monday (day=1), hour = 9
  87. after := time.Date(2026, 3, 14, 10, 0, 0, 0, tz)
  88. got := nextFixedWeeklyReset(1, 9, tz, after)
  89. // Next Monday = 2026-03-16
  90. want := time.Date(2026, 3, 16, 9, 0, 0, 0, tz)
  91. assert.Equal(t, want, got)
  92. }
  93. func TestNextFixedWeeklyReset_TargetDayToday_BeforeHour(t *testing.T) {
  94. tz := time.UTC
  95. // 2026-03-16 is Monday (day=1), target = Monday, hour = 9, before 9:00
  96. after := time.Date(2026, 3, 16, 6, 0, 0, 0, tz)
  97. got := nextFixedWeeklyReset(1, 9, tz, after)
  98. // Today at 9:00
  99. want := time.Date(2026, 3, 16, 9, 0, 0, 0, tz)
  100. assert.Equal(t, want, got)
  101. }
  102. func TestNextFixedWeeklyReset_TargetDayToday_AtHour(t *testing.T) {
  103. tz := time.UTC
  104. // 2026-03-16 is Monday, target = Monday, hour = 9, exactly at 9:00
  105. after := time.Date(2026, 3, 16, 9, 0, 0, 0, tz)
  106. got := nextFixedWeeklyReset(1, 9, tz, after)
  107. // Next Monday at 9:00
  108. want := time.Date(2026, 3, 23, 9, 0, 0, 0, tz)
  109. assert.Equal(t, want, got)
  110. }
  111. func TestNextFixedWeeklyReset_TargetDayToday_AfterHour(t *testing.T) {
  112. tz := time.UTC
  113. // 2026-03-16 is Monday, target = Monday, hour = 9, after 9:00
  114. after := time.Date(2026, 3, 16, 15, 0, 0, 0, tz)
  115. got := nextFixedWeeklyReset(1, 9, tz, after)
  116. // Next Monday at 9:00
  117. want := time.Date(2026, 3, 23, 9, 0, 0, 0, tz)
  118. assert.Equal(t, want, got)
  119. }
  120. func TestNextFixedWeeklyReset_TargetDayPast(t *testing.T) {
  121. tz := time.UTC
  122. // 2026-03-18 is Wednesday (day=3), target = Monday (day=1)
  123. after := time.Date(2026, 3, 18, 10, 0, 0, 0, tz)
  124. got := nextFixedWeeklyReset(1, 9, tz, after)
  125. // Next Monday = 2026-03-23
  126. want := time.Date(2026, 3, 23, 9, 0, 0, 0, tz)
  127. assert.Equal(t, want, got)
  128. }
  129. func TestNextFixedWeeklyReset_Sunday(t *testing.T) {
  130. tz := time.UTC
  131. // 2026-03-14 is Saturday (day=6), target = Sunday (day=0)
  132. after := time.Date(2026, 3, 14, 10, 0, 0, 0, tz)
  133. got := nextFixedWeeklyReset(0, 0, tz, after)
  134. // Next Sunday = 2026-03-15
  135. want := time.Date(2026, 3, 15, 0, 0, 0, 0, tz)
  136. assert.Equal(t, want, got)
  137. }
  138. // ---------------------------------------------------------------------------
  139. // lastFixedWeeklyReset
  140. // ---------------------------------------------------------------------------
  141. func TestLastFixedWeeklyReset_SameDay_AfterHour(t *testing.T) {
  142. tz := time.UTC
  143. // 2026-03-16 is Monday (day=1), target = Monday, hour = 9, now = 15:00
  144. now := time.Date(2026, 3, 16, 15, 0, 0, 0, tz)
  145. got := lastFixedWeeklyReset(1, 9, tz, now)
  146. // Today at 9:00
  147. want := time.Date(2026, 3, 16, 9, 0, 0, 0, tz)
  148. assert.Equal(t, want, got)
  149. }
  150. func TestLastFixedWeeklyReset_SameDay_BeforeHour(t *testing.T) {
  151. tz := time.UTC
  152. // 2026-03-16 is Monday, target = Monday, hour = 9, now = 06:00
  153. now := time.Date(2026, 3, 16, 6, 0, 0, 0, tz)
  154. got := lastFixedWeeklyReset(1, 9, tz, now)
  155. // Last Monday at 9:00 = 2026-03-09
  156. want := time.Date(2026, 3, 9, 9, 0, 0, 0, tz)
  157. assert.Equal(t, want, got)
  158. }
  159. func TestLastFixedWeeklyReset_DifferentDay(t *testing.T) {
  160. tz := time.UTC
  161. // 2026-03-18 is Wednesday (day=3), target = Monday (day=1)
  162. now := time.Date(2026, 3, 18, 10, 0, 0, 0, tz)
  163. got := lastFixedWeeklyReset(1, 9, tz, now)
  164. // Last Monday = 2026-03-16
  165. want := time.Date(2026, 3, 16, 9, 0, 0, 0, tz)
  166. assert.Equal(t, want, got)
  167. }
  168. // ---------------------------------------------------------------------------
  169. // isFixedDailyPeriodExpired
  170. // ---------------------------------------------------------------------------
  171. func TestIsFixedDailyPeriodExpired_ZeroPeriodStart(t *testing.T) {
  172. a := &Account{Extra: map[string]any{
  173. "quota_daily_reset_mode": "fixed",
  174. "quota_daily_reset_hour": float64(9),
  175. "quota_reset_timezone": "UTC",
  176. }}
  177. assert.True(t, a.isFixedDailyPeriodExpired(time.Time{}))
  178. }
  179. func TestIsFixedDailyPeriodExpired_NotExpired(t *testing.T) {
  180. a := &Account{Extra: map[string]any{
  181. "quota_daily_reset_mode": "fixed",
  182. "quota_daily_reset_hour": float64(9),
  183. "quota_reset_timezone": "UTC",
  184. }}
  185. // Anchor periodStart to today's 12:00 UTC: always strictly after today's
  186. // 09:00 UTC reset (and yesterday's). Using time.Now().Add(-1*time.Minute)
  187. // is flaky inside the 09:00-09:01 UTC reset window.
  188. now := time.Now().UTC()
  189. periodStart := time.Date(now.Year(), now.Month(), now.Day(), 12, 0, 0, 0, time.UTC)
  190. assert.False(t, a.isFixedDailyPeriodExpired(periodStart))
  191. }
  192. func TestIsFixedDailyPeriodExpired_Expired(t *testing.T) {
  193. a := &Account{Extra: map[string]any{
  194. "quota_daily_reset_mode": "fixed",
  195. "quota_daily_reset_hour": float64(9),
  196. "quota_reset_timezone": "UTC",
  197. }}
  198. // Period started 3 days ago → definitely expired
  199. periodStart := time.Now().Add(-72 * time.Hour)
  200. assert.True(t, a.isFixedDailyPeriodExpired(periodStart))
  201. }
  202. func TestIsFixedDailyPeriodExpired_InvalidTimezone(t *testing.T) {
  203. a := &Account{Extra: map[string]any{
  204. "quota_daily_reset_mode": "fixed",
  205. "quota_daily_reset_hour": float64(9),
  206. "quota_reset_timezone": "Invalid/Timezone",
  207. }}
  208. // Invalid timezone falls back to UTC
  209. periodStart := time.Now().Add(-72 * time.Hour)
  210. assert.True(t, a.isFixedDailyPeriodExpired(periodStart))
  211. }
  212. // ---------------------------------------------------------------------------
  213. // isFixedWeeklyPeriodExpired
  214. // ---------------------------------------------------------------------------
  215. func TestIsFixedWeeklyPeriodExpired_ZeroPeriodStart(t *testing.T) {
  216. a := &Account{Extra: map[string]any{
  217. "quota_weekly_reset_mode": "fixed",
  218. "quota_weekly_reset_day": float64(1),
  219. "quota_weekly_reset_hour": float64(9),
  220. "quota_reset_timezone": "UTC",
  221. }}
  222. assert.True(t, a.isFixedWeeklyPeriodExpired(time.Time{}))
  223. }
  224. func TestIsFixedWeeklyPeriodExpired_NotExpired(t *testing.T) {
  225. a := &Account{Extra: map[string]any{
  226. "quota_weekly_reset_mode": "fixed",
  227. "quota_weekly_reset_day": float64(1),
  228. "quota_weekly_reset_hour": float64(9),
  229. "quota_reset_timezone": "UTC",
  230. }}
  231. // Anchor periodStart to today's 12:00 UTC: always strictly after the most
  232. // recent Monday 09:00 UTC reset, regardless of which weekday/hour the test
  233. // runs. Using time.Now().Add(-1*time.Minute) is flaky inside the
  234. // Monday 09:00-09:01 UTC reset window.
  235. now := time.Now().UTC()
  236. periodStart := time.Date(now.Year(), now.Month(), now.Day(), 12, 0, 0, 0, time.UTC)
  237. assert.False(t, a.isFixedWeeklyPeriodExpired(periodStart))
  238. }
  239. func TestIsFixedWeeklyPeriodExpired_Expired(t *testing.T) {
  240. a := &Account{Extra: map[string]any{
  241. "quota_weekly_reset_mode": "fixed",
  242. "quota_weekly_reset_day": float64(1),
  243. "quota_weekly_reset_hour": float64(9),
  244. "quota_reset_timezone": "UTC",
  245. }}
  246. // Period started 10 days ago → definitely expired
  247. periodStart := time.Now().Add(-240 * time.Hour)
  248. assert.True(t, a.isFixedWeeklyPeriodExpired(periodStart))
  249. }
  250. // ---------------------------------------------------------------------------
  251. // ValidateQuotaResetConfig
  252. // ---------------------------------------------------------------------------
  253. func TestValidateQuotaResetConfig_NilExtra(t *testing.T) {
  254. assert.NoError(t, ValidateQuotaResetConfig(nil))
  255. }
  256. func TestValidateQuotaResetConfig_EmptyExtra(t *testing.T) {
  257. assert.NoError(t, ValidateQuotaResetConfig(map[string]any{}))
  258. }
  259. func TestValidateQuotaResetConfig_ValidFixed(t *testing.T) {
  260. extra := map[string]any{
  261. "quota_daily_reset_mode": "fixed",
  262. "quota_daily_reset_hour": float64(9),
  263. "quota_weekly_reset_mode": "fixed",
  264. "quota_weekly_reset_day": float64(1),
  265. "quota_weekly_reset_hour": float64(0),
  266. "quota_reset_timezone": "Asia/Shanghai",
  267. }
  268. assert.NoError(t, ValidateQuotaResetConfig(extra))
  269. }
  270. func TestValidateQuotaResetConfig_ValidRolling(t *testing.T) {
  271. extra := map[string]any{
  272. "quota_daily_reset_mode": "rolling",
  273. "quota_weekly_reset_mode": "rolling",
  274. }
  275. assert.NoError(t, ValidateQuotaResetConfig(extra))
  276. }
  277. func TestValidateQuotaResetConfig_InvalidTimezone(t *testing.T) {
  278. extra := map[string]any{
  279. "quota_reset_timezone": "Not/A/Timezone",
  280. }
  281. err := ValidateQuotaResetConfig(extra)
  282. require.Error(t, err)
  283. assert.Contains(t, err.Error(), "quota_reset_timezone")
  284. }
  285. func TestValidateQuotaResetConfig_InvalidDailyMode(t *testing.T) {
  286. extra := map[string]any{
  287. "quota_daily_reset_mode": "invalid",
  288. }
  289. err := ValidateQuotaResetConfig(extra)
  290. require.Error(t, err)
  291. assert.Contains(t, err.Error(), "quota_daily_reset_mode")
  292. }
  293. func TestValidateQuotaResetConfig_InvalidDailyHour_TooHigh(t *testing.T) {
  294. extra := map[string]any{
  295. "quota_daily_reset_hour": float64(24),
  296. }
  297. err := ValidateQuotaResetConfig(extra)
  298. require.Error(t, err)
  299. assert.Contains(t, err.Error(), "quota_daily_reset_hour")
  300. }
  301. func TestValidateQuotaResetConfig_InvalidDailyHour_Negative(t *testing.T) {
  302. extra := map[string]any{
  303. "quota_daily_reset_hour": float64(-1),
  304. }
  305. err := ValidateQuotaResetConfig(extra)
  306. require.Error(t, err)
  307. assert.Contains(t, err.Error(), "quota_daily_reset_hour")
  308. }
  309. func TestValidateQuotaResetConfig_InvalidWeeklyMode(t *testing.T) {
  310. extra := map[string]any{
  311. "quota_weekly_reset_mode": "unknown",
  312. }
  313. err := ValidateQuotaResetConfig(extra)
  314. require.Error(t, err)
  315. assert.Contains(t, err.Error(), "quota_weekly_reset_mode")
  316. }
  317. func TestValidateQuotaResetConfig_InvalidWeeklyDay_TooHigh(t *testing.T) {
  318. extra := map[string]any{
  319. "quota_weekly_reset_day": float64(7),
  320. }
  321. err := ValidateQuotaResetConfig(extra)
  322. require.Error(t, err)
  323. assert.Contains(t, err.Error(), "quota_weekly_reset_day")
  324. }
  325. func TestValidateQuotaResetConfig_InvalidWeeklyDay_Negative(t *testing.T) {
  326. extra := map[string]any{
  327. "quota_weekly_reset_day": float64(-1),
  328. }
  329. err := ValidateQuotaResetConfig(extra)
  330. require.Error(t, err)
  331. assert.Contains(t, err.Error(), "quota_weekly_reset_day")
  332. }
  333. func TestValidateQuotaResetConfig_InvalidWeeklyHour(t *testing.T) {
  334. extra := map[string]any{
  335. "quota_weekly_reset_hour": float64(25),
  336. }
  337. err := ValidateQuotaResetConfig(extra)
  338. require.Error(t, err)
  339. assert.Contains(t, err.Error(), "quota_weekly_reset_hour")
  340. }
  341. func TestValidateQuotaResetConfig_BoundaryValues(t *testing.T) {
  342. // All boundary values should be valid
  343. extra := map[string]any{
  344. "quota_daily_reset_hour": float64(23),
  345. "quota_weekly_reset_day": float64(0), // Sunday
  346. "quota_weekly_reset_hour": float64(0),
  347. "quota_reset_timezone": "UTC",
  348. }
  349. assert.NoError(t, ValidateQuotaResetConfig(extra))
  350. extra2 := map[string]any{
  351. "quota_daily_reset_hour": float64(0),
  352. "quota_weekly_reset_day": float64(6), // Saturday
  353. "quota_weekly_reset_hour": float64(23),
  354. }
  355. assert.NoError(t, ValidateQuotaResetConfig(extra2))
  356. }
  357. // ---------------------------------------------------------------------------
  358. // NormalizeFixedQuotaWindows
  359. // ---------------------------------------------------------------------------
  360. func TestNormalizeFixedQuotaWindows_ClearsExpiredWeeklyWindow(t *testing.T) {
  361. now := time.Now().UTC()
  362. daysSinceMonday := (int(now.Weekday()) + 6) % 7
  363. currentWeekStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC).AddDate(0, 0, -daysSinceMonday)
  364. staleStart := currentWeekStart.Add(-24 * time.Hour)
  365. extra := map[string]any{
  366. "quota_weekly_limit": 500.0,
  367. "quota_weekly_used": 76.0,
  368. "quota_weekly_start": staleStart.Format(time.RFC3339),
  369. "quota_weekly_reset_mode": "fixed",
  370. "quota_weekly_reset_day": float64(1),
  371. "quota_weekly_reset_hour": float64(0),
  372. "quota_reset_timezone": "UTC",
  373. }
  374. NormalizeFixedQuotaWindows(extra)
  375. assert.Equal(t, 0.0, extra["quota_weekly_used"])
  376. assert.Equal(t, currentWeekStart.Format(time.RFC3339), extra["quota_weekly_start"])
  377. }
  378. func TestNormalizeFixedQuotaWindows_KeepsActiveWeeklyWindow(t *testing.T) {
  379. now := time.Now().UTC()
  380. daysSinceMonday := (int(now.Weekday()) + 6) % 7
  381. currentWeekStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC).AddDate(0, 0, -daysSinceMonday)
  382. extra := map[string]any{
  383. "quota_weekly_limit": 500.0,
  384. "quota_weekly_used": 76.0,
  385. "quota_weekly_start": currentWeekStart.Format(time.RFC3339),
  386. "quota_weekly_reset_mode": "fixed",
  387. "quota_weekly_reset_day": float64(1),
  388. "quota_weekly_reset_hour": float64(0),
  389. "quota_reset_timezone": "UTC",
  390. }
  391. NormalizeFixedQuotaWindows(extra)
  392. assert.Equal(t, 76.0, extra["quota_weekly_used"])
  393. assert.Equal(t, currentWeekStart.Format(time.RFC3339), extra["quota_weekly_start"])
  394. }
  395. // ---------------------------------------------------------------------------
  396. // ComputeQuotaResetAt
  397. // ---------------------------------------------------------------------------
  398. func TestComputeQuotaResetAt_RollingMode_NoResetAt(t *testing.T) {
  399. extra := map[string]any{
  400. "quota_daily_reset_mode": "rolling",
  401. "quota_weekly_reset_mode": "rolling",
  402. }
  403. ComputeQuotaResetAt(extra)
  404. _, hasDailyResetAt := extra["quota_daily_reset_at"]
  405. _, hasWeeklyResetAt := extra["quota_weekly_reset_at"]
  406. assert.False(t, hasDailyResetAt, "rolling mode should not set quota_daily_reset_at")
  407. assert.False(t, hasWeeklyResetAt, "rolling mode should not set quota_weekly_reset_at")
  408. }
  409. func TestComputeQuotaResetAt_RollingMode_ClearsExistingResetAt(t *testing.T) {
  410. extra := map[string]any{
  411. "quota_daily_reset_mode": "rolling",
  412. "quota_weekly_reset_mode": "rolling",
  413. "quota_daily_reset_at": "2026-03-14T09:00:00Z",
  414. "quota_weekly_reset_at": "2026-03-16T09:00:00Z",
  415. }
  416. ComputeQuotaResetAt(extra)
  417. _, hasDailyResetAt := extra["quota_daily_reset_at"]
  418. _, hasWeeklyResetAt := extra["quota_weekly_reset_at"]
  419. assert.False(t, hasDailyResetAt, "rolling mode should remove quota_daily_reset_at")
  420. assert.False(t, hasWeeklyResetAt, "rolling mode should remove quota_weekly_reset_at")
  421. }
  422. func TestComputeQuotaResetAt_FixedDaily_SetsResetAt(t *testing.T) {
  423. extra := map[string]any{
  424. "quota_daily_reset_mode": "fixed",
  425. "quota_daily_reset_hour": float64(9),
  426. "quota_reset_timezone": "UTC",
  427. }
  428. ComputeQuotaResetAt(extra)
  429. resetAtStr, ok := extra["quota_daily_reset_at"].(string)
  430. require.True(t, ok, "quota_daily_reset_at should be set")
  431. resetAt, err := time.Parse(time.RFC3339, resetAtStr)
  432. require.NoError(t, err)
  433. // Reset time should be in the future
  434. assert.True(t, resetAt.After(time.Now()), "reset_at should be in the future")
  435. // Reset hour should be 9 UTC
  436. assert.Equal(t, 9, resetAt.UTC().Hour())
  437. }
  438. func TestComputeQuotaResetAt_FixedWeekly_SetsResetAt(t *testing.T) {
  439. extra := map[string]any{
  440. "quota_weekly_reset_mode": "fixed",
  441. "quota_weekly_reset_day": float64(1), // Monday
  442. "quota_weekly_reset_hour": float64(0),
  443. "quota_reset_timezone": "UTC",
  444. }
  445. ComputeQuotaResetAt(extra)
  446. resetAtStr, ok := extra["quota_weekly_reset_at"].(string)
  447. require.True(t, ok, "quota_weekly_reset_at should be set")
  448. resetAt, err := time.Parse(time.RFC3339, resetAtStr)
  449. require.NoError(t, err)
  450. // Reset time should be in the future
  451. assert.True(t, resetAt.After(time.Now()), "reset_at should be in the future")
  452. // Reset day should be Monday
  453. assert.Equal(t, time.Monday, resetAt.UTC().Weekday())
  454. }
  455. func TestComputeQuotaResetAt_FixedDaily_WithTimezone(t *testing.T) {
  456. tz, err := time.LoadLocation("Asia/Shanghai")
  457. require.NoError(t, err)
  458. extra := map[string]any{
  459. "quota_daily_reset_mode": "fixed",
  460. "quota_daily_reset_hour": float64(9),
  461. "quota_reset_timezone": "Asia/Shanghai",
  462. }
  463. ComputeQuotaResetAt(extra)
  464. resetAtStr, ok := extra["quota_daily_reset_at"].(string)
  465. require.True(t, ok)
  466. resetAt, err := time.Parse(time.RFC3339, resetAtStr)
  467. require.NoError(t, err)
  468. // In Shanghai timezone, the hour should be 9
  469. assert.Equal(t, 9, resetAt.In(tz).Hour())
  470. }
  471. func TestComputeQuotaResetAt_DefaultTimezone(t *testing.T) {
  472. extra := map[string]any{
  473. "quota_daily_reset_mode": "fixed",
  474. "quota_daily_reset_hour": float64(12),
  475. }
  476. ComputeQuotaResetAt(extra)
  477. resetAtStr, ok := extra["quota_daily_reset_at"].(string)
  478. require.True(t, ok)
  479. resetAt, err := time.Parse(time.RFC3339, resetAtStr)
  480. require.NoError(t, err)
  481. // Default timezone is UTC
  482. assert.Equal(t, 12, resetAt.UTC().Hour())
  483. }
  484. func TestComputeQuotaResetAt_InvalidHour_ClampedToZero(t *testing.T) {
  485. extra := map[string]any{
  486. "quota_daily_reset_mode": "fixed",
  487. "quota_daily_reset_hour": float64(99),
  488. "quota_reset_timezone": "UTC",
  489. }
  490. ComputeQuotaResetAt(extra)
  491. resetAtStr, ok := extra["quota_daily_reset_at"].(string)
  492. require.True(t, ok)
  493. resetAt, err := time.Parse(time.RFC3339, resetAtStr)
  494. require.NoError(t, err)
  495. // Invalid hour → clamped to 0
  496. assert.Equal(t, 0, resetAt.UTC().Hour())
  497. }