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

161 lines
5.2 KiB
JavaScript

const { describe, test, expect, beforeEach } = require("@jest/globals");
// Mock config
jest.mock("../../config", () => ({
app: {
delayMinutes: 5,
},
}));
// Mock logger
jest.mock("../../lib/logger", () => ({
rateLimitPause: jest.fn(),
}));
const rateLimiter = require("../../lib/rateLimiter");
describe("RateLimiter", () => {
beforeEach(() => {
jest.clearAllMocks();
rateLimiter.reset();
});
describe("getRandomDelay", () => {
test("should return delay within expected range", () => {
const delay = rateLimiter.getRandomDelay();
// Base delay is 5 minutes = 300,000ms
// With 20% variance and jitter, expect roughly 240,000 to 390,000ms
expect(delay).toBeGreaterThan(200000);
expect(delay).toBeLessThan(400000);
});
test("should return different delays each time (due to randomization)", () => {
const delay1 = rateLimiter.getRandomDelay();
const delay2 = rateLimiter.getRandomDelay();
const delay3 = rateLimiter.getRandomDelay();
// Extremely unlikely to be identical due to randomization
expect(delay1).not.toBe(delay2);
expect(delay2).not.toBe(delay3);
});
});
describe("shouldPause", () => {
test("should not pause initially", () => {
expect(rateLimiter.shouldPause()).toBe(false);
});
test("should pause after sending many emails in short time", () => {
// Simulate sending 20 emails in 1 hour (exceeds 15/hour limit)
rateLimiter.sentCount = 20;
rateLimiter.startTime = Date.now() - 60 * 60 * 1000; // 1 hour ago
expect(rateLimiter.shouldPause()).toBe(true);
});
test("should pause every 50 emails", () => {
rateLimiter.sentCount = 50;
expect(rateLimiter.shouldPause()).toBe(true);
rateLimiter.sentCount = 100;
expect(rateLimiter.shouldPause()).toBe(true);
});
test("should not pause if under rate limit", () => {
// Simulate sending 10 emails in 1 hour (under 15/hour limit)
rateLimiter.sentCount = 10;
rateLimiter.startTime = Date.now() - 60 * 60 * 1000; // 1 hour ago
expect(rateLimiter.shouldPause()).toBe(false);
});
});
describe("getPauseDuration", () => {
test("should return pause duration around 30 minutes", () => {
const duration = rateLimiter.getPauseDuration();
// Base pause is 30 minutes = 1,800,000ms
// With randomization, expect 30-40 minutes
expect(duration).toBeGreaterThan(30 * 60 * 1000);
expect(duration).toBeLessThan(40 * 60 * 1000);
});
});
describe("getNextSendDelay", () => {
test("should increment sent count", async () => {
const initialCount = rateLimiter.sentCount;
await rateLimiter.getNextSendDelay();
expect(rateLimiter.sentCount).toBe(initialCount + 1);
});
test("should return pause duration when should pause", async () => {
// Force a pause condition
rateLimiter.sentCount = 49; // Next increment will trigger pause at 50
const delay = await rateLimiter.getNextSendDelay();
// Should be a long pause, not normal delay
expect(delay).toBeGreaterThan(30 * 60 * 1000); // More than 30 minutes
});
test("should return normal delay when not pausing", async () => {
rateLimiter.sentCount = 5; // Low count, won't trigger pause
const delay = await rateLimiter.getNextSendDelay();
// Should be normal delay (around 5 minutes with variance)
expect(delay).toBeGreaterThan(200000);
expect(delay).toBeLessThan(400000);
});
});
describe("formatDelay", () => {
test("should format milliseconds to readable time", () => {
expect(rateLimiter.formatDelay(60000)).toBe("1m 0s");
expect(rateLimiter.formatDelay(90000)).toBe("1m 30s");
expect(rateLimiter.formatDelay(125000)).toBe("2m 5s");
expect(rateLimiter.formatDelay(3661000)).toBe("61m 1s");
});
});
describe("getStats", () => {
test("should return correct statistics", () => {
rateLimiter.sentCount = 10;
rateLimiter.startTime = Date.now() - 60 * 60 * 1000; // 1 hour ago
const stats = rateLimiter.getStats();
expect(stats.sentCount).toBe(10);
expect(stats.runtime).toBe(60); // 60 minutes
expect(stats.averageRate).toBe("10.0"); // 10 emails per hour
expect(stats.nextDelay).toMatch(/^\d+m \d+s$/); // Format like "5m 23s"
});
test("should handle zero runtime gracefully", () => {
rateLimiter.sentCount = 5;
rateLimiter.startTime = Date.now(); // Just started
const stats = rateLimiter.getStats();
expect(stats.sentCount).toBe(5);
expect(stats.runtime).toBe(0);
// Average rate should be very high for instant runtime
expect(parseFloat(stats.averageRate)).toBeGreaterThan(1000);
});
});
describe("reset", () => {
test("should reset all counters", () => {
rateLimiter.sentCount = 10;
rateLimiter.lastSentTime = Date.now() - 60000;
rateLimiter.reset();
expect(rateLimiter.sentCount).toBe(0);
expect(rateLimiter.lastSentTime).toBeNull();
expect(rateLimiter.startTime).toBeCloseTo(Date.now(), -1); // Within 10ms
});
});
});