e2e-error-rules.test.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. /**
  2. * End-to-End Test Script for Error Rules System
  3. *
  4. * Purpose:
  5. * - Verify complete workflow: Create → Cache Refresh → Detection → Delete → Verification
  6. * - Test Server Actions integration
  7. * - Test database persistence
  8. * - Test cache synchronization
  9. *
  10. * Usage:
  11. * bun run tests/e2e-error-rules.test.ts
  12. */
  13. import { afterAll, beforeAll, describe, expect, test } from "vitest";
  14. import {
  15. createErrorRuleAction,
  16. deleteErrorRuleAction,
  17. refreshCacheAction,
  18. } from "@/actions/error-rules";
  19. import { isNonRetryableClientError } from "@/app/v1/_lib/proxy/errors";
  20. import { errorRuleDetector } from "@/lib/error-rule-detector";
  21. // Mock session for Server Actions (requires admin role)
  22. const _mockAdminSession = {
  23. user: {
  24. id: 1,
  25. name: "Test Admin",
  26. role: "admin" as const,
  27. },
  28. };
  29. let createdRuleId: number | null = null;
  30. beforeAll(async () => {
  31. // Wait for initial cache load
  32. await new Promise((resolve) => setTimeout(resolve, 1000));
  33. });
  34. afterAll(async () => {
  35. // Cleanup: Delete test rule if it exists
  36. if (createdRuleId !== null) {
  37. await deleteErrorRuleAction(createdRuleId);
  38. }
  39. });
  40. describe("End-to-End Error Rules Workflow", () => {
  41. test("Step 1: Create new error rule via Server Action", async () => {
  42. const result = await createErrorRuleAction({
  43. pattern: "test.*custom.*error",
  44. category: "client_error",
  45. matchType: "regex",
  46. description: "E2E Test Rule - Safe to delete",
  47. });
  48. expect(result.ok).toBe(true);
  49. if (result.ok) {
  50. expect(result.data).toBeDefined();
  51. createdRuleId = result.data.id;
  52. expect(createdRuleId).toBeGreaterThan(0);
  53. expect(result.data.pattern).toBe("test.*custom.*error");
  54. expect(result.data.category).toBe("client_error");
  55. expect(result.data.isEnabled).toBe(true);
  56. }
  57. });
  58. test("Step 2: Verify cache auto-refresh after creation", async () => {
  59. // Wait for EventEmitter to trigger auto-refresh
  60. await new Promise((resolve) => setTimeout(resolve, 200));
  61. const stats = errorRuleDetector.getStats();
  62. expect(stats.totalCount).toBeGreaterThan(7); // More than just default rules
  63. });
  64. test("Step 3: Test detection with new rule", () => {
  65. const error = new Error("This is a test custom error message");
  66. const result = isNonRetryableClientError(error);
  67. // Should match the newly created rule
  68. expect(result).toBe(true);
  69. });
  70. test("Step 4: Verify detection result details", () => {
  71. const result = errorRuleDetector.detect("This is a test custom error message");
  72. expect(result.matched).toBe(true);
  73. expect(result.category).toBe("client_error");
  74. expect(result.matchType).toBe("regex");
  75. expect(result.pattern).toBe("test.*custom.*error");
  76. });
  77. test("Step 5: Manual cache refresh", async () => {
  78. const result = await refreshCacheAction();
  79. expect(result.ok).toBe(true);
  80. if (result.ok) {
  81. expect(result.data).toBeDefined();
  82. expect(result.data.stats.totalCount).toBeGreaterThan(7);
  83. expect(result.data.stats.isLoading).toBe(false);
  84. }
  85. });
  86. test("Step 6: Delete test rule", async () => {
  87. if (createdRuleId === null) {
  88. throw new Error("No rule to delete");
  89. }
  90. const result = await deleteErrorRuleAction(createdRuleId);
  91. expect(result.ok).toBe(true);
  92. createdRuleId = null; // Mark as deleted
  93. });
  94. test("Step 7: Verify cache refresh after deletion", async () => {
  95. // Wait for EventEmitter to trigger auto-refresh
  96. await new Promise((resolve) => setTimeout(resolve, 200));
  97. const stats = errorRuleDetector.getStats();
  98. expect(stats.totalCount).toBeGreaterThanOrEqual(7); // Back to default rules
  99. });
  100. test("Step 8: Verify detection no longer matches after deletion", () => {
  101. const error = new Error("This is a test custom error message");
  102. // Wait a bit more to ensure cache is fully refreshed
  103. setTimeout(() => {
  104. const _result = isNonRetryableClientError(error);
  105. // Should NOT match anymore (rule deleted)
  106. // Note: This might still match if there are other rules with similar patterns
  107. // So we check the detailed result
  108. const detailResult = errorRuleDetector.detect("This is a test custom error message");
  109. if (detailResult.matched) {
  110. // If still matched, it should NOT be from our deleted rule
  111. expect(detailResult.pattern).not.toBe("test.*custom.*error");
  112. }
  113. }, 100);
  114. });
  115. });
  116. describe("ReDoS Protection E2E", () => {
  117. test("Should reject dangerous regex pattern", async () => {
  118. const result = await createErrorRuleAction({
  119. pattern: "(a+)+",
  120. category: "client_error",
  121. matchType: "regex",
  122. description: "Dangerous ReDoS pattern - should be rejected",
  123. });
  124. expect(result.ok).toBe(false);
  125. if (!result.ok) {
  126. expect(result.error).toContain("ReDoS");
  127. }
  128. });
  129. test("Should reject nested quantifiers", async () => {
  130. const result = await createErrorRuleAction({
  131. pattern: "(x+)*",
  132. category: "client_error",
  133. matchType: "regex",
  134. description: "Another dangerous pattern",
  135. });
  136. expect(result.ok).toBe(false);
  137. if (!result.ok) {
  138. expect(result.error).toContain("ReDoS");
  139. }
  140. });
  141. test("Should accept safe regex pattern", async () => {
  142. const result = await createErrorRuleAction({
  143. pattern: "safe.*pattern.*test",
  144. category: "client_error",
  145. matchType: "regex",
  146. description: "Safe pattern - should be accepted",
  147. });
  148. expect(result.ok).toBe(true);
  149. // Cleanup
  150. if (result.ok && result.data) {
  151. await deleteErrorRuleAction(result.data.id);
  152. }
  153. });
  154. });
  155. describe("Default Rules Verification", () => {
  156. test("Should have exactly 7 default rules in database", async () => {
  157. const stats = errorRuleDetector.getStats();
  158. // After initialization, should have at least 7 default rules
  159. expect(stats.totalCount).toBeGreaterThanOrEqual(7);
  160. });
  161. test("All default rules should be enabled", () => {
  162. // Indirectly verify by testing all 7 default patterns
  163. const defaultPatterns = [
  164. "prompt is too long: 5000 tokens > 4096 maximum",
  165. "blocked by our content filter policy",
  166. "PDF has too many pages: 150 > 100 maximum pages",
  167. "thinking block format is invalid",
  168. "Missing required parameter: model",
  169. "非法请求",
  170. "cache_control limit exceeded: 5 blocks",
  171. "A maximum of 4 blocks with cache_control may be provided. Found 5.",
  172. ];
  173. for (const pattern of defaultPatterns) {
  174. const result = errorRuleDetector.detect(pattern);
  175. expect(result.matched).toBe(true);
  176. }
  177. });
  178. });
  179. describe("Performance Under Load", () => {
  180. test("Should handle rapid rule creation and deletion", async () => {
  181. const ruleIds: number[] = [];
  182. // Create 5 rules rapidly
  183. for (let i = 0; i < 5; i++) {
  184. const result = await createErrorRuleAction({
  185. pattern: `load.*test.*${i}`,
  186. category: "client_error",
  187. matchType: "regex",
  188. description: `Load test rule ${i}`,
  189. });
  190. if (result.ok && result.data) {
  191. ruleIds.push(result.data.id);
  192. }
  193. }
  194. expect(ruleIds.length).toBe(5);
  195. // Wait for cache refresh
  196. await new Promise((resolve) => setTimeout(resolve, 300));
  197. // Verify all rules are loaded
  198. const stats = errorRuleDetector.getStats();
  199. expect(stats.totalCount).toBeGreaterThanOrEqual(12); // 7 default + 5 new
  200. // Delete all test rules
  201. for (const id of ruleIds) {
  202. await deleteErrorRuleAction(id);
  203. }
  204. // Wait for cache refresh
  205. await new Promise((resolve) => setTimeout(resolve, 300));
  206. // Verify rules are removed
  207. const finalStats = errorRuleDetector.getStats();
  208. expect(finalStats.totalCount).toBeGreaterThanOrEqual(7);
  209. });
  210. });