request-filter-binding.test.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645
  1. import { beforeEach, describe, expect, test } from "vitest";
  2. import { requestFilterEngine } from "@/lib/request-filter-engine";
  3. import type { RequestFilter } from "@/repository/request-filters";
  4. // =============================================================================
  5. // Helper Functions
  6. // =============================================================================
  7. let filterId = 0;
  8. function createFilter(overrides: Partial<RequestFilter>): RequestFilter {
  9. return {
  10. id: ++filterId,
  11. name: `test-filter-${filterId}`,
  12. description: null,
  13. scope: "header",
  14. action: "set",
  15. matchType: null,
  16. target: "x-test",
  17. replacement: "test-value",
  18. priority: 0,
  19. isEnabled: true,
  20. bindingType: "global",
  21. providerIds: null,
  22. groupTags: null,
  23. createdAt: new Date(),
  24. updatedAt: new Date(),
  25. ...overrides,
  26. };
  27. }
  28. function createGlobalFilter(
  29. scope: "header" | "body",
  30. action: "remove" | "set" | "json_path" | "text_replace",
  31. target: string,
  32. replacement: unknown,
  33. priority = 0,
  34. matchType: "contains" | "exact" | "regex" | null = null
  35. ): RequestFilter {
  36. return createFilter({
  37. scope,
  38. action,
  39. target,
  40. replacement,
  41. priority,
  42. matchType,
  43. bindingType: "global",
  44. });
  45. }
  46. function createProviderFilter(
  47. providerIds: number[],
  48. scope: "header" | "body",
  49. action: "remove" | "set" | "json_path" | "text_replace",
  50. target: string,
  51. replacement: unknown,
  52. priority = 0,
  53. matchType: "contains" | "exact" | "regex" | null = null
  54. ): RequestFilter {
  55. return createFilter({
  56. scope,
  57. action,
  58. target,
  59. replacement,
  60. priority,
  61. matchType,
  62. bindingType: "providers",
  63. providerIds,
  64. });
  65. }
  66. function createGroupFilter(
  67. groupTags: string[],
  68. scope: "header" | "body",
  69. action: "remove" | "set" | "json_path" | "text_replace",
  70. target: string,
  71. replacement: unknown,
  72. priority = 0,
  73. matchType: "contains" | "exact" | "regex" | null = null
  74. ): RequestFilter {
  75. return createFilter({
  76. scope,
  77. action,
  78. target,
  79. replacement,
  80. priority,
  81. matchType,
  82. bindingType: "groups",
  83. groupTags,
  84. });
  85. }
  86. interface MockSession {
  87. headers: Headers;
  88. request: {
  89. message: Record<string, unknown>;
  90. log: string;
  91. model: string;
  92. };
  93. provider?: {
  94. id: number;
  95. groupTag: string | null;
  96. };
  97. }
  98. function createSession(): MockSession {
  99. return {
  100. headers: new Headers({
  101. "user-agent": "test-ua",
  102. "x-remove": "value-to-remove",
  103. "x-keep": "keep-this",
  104. }),
  105. request: {
  106. message: {
  107. text: "hello world secret data",
  108. nested: { secret: "abc123", keep: "preserved" },
  109. items: ["item1", "item2"],
  110. },
  111. log: "",
  112. model: "claude-3",
  113. },
  114. };
  115. }
  116. function createSessionWithProvider(
  117. providerId: number,
  118. groupTag: string | null = null
  119. ): MockSession {
  120. const session = createSession();
  121. session.provider = { id: providerId, groupTag };
  122. return session;
  123. }
  124. // =============================================================================
  125. // Tests
  126. // =============================================================================
  127. describe("Request Filter Engine - Binding Types", () => {
  128. beforeEach(() => {
  129. filterId = 0;
  130. requestFilterEngine.setFiltersForTest([]);
  131. });
  132. // ===========================================================================
  133. // 1. Global filters (applyGlobal) - 7 tests
  134. // ===========================================================================
  135. describe("Global filters (applyGlobal)", () => {
  136. test("should apply global header filter (remove)", async () => {
  137. const filter = createGlobalFilter("header", "remove", "x-remove", null);
  138. requestFilterEngine.setFiltersForTest([filter]);
  139. const session = createSession();
  140. expect(session.headers.has("x-remove")).toBe(true);
  141. await requestFilterEngine.applyGlobal(
  142. session as Parameters<typeof requestFilterEngine.applyGlobal>[0]
  143. );
  144. expect(session.headers.has("x-remove")).toBe(false);
  145. expect(session.headers.get("x-keep")).toBe("keep-this");
  146. });
  147. test("should apply global header filter (set)", async () => {
  148. const filter = createGlobalFilter("header", "set", "x-custom", "custom-value");
  149. requestFilterEngine.setFiltersForTest([filter]);
  150. const session = createSession();
  151. expect(session.headers.has("x-custom")).toBe(false);
  152. await requestFilterEngine.applyGlobal(
  153. session as Parameters<typeof requestFilterEngine.applyGlobal>[0]
  154. );
  155. expect(session.headers.get("x-custom")).toBe("custom-value");
  156. });
  157. test("should apply global body filter (json_path)", async () => {
  158. const filter = createGlobalFilter("body", "json_path", "nested.secret", "***REDACTED***");
  159. requestFilterEngine.setFiltersForTest([filter]);
  160. const session = createSession();
  161. expect(
  162. (session.request.message as Record<string, Record<string, string>>).nested.secret
  163. ).toBe("abc123");
  164. await requestFilterEngine.applyGlobal(
  165. session as Parameters<typeof requestFilterEngine.applyGlobal>[0]
  166. );
  167. expect(
  168. (session.request.message as Record<string, Record<string, string>>).nested.secret
  169. ).toBe("***REDACTED***");
  170. expect((session.request.message as Record<string, Record<string, string>>).nested.keep).toBe(
  171. "preserved"
  172. );
  173. });
  174. test("should apply global body filter (text_replace with contains)", async () => {
  175. const filter = createGlobalFilter(
  176. "body",
  177. "text_replace",
  178. "secret",
  179. "[HIDDEN]",
  180. 0,
  181. "contains"
  182. );
  183. requestFilterEngine.setFiltersForTest([filter]);
  184. const session = createSession();
  185. await requestFilterEngine.applyGlobal(
  186. session as Parameters<typeof requestFilterEngine.applyGlobal>[0]
  187. );
  188. expect((session.request.message as Record<string, string>).text).toBe(
  189. "hello world [HIDDEN] data"
  190. );
  191. });
  192. test("should apply global body filter (text_replace with regex)", async () => {
  193. const filter = createGlobalFilter("body", "text_replace", "\\d+", "[NUM]", 0, "regex");
  194. requestFilterEngine.setFiltersForTest([filter]);
  195. const session = createSession();
  196. await requestFilterEngine.applyGlobal(
  197. session as Parameters<typeof requestFilterEngine.applyGlobal>[0]
  198. );
  199. expect(
  200. (session.request.message as Record<string, Record<string, string>>).nested.secret
  201. ).toBe("abc[NUM]");
  202. });
  203. test("should apply multiple global filters in priority order", async () => {
  204. // Filters are sorted by priority (ascending), last one wins
  205. const filters = [
  206. createGlobalFilter("header", "set", "x-order", "first", 0),
  207. createGlobalFilter("header", "set", "x-order", "second", 1),
  208. createGlobalFilter("header", "set", "x-order", "third", 2),
  209. ];
  210. requestFilterEngine.setFiltersForTest(filters);
  211. const session = createSession();
  212. await requestFilterEngine.applyGlobal(
  213. session as Parameters<typeof requestFilterEngine.applyGlobal>[0]
  214. );
  215. // Applied in priority order: 0 -> 1 -> 2, last one (priority 2) wins
  216. expect(session.headers.get("x-order")).toBe("third");
  217. });
  218. test("should not fail on empty filters", async () => {
  219. requestFilterEngine.setFiltersForTest([]);
  220. const session = createSession();
  221. const originalHeaders = new Headers(session.headers);
  222. await expect(
  223. requestFilterEngine.applyGlobal(
  224. session as Parameters<typeof requestFilterEngine.applyGlobal>[0]
  225. )
  226. ).resolves.toBeUndefined();
  227. expect(session.headers.get("user-agent")).toBe(originalHeaders.get("user-agent"));
  228. });
  229. });
  230. // ===========================================================================
  231. // 2. Provider-specific filters (bindingType="providers") - 6 tests
  232. // ===========================================================================
  233. describe("Provider-specific filters (bindingType=providers)", () => {
  234. test("should apply filter when providerId matches single ID", async () => {
  235. const filter = createProviderFilter([1], "header", "set", "x-provider", "matched");
  236. requestFilterEngine.setFiltersForTest([filter]);
  237. const session = createSessionWithProvider(1);
  238. await requestFilterEngine.applyForProvider(
  239. session as Parameters<typeof requestFilterEngine.applyForProvider>[0]
  240. );
  241. expect(session.headers.get("x-provider")).toBe("matched");
  242. });
  243. test("should apply filter when providerId matches one of multiple IDs", async () => {
  244. const filter = createProviderFilter([1, 2, 3], "header", "set", "x-provider", "matched");
  245. requestFilterEngine.setFiltersForTest([filter]);
  246. const session = createSessionWithProvider(2);
  247. await requestFilterEngine.applyForProvider(
  248. session as Parameters<typeof requestFilterEngine.applyForProvider>[0]
  249. );
  250. expect(session.headers.get("x-provider")).toBe("matched");
  251. });
  252. test("should NOT apply filter when providerId not in list", async () => {
  253. const filter = createProviderFilter([1, 2, 3], "header", "set", "x-provider", "matched");
  254. requestFilterEngine.setFiltersForTest([filter]);
  255. const session = createSessionWithProvider(99);
  256. await requestFilterEngine.applyForProvider(
  257. session as Parameters<typeof requestFilterEngine.applyForProvider>[0]
  258. );
  259. expect(session.headers.has("x-provider")).toBe(false);
  260. });
  261. test("should apply header and body filters for matching provider", async () => {
  262. const filters = [
  263. createProviderFilter([5], "header", "set", "x-auth", "bearer-token"),
  264. createProviderFilter([5], "body", "json_path", "metadata.provider", "provider-5"),
  265. ];
  266. requestFilterEngine.setFiltersForTest(filters);
  267. const session = createSessionWithProvider(5);
  268. await requestFilterEngine.applyForProvider(
  269. session as Parameters<typeof requestFilterEngine.applyForProvider>[0]
  270. );
  271. expect(session.headers.get("x-auth")).toBe("bearer-token");
  272. expect(
  273. (session.request.message as Record<string, Record<string, string>>).metadata.provider
  274. ).toBe("provider-5");
  275. });
  276. test("should handle multiple provider filters with different priorities", async () => {
  277. // Filters are sorted by priority (ascending), last one wins
  278. const filters = [
  279. createProviderFilter([1], "header", "set", "x-priority", "high", 1),
  280. createProviderFilter([1], "header", "set", "x-priority", "low", 10),
  281. ];
  282. requestFilterEngine.setFiltersForTest(filters);
  283. const session = createSessionWithProvider(1);
  284. await requestFilterEngine.applyForProvider(
  285. session as Parameters<typeof requestFilterEngine.applyForProvider>[0]
  286. );
  287. // Applied in priority order: 1 -> 10, last one (priority 10) wins
  288. expect(session.headers.get("x-priority")).toBe("low");
  289. });
  290. test("should skip provider filter with empty providerIds array", async () => {
  291. const filter = createProviderFilter([], "header", "set", "x-empty", "should-not-apply");
  292. requestFilterEngine.setFiltersForTest([filter]);
  293. const session = createSessionWithProvider(1);
  294. await requestFilterEngine.applyForProvider(
  295. session as Parameters<typeof requestFilterEngine.applyForProvider>[0]
  296. );
  297. expect(session.headers.has("x-empty")).toBe(false);
  298. });
  299. });
  300. // ===========================================================================
  301. // 3. Group-specific filters (bindingType="groups") - 7 tests
  302. // ===========================================================================
  303. describe("Group-specific filters (bindingType=groups)", () => {
  304. test("should apply filter when provider groupTag matches exactly", async () => {
  305. const filter = createGroupFilter(["premium"], "header", "set", "x-tier", "premium-tier");
  306. requestFilterEngine.setFiltersForTest([filter]);
  307. const session = createSessionWithProvider(1, "premium");
  308. await requestFilterEngine.applyForProvider(
  309. session as Parameters<typeof requestFilterEngine.applyForProvider>[0]
  310. );
  311. expect(session.headers.get("x-tier")).toBe("premium-tier");
  312. });
  313. test("should apply filter when provider has comma-separated tags and one matches", async () => {
  314. const filter = createGroupFilter(["vip"], "header", "set", "x-vip", "true");
  315. requestFilterEngine.setFiltersForTest([filter]);
  316. // Provider has multiple tags: "basic, vip, beta"
  317. const session = createSessionWithProvider(1, "basic, vip, beta");
  318. await requestFilterEngine.applyForProvider(
  319. session as Parameters<typeof requestFilterEngine.applyForProvider>[0]
  320. );
  321. expect(session.headers.get("x-vip")).toBe("true");
  322. });
  323. test("should apply filter when filter has multiple groupTags and one matches", async () => {
  324. const filter = createGroupFilter(
  325. ["gold", "silver", "bronze"],
  326. "header",
  327. "set",
  328. "x-medal",
  329. "awarded"
  330. );
  331. requestFilterEngine.setFiltersForTest([filter]);
  332. const session = createSessionWithProvider(1, "silver");
  333. await requestFilterEngine.applyForProvider(
  334. session as Parameters<typeof requestFilterEngine.applyForProvider>[0]
  335. );
  336. expect(session.headers.get("x-medal")).toBe("awarded");
  337. });
  338. test("should NOT apply filter when no tag match", async () => {
  339. const filter = createGroupFilter(["premium", "vip"], "header", "set", "x-special", "yes");
  340. requestFilterEngine.setFiltersForTest([filter]);
  341. const session = createSessionWithProvider(1, "basic");
  342. await requestFilterEngine.applyForProvider(
  343. session as Parameters<typeof requestFilterEngine.applyForProvider>[0]
  344. );
  345. expect(session.headers.has("x-special")).toBe(false);
  346. });
  347. test("should NOT apply filter when provider has no groupTag (null)", async () => {
  348. const filter = createGroupFilter(["any-tag"], "header", "set", "x-null", "applied");
  349. requestFilterEngine.setFiltersForTest([filter]);
  350. const session = createSessionWithProvider(1, null);
  351. await requestFilterEngine.applyForProvider(
  352. session as Parameters<typeof requestFilterEngine.applyForProvider>[0]
  353. );
  354. expect(session.headers.has("x-null")).toBe(false);
  355. });
  356. test("should NOT apply filter when provider has empty groupTag", async () => {
  357. const filter = createGroupFilter(["tag"], "header", "set", "x-empty-tag", "applied");
  358. requestFilterEngine.setFiltersForTest([filter]);
  359. const session = createSessionWithProvider(1, "");
  360. await requestFilterEngine.applyForProvider(
  361. session as Parameters<typeof requestFilterEngine.applyForProvider>[0]
  362. );
  363. expect(session.headers.has("x-empty-tag")).toBe(false);
  364. });
  365. test("should skip group filter with empty groupTags array", async () => {
  366. const filter = createGroupFilter([], "header", "set", "x-no-tags", "applied");
  367. requestFilterEngine.setFiltersForTest([filter]);
  368. const session = createSessionWithProvider(1, "any-tag");
  369. await requestFilterEngine.applyForProvider(
  370. session as Parameters<typeof requestFilterEngine.applyForProvider>[0]
  371. );
  372. expect(session.headers.has("x-no-tags")).toBe(false);
  373. });
  374. });
  375. // ===========================================================================
  376. // 4. Combined filters (global + provider/group) - 4 tests
  377. // ===========================================================================
  378. describe("Combined filters (global + provider/group)", () => {
  379. test("should apply both global and provider filters in sequence", async () => {
  380. const filters = [
  381. createGlobalFilter("header", "set", "x-global", "global-value"),
  382. createProviderFilter([1], "header", "set", "x-provider", "provider-value"),
  383. ];
  384. requestFilterEngine.setFiltersForTest(filters);
  385. const session = createSessionWithProvider(1);
  386. // First apply global
  387. await requestFilterEngine.applyGlobal(
  388. session as Parameters<typeof requestFilterEngine.applyGlobal>[0]
  389. );
  390. expect(session.headers.get("x-global")).toBe("global-value");
  391. expect(session.headers.has("x-provider")).toBe(false);
  392. // Then apply provider-specific
  393. await requestFilterEngine.applyForProvider(
  394. session as Parameters<typeof requestFilterEngine.applyForProvider>[0]
  395. );
  396. expect(session.headers.get("x-global")).toBe("global-value");
  397. expect(session.headers.get("x-provider")).toBe("provider-value");
  398. });
  399. test("should apply both global and group filters in sequence", async () => {
  400. const filters = [
  401. createGlobalFilter("header", "set", "x-global", "from-global"),
  402. createGroupFilter(["enterprise"], "header", "set", "x-enterprise", "enterprise-tier"),
  403. ];
  404. requestFilterEngine.setFiltersForTest(filters);
  405. const session = createSessionWithProvider(1, "enterprise");
  406. await requestFilterEngine.applyGlobal(
  407. session as Parameters<typeof requestFilterEngine.applyGlobal>[0]
  408. );
  409. await requestFilterEngine.applyForProvider(
  410. session as Parameters<typeof requestFilterEngine.applyForProvider>[0]
  411. );
  412. expect(session.headers.get("x-global")).toBe("from-global");
  413. expect(session.headers.get("x-enterprise")).toBe("enterprise-tier");
  414. });
  415. test("should maintain priority order within each category", async () => {
  416. const filters = [
  417. createGlobalFilter("header", "set", "x-seq", "g1", 0),
  418. createGlobalFilter("header", "set", "x-seq", "g2", 1),
  419. createProviderFilter([1], "header", "set", "x-seq", "p1", 0),
  420. createProviderFilter([1], "header", "set", "x-seq", "p2", 1),
  421. ];
  422. requestFilterEngine.setFiltersForTest(filters);
  423. const session = createSessionWithProvider(1);
  424. await requestFilterEngine.applyGlobal(
  425. session as Parameters<typeof requestFilterEngine.applyGlobal>[0]
  426. );
  427. // After global: g1 -> g2, result is "g2"
  428. expect(session.headers.get("x-seq")).toBe("g2");
  429. await requestFilterEngine.applyForProvider(
  430. session as Parameters<typeof requestFilterEngine.applyForProvider>[0]
  431. );
  432. // After provider: p1 -> p2, result is "p2"
  433. expect(session.headers.get("x-seq")).toBe("p2");
  434. });
  435. test("should handle all three binding types together", async () => {
  436. const filters = [
  437. createGlobalFilter("header", "set", "x-binding", "global"),
  438. createProviderFilter([1, 2], "header", "set", "x-binding", "provider"),
  439. createGroupFilter(["vip"], "header", "set", "x-binding", "group"),
  440. ];
  441. requestFilterEngine.setFiltersForTest(filters);
  442. // Session with provider ID 1 and group tag "vip"
  443. const session = createSessionWithProvider(1, "vip");
  444. await requestFilterEngine.applyGlobal(
  445. session as Parameters<typeof requestFilterEngine.applyGlobal>[0]
  446. );
  447. expect(session.headers.get("x-binding")).toBe("global");
  448. await requestFilterEngine.applyForProvider(
  449. session as Parameters<typeof requestFilterEngine.applyForProvider>[0]
  450. );
  451. // Both provider and group filters match, applied in order: provider -> group
  452. expect(session.headers.get("x-binding")).toBe("group");
  453. });
  454. });
  455. // ===========================================================================
  456. // 5. Edge cases - 4 tests
  457. // ===========================================================================
  458. describe("Edge cases", () => {
  459. test("should return early from applyForProvider when no provider", async () => {
  460. const filter = createProviderFilter([1], "header", "set", "x-edge", "value");
  461. requestFilterEngine.setFiltersForTest([filter]);
  462. const session = createSession(); // No provider
  463. await expect(
  464. requestFilterEngine.applyForProvider(
  465. session as Parameters<typeof requestFilterEngine.applyForProvider>[0]
  466. )
  467. ).resolves.toBeUndefined();
  468. expect(session.headers.has("x-edge")).toBe(false);
  469. });
  470. test("should handle filter with null providerIds (treated as no match)", async () => {
  471. const filter = createFilter({
  472. bindingType: "providers",
  473. providerIds: null,
  474. scope: "header",
  475. action: "set",
  476. target: "x-null-ids",
  477. replacement: "value",
  478. });
  479. requestFilterEngine.setFiltersForTest([filter]);
  480. const session = createSessionWithProvider(1);
  481. await requestFilterEngine.applyForProvider(
  482. session as Parameters<typeof requestFilterEngine.applyForProvider>[0]
  483. );
  484. expect(session.headers.has("x-null-ids")).toBe(false);
  485. });
  486. test("should handle filter with null groupTags (treated as no match)", async () => {
  487. const filter = createFilter({
  488. bindingType: "groups",
  489. groupTags: null,
  490. scope: "header",
  491. action: "set",
  492. target: "x-null-tags",
  493. replacement: "value",
  494. });
  495. requestFilterEngine.setFiltersForTest([filter]);
  496. const session = createSessionWithProvider(1, "some-tag");
  497. await requestFilterEngine.applyForProvider(
  498. session as Parameters<typeof requestFilterEngine.applyForProvider>[0]
  499. );
  500. expect(session.headers.has("x-null-tags")).toBe(false);
  501. });
  502. test("should handle regex filter with provider binding", async () => {
  503. const filter = createProviderFilter(
  504. [10],
  505. "body",
  506. "text_replace",
  507. "secret",
  508. "[FILTERED]",
  509. 0,
  510. "contains"
  511. );
  512. requestFilterEngine.setFiltersForTest([filter]);
  513. const session = createSessionWithProvider(10);
  514. await requestFilterEngine.applyForProvider(
  515. session as Parameters<typeof requestFilterEngine.applyForProvider>[0]
  516. );
  517. expect((session.request.message as Record<string, string>).text).toBe(
  518. "hello world [FILTERED] data"
  519. );
  520. });
  521. });
  522. });