total-usage-semantics.test.ts 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. /**
  2. * total-usage-semantics tests
  3. *
  4. * Verify that total usage reads in display paths use ALL_TIME_MAX_AGE_DAYS (Infinity)
  5. * to skip the date filter entirely, querying all-time data.
  6. *
  7. * Key insight: The usage/quota aggregation functions default maxAgeDays to 365.
  8. * For display purposes (showing "total" usage), we want all-time semantics, which
  9. * means passing Infinity to skip the date filter.
  10. *
  11. * IMPORTANT: This test only covers DISPLAY paths. Enforcement paths (RateLimitService)
  12. * are intentionally NOT modified.
  13. */
  14. import { beforeEach, describe, expect, it, vi } from "vitest";
  15. // All-time max age constant - Infinity means no date filter
  16. const ALL_TIME_MAX_AGE_DAYS = Infinity;
  17. // Mock functions
  18. const getSessionMock = vi.fn();
  19. const sumUserTotalCostMock = vi.fn();
  20. const sumKeyQuotaCostsByIdMock = vi.fn();
  21. const sumUserQuotaCostsMock = vi.fn();
  22. const sumUserCostInTimeRangeMock = vi.fn();
  23. const getTimeRangeForPeriodMock = vi.fn();
  24. const getTimeRangeForPeriodWithModeMock = vi.fn();
  25. const getKeySessionCountMock = vi.fn();
  26. const getUserSessionCountMock = vi.fn();
  27. const findUserByIdMock = vi.fn();
  28. // Mock modules
  29. vi.mock("@/lib/auth", () => ({
  30. getSession: () => getSessionMock(),
  31. }));
  32. vi.mock("@/repository/statistics", () => ({
  33. sumUserCostInTimeRange: (...args: unknown[]) => sumUserCostInTimeRangeMock(...args),
  34. sumUserTotalCost: (...args: unknown[]) => sumUserTotalCostMock(...args),
  35. sumKeyQuotaCostsById: (...args: unknown[]) => sumKeyQuotaCostsByIdMock(...args),
  36. sumUserQuotaCosts: (...args: unknown[]) => sumUserQuotaCostsMock(...args),
  37. }));
  38. vi.mock("@/lib/rate-limit/time-utils", () => ({
  39. getTimeRangeForPeriod: (...args: unknown[]) => getTimeRangeForPeriodMock(...args),
  40. getTimeRangeForPeriodWithMode: (...args: unknown[]) => getTimeRangeForPeriodWithModeMock(...args),
  41. }));
  42. vi.mock("@/lib/session-tracker", () => ({
  43. SessionTracker: {
  44. getKeySessionCount: (...args: unknown[]) => getKeySessionCountMock(...args),
  45. getUserSessionCount: (...args: unknown[]) => getUserSessionCountMock(...args),
  46. },
  47. }));
  48. vi.mock("@/repository/user", () => ({
  49. findUserById: (...args: unknown[]) => findUserByIdMock(...args),
  50. }));
  51. vi.mock("@/lib/logger", () => ({
  52. logger: {
  53. trace: vi.fn(),
  54. debug: vi.fn(),
  55. info: vi.fn(),
  56. warn: vi.fn(),
  57. error: vi.fn(),
  58. },
  59. }));
  60. vi.mock("next-intl/server", () => ({
  61. getTranslations: vi.fn(() => (key: string) => key),
  62. }));
  63. describe("total-usage-semantics", () => {
  64. beforeEach(() => {
  65. vi.clearAllMocks();
  66. // Default time range mocks
  67. const now = new Date();
  68. const defaultRange = { startTime: now, endTime: now };
  69. getTimeRangeForPeriodMock.mockResolvedValue(defaultRange);
  70. getTimeRangeForPeriodWithModeMock.mockResolvedValue(defaultRange);
  71. // Default cost mocks
  72. sumUserTotalCostMock.mockResolvedValue(0);
  73. sumUserCostInTimeRangeMock.mockResolvedValue(0);
  74. getKeySessionCountMock.mockResolvedValue(0);
  75. getUserSessionCountMock.mockResolvedValue(0);
  76. const emptyCosts = {
  77. cost5h: 0,
  78. costDaily: 0,
  79. costWeekly: 0,
  80. costMonthly: 0,
  81. costTotal: 0,
  82. };
  83. sumKeyQuotaCostsByIdMock.mockResolvedValue(emptyCosts);
  84. sumUserQuotaCostsMock.mockResolvedValue(emptyCosts);
  85. });
  86. describe("getMyQuota in my-usage.ts", () => {
  87. it("should call sumKeyQuotaCostsById with ALL_TIME_MAX_AGE_DAYS for key total cost", async () => {
  88. // Setup session mock
  89. getSessionMock.mockResolvedValue({
  90. key: {
  91. id: 1,
  92. key: "test-key-hash",
  93. name: "Test Key",
  94. dailyResetTime: "00:00",
  95. dailyResetMode: "fixed",
  96. limit5hUsd: null,
  97. limitDailyUsd: null,
  98. limitWeeklyUsd: null,
  99. limitMonthlyUsd: null,
  100. limitTotalUsd: null,
  101. limitConcurrentSessions: null,
  102. providerGroup: null,
  103. isEnabled: true,
  104. expiresAt: null,
  105. },
  106. user: {
  107. id: 1,
  108. name: "Test User",
  109. dailyResetTime: "00:00",
  110. dailyResetMode: "fixed",
  111. limit5hUsd: null,
  112. dailyQuota: null,
  113. limitWeeklyUsd: null,
  114. limitMonthlyUsd: null,
  115. limitTotalUsd: null,
  116. limitConcurrentSessions: null,
  117. rpm: null,
  118. providerGroup: null,
  119. isEnabled: true,
  120. expiresAt: null,
  121. allowedModels: [],
  122. allowedClients: [],
  123. },
  124. });
  125. // Import and call the function
  126. const { getMyQuota } = await import("@/actions/my-usage");
  127. await getMyQuota();
  128. expect(sumKeyQuotaCostsByIdMock).toHaveBeenCalledWith(
  129. 1,
  130. expect.any(Object),
  131. ALL_TIME_MAX_AGE_DAYS
  132. );
  133. });
  134. it("should call sumUserQuotaCosts with ALL_TIME_MAX_AGE_DAYS for user total cost", async () => {
  135. // Setup session mock
  136. getSessionMock.mockResolvedValue({
  137. key: {
  138. id: 1,
  139. key: "test-key-hash",
  140. name: "Test Key",
  141. dailyResetTime: "00:00",
  142. dailyResetMode: "fixed",
  143. limit5hUsd: null,
  144. limitDailyUsd: null,
  145. limitWeeklyUsd: null,
  146. limitMonthlyUsd: null,
  147. limitTotalUsd: null,
  148. limitConcurrentSessions: null,
  149. providerGroup: null,
  150. isEnabled: true,
  151. expiresAt: null,
  152. },
  153. user: {
  154. id: 1,
  155. name: "Test User",
  156. dailyResetTime: "00:00",
  157. dailyResetMode: "fixed",
  158. limit5hUsd: null,
  159. dailyQuota: null,
  160. limitWeeklyUsd: null,
  161. limitMonthlyUsd: null,
  162. limitTotalUsd: null,
  163. limitConcurrentSessions: null,
  164. rpm: null,
  165. providerGroup: null,
  166. isEnabled: true,
  167. expiresAt: null,
  168. allowedModels: [],
  169. allowedClients: [],
  170. },
  171. });
  172. // Import and call the function
  173. const { getMyQuota } = await import("@/actions/my-usage");
  174. await getMyQuota();
  175. expect(sumUserQuotaCostsMock).toHaveBeenCalledWith(
  176. 1,
  177. expect.any(Object),
  178. ALL_TIME_MAX_AGE_DAYS
  179. );
  180. });
  181. });
  182. describe("getUserAllLimitUsage in users.ts", () => {
  183. it("should call sumUserTotalCost with ALL_TIME_MAX_AGE_DAYS", async () => {
  184. // Setup session mock
  185. getSessionMock.mockResolvedValue({
  186. user: {
  187. id: 1,
  188. role: "admin",
  189. },
  190. });
  191. // Setup user mock
  192. findUserByIdMock.mockResolvedValue({
  193. id: 1,
  194. name: "Test User",
  195. dailyResetTime: "00:00",
  196. dailyResetMode: "fixed",
  197. limit5hUsd: null,
  198. dailyQuota: null,
  199. limitWeeklyUsd: null,
  200. limitMonthlyUsd: null,
  201. limitTotalUsd: null,
  202. });
  203. // Import and call the function
  204. const { getUserAllLimitUsage } = await import("@/actions/users");
  205. await getUserAllLimitUsage(1);
  206. // Verify sumUserTotalCost was called with Infinity (all-time)
  207. expect(sumUserTotalCostMock).toHaveBeenCalledWith(1, Infinity);
  208. });
  209. });
  210. describe("ALL_TIME_MAX_AGE_DAYS constant value", () => {
  211. it("should be Infinity for all-time semantics", () => {
  212. expect(ALL_TIME_MAX_AGE_DAYS).toBe(Infinity);
  213. });
  214. });
  215. describe("source code verification", () => {
  216. it("should verify sumUserCost passes ALL_TIME_MAX_AGE_DAYS when period is total", async () => {
  217. // This test verifies the implementation by reading the source code pattern
  218. // Ensure we call quota aggregation functions with ALL_TIME_MAX_AGE_DAYS for all-time usage.
  219. const fs = await import("node:fs/promises");
  220. const path = await import("node:path");
  221. const myUsagePath = path.join(process.cwd(), "src/actions/my-usage.ts");
  222. const content = await fs.readFile(myUsagePath, "utf-8");
  223. // Verify the constant is defined as Infinity
  224. expect(content).toContain("const ALL_TIME_MAX_AGE_DAYS = Infinity");
  225. // Verify quota aggregation uses the constant for all-time usage
  226. expect(content).toMatch(/sumUserQuotaCosts\([^)]*ALL_TIME_MAX_AGE_DAYS\s*\)/);
  227. expect(content).toMatch(/sumKeyQuotaCostsById\([^)]*ALL_TIME_MAX_AGE_DAYS\s*\)/);
  228. });
  229. it("should verify getUserAllLimitUsage passes ALL_TIME_MAX_AGE_DAYS", async () => {
  230. const fs = await import("node:fs/promises");
  231. const path = await import("node:path");
  232. const usersPath = path.join(process.cwd(), "src/actions/users.ts");
  233. const content = await fs.readFile(usersPath, "utf-8");
  234. // Verify the constant is defined as Infinity
  235. expect(content).toContain("const ALL_TIME_MAX_AGE_DAYS = Infinity");
  236. // Verify sumUserTotalCost is called with the constant
  237. expect(content).toContain("sumUserTotalCost(userId, ALL_TIME_MAX_AGE_DAYS)");
  238. });
  239. });
  240. });