outreach/tests/lib/errorHandler.test.js
2025-08-15 01:03:38 -08:00

196 lines
6.3 KiB
JavaScript

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);
});
});
});