rateLimiter.test.ts 3.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. import { describe, expect, test } from "bun:test"
  2. import { getRetryAfterDay, getRetryAfterHour } from "../src/routes/zen/util/rateLimiter"
  3. describe("getRetryAfterDay", () => {
  4. test("returns full day at midnight UTC", () => {
  5. const midnight = Date.UTC(2026, 0, 15, 0, 0, 0, 0)
  6. expect(getRetryAfterDay(midnight)).toBe(86_400)
  7. })
  8. test("returns remaining seconds until next UTC day", () => {
  9. const noon = Date.UTC(2026, 0, 15, 12, 0, 0, 0)
  10. expect(getRetryAfterDay(noon)).toBe(43_200)
  11. })
  12. test("rounds up to nearest second", () => {
  13. const almost = Date.UTC(2026, 0, 15, 23, 59, 59, 500)
  14. expect(getRetryAfterDay(almost)).toBe(1)
  15. })
  16. })
  17. describe("getRetryAfterHour", () => {
  18. // 14:30:00 UTC — 30 minutes into the current hour
  19. const now = Date.UTC(2026, 0, 15, 14, 30, 0, 0)
  20. const intervals = ["2026011514", "2026011513", "2026011512"]
  21. test("waits 3 hours when all usage is in current hour", () => {
  22. const rows = [{ interval: "2026011514", count: 10 }]
  23. // only current hour has usage — it won't leave the window for 3 hours from hour start
  24. // 3 * 3600 - 1800 = 9000s
  25. expect(getRetryAfterHour(rows, intervals, 10, now)).toBe(9000)
  26. })
  27. test("waits 1 hour when dropping oldest interval is sufficient", () => {
  28. const rows = [
  29. { interval: "2026011514", count: 2 },
  30. { interval: "2026011512", count: 10 },
  31. ]
  32. // total=12, drop oldest (-2h, count=10) -> 2 < 10
  33. // hours = 3 - 2 = 1 -> 1 * 3600 - 1800 = 1800s
  34. expect(getRetryAfterHour(rows, intervals, 10, now)).toBe(1800)
  35. })
  36. test("waits 2 hours when usage spans oldest two intervals", () => {
  37. const rows = [
  38. { interval: "2026011513", count: 8 },
  39. { interval: "2026011512", count: 5 },
  40. ]
  41. // total=13, drop -2h (5) -> 8, 8 >= 8, drop -1h (8) -> 0 < 8
  42. // hours = 3 - 1 = 2 -> 2 * 3600 - 1800 = 5400s
  43. expect(getRetryAfterHour(rows, intervals, 8, now)).toBe(5400)
  44. })
  45. test("waits 1 hour when oldest interval alone pushes over limit", () => {
  46. const rows = [
  47. { interval: "2026011514", count: 1 },
  48. { interval: "2026011513", count: 1 },
  49. { interval: "2026011512", count: 10 },
  50. ]
  51. // total=12, drop -2h (10) -> 2 < 10
  52. // hours = 3 - 2 = 1 -> 1800s
  53. expect(getRetryAfterHour(rows, intervals, 10, now)).toBe(1800)
  54. })
  55. test("waits 2 hours when middle interval keeps total over limit", () => {
  56. const rows = [
  57. { interval: "2026011514", count: 4 },
  58. { interval: "2026011513", count: 4 },
  59. { interval: "2026011512", count: 4 },
  60. ]
  61. // total=12, drop -2h (4) -> 8, 8 >= 5, drop -1h (4) -> 4 < 5
  62. // hours = 3 - 1 = 2 -> 5400s
  63. expect(getRetryAfterHour(rows, intervals, 5, now)).toBe(5400)
  64. })
  65. test("rounds up to nearest second", () => {
  66. const offset = Date.UTC(2026, 0, 15, 14, 30, 0, 500)
  67. const rows = [
  68. { interval: "2026011514", count: 2 },
  69. { interval: "2026011512", count: 10 },
  70. ]
  71. // hours=1 -> 3_600_000 - 1_800_500 = 1_799_500ms -> ceil(1799.5) = 1800
  72. expect(getRetryAfterHour(rows, intervals, 10, offset)).toBe(1800)
  73. })
  74. test("fallback returns time until next hour when rows are empty", () => {
  75. // edge case: rows empty but function called (shouldn't happen in practice)
  76. // loop drops all zeros, running stays 0 which is < any positive limit on first iteration
  77. const rows: { interval: string; count: number }[] = []
  78. // drop -2h (0) -> 0 < 1 -> hours = 3 - 2 = 1 -> 1800s
  79. expect(getRetryAfterHour(rows, intervals, 1, now)).toBe(1800)
  80. })
  81. })