UNPKG

@sailboat-computer/event-bus

Version:

Standardized event bus for sailboat computer v3 with resilience features and offline capabilities

282 lines (247 loc) 8.38 kB
/** * Redis adapter tests */ import { RedisAdapter, createRedisAdapter } from '../../src/adapters'; import { EventPriority, EventCategory } from '../../src/types'; import Redis from 'ioredis'; // Mock Redis jest.mock('ioredis', () => { return jest.fn().mockImplementation(() => { return { status: 'ready', on: jest.fn(), once: jest.fn(), quit: jest.fn().mockResolvedValue(undefined), xadd: jest.fn().mockResolvedValue('mock-id'), xgroup: jest.fn().mockResolvedValue('OK'), xreadgroup: jest.fn().mockResolvedValue(null), xack: jest.fn().mockResolvedValue(1), ping: jest.fn().mockResolvedValue('PONG') }; }); }); describe('RedisAdapter', () => { let adapter: RedisAdapter; let mockRedis: any; beforeEach(() => { // Reset mocks jest.clearAllMocks(); // Create mock Redis instance mockRedis = { status: 'ready', on: jest.fn(), once: jest.fn(), quit: jest.fn().mockResolvedValue(undefined), xadd: jest.fn().mockResolvedValue('mock-id'), xgroup: jest.fn().mockResolvedValue('OK'), xreadgroup: jest.fn().mockResolvedValue(null), xack: jest.fn().mockResolvedValue(1), ping: jest.fn().mockResolvedValue('PONG') }; // Mock Redis constructor (Redis as unknown as jest.Mock).mockImplementation(() => mockRedis); // Create adapter adapter = createRedisAdapter(); }); afterEach(async () => { try { await adapter.close(); } catch (error) { // Ignore errors } }); describe('initialize', () => { it('should initialize the adapter', async () => { await adapter.initialize({ serviceName: 'test-service', url: 'redis://localhost:6379', consumerGroup: 'test-group', consumerName: 'test-consumer', maxBatchSize: 100, pollInterval: 1000, reconnectOptions: { baseDelay: 1000, maxDelay: 30000, maxRetries: 10 } }); expect(Redis as unknown as jest.Mock).toHaveBeenCalledWith('redis://localhost:6379', expect.any(Object)); expect(mockRedis.on).toHaveBeenCalledWith('connect', expect.any(Function)); expect(mockRedis.on).toHaveBeenCalledWith('error', expect.any(Function)); expect(mockRedis.on).toHaveBeenCalledWith('close', expect.any(Function)); expect(mockRedis.on).toHaveBeenCalledWith('reconnecting', expect.any(Function)); expect(adapter.isConnected()).toBe(true); }); it('should handle connection errors', async () => { // Mock Redis constructor to throw error (Redis as unknown as jest.Mock).mockImplementation(() => { throw new Error('Connection error'); }); await expect(adapter.initialize({ serviceName: 'test-service', url: 'redis://localhost:6379', consumerGroup: 'test-group', consumerName: 'test-consumer', maxBatchSize: 100, pollInterval: 1000, reconnectOptions: { baseDelay: 1000, maxDelay: 30000, maxRetries: 10 } })).rejects.toThrow('Failed to initialize Redis adapter: Connection error'); expect(adapter.isConnected()).toBe(false); }); }); describe('publish', () => { beforeEach(async () => { await adapter.initialize({ serviceName: 'test-service', url: 'redis://localhost:6379', consumerGroup: 'test-group', consumerName: 'test-consumer', maxBatchSize: 100, pollInterval: 1000, reconnectOptions: { baseDelay: 1000, maxDelay: 30000, maxRetries: 10 } }); }); it('should publish an event', async () => { const eventId = await adapter.publish({ id: 'test-id', type: 'test.event', timestamp: new Date(), source: 'test-service', version: '1.0', data: { message: 'Hello, world!' }, metadata: { priority: EventPriority.NORMAL, category: EventCategory.DATA } }); expect(eventId).toBe('test-id'); // Verify xadd was called with the correct stream name expect(mockRedis.xadd).toHaveBeenCalled(); expect(mockRedis.xadd.mock.calls[0][0]).toBe('event:test:event'); expect(mockRedis.xadd.mock.calls[0][1]).toBe('MAXLEN'); expect(mockRedis.xadd.mock.calls[0][2]).toBe('~'); expect(mockRedis.xadd.mock.calls[0][3]).toBe('10000'); expect(mockRedis.xadd.mock.calls[0][4]).toBe('*'); }); it('should handle publish errors', async () => { mockRedis.xadd.mockRejectedValue(new Error('Publish error')); await expect(adapter.publish({ id: 'test-id', type: 'test.event', timestamp: new Date(), source: 'test-service', version: '1.0', data: { message: 'Hello, world!' }, metadata: { priority: EventPriority.NORMAL, category: EventCategory.DATA } })).rejects.toThrow('Failed to publish event: Publish error'); }); }); describe('subscribe', () => { beforeEach(async () => { await adapter.initialize({ serviceName: 'test-service', url: 'redis://localhost:6379', consumerGroup: 'test-group', consumerName: 'test-consumer', maxBatchSize: 100, pollInterval: 1000, reconnectOptions: { baseDelay: 1000, maxDelay: 30000, maxRetries: 10 } }); }); it('should subscribe to an event', async () => { const handler = jest.fn(); const subscription = await adapter.subscribe('test.event', handler); expect(subscription).toBeDefined(); expect(subscription.id).toBeDefined(); expect(subscription.eventType).toBe('test.event'); expect(subscription.unsubscribe).toBeDefined(); expect(mockRedis.xgroup).toHaveBeenCalledWith( 'CREATE', 'event:test:event', 'test-group', '$', 'MKSTREAM' ); }); it('should handle subscribe errors', async () => { mockRedis.xgroup.mockRejectedValue(new Error('Subscribe error')); await expect(adapter.subscribe('test.event', jest.fn())).rejects.toThrow( 'Failed to subscribe to event test.event: Subscribe error' ); }); it('should ignore BUSYGROUP errors', async () => { const error = new Error('BUSYGROUP Consumer Group name already exists'); mockRedis.xgroup.mockRejectedValueOnce(error); const handler = jest.fn(); const subscription = await adapter.subscribe('test.event', handler); expect(subscription).toBeDefined(); }); }); describe('acknowledgeEvent', () => { beforeEach(async () => { await adapter.initialize({ serviceName: 'test-service', url: 'redis://localhost:6379', consumerGroup: 'test-group', consumerName: 'test-consumer', maxBatchSize: 100, pollInterval: 1000, reconnectOptions: { baseDelay: 1000, maxDelay: 30000, maxRetries: 10 } }); }); it('should acknowledge an event', async () => { await adapter.acknowledgeEvent('test-id', 'test.event'); expect(mockRedis.xack).toHaveBeenCalledWith( 'event:test:event', 'test-group', 'test-id' ); }); it('should handle acknowledge errors', async () => { mockRedis.xack.mockRejectedValue(new Error('Acknowledge error')); // Should not throw await adapter.acknowledgeEvent('test-id', 'test.event'); }); }); describe('close', () => { beforeEach(async () => { await adapter.initialize({ serviceName: 'test-service', url: 'redis://localhost:6379', consumerGroup: 'test-group', consumerName: 'test-consumer', maxBatchSize: 100, pollInterval: 1000, reconnectOptions: { baseDelay: 1000, maxDelay: 30000, maxRetries: 10 } }); }); it('should close the adapter', async () => { await adapter.close(); expect(mockRedis.quit).toHaveBeenCalled(); expect(adapter.isConnected()).toBe(false); }); }); });