client-guard.test.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. import { describe, expect, test, vi, beforeEach } from "vitest";
  2. import { ProxyClientGuard } from "@/app/v1/_lib/proxy/client-guard";
  3. import type { ProxySession } from "@/app/v1/_lib/proxy/session";
  4. // Mock ProxyResponses
  5. vi.mock("@/app/v1/_lib/proxy/responses", () => ({
  6. ProxyResponses: {
  7. buildError: (status: number, message: string, code: string) =>
  8. new Response(JSON.stringify({ error: { message, type: code } }), { status }),
  9. },
  10. }));
  11. // Helper to create mock session
  12. function createMockSession(
  13. userAgent: string | undefined,
  14. allowedClients: string[] = []
  15. ): ProxySession {
  16. return {
  17. userAgent,
  18. authState: {
  19. user: {
  20. allowedClients,
  21. },
  22. },
  23. } as unknown as ProxySession;
  24. }
  25. describe("ProxyClientGuard", () => {
  26. describe("when authState is missing", () => {
  27. test("should allow request when authState is undefined", async () => {
  28. const session = { userAgent: "SomeClient/1.0" } as unknown as ProxySession;
  29. const result = await ProxyClientGuard.ensure(session);
  30. expect(result).toBeNull();
  31. });
  32. test("should allow request when authState.user is undefined", async () => {
  33. const session = {
  34. userAgent: "SomeClient/1.0",
  35. authState: {},
  36. } as unknown as ProxySession;
  37. const result = await ProxyClientGuard.ensure(session);
  38. expect(result).toBeNull();
  39. });
  40. });
  41. describe("when no restrictions configured", () => {
  42. test("should allow request when allowedClients is empty", async () => {
  43. const session = createMockSession("AnyClient/1.0", []);
  44. const result = await ProxyClientGuard.ensure(session);
  45. expect(result).toBeNull();
  46. });
  47. test("should allow request when allowedClients is undefined", async () => {
  48. const session = createMockSession("AnyClient/1.0", undefined as unknown as string[]);
  49. const result = await ProxyClientGuard.ensure(session);
  50. expect(result).toBeNull();
  51. });
  52. });
  53. describe("when restrictions are configured", () => {
  54. test("should reject when User-Agent is missing", async () => {
  55. const session = createMockSession(undefined, ["claude-cli"]);
  56. const result = await ProxyClientGuard.ensure(session);
  57. expect(result).not.toBeNull();
  58. expect(result?.status).toBe(400);
  59. });
  60. test("should reject when User-Agent is empty", async () => {
  61. const session = createMockSession("", ["claude-cli"]);
  62. const result = await ProxyClientGuard.ensure(session);
  63. expect(result).not.toBeNull();
  64. expect(result?.status).toBe(400);
  65. });
  66. test("should reject when User-Agent is whitespace only", async () => {
  67. const session = createMockSession(" ", ["claude-cli"]);
  68. const result = await ProxyClientGuard.ensure(session);
  69. expect(result).not.toBeNull();
  70. expect(result?.status).toBe(400);
  71. });
  72. });
  73. describe("pattern matching with hyphen/underscore normalization", () => {
  74. test("should match gemini-cli pattern against GeminiCLI User-Agent", async () => {
  75. const session = createMockSession("GeminiCLI/0.22.5/gemini-3-pro-preview (darwin; arm64)", [
  76. "gemini-cli",
  77. ]);
  78. const result = await ProxyClientGuard.ensure(session);
  79. expect(result).toBeNull();
  80. });
  81. test("should match claude-cli pattern against claude_cli User-Agent", async () => {
  82. const session = createMockSession("claude_cli/1.0", ["claude-cli"]);
  83. const result = await ProxyClientGuard.ensure(session);
  84. expect(result).toBeNull();
  85. });
  86. test("should match codex-cli pattern against codexcli User-Agent", async () => {
  87. const session = createMockSession("codexcli/2.0", ["codex-cli"]);
  88. const result = await ProxyClientGuard.ensure(session);
  89. expect(result).toBeNull();
  90. });
  91. test("should match factory-cli pattern against FactoryCLI User-Agent", async () => {
  92. const session = createMockSession("FactoryCLI/1.0", ["factory-cli"]);
  93. const result = await ProxyClientGuard.ensure(session);
  94. expect(result).toBeNull();
  95. });
  96. test("should be case-insensitive", async () => {
  97. const session = createMockSession("GEMINICLI/1.0", ["gemini-cli"]);
  98. const result = await ProxyClientGuard.ensure(session);
  99. expect(result).toBeNull();
  100. });
  101. });
  102. describe("pattern matching without normalization needed", () => {
  103. test("should match exact substring", async () => {
  104. const session = createMockSession("claude-cli/1.0.0", ["claude-cli"]);
  105. const result = await ProxyClientGuard.ensure(session);
  106. expect(result).toBeNull();
  107. });
  108. test("should match when User-Agent contains pattern as substring", async () => {
  109. const session = createMockSession("Mozilla/5.0 claude-cli/1.0 Compatible", ["claude-cli"]);
  110. const result = await ProxyClientGuard.ensure(session);
  111. expect(result).toBeNull();
  112. });
  113. });
  114. describe("multiple patterns", () => {
  115. test("should allow when one of multiple patterns matches", async () => {
  116. const session = createMockSession("GeminiCLI/1.0", ["claude-cli", "gemini-cli", "codex-cli"]);
  117. const result = await ProxyClientGuard.ensure(session);
  118. expect(result).toBeNull();
  119. });
  120. test("should reject when no patterns match", async () => {
  121. const session = createMockSession("UnknownClient/1.0", ["claude-cli", "gemini-cli"]);
  122. const result = await ProxyClientGuard.ensure(session);
  123. expect(result).not.toBeNull();
  124. expect(result?.status).toBe(400);
  125. });
  126. });
  127. describe("edge cases", () => {
  128. test("should handle pattern with multiple hyphens", async () => {
  129. const session = createMockSession("my-special-cli/1.0", ["my-special-cli"]);
  130. const result = await ProxyClientGuard.ensure(session);
  131. expect(result).toBeNull();
  132. });
  133. test("should handle pattern with underscores", async () => {
  134. const session = createMockSession("my_special_cli/1.0", ["my-special-cli"]);
  135. const result = await ProxyClientGuard.ensure(session);
  136. expect(result).toBeNull();
  137. });
  138. test("should handle mixed hyphen and underscore", async () => {
  139. const session = createMockSession("my_special-cli/1.0", ["my-special_cli"]);
  140. const result = await ProxyClientGuard.ensure(session);
  141. expect(result).toBeNull();
  142. });
  143. test("should reject when pattern normalizes to empty string", async () => {
  144. const session = createMockSession("AnyClient/1.0", ["-"]);
  145. const result = await ProxyClientGuard.ensure(session);
  146. expect(result).not.toBeNull();
  147. expect(result?.status).toBe(400);
  148. });
  149. test("should reject when pattern is only underscores", async () => {
  150. const session = createMockSession("AnyClient/1.0", ["___"]);
  151. const result = await ProxyClientGuard.ensure(session);
  152. expect(result).not.toBeNull();
  153. expect(result?.status).toBe(400);
  154. });
  155. test("should reject when pattern is only hyphens and underscores", async () => {
  156. const session = createMockSession("AnyClient/1.0", ["-_-_-"]);
  157. const result = await ProxyClientGuard.ensure(session);
  158. expect(result).not.toBeNull();
  159. expect(result?.status).toBe(400);
  160. });
  161. test("should reject when all patterns normalize to empty", async () => {
  162. const session = createMockSession("AnyClient/1.0", ["-", "_", "--"]);
  163. const result = await ProxyClientGuard.ensure(session);
  164. expect(result).not.toBeNull();
  165. expect(result?.status).toBe(400);
  166. });
  167. test("should allow when at least one pattern is valid after normalization", async () => {
  168. const session = createMockSession("ValidClient/1.0", ["-", "valid", "_"]);
  169. const result = await ProxyClientGuard.ensure(session);
  170. expect(result).toBeNull();
  171. });
  172. });
  173. });