import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { createKafkaConsumers } from '../src/kafka/consumer.js'; import kafka from 'kafka-node'; // Mock kafka-node vi.mock('kafka-node', () => { return { ConsumerGroup: vi.fn(), default: { ConsumerGroup: vi.fn() } }; }); describe('Consumer Reliability', () => { let mockConsumer; let onMessage; let onError; let healthCheck; const kafkaConfig = { brokers: ['localhost:9092'], groupId: 'test-group', clientId: 'test-client', topic: 'test-topic', autoCommitIntervalMs: 5000 }; beforeEach(() => { vi.clearAllMocks(); mockConsumer = { on: vi.fn(), commit: vi.fn(), pause: vi.fn(), resume: vi.fn(), close: vi.fn() }; kafka.ConsumerGroup.mockImplementation(function() { return mockConsumer; }); onMessage = vi.fn().mockResolvedValue(true); onError = vi.fn(); healthCheck = { shouldPause: vi.fn().mockResolvedValue(false), check: vi.fn().mockResolvedValue(true) }; }); it('should initialize with autoCommit: false', () => { createKafkaConsumers({ kafkaConfig, onMessage, onError }); expect(kafka.ConsumerGroup).toHaveBeenCalledWith( expect.objectContaining({ autoCommit: false }), expect.anything() ); }); it('should commit offset after successful message processing', async () => { createKafkaConsumers({ kafkaConfig, onMessage, onError }); // Simulate 'message' event const message = { value: 'test' }; const messageHandler = mockConsumer.on.mock.calls.find(call => call[0] === 'message')[1]; await messageHandler(message); expect(onMessage).toHaveBeenCalledWith(message); expect(mockConsumer.commit).toHaveBeenCalled(); }); it('should NOT commit if processing fails and health check says pause', async () => { onMessage.mockRejectedValue(new Error('Fail')); healthCheck.shouldPause.mockResolvedValue(true); createKafkaConsumers({ kafkaConfig, onMessage, onError, healthCheck }); const messageHandler = mockConsumer.on.mock.calls.find(call => call[0] === 'message')[1]; await messageHandler({ value: 'test' }); expect(mockConsumer.commit).not.toHaveBeenCalled(); expect(onError).toHaveBeenCalled(); }); it('should commit if processing fails but health check says continue (Data Error)', async () => { onMessage.mockRejectedValue(new Error('Data Error')); healthCheck.shouldPause.mockResolvedValue(false); // Do not pause, it's just bad data createKafkaConsumers({ kafkaConfig, onMessage, onError, healthCheck }); const messageHandler = mockConsumer.on.mock.calls.find(call => call[0] === 'message')[1]; await messageHandler({ value: 'bad_data' }); expect(mockConsumer.commit).toHaveBeenCalled(); // Should commit to move past bad data expect(onError).toHaveBeenCalled(); // Should still report error }); it('should pause and enter recovery mode if healthCheck.shouldPause returns true', async () => { vi.useFakeTimers(); onMessage.mockRejectedValue(new Error('DB Error')); healthCheck.shouldPause.mockResolvedValue(true); healthCheck.check.mockResolvedValueOnce(false).mockResolvedValueOnce(true); // Fail once, then succeed createKafkaConsumers({ kafkaConfig, onMessage, onError, healthCheck }); const messageHandler = mockConsumer.on.mock.calls.find(call => call[0] === 'message')[1]; // Trigger error await messageHandler({ value: 'fail' }); expect(mockConsumer.pause).toHaveBeenCalled(); expect(healthCheck.shouldPause).toHaveBeenCalled(); // Fast-forward time for interval check (1st check - fails) await vi.advanceTimersByTimeAsync(60000); expect(healthCheck.check).toHaveBeenCalledTimes(1); expect(mockConsumer.resume).not.toHaveBeenCalled(); // Fast-forward time for interval check (2nd check - succeeds) await vi.advanceTimersByTimeAsync(60000); expect(healthCheck.check).toHaveBeenCalledTimes(2); expect(mockConsumer.resume).toHaveBeenCalled(); vi.useRealTimers(); }); });