rate-limit-guard.test.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599
  1. import { beforeEach, describe, expect, it, vi } from "vitest";
  2. const generateSessionIdMock = vi.hoisted(() => vi.fn(() => "sess_generated"));
  3. const rateLimitServiceMock = {
  4. checkTotalCostLimit: vi.fn(),
  5. checkAndTrackKeyUserSession: vi.fn(),
  6. checkRpmLimit: vi.fn(),
  7. checkCostLimitsWithLease: vi.fn(),
  8. checkUserDailyCost: vi.fn(),
  9. };
  10. vi.mock("@/lib/rate-limit", () => ({
  11. RateLimitService: rateLimitServiceMock,
  12. }));
  13. vi.mock("@/lib/session-manager", () => ({
  14. SessionManager: {
  15. generateSessionId: generateSessionIdMock,
  16. },
  17. }));
  18. vi.mock("@/lib/logger", () => ({
  19. logger: {
  20. warn: vi.fn(),
  21. info: vi.fn(),
  22. error: vi.fn(),
  23. debug: vi.fn(),
  24. },
  25. }));
  26. vi.mock("next-intl/server", () => ({
  27. getLocale: vi.fn(async () => "zh-CN"),
  28. }));
  29. const getErrorMessageServerMock = vi.fn(async () => "mock rate limit message");
  30. vi.mock("@/lib/utils/error-messages", () => ({
  31. ERROR_CODES: {
  32. RATE_LIMIT_TOTAL_EXCEEDED: "RATE_LIMIT_TOTAL_EXCEEDED",
  33. RATE_LIMIT_CONCURRENT_SESSIONS_EXCEEDED: "RATE_LIMIT_CONCURRENT_SESSIONS_EXCEEDED",
  34. RATE_LIMIT_RPM_EXCEEDED: "RATE_LIMIT_RPM_EXCEEDED",
  35. RATE_LIMIT_DAILY_QUOTA_EXCEEDED: "RATE_LIMIT_DAILY_QUOTA_EXCEEDED",
  36. RATE_LIMIT_5H_EXCEEDED: "RATE_LIMIT_5H_EXCEEDED",
  37. RATE_LIMIT_WEEKLY_EXCEEDED: "RATE_LIMIT_WEEKLY_EXCEEDED",
  38. RATE_LIMIT_MONTHLY_EXCEEDED: "RATE_LIMIT_MONTHLY_EXCEEDED",
  39. },
  40. getErrorMessageServer: getErrorMessageServerMock,
  41. }));
  42. describe("ProxyRateLimitGuard - key daily limit enforcement", () => {
  43. const createSession = (overrides?: {
  44. user?: Partial<{
  45. id: number;
  46. rpm: number | null;
  47. dailyQuota: number | null;
  48. dailyResetMode: "fixed" | "rolling";
  49. dailyResetTime: string;
  50. limit5hUsd: number | null;
  51. limitWeeklyUsd: number | null;
  52. limitMonthlyUsd: number | null;
  53. limitTotalUsd: number | null;
  54. limitConcurrentSessions: number | null;
  55. }>;
  56. key?: Partial<{
  57. id: number;
  58. key: string;
  59. limit5hUsd: number | null;
  60. limitDailyUsd: number | null;
  61. dailyResetMode: "fixed" | "rolling";
  62. dailyResetTime: string;
  63. limitWeeklyUsd: number | null;
  64. limitMonthlyUsd: number | null;
  65. limitTotalUsd: number | null;
  66. limitConcurrentSessions: number;
  67. }>;
  68. }) => {
  69. const session = {
  70. sessionId: "sess_test",
  71. authState: {
  72. user: {
  73. id: 1,
  74. rpm: null,
  75. dailyQuota: null,
  76. dailyResetMode: "fixed",
  77. dailyResetTime: "00:00",
  78. limit5hUsd: null,
  79. limitWeeklyUsd: null,
  80. limitMonthlyUsd: null,
  81. limitTotalUsd: null,
  82. limitConcurrentSessions: null,
  83. ...overrides?.user,
  84. },
  85. key: {
  86. id: 2,
  87. key: "k_test",
  88. limit5hUsd: null,
  89. limitDailyUsd: null,
  90. dailyResetMode: "fixed",
  91. dailyResetTime: "00:00",
  92. limitWeeklyUsd: null,
  93. limitMonthlyUsd: null,
  94. limitTotalUsd: null,
  95. limitConcurrentSessions: 0,
  96. ...overrides?.key,
  97. },
  98. },
  99. } as any;
  100. session.setSessionId = (id: string) => {
  101. session.sessionId = id;
  102. };
  103. return session;
  104. };
  105. beforeEach(() => {
  106. vi.clearAllMocks();
  107. generateSessionIdMock.mockReturnValue("sess_generated");
  108. rateLimitServiceMock.checkTotalCostLimit.mockResolvedValue({ allowed: true });
  109. rateLimitServiceMock.checkAndTrackKeyUserSession.mockResolvedValue({
  110. allowed: true,
  111. keyCount: 0,
  112. userCount: 0,
  113. trackedKey: false,
  114. trackedUser: false,
  115. });
  116. rateLimitServiceMock.checkRpmLimit.mockResolvedValue({ allowed: true });
  117. rateLimitServiceMock.checkUserDailyCost.mockResolvedValue({ allowed: true });
  118. rateLimitServiceMock.checkCostLimitsWithLease.mockResolvedValue({ allowed: true });
  119. });
  120. it("当用户未设置每日额度时,Key 每日额度已超限也必须拦截", async () => {
  121. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  122. rateLimitServiceMock.checkCostLimitsWithLease
  123. .mockResolvedValueOnce({ allowed: true }) // key 5h
  124. .mockResolvedValueOnce({ allowed: true }) // user 5h
  125. .mockResolvedValueOnce({
  126. allowed: false,
  127. reason: "Key daily cost limit reached (usage: 20.0000/10.0000)",
  128. }); // key daily
  129. const session = createSession({
  130. user: { dailyQuota: null },
  131. key: { limitDailyUsd: 10 },
  132. });
  133. await expect(ProxyRateLimitGuard.ensure(session)).rejects.toMatchObject({
  134. name: "RateLimitError",
  135. limitType: "daily_quota",
  136. currentUsage: 20,
  137. limitValue: 10,
  138. });
  139. expect(rateLimitServiceMock.checkUserDailyCost).not.toHaveBeenCalled();
  140. expect(rateLimitServiceMock.checkCostLimitsWithLease).toHaveBeenCalledWith(2, "key", {
  141. limit_5h_usd: null,
  142. limit_daily_usd: 10,
  143. daily_reset_mode: "fixed",
  144. daily_reset_time: "00:00",
  145. limit_weekly_usd: null,
  146. limit_monthly_usd: null,
  147. });
  148. });
  149. it("当 Key 每日额度超限时,应在用户每日检查之前直接拦截(Key 优先)", async () => {
  150. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  151. rateLimitServiceMock.checkCostLimitsWithLease
  152. .mockResolvedValueOnce({ allowed: true }) // key 5h
  153. .mockResolvedValueOnce({ allowed: true }) // user 5h
  154. .mockResolvedValueOnce({
  155. allowed: false,
  156. reason: "Key daily cost limit reached (usage: 20.0000/10.0000)",
  157. }); // key daily
  158. const session = createSession({
  159. user: { dailyQuota: 999 },
  160. key: { limitDailyUsd: 10 },
  161. });
  162. await expect(ProxyRateLimitGuard.ensure(session)).rejects.toMatchObject({
  163. name: "RateLimitError",
  164. limitType: "daily_quota",
  165. });
  166. expect(rateLimitServiceMock.checkUserDailyCost).not.toHaveBeenCalled();
  167. });
  168. it("当 Key 未设置每日额度且用户每日额度已超限时,仍应拦截用户每日额度", async () => {
  169. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  170. rateLimitServiceMock.checkCostLimitsWithLease
  171. .mockResolvedValueOnce({ allowed: true }) // key 5h
  172. .mockResolvedValueOnce({ allowed: true }) // user 5h
  173. .mockResolvedValueOnce({ allowed: true }) // key daily (limit null)
  174. .mockResolvedValueOnce({
  175. allowed: false,
  176. reason: "User daily cost limit reached (usage: 20.0000/10.0000)",
  177. }); // user daily
  178. const session = createSession({
  179. user: { dailyQuota: 10 },
  180. key: { limitDailyUsd: null },
  181. });
  182. await expect(ProxyRateLimitGuard.ensure(session)).rejects.toMatchObject({
  183. name: "RateLimitError",
  184. limitType: "daily_quota",
  185. currentUsage: 20,
  186. limitValue: 10,
  187. });
  188. // User daily 现在使用 checkCostLimitsWithLease 而不是 checkUserDailyCost
  189. expect(rateLimitServiceMock.checkUserDailyCost).not.toHaveBeenCalled();
  190. expect(rateLimitServiceMock.checkCostLimitsWithLease).toHaveBeenCalledWith(1, "user", {
  191. limit_5h_usd: null,
  192. limit_daily_usd: 10,
  193. daily_reset_time: "00:00",
  194. daily_reset_mode: "fixed",
  195. limit_weekly_usd: null,
  196. limit_monthly_usd: null,
  197. });
  198. });
  199. it("Key 总限额超限应拦截(usd_total)", async () => {
  200. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  201. rateLimitServiceMock.checkTotalCostLimit.mockResolvedValueOnce({
  202. allowed: false,
  203. current: 20,
  204. reason: "Key total limit exceeded",
  205. });
  206. const session = createSession({
  207. key: { limitTotalUsd: 10 },
  208. });
  209. await expect(ProxyRateLimitGuard.ensure(session)).rejects.toMatchObject({
  210. name: "RateLimitError",
  211. limitType: "usd_total",
  212. currentUsage: 20,
  213. limitValue: 10,
  214. });
  215. });
  216. it("User 总限额超限应拦截(usd_total)", async () => {
  217. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  218. rateLimitServiceMock.checkTotalCostLimit
  219. .mockResolvedValueOnce({ allowed: true }) // key total
  220. .mockResolvedValueOnce({ allowed: false, current: 20, reason: "User total limit exceeded" }); // user total
  221. const session = createSession({
  222. user: { limitTotalUsd: 10 },
  223. });
  224. await expect(ProxyRateLimitGuard.ensure(session)).rejects.toMatchObject({
  225. name: "RateLimitError",
  226. limitType: "usd_total",
  227. currentUsage: 20,
  228. limitValue: 10,
  229. });
  230. });
  231. it("Key 并发 Session 超限应拦截(concurrent_sessions)", async () => {
  232. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  233. rateLimitServiceMock.checkAndTrackKeyUserSession.mockResolvedValueOnce({
  234. allowed: false,
  235. rejectedBy: "key",
  236. reasonCode: "RATE_LIMIT_CONCURRENT_SESSIONS_EXCEEDED",
  237. reasonParams: { current: 2, limit: 1, target: "key" },
  238. keyCount: 2,
  239. userCount: 0,
  240. trackedKey: false,
  241. trackedUser: false,
  242. });
  243. const session = createSession({
  244. key: { limitConcurrentSessions: 1 },
  245. });
  246. await expect(ProxyRateLimitGuard.ensure(session)).rejects.toMatchObject({
  247. name: "RateLimitError",
  248. limitType: "concurrent_sessions",
  249. currentUsage: 2,
  250. limitValue: 1,
  251. });
  252. });
  253. it("User 并发 Session 超限应拦截(concurrent_sessions)", async () => {
  254. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  255. rateLimitServiceMock.checkAndTrackKeyUserSession.mockResolvedValueOnce({
  256. allowed: false,
  257. rejectedBy: "user",
  258. reasonCode: "RATE_LIMIT_CONCURRENT_SESSIONS_EXCEEDED",
  259. reasonParams: { current: 2, limit: 1, target: "user" },
  260. keyCount: 0,
  261. userCount: 2,
  262. trackedKey: false,
  263. trackedUser: false,
  264. });
  265. const session = createSession({
  266. user: { limitConcurrentSessions: 1 },
  267. key: { limitConcurrentSessions: 10 },
  268. });
  269. await expect(ProxyRateLimitGuard.ensure(session)).rejects.toMatchObject({
  270. name: "RateLimitError",
  271. limitType: "concurrent_sessions",
  272. currentUsage: 2,
  273. limitValue: 1,
  274. });
  275. });
  276. it("当 Key 并发未设置(0)且 User 并发已设置时,Key 并发检查应继承 User 并发上限", async () => {
  277. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  278. const session = createSession({
  279. user: { limitConcurrentSessions: 15 },
  280. key: { limitConcurrentSessions: 0 },
  281. });
  282. await expect(ProxyRateLimitGuard.ensure(session)).resolves.toBeUndefined();
  283. expect(rateLimitServiceMock.checkAndTrackKeyUserSession).toHaveBeenCalledWith(
  284. 2,
  285. 1,
  286. "sess_test",
  287. 15,
  288. 15
  289. );
  290. });
  291. it("User RPM 超限应拦截(rpm)", async () => {
  292. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  293. rateLimitServiceMock.checkRpmLimit.mockResolvedValueOnce({
  294. allowed: false,
  295. current: 10,
  296. reason: "用户每分钟请求数上限已达到(10/5)",
  297. });
  298. const session = createSession({
  299. user: { rpm: 5 },
  300. });
  301. await expect(ProxyRateLimitGuard.ensure(session)).rejects.toMatchObject({
  302. name: "RateLimitError",
  303. limitType: "rpm",
  304. currentUsage: 10,
  305. limitValue: 5,
  306. });
  307. });
  308. it("Key 5h 超限应拦截(usd_5h)", async () => {
  309. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  310. rateLimitServiceMock.checkCostLimitsWithLease.mockResolvedValueOnce({
  311. allowed: false,
  312. reason: "Key 5h cost limit reached (usage: 20.0000/10.0000)",
  313. });
  314. const session = createSession({
  315. key: { limit5hUsd: 10 },
  316. });
  317. await expect(ProxyRateLimitGuard.ensure(session)).rejects.toMatchObject({
  318. name: "RateLimitError",
  319. limitType: "usd_5h",
  320. currentUsage: 20,
  321. limitValue: 10,
  322. });
  323. });
  324. it("User 5h 超限应拦截(usd_5h)", async () => {
  325. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  326. rateLimitServiceMock.checkCostLimitsWithLease
  327. .mockResolvedValueOnce({ allowed: true }) // key 5h
  328. .mockResolvedValueOnce({
  329. allowed: false,
  330. reason: "User 5h cost limit reached (usage: 20.0000/10.0000)",
  331. }); // user 5h
  332. const session = createSession({
  333. user: { limit5hUsd: 10 },
  334. });
  335. await expect(ProxyRateLimitGuard.ensure(session)).rejects.toMatchObject({
  336. name: "RateLimitError",
  337. limitType: "usd_5h",
  338. currentUsage: 20,
  339. limitValue: 10,
  340. });
  341. });
  342. it("Key 周限额超限应拦截(usd_weekly)", async () => {
  343. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  344. rateLimitServiceMock.checkCostLimitsWithLease
  345. .mockResolvedValueOnce({ allowed: true }) // key 5h
  346. .mockResolvedValueOnce({ allowed: true }) // user 5h
  347. .mockResolvedValueOnce({ allowed: true }) // key daily
  348. .mockResolvedValueOnce({ allowed: true }) // user daily (new with lease migration)
  349. .mockResolvedValueOnce({
  350. allowed: false,
  351. reason: "Key weekly cost limit reached (usage: 100.0000/10.0000)",
  352. }); // key weekly
  353. const session = createSession({
  354. key: { limitWeeklyUsd: 10 },
  355. });
  356. await expect(ProxyRateLimitGuard.ensure(session)).rejects.toMatchObject({
  357. name: "RateLimitError",
  358. limitType: "usd_weekly",
  359. currentUsage: 100,
  360. limitValue: 10,
  361. });
  362. });
  363. it("User 周限额超限应拦截(usd_weekly)", async () => {
  364. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  365. rateLimitServiceMock.checkCostLimitsWithLease
  366. .mockResolvedValueOnce({ allowed: true }) // key 5h
  367. .mockResolvedValueOnce({ allowed: true }) // user 5h
  368. .mockResolvedValueOnce({ allowed: true }) // key daily
  369. .mockResolvedValueOnce({ allowed: true }) // user daily (new with lease migration)
  370. .mockResolvedValueOnce({ allowed: true }) // key weekly
  371. .mockResolvedValueOnce({
  372. allowed: false,
  373. reason: "User weekly cost limit reached (usage: 100.0000/10.0000)",
  374. }); // user weekly
  375. const session = createSession({
  376. user: { limitWeeklyUsd: 10 },
  377. });
  378. await expect(ProxyRateLimitGuard.ensure(session)).rejects.toMatchObject({
  379. name: "RateLimitError",
  380. limitType: "usd_weekly",
  381. currentUsage: 100,
  382. limitValue: 10,
  383. });
  384. });
  385. it("Key 月限额超限应拦截(usd_monthly)", async () => {
  386. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  387. rateLimitServiceMock.checkCostLimitsWithLease
  388. .mockResolvedValueOnce({ allowed: true }) // key 5h
  389. .mockResolvedValueOnce({ allowed: true }) // user 5h
  390. .mockResolvedValueOnce({ allowed: true }) // key daily
  391. .mockResolvedValueOnce({ allowed: true }) // user daily (new with lease migration)
  392. .mockResolvedValueOnce({ allowed: true }) // key weekly
  393. .mockResolvedValueOnce({ allowed: true }) // user weekly
  394. .mockResolvedValueOnce({
  395. allowed: false,
  396. reason: "Key monthly cost limit reached (usage: 200.0000/10.0000)",
  397. }); // key monthly
  398. const session = createSession({
  399. key: { limitMonthlyUsd: 10 },
  400. });
  401. await expect(ProxyRateLimitGuard.ensure(session)).rejects.toMatchObject({
  402. name: "RateLimitError",
  403. limitType: "usd_monthly",
  404. currentUsage: 200,
  405. limitValue: 10,
  406. });
  407. });
  408. it("User 月限额超限应拦截(usd_monthly)", async () => {
  409. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  410. rateLimitServiceMock.checkCostLimitsWithLease
  411. .mockResolvedValueOnce({ allowed: true }) // key 5h
  412. .mockResolvedValueOnce({ allowed: true }) // user 5h
  413. .mockResolvedValueOnce({ allowed: true }) // key daily
  414. .mockResolvedValueOnce({ allowed: true }) // user daily (new with lease migration)
  415. .mockResolvedValueOnce({ allowed: true }) // key weekly
  416. .mockResolvedValueOnce({ allowed: true }) // user weekly
  417. .mockResolvedValueOnce({ allowed: true }) // key monthly
  418. .mockResolvedValueOnce({
  419. allowed: false,
  420. reason: "User monthly cost limit reached (usage: 200.0000/10.0000)",
  421. }); // user monthly
  422. const session = createSession({
  423. user: { limitMonthlyUsd: 10 },
  424. });
  425. await expect(ProxyRateLimitGuard.ensure(session)).rejects.toMatchObject({
  426. name: "RateLimitError",
  427. limitType: "usd_monthly",
  428. currentUsage: 200,
  429. limitValue: 10,
  430. });
  431. });
  432. it("所有限额均未触发时应放行", async () => {
  433. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  434. const session = createSession();
  435. await expect(ProxyRateLimitGuard.ensure(session)).resolves.toBeUndefined();
  436. });
  437. it("当 sessionId 缺失时,应兜底生成并继续并发检查", async () => {
  438. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  439. const session = createSession() as any;
  440. session.sessionId = undefined;
  441. await expect(ProxyRateLimitGuard.ensure(session)).resolves.toBeUndefined();
  442. expect(generateSessionIdMock).toHaveBeenCalledTimes(1);
  443. expect(session.sessionId).toBe("sess_generated");
  444. expect(rateLimitServiceMock.checkAndTrackKeyUserSession).toHaveBeenCalledWith(
  445. 2,
  446. 1,
  447. "sess_generated",
  448. expect.any(Number),
  449. expect.any(Number)
  450. );
  451. });
  452. it("User daily (rolling mode) 超限应使用 checkCostLimitsWithLease", async () => {
  453. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  454. rateLimitServiceMock.checkCostLimitsWithLease
  455. .mockResolvedValueOnce({ allowed: true }) // key 5h
  456. .mockResolvedValueOnce({ allowed: true }) // user 5h
  457. .mockResolvedValueOnce({ allowed: true }) // key daily (limit null)
  458. .mockResolvedValueOnce({
  459. allowed: false,
  460. reason: "User daily cost limit reached (usage: 15.0000/10.0000)",
  461. }); // user daily rolling
  462. const session = createSession({
  463. user: { dailyQuota: 10, dailyResetMode: "rolling", dailyResetTime: "12:00" },
  464. key: { limitDailyUsd: null },
  465. });
  466. await expect(ProxyRateLimitGuard.ensure(session)).rejects.toMatchObject({
  467. name: "RateLimitError",
  468. limitType: "daily_quota",
  469. currentUsage: 15,
  470. limitValue: 10,
  471. resetTime: null, // rolling 模式没有固定重置时间
  472. });
  473. // Verify checkCostLimitsWithLease was called with rolling mode
  474. expect(rateLimitServiceMock.checkCostLimitsWithLease).toHaveBeenCalledWith(1, "user", {
  475. limit_5h_usd: null,
  476. limit_daily_usd: 10,
  477. daily_reset_time: "12:00",
  478. daily_reset_mode: "rolling",
  479. limit_weekly_usd: null,
  480. limit_monthly_usd: null,
  481. });
  482. // checkUserDailyCost should NOT be called (migrated to lease)
  483. expect(rateLimitServiceMock.checkUserDailyCost).not.toHaveBeenCalled();
  484. });
  485. it("User daily 检查顺序:Key daily 先于 User daily", async () => {
  486. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  487. const callOrder: string[] = [];
  488. rateLimitServiceMock.checkCostLimitsWithLease.mockImplementation(async (_id, type, limits) => {
  489. if (limits.limit_daily_usd !== null) {
  490. callOrder.push(`${type}_daily`);
  491. }
  492. return { allowed: true };
  493. });
  494. const session = createSession({
  495. user: { dailyQuota: 10 },
  496. key: { limitDailyUsd: 20 },
  497. });
  498. await ProxyRateLimitGuard.ensure(session);
  499. // Key daily should be checked before User daily
  500. const keyDailyIdx = callOrder.indexOf("key_daily");
  501. const userDailyIdx = callOrder.indexOf("user_daily");
  502. expect(keyDailyIdx).toBeLessThan(userDailyIdx);
  503. });
  504. });