auth-guard-precheck.test.ts 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. import { beforeEach, describe, expect, it, vi } from "vitest";
  2. const validateApiKeyAndGetUser = vi.hoisted(() => vi.fn());
  3. const policyCheck = vi.hoisted(() => vi.fn());
  4. const policyRecordSuccess = vi.hoisted(() => vi.fn());
  5. const policyRecordFailure = vi.hoisted(() => vi.fn());
  6. vi.mock("@/repository/key", () => ({
  7. validateApiKeyAndGetUser,
  8. }));
  9. vi.mock("@/repository/user", () => ({
  10. markUserExpired: vi.fn().mockResolvedValue(undefined),
  11. }));
  12. vi.mock("@/lib/logger", () => ({
  13. logger: {
  14. debug: vi.fn(),
  15. warn: vi.fn(),
  16. error: vi.fn(),
  17. },
  18. }));
  19. vi.mock("@/lib/security/login-abuse-policy", () => ({
  20. LoginAbusePolicy: class {
  21. check = policyCheck;
  22. recordSuccess = policyRecordSuccess;
  23. recordFailure = policyRecordFailure;
  24. },
  25. }));
  26. function makeSession(ip: string, apiKey: string) {
  27. return {
  28. headers: new Headers({
  29. "x-real-ip": ip,
  30. "x-api-key": apiKey,
  31. }),
  32. requestUrl: new URL("http://localhost/v1/models"),
  33. clientIp: null as string | null,
  34. authState: null as unknown,
  35. setAuthState(state: unknown) {
  36. this.authState = state;
  37. },
  38. };
  39. }
  40. describe("ProxyAuthenticator pre-auth candidate key lockout", () => {
  41. beforeEach(() => {
  42. vi.resetModules();
  43. validateApiKeyAndGetUser.mockReset();
  44. policyCheck.mockReset();
  45. policyRecordSuccess.mockReset();
  46. policyRecordFailure.mockReset();
  47. });
  48. it("blocks a locked API key before validation and passes candidate key into the check", async () => {
  49. policyCheck.mockReturnValue({
  50. allowed: false,
  51. retryAfterSeconds: 42,
  52. reason: "key_rate_limited",
  53. });
  54. const { ProxyAuthenticator } = await import("@/app/v1/_lib/proxy/auth-guard");
  55. const session = makeSession("198.51.100.77", "sk-shared");
  56. const response = await ProxyAuthenticator.ensure(session as never);
  57. expect(response?.status).toBe(429);
  58. expect(validateApiKeyAndGetUser).not.toHaveBeenCalled();
  59. expect(policyCheck).toHaveBeenCalledWith("198.51.100.77", "sk-shared");
  60. expect(session.clientIp).toBe("198.51.100.77");
  61. });
  62. it("resets both IP and key scopes on successful authentication", async () => {
  63. policyCheck.mockReturnValue({ allowed: true });
  64. validateApiKeyAndGetUser.mockResolvedValue({
  65. user: { id: 1, name: "alice", isEnabled: true, expiresAt: null },
  66. key: { name: "primary-key" },
  67. });
  68. const { ProxyAuthenticator } = await import("@/app/v1/_lib/proxy/auth-guard");
  69. const session = makeSession("203.0.113.10", "sk-success");
  70. const response = await ProxyAuthenticator.ensure(session as never);
  71. expect(response).toBeNull();
  72. expect(policyRecordSuccess).toHaveBeenCalledWith("203.0.113.10", "sk-success");
  73. expect(policyRecordFailure).not.toHaveBeenCalled();
  74. });
  75. it("records failures against both IP and candidate key", async () => {
  76. policyCheck.mockReturnValue({ allowed: true });
  77. validateApiKeyAndGetUser.mockResolvedValue(null);
  78. const { ProxyAuthenticator } = await import("@/app/v1/_lib/proxy/auth-guard");
  79. const session = makeSession("203.0.113.11", "sk-invalid");
  80. const response = await ProxyAuthenticator.ensure(session as never);
  81. expect(response?.status).toBe(401);
  82. expect(policyRecordFailure).toHaveBeenCalledWith("203.0.113.11", "sk-invalid");
  83. expect(policyRecordSuccess).not.toHaveBeenCalled();
  84. });
  85. });