auth-validateKey-cache.test.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. import { beforeEach, describe, expect, test, vi } from "vitest";
  2. import type { Key } from "@/types/key";
  3. import type { User } from "@/types/user";
  4. const isDefinitelyNotPresent = vi.fn(() => false);
  5. const noteExistingKey = vi.fn();
  6. const getCachedActiveKey = vi.fn();
  7. const getCachedUser = vi.fn();
  8. // 如果缓存路径未命中,这些 DB 调用会触发并让测试失败
  9. vi.mock("@/drizzle/db", () => ({
  10. db: {
  11. select: vi.fn(() => {
  12. throw new Error("DB_ACCESS");
  13. }),
  14. insert: vi.fn(() => {
  15. throw new Error("DB_ACCESS");
  16. }),
  17. update: vi.fn(() => {
  18. throw new Error("DB_ACCESS");
  19. }),
  20. },
  21. }));
  22. vi.mock("@/lib/security/api-key-vacuum-filter", () => ({
  23. apiKeyVacuumFilter: {
  24. isDefinitelyNotPresent,
  25. noteExistingKey,
  26. startBackgroundReload: vi.fn(),
  27. invalidateAndReload: vi.fn(),
  28. getStats: vi.fn(),
  29. },
  30. }));
  31. vi.mock("@/lib/security/api-key-auth-cache", () => ({
  32. getCachedActiveKey,
  33. getCachedUser,
  34. cacheActiveKey: vi.fn(async () => {}),
  35. cacheAuthResult: vi.fn(async () => {}),
  36. cacheUser: vi.fn(async () => {}),
  37. invalidateCachedKey: vi.fn(async () => {}),
  38. invalidateCachedUser: vi.fn(async () => {}),
  39. }));
  40. function buildKey(overrides?: Partial<Key>): Key {
  41. const now = new Date("2026-02-08T00:00:00.000Z");
  42. return {
  43. id: 1,
  44. userId: 10,
  45. name: "k1",
  46. key: "sk-user-login",
  47. isEnabled: true,
  48. expiresAt: undefined,
  49. canLoginWebUi: true,
  50. limit5hUsd: null,
  51. limitDailyUsd: null,
  52. dailyResetMode: "fixed",
  53. dailyResetTime: "00:00",
  54. limitWeeklyUsd: null,
  55. limitMonthlyUsd: null,
  56. limitTotalUsd: null,
  57. limitConcurrentSessions: 0,
  58. providerGroup: null,
  59. cacheTtlPreference: null,
  60. createdAt: now,
  61. updatedAt: now,
  62. deletedAt: undefined,
  63. ...overrides,
  64. };
  65. }
  66. function buildUser(overrides?: Partial<User>): User {
  67. const now = new Date("2026-02-08T00:00:00.000Z");
  68. return {
  69. id: 10,
  70. name: "u1",
  71. description: "",
  72. role: "user",
  73. rpm: null,
  74. dailyQuota: null,
  75. providerGroup: null,
  76. tags: [],
  77. createdAt: now,
  78. updatedAt: now,
  79. deletedAt: undefined,
  80. dailyResetMode: "fixed",
  81. dailyResetTime: "00:00",
  82. isEnabled: true,
  83. expiresAt: null,
  84. allowedClients: [],
  85. allowedModels: [],
  86. ...overrides,
  87. };
  88. }
  89. describe("auth.ts:validateKey(Vacuum Filter -> Redis -> DB)", () => {
  90. beforeEach(() => {
  91. vi.clearAllMocks();
  92. isDefinitelyNotPresent.mockReturnValue(false);
  93. getCachedActiveKey.mockResolvedValue(null);
  94. getCachedUser.mockResolvedValue(null);
  95. });
  96. test("Redis key+user 命中时:validateKey 应不访问 DB 且返回 session(保护 login 侧热路径)", async () => {
  97. const cachedKey = buildKey({ key: "sk-user-login", canLoginWebUi: true, userId: 10 });
  98. const cachedUser = buildUser({ id: 10 });
  99. getCachedActiveKey.mockResolvedValueOnce(cachedKey);
  100. getCachedUser.mockResolvedValueOnce(cachedUser);
  101. const { validateKey } = await import("@/lib/auth");
  102. await expect(validateKey("sk-user-login")).resolves.toEqual({
  103. user: cachedUser,
  104. key: cachedKey,
  105. });
  106. expect(isDefinitelyNotPresent).toHaveBeenCalledWith("sk-user-login");
  107. });
  108. test("用户禁用:缓存命中也应拒绝(保护登录/会话)", async () => {
  109. const cachedKey = buildKey({ key: "sk-user-disabled", canLoginWebUi: true, userId: 10 });
  110. const cachedUser = buildUser({ id: 10, isEnabled: false });
  111. getCachedActiveKey.mockResolvedValueOnce(cachedKey);
  112. getCachedUser.mockResolvedValueOnce(cachedUser);
  113. const { validateKey } = await import("@/lib/auth");
  114. await expect(validateKey("sk-user-disabled")).resolves.toBeNull();
  115. });
  116. test("用户过期:缓存命中也应拒绝(保护登录/会话)", async () => {
  117. const cachedKey = buildKey({ key: "sk-user-expired", canLoginWebUi: true, userId: 10 });
  118. const cachedUser = buildUser({ id: 10, expiresAt: new Date("2000-01-01T00:00:00.000Z") });
  119. getCachedActiveKey.mockResolvedValueOnce(cachedKey);
  120. getCachedUser.mockResolvedValueOnce(cachedUser);
  121. const { validateKey } = await import("@/lib/auth");
  122. await expect(validateKey("sk-user-expired")).resolves.toBeNull();
  123. });
  124. test("canLoginWebUi=false 且 allowReadOnlyAccess=false:缓存命中也应拒绝", async () => {
  125. const cachedKey = buildKey({ key: "sk-no-webui", canLoginWebUi: false, userId: 10 });
  126. const cachedUser = buildUser({ id: 10 });
  127. getCachedActiveKey.mockResolvedValueOnce(cachedKey);
  128. getCachedUser.mockResolvedValueOnce(cachedUser);
  129. const { validateKey } = await import("@/lib/auth");
  130. await expect(validateKey("sk-no-webui", { allowReadOnlyAccess: false })).resolves.toBeNull();
  131. });
  132. test("allowReadOnlyAccess=true:应允许 canLoginWebUi=false 的 key 登录只读页面", async () => {
  133. const cachedKey = buildKey({ key: "sk-readonly", canLoginWebUi: false, userId: 10 });
  134. const cachedUser = buildUser({ id: 10 });
  135. getCachedActiveKey.mockResolvedValueOnce(cachedKey);
  136. getCachedUser.mockResolvedValueOnce(cachedUser);
  137. const { validateKey } = await import("@/lib/auth");
  138. await expect(validateKey("sk-readonly", { allowReadOnlyAccess: true })).resolves.toEqual({
  139. user: cachedUser,
  140. key: cachedKey,
  141. });
  142. });
  143. });