rate-limit-guard.test.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602
  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. cost_reset_at: null,
  148. });
  149. });
  150. it("当 Key 每日额度超限时,应在用户每日检查之前直接拦截(Key 优先)", async () => {
  151. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  152. rateLimitServiceMock.checkCostLimitsWithLease
  153. .mockResolvedValueOnce({ allowed: true }) // key 5h
  154. .mockResolvedValueOnce({ allowed: true }) // user 5h
  155. .mockResolvedValueOnce({
  156. allowed: false,
  157. reason: "Key daily cost limit reached (usage: 20.0000/10.0000)",
  158. }); // key daily
  159. const session = createSession({
  160. user: { dailyQuota: 999 },
  161. key: { limitDailyUsd: 10 },
  162. });
  163. await expect(ProxyRateLimitGuard.ensure(session)).rejects.toMatchObject({
  164. name: "RateLimitError",
  165. limitType: "daily_quota",
  166. });
  167. expect(rateLimitServiceMock.checkUserDailyCost).not.toHaveBeenCalled();
  168. });
  169. it("当 Key 未设置每日额度且用户每日额度已超限时,仍应拦截用户每日额度", async () => {
  170. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  171. rateLimitServiceMock.checkCostLimitsWithLease
  172. .mockResolvedValueOnce({ allowed: true }) // key 5h
  173. .mockResolvedValueOnce({ allowed: true }) // user 5h
  174. .mockResolvedValueOnce({ allowed: true }) // key daily (limit null)
  175. .mockResolvedValueOnce({
  176. allowed: false,
  177. reason: "User daily cost limit reached (usage: 20.0000/10.0000)",
  178. }); // user daily
  179. const session = createSession({
  180. user: { dailyQuota: 10 },
  181. key: { limitDailyUsd: null },
  182. });
  183. await expect(ProxyRateLimitGuard.ensure(session)).rejects.toMatchObject({
  184. name: "RateLimitError",
  185. limitType: "daily_quota",
  186. currentUsage: 20,
  187. limitValue: 10,
  188. });
  189. // User daily 现在使用 checkCostLimitsWithLease 而不是 checkUserDailyCost
  190. expect(rateLimitServiceMock.checkUserDailyCost).not.toHaveBeenCalled();
  191. expect(rateLimitServiceMock.checkCostLimitsWithLease).toHaveBeenCalledWith(1, "user", {
  192. limit_5h_usd: null,
  193. limit_daily_usd: 10,
  194. daily_reset_time: "00:00",
  195. daily_reset_mode: "fixed",
  196. limit_weekly_usd: null,
  197. limit_monthly_usd: null,
  198. cost_reset_at: null,
  199. });
  200. });
  201. it("Key 总限额超限应拦截(usd_total)", async () => {
  202. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  203. rateLimitServiceMock.checkTotalCostLimit.mockResolvedValueOnce({
  204. allowed: false,
  205. current: 20,
  206. reason: "Key total limit exceeded",
  207. });
  208. const session = createSession({
  209. key: { limitTotalUsd: 10 },
  210. });
  211. await expect(ProxyRateLimitGuard.ensure(session)).rejects.toMatchObject({
  212. name: "RateLimitError",
  213. limitType: "usd_total",
  214. currentUsage: 20,
  215. limitValue: 10,
  216. });
  217. });
  218. it("User 总限额超限应拦截(usd_total)", async () => {
  219. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  220. rateLimitServiceMock.checkTotalCostLimit
  221. .mockResolvedValueOnce({ allowed: true }) // key total
  222. .mockResolvedValueOnce({ allowed: false, current: 20, reason: "User total limit exceeded" }); // user total
  223. const session = createSession({
  224. user: { limitTotalUsd: 10 },
  225. });
  226. await expect(ProxyRateLimitGuard.ensure(session)).rejects.toMatchObject({
  227. name: "RateLimitError",
  228. limitType: "usd_total",
  229. currentUsage: 20,
  230. limitValue: 10,
  231. });
  232. });
  233. it("Key 并发 Session 超限应拦截(concurrent_sessions)", async () => {
  234. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  235. rateLimitServiceMock.checkAndTrackKeyUserSession.mockResolvedValueOnce({
  236. allowed: false,
  237. rejectedBy: "key",
  238. reasonCode: "RATE_LIMIT_CONCURRENT_SESSIONS_EXCEEDED",
  239. reasonParams: { current: 2, limit: 1, target: "key" },
  240. keyCount: 2,
  241. userCount: 0,
  242. trackedKey: false,
  243. trackedUser: false,
  244. });
  245. const session = createSession({
  246. key: { limitConcurrentSessions: 1 },
  247. });
  248. await expect(ProxyRateLimitGuard.ensure(session)).rejects.toMatchObject({
  249. name: "RateLimitError",
  250. limitType: "concurrent_sessions",
  251. currentUsage: 2,
  252. limitValue: 1,
  253. });
  254. });
  255. it("User 并发 Session 超限应拦截(concurrent_sessions)", async () => {
  256. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  257. rateLimitServiceMock.checkAndTrackKeyUserSession.mockResolvedValueOnce({
  258. allowed: false,
  259. rejectedBy: "user",
  260. reasonCode: "RATE_LIMIT_CONCURRENT_SESSIONS_EXCEEDED",
  261. reasonParams: { current: 2, limit: 1, target: "user" },
  262. keyCount: 0,
  263. userCount: 2,
  264. trackedKey: false,
  265. trackedUser: false,
  266. });
  267. const session = createSession({
  268. user: { limitConcurrentSessions: 1 },
  269. key: { limitConcurrentSessions: 10 },
  270. });
  271. await expect(ProxyRateLimitGuard.ensure(session)).rejects.toMatchObject({
  272. name: "RateLimitError",
  273. limitType: "concurrent_sessions",
  274. currentUsage: 2,
  275. limitValue: 1,
  276. });
  277. });
  278. it("当 Key 并发未设置(0)且 User 并发已设置时,Key 并发检查应继承 User 并发上限", async () => {
  279. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  280. const session = createSession({
  281. user: { limitConcurrentSessions: 15 },
  282. key: { limitConcurrentSessions: 0 },
  283. });
  284. await expect(ProxyRateLimitGuard.ensure(session)).resolves.toBeUndefined();
  285. expect(rateLimitServiceMock.checkAndTrackKeyUserSession).toHaveBeenCalledWith(
  286. 2,
  287. 1,
  288. "sess_test",
  289. 15,
  290. 15
  291. );
  292. });
  293. it("User RPM 超限应拦截(rpm)", async () => {
  294. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  295. rateLimitServiceMock.checkRpmLimit.mockResolvedValueOnce({
  296. allowed: false,
  297. current: 10,
  298. reason: "用户每分钟请求数上限已达到(10/5)",
  299. });
  300. const session = createSession({
  301. user: { rpm: 5 },
  302. });
  303. await expect(ProxyRateLimitGuard.ensure(session)).rejects.toMatchObject({
  304. name: "RateLimitError",
  305. limitType: "rpm",
  306. currentUsage: 10,
  307. limitValue: 5,
  308. });
  309. });
  310. it("Key 5h 超限应拦截(usd_5h)", async () => {
  311. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  312. rateLimitServiceMock.checkCostLimitsWithLease.mockResolvedValueOnce({
  313. allowed: false,
  314. reason: "Key 5h cost limit reached (usage: 20.0000/10.0000)",
  315. });
  316. const session = createSession({
  317. key: { limit5hUsd: 10 },
  318. });
  319. await expect(ProxyRateLimitGuard.ensure(session)).rejects.toMatchObject({
  320. name: "RateLimitError",
  321. limitType: "usd_5h",
  322. currentUsage: 20,
  323. limitValue: 10,
  324. });
  325. });
  326. it("User 5h 超限应拦截(usd_5h)", async () => {
  327. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  328. rateLimitServiceMock.checkCostLimitsWithLease
  329. .mockResolvedValueOnce({ allowed: true }) // key 5h
  330. .mockResolvedValueOnce({
  331. allowed: false,
  332. reason: "User 5h cost limit reached (usage: 20.0000/10.0000)",
  333. }); // user 5h
  334. const session = createSession({
  335. user: { limit5hUsd: 10 },
  336. });
  337. await expect(ProxyRateLimitGuard.ensure(session)).rejects.toMatchObject({
  338. name: "RateLimitError",
  339. limitType: "usd_5h",
  340. currentUsage: 20,
  341. limitValue: 10,
  342. });
  343. });
  344. it("Key 周限额超限应拦截(usd_weekly)", async () => {
  345. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  346. rateLimitServiceMock.checkCostLimitsWithLease
  347. .mockResolvedValueOnce({ allowed: true }) // key 5h
  348. .mockResolvedValueOnce({ allowed: true }) // user 5h
  349. .mockResolvedValueOnce({ allowed: true }) // key daily
  350. .mockResolvedValueOnce({ allowed: true }) // user daily (new with lease migration)
  351. .mockResolvedValueOnce({
  352. allowed: false,
  353. reason: "Key weekly cost limit reached (usage: 100.0000/10.0000)",
  354. }); // key weekly
  355. const session = createSession({
  356. key: { limitWeeklyUsd: 10 },
  357. });
  358. await expect(ProxyRateLimitGuard.ensure(session)).rejects.toMatchObject({
  359. name: "RateLimitError",
  360. limitType: "usd_weekly",
  361. currentUsage: 100,
  362. limitValue: 10,
  363. });
  364. });
  365. it("User 周限额超限应拦截(usd_weekly)", async () => {
  366. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  367. rateLimitServiceMock.checkCostLimitsWithLease
  368. .mockResolvedValueOnce({ allowed: true }) // key 5h
  369. .mockResolvedValueOnce({ allowed: true }) // user 5h
  370. .mockResolvedValueOnce({ allowed: true }) // key daily
  371. .mockResolvedValueOnce({ allowed: true }) // user daily (new with lease migration)
  372. .mockResolvedValueOnce({ allowed: true }) // key weekly
  373. .mockResolvedValueOnce({
  374. allowed: false,
  375. reason: "User weekly cost limit reached (usage: 100.0000/10.0000)",
  376. }); // user weekly
  377. const session = createSession({
  378. user: { limitWeeklyUsd: 10 },
  379. });
  380. await expect(ProxyRateLimitGuard.ensure(session)).rejects.toMatchObject({
  381. name: "RateLimitError",
  382. limitType: "usd_weekly",
  383. currentUsage: 100,
  384. limitValue: 10,
  385. });
  386. });
  387. it("Key 月限额超限应拦截(usd_monthly)", async () => {
  388. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  389. rateLimitServiceMock.checkCostLimitsWithLease
  390. .mockResolvedValueOnce({ allowed: true }) // key 5h
  391. .mockResolvedValueOnce({ allowed: true }) // user 5h
  392. .mockResolvedValueOnce({ allowed: true }) // key daily
  393. .mockResolvedValueOnce({ allowed: true }) // user daily (new with lease migration)
  394. .mockResolvedValueOnce({ allowed: true }) // key weekly
  395. .mockResolvedValueOnce({ allowed: true }) // user weekly
  396. .mockResolvedValueOnce({
  397. allowed: false,
  398. reason: "Key monthly cost limit reached (usage: 200.0000/10.0000)",
  399. }); // key monthly
  400. const session = createSession({
  401. key: { limitMonthlyUsd: 10 },
  402. });
  403. await expect(ProxyRateLimitGuard.ensure(session)).rejects.toMatchObject({
  404. name: "RateLimitError",
  405. limitType: "usd_monthly",
  406. currentUsage: 200,
  407. limitValue: 10,
  408. });
  409. });
  410. it("User 月限额超限应拦截(usd_monthly)", async () => {
  411. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  412. rateLimitServiceMock.checkCostLimitsWithLease
  413. .mockResolvedValueOnce({ allowed: true }) // key 5h
  414. .mockResolvedValueOnce({ allowed: true }) // user 5h
  415. .mockResolvedValueOnce({ allowed: true }) // key daily
  416. .mockResolvedValueOnce({ allowed: true }) // user daily (new with lease migration)
  417. .mockResolvedValueOnce({ allowed: true }) // key weekly
  418. .mockResolvedValueOnce({ allowed: true }) // user weekly
  419. .mockResolvedValueOnce({ allowed: true }) // key monthly
  420. .mockResolvedValueOnce({
  421. allowed: false,
  422. reason: "User monthly cost limit reached (usage: 200.0000/10.0000)",
  423. }); // user monthly
  424. const session = createSession({
  425. user: { limitMonthlyUsd: 10 },
  426. });
  427. await expect(ProxyRateLimitGuard.ensure(session)).rejects.toMatchObject({
  428. name: "RateLimitError",
  429. limitType: "usd_monthly",
  430. currentUsage: 200,
  431. limitValue: 10,
  432. });
  433. });
  434. it("所有限额均未触发时应放行", async () => {
  435. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  436. const session = createSession();
  437. await expect(ProxyRateLimitGuard.ensure(session)).resolves.toBeUndefined();
  438. });
  439. it("当 sessionId 缺失时,应兜底生成并继续并发检查", async () => {
  440. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  441. const session = createSession() as any;
  442. session.sessionId = undefined;
  443. await expect(ProxyRateLimitGuard.ensure(session)).resolves.toBeUndefined();
  444. expect(generateSessionIdMock).toHaveBeenCalledTimes(1);
  445. expect(session.sessionId).toBe("sess_generated");
  446. expect(rateLimitServiceMock.checkAndTrackKeyUserSession).toHaveBeenCalledWith(
  447. 2,
  448. 1,
  449. "sess_generated",
  450. expect.any(Number),
  451. expect.any(Number)
  452. );
  453. });
  454. it("User daily (rolling mode) 超限应使用 checkCostLimitsWithLease", async () => {
  455. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  456. rateLimitServiceMock.checkCostLimitsWithLease
  457. .mockResolvedValueOnce({ allowed: true }) // key 5h
  458. .mockResolvedValueOnce({ allowed: true }) // user 5h
  459. .mockResolvedValueOnce({ allowed: true }) // key daily (limit null)
  460. .mockResolvedValueOnce({
  461. allowed: false,
  462. reason: "User daily cost limit reached (usage: 15.0000/10.0000)",
  463. }); // user daily rolling
  464. const session = createSession({
  465. user: { dailyQuota: 10, dailyResetMode: "rolling", dailyResetTime: "12:00" },
  466. key: { limitDailyUsd: null },
  467. });
  468. await expect(ProxyRateLimitGuard.ensure(session)).rejects.toMatchObject({
  469. name: "RateLimitError",
  470. limitType: "daily_quota",
  471. currentUsage: 15,
  472. limitValue: 10,
  473. resetTime: null, // rolling 模式没有固定重置时间
  474. });
  475. // Verify checkCostLimitsWithLease was called with rolling mode
  476. expect(rateLimitServiceMock.checkCostLimitsWithLease).toHaveBeenCalledWith(1, "user", {
  477. limit_5h_usd: null,
  478. limit_daily_usd: 10,
  479. daily_reset_time: "12:00",
  480. daily_reset_mode: "rolling",
  481. limit_weekly_usd: null,
  482. limit_monthly_usd: null,
  483. cost_reset_at: null,
  484. });
  485. // checkUserDailyCost should NOT be called (migrated to lease)
  486. expect(rateLimitServiceMock.checkUserDailyCost).not.toHaveBeenCalled();
  487. });
  488. it("User daily 检查顺序:Key daily 先于 User daily", async () => {
  489. const { ProxyRateLimitGuard } = await import("@/app/v1/_lib/proxy/rate-limit-guard");
  490. const callOrder: string[] = [];
  491. rateLimitServiceMock.checkCostLimitsWithLease.mockImplementation(async (_id, type, limits) => {
  492. if (limits.limit_daily_usd !== null) {
  493. callOrder.push(`${type}_daily`);
  494. }
  495. return { allowed: true };
  496. });
  497. const session = createSession({
  498. user: { dailyQuota: 10 },
  499. key: { limitDailyUsd: 20 },
  500. });
  501. await ProxyRateLimitGuard.ensure(session);
  502. // Key daily should be checked before User daily
  503. const keyDailyIdx = callOrder.indexOf("key_daily");
  504. const userDailyIdx = callOrder.indexOf("user_daily");
  505. expect(keyDailyIdx).toBeLessThan(userDailyIdx);
  506. });
  507. });