const { describe, test, expect, beforeEach } = require("@jest/globals"); // Mock config jest.mock("../../config", () => ({ errorHandling: { maxRetries: 3, }, })); // Mock logger jest.mock("../../lib/logger", () => ({ emailFailed: jest.fn(), emailRetry: jest.fn(), emailPermanentFailure: jest.fn(), })); const errorHandler = require("../../lib/errorHandler"); describe("ErrorHandler", () => { beforeEach(() => { jest.clearAllMocks(); errorHandler.clearRetries(); }); describe("classifyError", () => { test("should classify authentication errors", () => { const error = new Error("Invalid login credentials"); const result = errorHandler.classifyError(error); expect(result).toBe("AUTH_ERROR"); }); test("should classify rate limit errors", () => { const error = new Error("Rate limit exceeded"); const result = errorHandler.classifyError(error); expect(result).toBe("RATE_LIMIT"); }); test("should classify network errors", () => { const error = new Error("Connection timeout"); const result = errorHandler.classifyError(error); expect(result).toBe("NETWORK_ERROR"); }); test("should classify recipient errors", () => { const error = new Error("Invalid recipient address"); const result = errorHandler.classifyError(error); expect(result).toBe("RECIPIENT_ERROR"); }); test("should classify message errors", () => { const error = new Error("Message too large"); const result = errorHandler.classifyError(error); expect(result).toBe("MESSAGE_ERROR"); }); test("should classify unknown errors", () => { const error = new Error("Something unexpected happened"); const result = errorHandler.classifyError(error); expect(result).toBe("UNKNOWN_ERROR"); }); }); describe("isRetryable", () => { test("should mark retryable errors as retryable", () => { expect(errorHandler.isRetryable("RATE_LIMIT")).toBe(true); expect(errorHandler.isRetryable("NETWORK_ERROR")).toBe(true); expect(errorHandler.isRetryable("UNKNOWN_ERROR")).toBe(true); }); test("should mark non-retryable errors as non-retryable", () => { expect(errorHandler.isRetryable("AUTH_ERROR")).toBe(false); expect(errorHandler.isRetryable("RECIPIENT_ERROR")).toBe(false); expect(errorHandler.isRetryable("MESSAGE_ERROR")).toBe(false); }); }); describe("getRetryDelay", () => { test("should calculate exponential backoff delay", () => { const delay1 = errorHandler.getRetryDelay(1); const delay2 = errorHandler.getRetryDelay(2); const delay3 = errorHandler.getRetryDelay(3); // Each delay should be roughly double the previous (with jitter) expect(delay1).toBeGreaterThan(45000); // ~1 minute with jitter expect(delay1).toBeLessThan(90000); expect(delay2).toBeGreaterThan(90000); // ~2 minutes with jitter expect(delay2).toBeLessThan(180000); expect(delay3).toBeGreaterThan(180000); // ~4 minutes with jitter expect(delay3).toBeLessThan(360000); }); test("should cap delay at maximum", () => { const delay = errorHandler.getRetryDelay(10); // Very high attempt number expect(delay).toBeLessThanOrEqual(30 * 60 * 1000); // 30 minutes max }); }); describe("handleError", () => { test("should schedule retry for retryable errors", async () => { const email = { subject: "Test", firmName: "Test Firm" }; const recipient = "test@example.com"; const error = new Error("Network timeout"); const transporter = {}; const result = await errorHandler.handleError( email, recipient, error, transporter ); expect(result).toBe(true); // Indicates retry scheduled const stats = errorHandler.getRetryStats(); expect(stats.totalFailed).toBe(1); }); test("should not schedule retry for non-retryable errors", async () => { const email = { subject: "Test", firmName: "Test Firm" }; const recipient = "test@example.com"; const error = new Error("Invalid login"); const transporter = {}; const result = await errorHandler.handleError( email, recipient, error, transporter ); expect(result).toBe(false); // Indicates permanent failure const stats = errorHandler.getRetryStats(); expect(stats.totalFailed).toBe(0); }); test("should not retry after max attempts reached", async () => { const email = { subject: "Test", firmName: "Test Firm" }; const recipient = "test@example.com"; const error = new Error("Network timeout"); const transporter = {}; // Schedule maximum retries await errorHandler.handleError(email, recipient, error, transporter); await errorHandler.handleError(email, recipient, error, transporter); await errorHandler.handleError(email, recipient, error, transporter); // This should be a permanent failure const result = await errorHandler.handleError( email, recipient, error, transporter ); expect(result).toBe(false); }); }); describe("getRetryStats", () => { test("should return correct retry statistics", () => { // Initially no retries let stats = errorHandler.getRetryStats(); expect(stats.totalFailed).toBe(0); expect(stats.pendingRetries).toBe(0); expect(stats.readyToRetry).toBe(0); // Add a retry item errorHandler.failedEmails.push({ recipient: "test@example.com", retryAt: Date.now() + 60000, // 1 minute from now }); stats = errorHandler.getRetryStats(); expect(stats.totalFailed).toBe(1); expect(stats.pendingRetries).toBe(1); expect(stats.readyToRetry).toBe(0); }); }); describe("clearRetries", () => { test("should clear all retry data", () => { // Add some retry data errorHandler.failedEmails.push({ recipient: "test@example.com" }); errorHandler.retryAttempts.set("test-key", 2); errorHandler.clearRetries(); expect(errorHandler.failedEmails).toHaveLength(0); expect(errorHandler.retryAttempts.size).toBe(0); }); }); });