client-guard.test.ts 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. import { describe, expect, test, vi } 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. blockedClients: string[] = []
  16. ): ProxySession {
  17. return {
  18. userAgent,
  19. headers: new Headers(),
  20. request: { message: {} },
  21. authState: {
  22. user: {
  23. allowedClients,
  24. blockedClients,
  25. },
  26. },
  27. } as unknown as ProxySession;
  28. }
  29. describe("ProxyClientGuard", () => {
  30. describe("when authState is missing", () => {
  31. test("should allow request when authState is undefined", async () => {
  32. const session = { userAgent: "SomeClient/1.0" } as unknown as ProxySession;
  33. const result = await ProxyClientGuard.ensure(session);
  34. expect(result).toBeNull();
  35. });
  36. test("should allow request when authState.user is undefined", async () => {
  37. const session = {
  38. userAgent: "SomeClient/1.0",
  39. authState: {},
  40. } as unknown as ProxySession;
  41. const result = await ProxyClientGuard.ensure(session);
  42. expect(result).toBeNull();
  43. });
  44. });
  45. describe("when no restrictions configured", () => {
  46. test("should allow request when allowedClients is empty", async () => {
  47. const session = createMockSession("AnyClient/1.0", []);
  48. const result = await ProxyClientGuard.ensure(session);
  49. expect(result).toBeNull();
  50. });
  51. test("should allow request when allowedClients is undefined", async () => {
  52. const session = createMockSession("AnyClient/1.0", undefined as unknown as string[]);
  53. const result = await ProxyClientGuard.ensure(session);
  54. expect(result).toBeNull();
  55. });
  56. });
  57. describe("when both allowedClients and blockedClients are empty", () => {
  58. test("should allow request", async () => {
  59. const session = createMockSession("AnyClient/1.0", [], []);
  60. const result = await ProxyClientGuard.ensure(session);
  61. expect(result).toBeNull();
  62. });
  63. });
  64. describe("when restrictions are configured", () => {
  65. test("should reject when User-Agent is missing", async () => {
  66. const session = createMockSession(undefined, ["claude-cli"]);
  67. const result = await ProxyClientGuard.ensure(session);
  68. expect(result).not.toBeNull();
  69. expect(result?.status).toBe(400);
  70. });
  71. test("should reject when User-Agent is empty", async () => {
  72. const session = createMockSession("", ["claude-cli"]);
  73. const result = await ProxyClientGuard.ensure(session);
  74. expect(result).not.toBeNull();
  75. expect(result?.status).toBe(400);
  76. });
  77. test("should reject when User-Agent is whitespace only", async () => {
  78. const session = createMockSession(" ", ["claude-cli"]);
  79. const result = await ProxyClientGuard.ensure(session);
  80. expect(result).not.toBeNull();
  81. expect(result?.status).toBe(400);
  82. });
  83. });
  84. describe("pattern matching with hyphen/underscore normalization", () => {
  85. test("should match gemini-cli pattern against GeminiCLI User-Agent", async () => {
  86. const session = createMockSession("GeminiCLI/0.22.5/gemini-3-pro-preview (darwin; arm64)", [
  87. "gemini-cli",
  88. ]);
  89. const result = await ProxyClientGuard.ensure(session);
  90. expect(result).toBeNull();
  91. });
  92. test("should match claude-cli pattern against claude_cli User-Agent", async () => {
  93. const session = createMockSession("claude_cli/1.0", ["claude-cli"]);
  94. const result = await ProxyClientGuard.ensure(session);
  95. expect(result).toBeNull();
  96. });
  97. test("should match codex-cli pattern against codexcli User-Agent", async () => {
  98. const session = createMockSession("codexcli/2.0", ["codex-cli"]);
  99. const result = await ProxyClientGuard.ensure(session);
  100. expect(result).toBeNull();
  101. });
  102. test("should match factory-cli pattern against FactoryCLI User-Agent", async () => {
  103. const session = createMockSession("FactoryCLI/1.0", ["factory-cli"]);
  104. const result = await ProxyClientGuard.ensure(session);
  105. expect(result).toBeNull();
  106. });
  107. test("should be case-insensitive", async () => {
  108. const session = createMockSession("GEMINICLI/1.0", ["gemini-cli"]);
  109. const result = await ProxyClientGuard.ensure(session);
  110. expect(result).toBeNull();
  111. });
  112. });
  113. describe("pattern matching without normalization needed", () => {
  114. test("should match exact substring", async () => {
  115. const session = createMockSession("claude-cli/1.0.0", ["claude-cli"]);
  116. const result = await ProxyClientGuard.ensure(session);
  117. expect(result).toBeNull();
  118. });
  119. test("should match when User-Agent contains pattern as substring", async () => {
  120. const session = createMockSession("Mozilla/5.0 claude-cli/1.0 Compatible", ["claude-cli"]);
  121. const result = await ProxyClientGuard.ensure(session);
  122. expect(result).toBeNull();
  123. });
  124. });
  125. describe("multiple patterns", () => {
  126. test("should allow when one of multiple patterns matches", async () => {
  127. const session = createMockSession("GeminiCLI/1.0", ["claude-cli", "gemini-cli", "codex-cli"]);
  128. const result = await ProxyClientGuard.ensure(session);
  129. expect(result).toBeNull();
  130. });
  131. test("should reject when no patterns match", async () => {
  132. const session = createMockSession("UnknownClient/1.0", ["claude-cli", "gemini-cli"]);
  133. const result = await ProxyClientGuard.ensure(session);
  134. expect(result).not.toBeNull();
  135. expect(result?.status).toBe(400);
  136. });
  137. });
  138. describe("edge cases", () => {
  139. test("should handle pattern with multiple hyphens", async () => {
  140. const session = createMockSession("my-special-cli/1.0", ["my-special-cli"]);
  141. const result = await ProxyClientGuard.ensure(session);
  142. expect(result).toBeNull();
  143. });
  144. test("should handle pattern with underscores", async () => {
  145. const session = createMockSession("my_special_cli/1.0", ["my-special-cli"]);
  146. const result = await ProxyClientGuard.ensure(session);
  147. expect(result).toBeNull();
  148. });
  149. test("should handle mixed hyphen and underscore", async () => {
  150. const session = createMockSession("my_special-cli/1.0", ["my-special_cli"]);
  151. const result = await ProxyClientGuard.ensure(session);
  152. expect(result).toBeNull();
  153. });
  154. test("should reject when pattern normalizes to empty string", async () => {
  155. const session = createMockSession("AnyClient/1.0", ["-"]);
  156. const result = await ProxyClientGuard.ensure(session);
  157. expect(result).not.toBeNull();
  158. expect(result?.status).toBe(400);
  159. });
  160. test("should reject when pattern is only underscores", async () => {
  161. const session = createMockSession("AnyClient/1.0", ["___"]);
  162. const result = await ProxyClientGuard.ensure(session);
  163. expect(result).not.toBeNull();
  164. expect(result?.status).toBe(400);
  165. });
  166. test("should reject when pattern is only hyphens and underscores", async () => {
  167. const session = createMockSession("AnyClient/1.0", ["-_-_-"]);
  168. const result = await ProxyClientGuard.ensure(session);
  169. expect(result).not.toBeNull();
  170. expect(result?.status).toBe(400);
  171. });
  172. test("should reject when all patterns normalize to empty", async () => {
  173. const session = createMockSession("AnyClient/1.0", ["-", "_", "--"]);
  174. const result = await ProxyClientGuard.ensure(session);
  175. expect(result).not.toBeNull();
  176. expect(result?.status).toBe(400);
  177. });
  178. test("should allow when at least one pattern is valid after normalization", async () => {
  179. const session = createMockSession("ValidClient/1.0", ["-", "valid", "_"]);
  180. const result = await ProxyClientGuard.ensure(session);
  181. expect(result).toBeNull();
  182. });
  183. });
  184. describe("when blockedClients is configured", () => {
  185. test("should reject when client matches blocked pattern", async () => {
  186. const session = createMockSession("GeminiCLI/1.0", [], ["gemini-cli"]);
  187. const result = await ProxyClientGuard.ensure(session);
  188. expect(result).not.toBeNull();
  189. expect(result?.status).toBe(400);
  190. });
  191. test("should allow when client does not match blocked pattern", async () => {
  192. const session = createMockSession("CodexCLI/1.0", [], ["gemini-cli"]);
  193. const result = await ProxyClientGuard.ensure(session);
  194. expect(result).toBeNull();
  195. });
  196. test("should reject even when allowedClients matches", async () => {
  197. const session = createMockSession("gemini-cli/1.0", ["gemini-cli"], ["gemini-cli"]);
  198. const result = await ProxyClientGuard.ensure(session);
  199. expect(result).not.toBeNull();
  200. expect(result?.status).toBe(400);
  201. });
  202. });
  203. describe("when only blockedClients is configured (no allowedClients)", () => {
  204. test("should reject matching client", async () => {
  205. const session = createMockSession("codex-cli/2.0", [], ["codex-cli"]);
  206. const result = await ProxyClientGuard.ensure(session);
  207. expect(result).not.toBeNull();
  208. expect(result?.status).toBe(400);
  209. });
  210. test("should allow non-matching client", async () => {
  211. const session = createMockSession("claude-cli/1.0", [], ["codex-cli"]);
  212. const result = await ProxyClientGuard.ensure(session);
  213. expect(result).toBeNull();
  214. });
  215. });
  216. });