Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 159 additions & 22 deletions src/workers/__tests__/sms-cluster-worker.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import cluster from 'cluster';
import { startSMSClusterWorker } from '../sms-cluster-worker';
import {
startSMSClusterWorker,
sanitizeAndValidateSMS,
getSecurityEvents,
getQueueSize,
getRateLimitStatus
} from '../sms-cluster-worker';

jest.mock('cluster', () => ({
isPrimary: true,
Expand All @@ -14,41 +20,172 @@ jest.mock('os', () => ({
describe('SMS Cluster Worker', () => {
beforeEach(() => {
jest.clearAllMocks();
// Clear security events before each test
const events = getSecurityEvents();
events.length = 0;
});

it('should fork a worker for each CPU if primary', () => {
// Override cluster.isPrimary just in case
Object.defineProperty(cluster, 'isPrimary', { value: true, configurable: true });
describe('Cluster Management', () => {
it('should fork a worker for each CPU if primary', () => {
Object.defineProperty(cluster, 'isPrimary', { value: true, configurable: true });

const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();

startSMSClusterWorker();
startSMSClusterWorker();

expect(cluster.fork).toHaveBeenCalledTimes(4);
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('Setting up cluster with 4 workers'),
);
expect(cluster.fork).toHaveBeenCalledTimes(4);
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('Setting up cluster with 4 workers'),
);

consoleSpy.mockRestore();
consoleSpy.mockRestore();
});

it('should bind an exit handler to auto-heal workers', () => {
Object.defineProperty(cluster, 'isPrimary', { value: true, configurable: true });

startSMSClusterWorker();

expect(cluster.on).toHaveBeenCalledWith('exit', expect.any(Function));
});

it('should execute worker logic if not primary', () => {
Object.defineProperty(cluster, 'isPrimary', { value: false, configurable: true });
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();

startSMSClusterWorker();

expect(cluster.fork).not.toHaveBeenCalled();
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Worker'));

consoleSpy.mockRestore();
});

it('should set worker resource limits', () => {
Object.defineProperty(cluster, 'isPrimary', { value: true, configurable: true });

startSMSClusterWorker();

expect(cluster.fork).toHaveBeenCalledWith(
expect.objectContaining({
WORKER_ID: expect.any(Number),
NODE_OPTIONS: expect.stringContaining('--max-old-space-size=512'),
})
);
});
});

it('should bind an exit handler to auto-heal workers', () => {
Object.defineProperty(cluster, 'isPrimary', { value: true, configurable: true });
describe('Input Validation', () => {
it('should validate correct phone numbers', () => {
const result = sanitizeAndValidateSMS('+15551234567', 'Test message');
expect(result.valid).toBe(true);
expect(result.sanitized).toBeDefined();
});

it('should reject invalid phone number format', () => {
const result = sanitizeAndValidateSMS('5551234567', 'Test message');
expect(result.valid).toBe(false);
expect(result.reason).toContain('Invalid phone number format');
});

startSMSClusterWorker();
it('should reject phone numbers that are too long', () => {
const result = sanitizeAndValidateSMS('+155512345678901234567', 'Test message');
expect(result.valid).toBe(false);
expect(result.reason).toContain('too long');
});

expect(cluster.on).toHaveBeenCalledWith('exit', expect.any(Function));
it('should reject non-string phone numbers', () => {
const result = sanitizeAndValidateSMS(1234567890 as any, 'Test message');
expect(result.valid).toBe(false);
expect(result.reason).toContain('must be a string');
});

it('should reject empty messages', () => {
const result = sanitizeAndValidateSMS('+15551234567', '');
expect(result.valid).toBe(false);
expect(result.reason).toContain('cannot be empty');
});

it('should reject messages that are too long', () => {
const longMessage = 'a'.repeat(2000);
const result = sanitizeAndValidateSMS('+15551234567', longMessage);
expect(result.valid).toBe(false);
expect(result.reason).toContain('too long');
});

it('should sanitize message content', () => {
const result = sanitizeAndValidateSMS('+15551234567', '<script>alert("xss")</script>');
expect(result.valid).toBe(true);
expect(result.sanitized?.message).not.toContain('<script>');
});
});

describe('Rate Limiting', () => {
it('should allow messages within rate limit', () => {
for (let i = 0; i < 50; i++) {
const result = sanitizeAndValidateSMS('+15551234567', `Test message ${i}`);
expect(result.valid).toBe(true);
}
});

it('should enforce rate limit after threshold', () => {
// Send 100 messages (should hit the limit)
for (let i = 0; i < 100; i++) {
sanitizeAndValidateSMS('+15551234567', `Test message ${i}`);
}

// 101st message should be rate limited
const result = sanitizeAndValidateSMS('+15551234567', 'Test message 100');
expect(result.valid).toBe(false);
expect(result.reason).toContain('Rate limit exceeded');
});

it('should track rate limit status correctly', () => {
for (let i = 0; i < 50; i++) {
sanitizeAndValidateSMS('+15551234567', `Test message ${i}`);
}

const status = getRateLimitStatus('+15551234567');
expect(status.remaining).toBeLessThanOrEqual(50);
expect(status.resetTime).toBeGreaterThan(Date.now());
});
});

it('should execute worker logic if not primary', () => {
Object.defineProperty(cluster, 'isPrimary', { value: false, configurable: true });
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
describe('Security Event Logging', () => {
it('should log validation failures', () => {
sanitizeAndValidateSMS('invalid', 'Test message');
const events = getSecurityEvents();

expect(events.length).toBeGreaterThan(0);
expect(events[events.length - 1].type).toBe('validation_failure');
});

startSMSClusterWorker();
it('should log rate limit exceeded events', () => {
for (let i = 0; i < 101; i++) {
sanitizeAndValidateSMS('+15551234567', `Test message ${i}`);
}

const events = getSecurityEvents();
const rateLimitEvents = events.filter(e => e.type === 'rate_limit_exceeded');
expect(rateLimitEvents.length).toBeGreaterThan(0);
});

expect(cluster.fork).not.toHaveBeenCalled();
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Worker'));
it('should limit security event history size', () => {
// Generate more than 1000 events
for (let i = 0; i < 1100; i++) {
sanitizeAndValidateSMS(`+155512345${i % 100}`, `Test message ${i}`);
}

const events = getSecurityEvents();
expect(events.length).toBeLessThanOrEqual(1000);
});
});

consoleSpy.mockRestore();
describe('Queue Management', () => {
it('should return current queue size', () => {
const size = getQueueSize();
expect(typeof size).toBe('number');
expect(size).toBeGreaterThanOrEqual(0);
});
});
});
Loading
Loading