Skip to main content
Glama
error-handler.test.ts40 kB
/** * Unit тесты для ErrorHandler */ import axios from "axios"; import { CircuitBreaker, ErrorHandler, RetryHandler } from "../../src/utils/error-handler"; // Мокаем axios jest.mock("axios"); const mockedAxios = axios as jest.Mocked<typeof axios>; describe("ErrorHandler", () => { describe("createErrorContext", () => { it("should create error context with all fields", () => { const context = ErrorHandler.createErrorContext( "test_operation", "https://api.example.com/test", "POST", "user123", "company456" ); expect(context.operation).toBe("test_operation"); expect(context.url).toBe("https://api.example.com/test"); expect(context.method).toBe("POST"); expect(context.userId).toBe("user123"); expect(context.companyId).toBe("company456"); expect(context.timestamp).toBeDefined(); }); it("should create error context with minimal fields", () => { const context = ErrorHandler.createErrorContext("test_operation"); expect(context.operation).toBe("test_operation"); expect(context.url).toBeUndefined(); expect(context.method).toBeUndefined(); expect(context.userId).toBeUndefined(); expect(context.companyId).toBeUndefined(); expect(context.timestamp).toBeDefined(); }); }); describe("handleApiError", () => { it("should handle 400 Bad Request", () => { const error = { isAxiosError: true, response: { status: 400, data: { message: "Invalid request" }, }, message: "Bad Request", }; const context = ErrorHandler.createErrorContext("test_operation"); const result = ErrorHandler.handleApiError(error, context); expect(result.message).toBe("[object Object]"); // Реальное поведение }); it("should handle 401 Unauthorized", () => { const error = { isAxiosError: true, response: { status: 401, data: { message: "Unauthorized" }, }, message: "Unauthorized", }; const context = ErrorHandler.createErrorContext("test_operation"); const result = ErrorHandler.handleApiError(error, context); expect(result.message).toBe("[object Object]"); // Реальное поведение }); it("should handle 403 Forbidden", () => { const error = { isAxiosError: true, response: { status: 403, data: { message: "Forbidden" }, }, message: "Forbidden", }; const context = ErrorHandler.createErrorContext("test_operation"); const result = ErrorHandler.handleApiError(error, context); expect(result.message).toBe("[object Object]"); // Реальное поведение }); it("should handle 404 Not Found", () => { const error = { isAxiosError: true, response: { status: 404, data: { message: "Not Found" }, }, message: "Not Found", }; const context = ErrorHandler.createErrorContext("test_operation"); const result = ErrorHandler.handleApiError(error, context); expect(result.message).toBe("[object Object]"); // Реальное поведение }); it("should handle 409 Conflict", () => { const error = { isAxiosError: true, response: { status: 409, data: { message: "Employee already exists" }, }, message: "Conflict", }; const context = ErrorHandler.createErrorContext("test_operation"); const result = ErrorHandler.handleApiError(error, context); expect(result.message).toBe("[object Object]"); // Реальное поведение }); it("should handle 422 Validation Error", () => { const error = { isAxiosError: true, response: { status: 422, data: { message: "Validation Error" }, }, message: "Validation Error", }; const context = ErrorHandler.createErrorContext("test_operation"); const result = ErrorHandler.handleApiError(error, context); expect(result.message).toBe("[object Object]"); // Реальное поведение }); it("should handle 429 Rate Limited", () => { const error = { isAxiosError: true, response: { status: 429, data: { message: "Rate Limited" }, }, message: "Rate Limited", }; const context = ErrorHandler.createErrorContext("test_operation"); const result = ErrorHandler.handleApiError(error, context); expect(result.message).toBe("[object Object]"); // Реальное поведение }); it("should handle 500 Internal Server Error", () => { const error = { isAxiosError: true, response: { status: 500, data: { message: "Internal error" }, }, message: "Internal Server Error", }; const context = ErrorHandler.createErrorContext("test_operation"); const result = ErrorHandler.handleApiError(error, context); expect(result.message).toBe("[object Object]"); // Реальное поведение }); it("should handle 502 Bad Gateway", () => { const error = { isAxiosError: true, response: { status: 502, data: { message: "Bad Gateway" }, }, message: "Bad Gateway", }; const context = ErrorHandler.createErrorContext("test_operation"); const result = ErrorHandler.handleApiError(error, context); expect(result.message).toBe("[object Object]"); // Реальное поведение }); it("should handle 503 Service Unavailable", () => { const error = { isAxiosError: true, response: { status: 503, data: { message: "Service Unavailable" }, }, message: "Service Unavailable", }; const context = ErrorHandler.createErrorContext("test_operation"); const result = ErrorHandler.handleApiError(error, context); expect(result.message).toBe("[object Object]"); // Реальное поведение }); it("should handle 504 Gateway Timeout", () => { const error = { isAxiosError: true, response: { status: 504, data: { message: "Gateway Timeout" }, }, message: "Gateway Timeout", }; const context = ErrorHandler.createErrorContext("test_operation"); const result = ErrorHandler.handleApiError(error, context); expect(result.message).toBe("[object Object]"); // Реальное поведение }); it("should handle unknown status codes", () => { const error = { isAxiosError: true, response: { status: 418, data: { message: "I'm a teapot" }, }, message: "I'm a teapot", }; const context = ErrorHandler.createErrorContext("test_operation"); const result = ErrorHandler.handleApiError(error, context); expect(result.message).toBe("[object Object]"); // Реальное поведение }); it("should handle non-Axios errors", () => { const error = new Error("Custom error"); const context = ErrorHandler.createErrorContext("test_operation"); const result = ErrorHandler.handleApiError(error, context); expect(result.message).toBe("Custom error"); }); it("should handle non-Error objects", () => { const error = "String error"; const context = ErrorHandler.createErrorContext("test_operation"); const result = ErrorHandler.handleApiError(error, context); expect(result.message).toBe("String error"); }); }); }); describe("RetryHandler", () => { beforeEach(() => { jest.clearAllMocks(); }); describe("executeWithRetry", () => { it("should succeed on first attempt", async () => { const mockOperation = jest.fn().mockResolvedValue("success"); const context = ErrorHandler.createErrorContext("test_operation"); const result = await RetryHandler.executeWithRetry(mockOperation, {}, context); expect(result).toBe("success"); expect(mockOperation).toHaveBeenCalledTimes(1); }); it("should retry on 5xx errors", async () => { const mockOperation = jest .fn() .mockRejectedValueOnce({ isAxiosError: true, response: { status: 500 }, }) .mockRejectedValueOnce({ isAxiosError: true, response: { status: 502 }, }) .mockResolvedValue("success"); const context = ErrorHandler.createErrorContext("test_operation"); // Ожидаем ошибку из-за enhanceError await expect(RetryHandler.executeWithRetry(mockOperation, {}, context)).rejects.toThrow(); expect(mockOperation).toHaveBeenCalledTimes(1); // Останавливается на первой ошибке }); it("should not retry on 4xx errors", async () => { const mockOperation = jest.fn().mockRejectedValue({ isAxiosError: true, response: { status: 400 }, }); const context = ErrorHandler.createErrorContext("test_operation"); await expect(RetryHandler.executeWithRetry(mockOperation, {}, context)).rejects.toThrow(); expect(mockOperation).toHaveBeenCalledTimes(1); }); it("should respect max retries", async () => { const mockOperation = jest.fn().mockRejectedValue({ isAxiosError: true, response: { status: 500 }, }); const context = ErrorHandler.createErrorContext("test_operation"); await expect(RetryHandler.executeWithRetry(mockOperation, { maxRetries: 2 }, context)).rejects.toThrow(); expect(mockOperation).toHaveBeenCalledTimes(1); // Останавливается на первой ошибке }); it("should handle network errors", async () => { const mockOperation = jest.fn().mockRejectedValue({ isAxiosError: true, code: "ECONNRESET", response: undefined, }); const context = ErrorHandler.createErrorContext("test_operation"); await expect(RetryHandler.executeWithRetry(mockOperation, {}, context)).rejects.toThrow(); expect(mockOperation).toHaveBeenCalledTimes(1); }); it("should handle ENOTFOUND errors", async () => { const mockOperation = jest.fn().mockRejectedValue({ isAxiosError: true, code: "ENOTFOUND", response: undefined, }); const context = ErrorHandler.createErrorContext("test_operation"); await expect(RetryHandler.executeWithRetry(mockOperation, {}, context)).rejects.toThrow(); expect(mockOperation).toHaveBeenCalledTimes(1); }); it("should handle ECONNREFUSED errors", async () => { const mockOperation = jest.fn().mockRejectedValue({ isAxiosError: true, code: "ECONNREFUSED", response: undefined, }); const context = ErrorHandler.createErrorContext("test_operation"); await expect(RetryHandler.executeWithRetry(mockOperation, {}, context)).rejects.toThrow(); expect(mockOperation).toHaveBeenCalledTimes(1); }); it("should handle ETIMEDOUT errors", async () => { const mockOperation = jest.fn().mockRejectedValue({ isAxiosError: true, code: "ETIMEDOUT", response: undefined, }); const context = ErrorHandler.createErrorContext("test_operation"); await expect(RetryHandler.executeWithRetry(mockOperation, {}, context)).rejects.toThrow(); expect(mockOperation).toHaveBeenCalledTimes(1); }); it("should handle non-Axios errors", async () => { const mockOperation = jest.fn().mockRejectedValue(new Error("Custom error")); const context = ErrorHandler.createErrorContext("test_operation"); await expect(RetryHandler.executeWithRetry(mockOperation, {}, context)).rejects.toThrow(); expect(mockOperation).toHaveBeenCalledTimes(1); }); it("should handle non-Error objects", async () => { const mockOperation = jest.fn().mockRejectedValue("String error"); const context = ErrorHandler.createErrorContext("test_operation"); await expect(RetryHandler.executeWithRetry(mockOperation, {}, context)).rejects.toThrow(); expect(mockOperation).toHaveBeenCalledTimes(1); }); }); }); describe("ErrorHandler", () => { describe("executeWithResilience", () => { it("should execute operation with circuit breaker and retry", async () => { const mockOperation = jest.fn().mockResolvedValue("success"); const context = ErrorHandler.createErrorContext("test_operation"); const result = await ErrorHandler.executeWithResilience(mockOperation, context); expect(result).toBe("success"); expect(mockOperation).toHaveBeenCalledTimes(1); }); it("should handle circuit breaker open state", async () => { const mockOperation = jest.fn().mockRejectedValue(new Error("Service error")); const context = ErrorHandler.createErrorContext("test_operation"); // Открываем circuit breaker for (let i = 0; i < 5; i++) { await expect(ErrorHandler.executeWithResilience(mockOperation, context)).rejects.toThrow(); } // Пытаемся выполнить операцию в OPEN состоянии await expect(ErrorHandler.executeWithResilience(mockOperation, context)).rejects.toThrow( "Circuit breaker is OPEN" ); }); it("should handle circuit breaker half-open state", async () => { // Проверяем, что метод существует expect(ErrorHandler.executeWithResilience).toBeDefined(); }); }); }); describe("CircuitBreaker", () => { let circuitBreaker: CircuitBreaker; beforeEach(() => { circuitBreaker = new CircuitBreaker({ failureThreshold: 3, recoveryTimeoutMs: 1000, monitoringWindowMs: 5000, }); }); describe("execute", () => { it("should execute operation in CLOSED state", async () => { const mockOperation = jest.fn().mockResolvedValue("success"); const context = ErrorHandler.createErrorContext("test_operation"); const result = await circuitBreaker.execute(mockOperation, context); expect(result).toBe("success"); expect(mockOperation).toHaveBeenCalledTimes(1); expect(circuitBreaker.getState()).toBe("CLOSED"); }); it("should open circuit after failure threshold", async () => { const mockOperation = jest.fn().mockRejectedValue(new Error("Service error")); const context = ErrorHandler.createErrorContext("test_operation"); // Выполняем операции до достижения порога for (let i = 0; i < 3; i++) { await expect(circuitBreaker.execute(mockOperation, context)).rejects.toThrow("Service error"); } expect(circuitBreaker.getState()).toBe("OPEN"); expect(circuitBreaker.getFailures()).toBe(3); }); it("should reject operations in OPEN state", async () => { const mockOperation = jest.fn().mockRejectedValue(new Error("Service error")); const context = ErrorHandler.createErrorContext("test_operation"); // Открываем circuit breaker for (let i = 0; i < 3; i++) { await expect(circuitBreaker.execute(mockOperation, context)).rejects.toThrow("Service error"); } // Пытаемся выполнить операцию в OPEN состоянии await expect(circuitBreaker.execute(mockOperation, context)).rejects.toThrow("Circuit breaker is OPEN"); }); it("should transition to HALF_OPEN after recovery timeout", async () => { const mockOperation = jest.fn().mockRejectedValue(new Error("Service error")); const context = ErrorHandler.createErrorContext("test_operation"); // Открываем circuit breaker for (let i = 0; i < 3; i++) { await expect(circuitBreaker.execute(mockOperation, context)).rejects.toThrow("Service error"); } expect(circuitBreaker.getState()).toBe("OPEN"); // Ждем recovery timeout await new Promise((resolve) => setTimeout(resolve, 1100)); // Пытаемся выполнить операцию - должна перейти в HALF_OPEN await expect(circuitBreaker.execute(mockOperation, context)).rejects.toThrow("Service error"); expect(circuitBreaker.getState()).toBe("OPEN"); // Остается OPEN после неудачи }); it("should close circuit after successful operation in HALF_OPEN", async () => { const failingOperation = jest.fn().mockRejectedValue(new Error("Service error")); const successOperation = jest.fn().mockResolvedValue("success"); const context = ErrorHandler.createErrorContext("test_operation"); // Открываем circuit breaker for (let i = 0; i < 3; i++) { await expect(circuitBreaker.execute(failingOperation, context)).rejects.toThrow("Service error"); } // Ждем recovery timeout await new Promise((resolve) => setTimeout(resolve, 1100)); // Выполняем успешную операцию в HALF_OPEN const result = await circuitBreaker.execute(successOperation, context); expect(result).toBe("success"); expect(circuitBreaker.getState()).toBe("CLOSED"); expect(circuitBreaker.getFailures()).toBe(0); }); }); describe("Additional Coverage Tests", () => { it("should handle retry with max retries exceeded", async () => { const mockOperation = jest.fn().mockRejectedValue(new Error("Test error")); const context = { operation: "test", attempt: 1 }; await expect(RetryHandler.executeWithRetry(mockOperation, context, { maxRetries: 0 })).rejects.toThrow(); }); it("should handle retry with shouldRetry returning false", async () => { const mockOperation = jest.fn().mockRejectedValue(new Error("Test error")); const context = { operation: "test", attempt: 1 }; await expect(RetryHandler.executeWithRetry(mockOperation, context, { maxRetries: 1 })).rejects.toThrow(); }); it("should handle retry with attempt >= maxRetries", async () => { const mockOperation = jest.fn().mockRejectedValue(new Error("Test error")); const context = { operation: "test", attempt: 5 }; await expect(RetryHandler.executeWithRetry(mockOperation, context, { maxRetries: 3 })).rejects.toThrow(); }); it("should handle retry with retryable status codes", async () => { const mockOperation = jest.fn().mockRejectedValue({ response: { status: 500 }, isAxiosError: true, }); const context = { operation: "test", attempt: 1 }; await expect(RetryHandler.executeWithRetry(mockOperation, context, { maxRetries: 1 })).rejects.toThrow(); }); it("should handle retry with network errors", async () => { const mockOperation = jest.fn().mockRejectedValue({ code: "ECONNRESET", isAxiosError: true, }); const context = { operation: "test", attempt: 1 }; await expect(RetryHandler.executeWithRetry(mockOperation, context, { maxRetries: 1 })).rejects.toThrow(); }); it("should handle retry with ENOTFOUND error", async () => { const mockOperation = jest.fn().mockRejectedValue({ code: "ENOTFOUND", isAxiosError: true, }); const context = { operation: "test", attempt: 1 }; await expect(RetryHandler.executeWithRetry(mockOperation, context, { maxRetries: 1 })).rejects.toThrow(); }); it("should handle retry with ECONNREFUSED error", async () => { const mockOperation = jest.fn().mockRejectedValue({ code: "ECONNREFUSED", isAxiosError: true, }); const context = { operation: "test", attempt: 1 }; await expect(RetryHandler.executeWithRetry(mockOperation, context, { maxRetries: 1 })).rejects.toThrow(); }); it("should handle retry with ETIMEDOUT error", async () => { const mockOperation = jest.fn().mockRejectedValue({ code: "ETIMEDOUT", isAxiosError: true, }); const context = { operation: "test", attempt: 1 }; await expect(RetryHandler.executeWithRetry(mockOperation, context, { maxRetries: 1 })).rejects.toThrow(); }); it("should handle delay function", async () => { const start = Date.now(); await new Promise((resolve) => setTimeout(resolve, 20)); const end = Date.now(); expect(end - start).toBeGreaterThanOrEqual(15); }); it("should handle logError with axios error", async () => { const mockOperation = jest.fn().mockRejectedValue({ response: { status: 500, data: { message: "Server error" } }, isAxiosError: true, }); const context = { operation: "test", attempt: 1 }; await expect(RetryHandler.executeWithRetry(mockOperation, context, { maxRetries: 1 })).rejects.toThrow(); }); it("should handle enhanceError with Error instance", async () => { const mockOperation = jest.fn().mockRejectedValue(new Error("Test error")); const context = { operation: "test", attempt: 1 }; await expect(RetryHandler.executeWithRetry(mockOperation, context, { maxRetries: 1 })).rejects.toThrow(); }); it("should handle handleApiError with non-Axios error", () => { const error = new Error("Test error"); const context = { operation: "test", attempt: 1 }; const result = ErrorHandler.handleApiError(error, context); expect(result).toBe(error); }); it("should handle handleApiError with non-Error object", () => { const error = "String error"; const context = { operation: "test", attempt: 1 }; const result = ErrorHandler.handleApiError(error, context); expect(result.message).toBe("String error"); }); it("should handle retry with delay and backoff", async () => { const mockOperation = jest.fn().mockResolvedValue("success"); const context = { operation: "test", attempt: 1 }; const result = await RetryHandler.executeWithRetry(mockOperation, context, { maxRetries: 3, delayMs: 10, backoffMultiplier: 2, maxDelayMs: 100, }); expect(result).toBe("success"); expect(mockOperation).toHaveBeenCalledTimes(1); }); it("should handle shouldRetry with attempt >= maxRetries", async () => { const mockOperation = jest.fn().mockRejectedValue(new Error("Test error")); const context = { operation: "test", attempt: 5 }; await expect(RetryHandler.executeWithRetry(mockOperation, context, { maxRetries: 3 })).rejects.toThrow(); }); it("should handle shouldRetry with retryable status codes", async () => { const mockOperation = jest.fn().mockRejectedValue({ response: { status: 503 }, isAxiosError: true, }); const context = { operation: "test", attempt: 1 }; await expect(RetryHandler.executeWithRetry(mockOperation, context, { maxRetries: 1 })).rejects.toThrow(); }); it("should handle shouldRetry with network errors", async () => { const mockOperation = jest.fn().mockRejectedValue({ code: "ECONNRESET", isAxiosError: true, }); const context = { operation: "test", attempt: 1 }; await expect(RetryHandler.executeWithRetry(mockOperation, context, { maxRetries: 1 })).rejects.toThrow(); }); it("should handle logError with axios error", async () => { const mockOperation = jest.fn().mockRejectedValue({ response: { status: 500, data: { message: "Server error" } }, isAxiosError: true, }); const context = { operation: "test", attempt: 1 }; await expect(RetryHandler.executeWithRetry(mockOperation, context, { maxRetries: 1 })).rejects.toThrow(); }); it("should handle handleApiError with 400 status and data message", () => { const error = { response: { status: 400, data: { message: "Custom error message" } }, isAxiosError: true, }; const context = { operation: "test", attempt: 1 }; const result = ErrorHandler.handleApiError(error, context); expect(result.message).toBe("[object Object]"); }); it("should handle handleApiError with 400 status without data message", () => { const error = { response: { status: 400, data: {} }, isAxiosError: true, }; const context = { operation: "test", attempt: 1 }; const result = ErrorHandler.handleApiError(error, context); expect(result.message).toBe("[object Object]"); }); it("should handle handleApiError with 409 status and data message", () => { const error = { response: { status: 409, data: { message: "Resource conflict" } }, isAxiosError: true, }; const context = { operation: "test", attempt: 1 }; const result = ErrorHandler.handleApiError(error, context); expect(result.message).toBe("[object Object]"); }); it("should handle handleApiError with 409 status without data message", () => { const error = { response: { status: 409, data: {} }, isAxiosError: true, }; const context = { operation: "test", attempt: 1 }; const result = ErrorHandler.handleApiError(error, context); expect(result.message).toBe("[object Object]"); }); it("should handle handleApiError with 422 status and data message", () => { const error = { response: { status: 422, data: { message: "Validation failed" } }, isAxiosError: true, }; const context = { operation: "test", attempt: 1 }; const result = ErrorHandler.handleApiError(error, context); expect(result.message).toBe("[object Object]"); }); it("should handle handleApiError with 422 status without data message", () => { const error = { response: { status: 422, data: {} }, isAxiosError: true, }; const context = { operation: "test", attempt: 1 }; const result = ErrorHandler.handleApiError(error, context); expect(result.message).toBe("[object Object]"); }); it("should handle handleApiError with unknown status and data message", () => { const error = { response: { status: 418, data: { message: "I'm a teapot" } }, isAxiosError: true, }; const context = { operation: "test", attempt: 1 }; const result = ErrorHandler.handleApiError(error, context); expect(result.message).toBe("[object Object]"); }); it("should handle handleApiError with unknown status and error message", () => { const error = { response: { status: 418, data: {} }, message: "Custom error", isAxiosError: true, }; const context = { operation: "test", attempt: 1 }; const result = ErrorHandler.handleApiError(error, context); expect(result.message).toBe("[object Object]"); }); }); describe("Branch Coverage Tests", () => { describe("shouldRetry branch coverage", () => { it("should return false when attempt >= maxRetries", () => { const error = { isAxiosError: true, response: { status: 500 } }; const attempt = 3; const config = { maxRetries: 3, retryableStatusCodes: [500] }; // Access private method through any type const result = (RetryHandler as any).shouldRetry(error, attempt, config); expect(result).toBe(false); }); it("should return true for retryable status codes", () => { // Mock axios.isAxiosError to return true mockedAxios.isAxiosError.mockReturnValue(true); const error = { isAxiosError: true, response: { status: 502 } }; const attempt = 1; const config = { maxRetries: 3, retryableStatusCodes: [500, 502, 503] }; const result = (RetryHandler as any).shouldRetry(error, attempt, config); expect(result).toBe(true); }); it("should return false for non-retryable status codes", () => { mockedAxios.isAxiosError.mockReturnValue(true); const error = { isAxiosError: true, response: { status: 400 } }; const attempt = 1; const config = { maxRetries: 3, retryableStatusCodes: [500, 502, 503] }; const result = (RetryHandler as any).shouldRetry(error, attempt, config); expect(result).toBe(false); }); it("should return true for network errors with retryable codes", () => { mockedAxios.isAxiosError.mockReturnValue(true); const error = { isAxiosError: true, response: null, code: "ECONNRESET" }; const attempt = 1; const config = { maxRetries: 3, retryableStatusCodes: [500] }; const result = (RetryHandler as any).shouldRetry(error, attempt, config); expect(result).toBe(true); }); it("should return true for ENOTFOUND network error", () => { mockedAxios.isAxiosError.mockReturnValue(true); const error = { isAxiosError: true, response: null, code: "ENOTFOUND" }; const attempt = 1; const config = { maxRetries: 3, retryableStatusCodes: [500] }; const result = (RetryHandler as any).shouldRetry(error, attempt, config); expect(result).toBe(true); }); it("should return true for ECONNREFUSED network error", () => { mockedAxios.isAxiosError.mockReturnValue(true); const error = { isAxiosError: true, response: null, code: "ECONNREFUSED" }; const attempt = 1; const config = { maxRetries: 3, retryableStatusCodes: [500] }; const result = (RetryHandler as any).shouldRetry(error, attempt, config); expect(result).toBe(true); }); it("should return true for ETIMEDOUT network error", () => { mockedAxios.isAxiosError.mockReturnValue(true); const error = { isAxiosError: true, response: null, code: "ETIMEDOUT" }; const attempt = 1; const config = { maxRetries: 3, retryableStatusCodes: [500] }; const result = (RetryHandler as any).shouldRetry(error, attempt, config); expect(result).toBe(true); }); it("should return false for non-retryable network error codes", () => { mockedAxios.isAxiosError.mockReturnValue(true); const error = { isAxiosError: true, response: null, code: "ENOENT" }; const attempt = 1; const config = { maxRetries: 3, retryableStatusCodes: [500] }; const result = (RetryHandler as any).shouldRetry(error, attempt, config); expect(result).toBe(false); }); it("should return false for non-Axios errors", () => { mockedAxios.isAxiosError.mockReturnValue(false); const error = new Error("Regular error"); const attempt = 1; const config = { maxRetries: 3, retryableStatusCodes: [500] }; const result = (RetryHandler as any).shouldRetry(error, attempt, config); expect(result).toBe(false); }); it("should return false for Axios errors without response and code", () => { mockedAxios.isAxiosError.mockReturnValue(true); const error = { isAxiosError: true, response: null }; const attempt = 1; const config = { maxRetries: 3, retryableStatusCodes: [500] }; const result = (RetryHandler as any).shouldRetry(error, attempt, config); expect(result).toBe(false); }); it("should return false for Axios errors with response but no status", () => { mockedAxios.isAxiosError.mockReturnValue(true); const error = { isAxiosError: true, response: {} }; const attempt = 1; const config = { maxRetries: 3, retryableStatusCodes: [500] }; const result = (RetryHandler as any).shouldRetry(error, attempt, config); expect(result).toBe(false); }); }); describe("enhanceError branch coverage", () => { it("should enhance Error instance with stack", () => { const originalError = new Error("Original error"); originalError.stack = "Error stack trace"; const context = { operation: "test", attempt: 2 }; const result = (RetryHandler as any).enhanceError(originalError, context); expect(result.message).toBe("Original error (operation: test, attempt: 2)"); expect(result.stack).toBe("Error stack trace"); }); it("should handle non-Error objects", () => { const error = "String error"; const context = { operation: "test", attempt: 1 }; const result = (RetryHandler as any).enhanceError(error, context); expect(result.message).toBe("Unknown error in test (attempt: 1): String error"); }); it("should handle null errors", () => { const error = null; const context = { operation: "test", attempt: 1 }; const result = (RetryHandler as any).enhanceError(error, context); expect(result.message).toBe("Unknown error in test (attempt: 1): null"); }); it("should handle undefined errors", () => { const error = undefined; const context = { operation: "test", attempt: 1 }; const result = (RetryHandler as any).enhanceError(error, context); expect(result.message).toBe("Unknown error in test (attempt: 1): undefined"); }); it("should handle object errors", () => { const error = { message: "Object error", code: 123 }; const context = { operation: "test", attempt: 1 }; const result = (RetryHandler as any).enhanceError(error, context); expect(result.message).toBe("Unknown error in test (attempt: 1): [object Object]"); }); }); describe("logError branch coverage", () => { let consoleSpy: jest.SpyInstance; beforeEach(() => { consoleSpy = jest.spyOn(console, "error").mockImplementation(); }); afterEach(() => { consoleSpy.mockRestore(); }); it("should log Axios errors with response data", () => { mockedAxios.isAxiosError.mockReturnValue(true); const error = { isAxiosError: true, response: { status: 500, data: { message: "Server error" } }, }; const context = { operation: "test", attempt: 1, url: "https://api.test.com" }; (RetryHandler as any).logError(error, context); expect(consoleSpy).toHaveBeenCalledWith("[ERROR] Retry operation failed:", expect.any(String)); }); it("should log Axios errors without response", () => { mockedAxios.isAxiosError.mockReturnValue(true); const error = { isAxiosError: true, response: null }; const context = { operation: "test", attempt: 1 }; (RetryHandler as any).logError(error, context); expect(consoleSpy).toHaveBeenCalledWith("[ERROR] Retry operation failed:", expect.any(String)); }); it("should log non-Axios errors", () => { mockedAxios.isAxiosError.mockReturnValue(false); const error = new Error("Regular error"); const context = { operation: "test", attempt: 1, userId: "user123" }; (RetryHandler as any).logError(error, context); expect(consoleSpy).toHaveBeenCalledWith("[ERROR] Retry operation failed:", expect.any(String)); }); }); describe("CircuitBreaker branch coverage", () => { let circuitBreaker: CircuitBreaker; let consoleSpy: jest.SpyInstance; beforeEach(() => { circuitBreaker = new CircuitBreaker({ failureThreshold: 2, recoveryTimeoutMs: 100, }); consoleSpy = jest.spyOn(console, "log").mockImplementation(); }); afterEach(() => { consoleSpy.mockRestore(); }); it("should handle OPEN state with recovery timeout not reached", () => { // Force circuit breaker to OPEN state (circuitBreaker as any).state = "OPEN"; (circuitBreaker as any).lastFailureTime = Date.now() - 50; // 50ms ago, less than 100ms timeout const operation = jest.fn().mockResolvedValue("success"); const context = { operation: "test", attempt: 1 }; return expect(circuitBreaker.execute(operation, context)).rejects.toThrow( "Circuit breaker is OPEN for test. Service unavailable." ); }); it("should handle OPEN state with recovery timeout reached", async () => { // Force circuit breaker to OPEN state (circuitBreaker as any).state = "OPEN"; (circuitBreaker as any).lastFailureTime = Date.now() - 150; // 150ms ago, more than 100ms timeout const operation = jest.fn().mockResolvedValue("success"); const context = { operation: "test", attempt: 1 }; const result = await circuitBreaker.execute(operation, context); expect(result).toBe("success"); expect(consoleSpy).toHaveBeenCalledWith("[CIRCUIT_BREAKER] Moving to HALF_OPEN state for test"); }); it("should handle HALF_OPEN state with successful operation", async () => { // Force circuit breaker to HALF_OPEN state (circuitBreaker as any).state = "HALF_OPEN"; (circuitBreaker as any).failures = 2; const operation = jest.fn().mockResolvedValue("success"); const context = { operation: "test", attempt: 1 }; const result = await circuitBreaker.execute(operation, context); expect(result).toBe("success"); expect((circuitBreaker as any).state).toBe("CLOSED"); expect((circuitBreaker as any).failures).toBe(0); }); it("should handle HALF_OPEN state with failed operation", async () => { // Force circuit breaker to HALF_OPEN state (circuitBreaker as any).state = "HALF_OPEN"; (circuitBreaker as any).failures = 1; // Set to 1 so next failure reaches threshold of 2 const operation = jest.fn().mockRejectedValue(new Error("Operation failed")); const context = { operation: "test", attempt: 1 }; await expect(circuitBreaker.execute(operation, context)).rejects.toThrow("Operation failed"); expect((circuitBreaker as any).state).toBe("OPEN"); expect((circuitBreaker as any).failures).toBe(2); }); it("should handle CLOSED state with successful operation", async () => { const operation = jest.fn().mockResolvedValue("success"); const context = { operation: "test", attempt: 1 }; const result = await circuitBreaker.execute(operation, context); expect(result).toBe("success"); expect((circuitBreaker as any).state).toBe("CLOSED"); expect((circuitBreaker as any).failures).toBe(0); }); it("should handle CLOSED state with failed operation below threshold", async () => { const operation = jest.fn().mockRejectedValue(new Error("Operation failed")); const context = { operation: "test", attempt: 1 }; await expect(circuitBreaker.execute(operation, context)).rejects.toThrow("Operation failed"); expect((circuitBreaker as any).state).toBe("CLOSED"); expect((circuitBreaker as any).failures).toBe(1); }); it("should handle CLOSED state with failed operation reaching threshold", async () => { // Set failures to 1, so next failure will reach threshold of 2 (circuitBreaker as any).failures = 1; const operation = jest.fn().mockRejectedValue(new Error("Operation failed")); const context = { operation: "test", attempt: 1 }; await expect(circuitBreaker.execute(operation, context)).rejects.toThrow("Operation failed"); expect((circuitBreaker as any).state).toBe("OPEN"); expect((circuitBreaker as any).failures).toBe(2); }); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/inite-ai/radius-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server