UNPKG

@sailboat-computer/event-bus

Version:

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

585 lines (517 loc) 15.7 kB
/** * Integration tests for the Redis adapter * * These tests require a running Redis instance on localhost:6379 * If Redis is not available, the tests will be skipped */ import { createEventBus, EventPriority, EventCategory } from '../../src'; import Redis from 'ioredis'; // Define test event types interface TestEvent { id: string; message: string; timestamp: Date; } // Check if Redis is available let redisAvailable = false; let redis: Redis | null = null; beforeAll(async () => { try { // Use Redis URL format from environment variable const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379'; redis = new Redis(redisUrl, { connectTimeout: 1000, maxRetriesPerRequest: 1 }); await redis.ping(); redisAvailable = true; // Clean up any test streams try { const keys = await redis.keys('event:test:*'); if (keys && keys.length > 0) { await redis.del(...keys); } } catch (error) { console.warn('Could not clean up test streams using keys command, skipping cleanup'); // Just skip cleanup if keys method doesn't work } } catch (error) { console.warn('Redis not available, skipping integration tests'); redisAvailable = false; } }); afterAll(async () => { if (redis) { // Clean up any test streams try { const keys = await redis.keys('event:test:*'); if (keys && keys.length > 0) { await redis.del(...keys); } } catch (error) { console.warn('Could not clean up test streams using keys command, skipping cleanup'); // Just skip cleanup if keys method doesn't work } await redis.quit(); } }); describe('Redis Adapter Integration', () => { // Skip all tests if Redis is not available beforeEach(() => { if (!redisAvailable) { return; } }); it('should publish and subscribe to events', async () => { if (!redisAvailable) { return; } // Create event bus with Redis adapter const eventBus = createEventBus({ adapter: { type: 'redis', config: { url: process.env.REDIS_URL || 'redis://localhost:6379', consumerGroup: 'test-group', consumerName: 'test-consumer', maxBatchSize: 100, pollInterval: 100, reconnectOptions: { baseDelay: 100, maxDelay: 1000, maxRetries: 3 }, serviceName: 'test-service' } }, offlineBuffer: { maxSize: 100, priorityRetention: true }, metrics: { enabled: true, detailedTimings: true } }); await eventBus.initialize({ adapter: { type: 'redis', config: { url: process.env.REDIS_URL || 'redis://localhost:6379', consumerGroup: 'test-group', consumerName: 'test-consumer', maxBatchSize: 100, pollInterval: 100, reconnectOptions: { baseDelay: 100, maxDelay: 1000, maxRetries: 3 }, serviceName: 'test-service' } }, offlineBuffer: { maxSize: 100, priorityRetention: true }, metrics: { enabled: true, detailedTimings: true } }); // Create a promise that will be resolved when the event is received const eventReceived = new Promise<TestEvent>((resolve) => { eventBus.subscribe<TestEvent>('test.event', (event) => { resolve(event.data); }); }); // Publish an event const testEvent: TestEvent = { id: 'test-id', message: 'Hello, world!', timestamp: new Date() }; await eventBus.publish<TestEvent>( 'test.event', testEvent, { priority: EventPriority.NORMAL, category: EventCategory.DATA, tags: ['test'] } ); // Wait for the event to be received const receivedEvent = await eventReceived; // Verify the event data expect(receivedEvent).toEqual(testEvent); // Shutdown the event bus await eventBus.shutdown(); }, 10000); // Increase timeout to 10 seconds it('should handle multiple consumers in the same group', async () => { if (!redisAvailable) { return; } // Create two event buses with the same consumer group but different consumer names const eventBus1 = createEventBus({ adapter: { type: 'redis', config: { url: process.env.REDIS_URL || 'redis://localhost:6379', consumerGroup: 'multi-consumer-group', consumerName: 'consumer-1', maxBatchSize: 100, pollInterval: 100, reconnectOptions: { baseDelay: 100, maxDelay: 1000, maxRetries: 3 }, serviceName: 'test-service-1' } }, offlineBuffer: { maxSize: 100, priorityRetention: true }, metrics: { enabled: true, detailedTimings: true } }); const eventBus2 = createEventBus({ adapter: { type: 'redis', config: { url: process.env.REDIS_URL || 'redis://localhost:6379', consumerGroup: 'multi-consumer-group', consumerName: 'consumer-2', maxBatchSize: 100, pollInterval: 100, reconnectOptions: { baseDelay: 100, maxDelay: 1000, maxRetries: 3 }, serviceName: 'test-service-2' } }, offlineBuffer: { maxSize: 100, priorityRetention: true }, metrics: { enabled: true, detailedTimings: true } }); await eventBus1.initialize({ adapter: { type: 'redis', config: { url: process.env.REDIS_URL || 'redis://localhost:6379', consumerGroup: 'multi-consumer-group', consumerName: 'consumer-1', maxBatchSize: 100, pollInterval: 100, reconnectOptions: { baseDelay: 100, maxDelay: 1000, maxRetries: 3 }, serviceName: 'test-service-1' } }, offlineBuffer: { maxSize: 100, priorityRetention: true }, metrics: { enabled: true, detailedTimings: true } }); await eventBus2.initialize({ adapter: { type: 'redis', config: { url: process.env.REDIS_URL || 'redis://localhost:6379', consumerGroup: 'multi-consumer-group', consumerName: 'consumer-2', maxBatchSize: 100, pollInterval: 100, reconnectOptions: { baseDelay: 100, maxDelay: 1000, maxRetries: 3 }, serviceName: 'test-service-2' } }, offlineBuffer: { maxSize: 100, priorityRetention: true }, metrics: { enabled: true, detailedTimings: true } }); // Create promises that will be resolved when events are received const events1: TestEvent[] = []; const events2: TestEvent[] = []; const subscription1 = await eventBus1.subscribe<TestEvent>('test.multi.event', (event) => { events1.push(event.data); }); const subscription2 = await eventBus2.subscribe<TestEvent>('test.multi.event', (event) => { events2.push(event.data); }); // Publish multiple events const testEvents: TestEvent[] = []; for (let i = 0; i < 10; i++) { const event: TestEvent = { id: `test-id-${i}`, message: `Message ${i}`, timestamp: new Date() }; testEvents.push(event); await eventBus1.publish<TestEvent>( 'test.multi.event', event, { priority: EventPriority.NORMAL, category: EventCategory.DATA, tags: ['test'] } ); } // Wait for events to be processed await new Promise(resolve => setTimeout(resolve, 2000)); // Verify that all events were received by exactly one consumer const allReceivedEvents = [...events1, ...events2]; expect(allReceivedEvents.length).toBe(testEvents.length); // Verify that each event was received exactly once const receivedIds = allReceivedEvents.map(e => e.id); const expectedIds = testEvents.map(e => e.id); expect(new Set(receivedIds).size).toBe(expectedIds.length); expect(receivedIds.sort()).toEqual(expectedIds.sort()); // Shutdown the event buses await eventBus1.shutdown(); await eventBus2.shutdown(); }, 15000); // Increase timeout to 15 seconds it('should handle reconnection', async () => { if (!redisAvailable) { return; } // Create event bus with Redis adapter const eventBus = createEventBus({ adapter: { type: 'redis', config: { url: process.env.REDIS_URL || 'redis://localhost:6379', consumerGroup: 'reconnect-group', consumerName: 'reconnect-consumer', maxBatchSize: 100, pollInterval: 100, reconnectOptions: { baseDelay: 100, maxDelay: 1000, maxRetries: 3 }, serviceName: 'test-service' } }, offlineBuffer: { maxSize: 100, priorityRetention: true }, metrics: { enabled: true, detailedTimings: true } }); await eventBus.initialize({ adapter: { type: 'redis', config: { url: process.env.REDIS_URL || 'redis://localhost:6379', consumerGroup: 'reconnect-group', consumerName: 'reconnect-consumer', maxBatchSize: 100, pollInterval: 100, reconnectOptions: { baseDelay: 100, maxDelay: 1000, maxRetries: 3 }, serviceName: 'test-service' } }, offlineBuffer: { maxSize: 100, priorityRetention: true }, metrics: { enabled: true, detailedTimings: true } }); // Create a promise that will be resolved when the event is received const eventReceived = new Promise<TestEvent>((resolve) => { eventBus.subscribe<TestEvent>('test.reconnect.event', (event) => { resolve(event.data); }); }); // Simulate a Redis disconnection by forcing a reconnection in the Redis client // This is a bit of a hack, but it's the easiest way to test reconnection // @ts-ignore - Accessing private property for testing const adapter = (eventBus as any).adapter; // @ts-ignore - Accessing private property for testing const client = adapter.client; if (client) { // Force a disconnect client.disconnect(); // Wait for reconnection await new Promise(resolve => setTimeout(resolve, 500)); } // Publish an event const testEvent: TestEvent = { id: 'reconnect-id', message: 'Reconnection test', timestamp: new Date() }; await eventBus.publish<TestEvent>( 'test.reconnect.event', testEvent, { priority: EventPriority.NORMAL, category: EventCategory.DATA, tags: ['test'] } ); // Wait for the event to be received const receivedEvent = await eventReceived; // Verify the event data expect(receivedEvent).toEqual(testEvent); // Shutdown the event bus await eventBus.shutdown(); }, 10000); // Increase timeout to 10 seconds it('should buffer events when offline', async () => { if (!redisAvailable) { return; } // Create event bus with Redis adapter const eventBus = createEventBus({ adapter: { type: 'redis', config: { url: process.env.REDIS_URL || 'redis://localhost:6379', consumerGroup: 'offline-group', consumerName: 'offline-consumer', maxBatchSize: 100, pollInterval: 100, reconnectOptions: { baseDelay: 100, maxDelay: 1000, maxRetries: 3 }, serviceName: 'test-service' } }, offlineBuffer: { maxSize: 100, priorityRetention: true }, metrics: { enabled: true, detailedTimings: true } }); await eventBus.initialize({ adapter: { type: 'redis', config: { url: process.env.REDIS_URL || 'redis://localhost:6379', consumerGroup: 'offline-group', consumerName: 'offline-consumer', maxBatchSize: 100, pollInterval: 100, reconnectOptions: { baseDelay: 100, maxDelay: 1000, maxRetries: 3 }, serviceName: 'test-service' } }, offlineBuffer: { maxSize: 100, priorityRetention: true }, metrics: { enabled: true, detailedTimings: true } }); // Create a promise that will be resolved when the event is received const eventsReceived: TestEvent[] = []; const subscription = await eventBus.subscribe<TestEvent>('test.offline.event', (event) => { eventsReceived.push(event.data); }); // Simulate a Redis disconnection by forcing a reconnection in the Redis client // @ts-ignore - Accessing private property for testing const adapter = (eventBus as any).adapter; // @ts-ignore - Accessing private property for testing const client = adapter.client; if (client) { // Force a disconnect client.disconnect(); // Set connected to false to simulate offline mode // @ts-ignore - Accessing private property for testing adapter.connected = false; } // Publish events while offline const offlineEvents: TestEvent[] = []; for (let i = 0; i < 5; i++) { const event: TestEvent = { id: `offline-id-${i}`, message: `Offline message ${i}`, timestamp: new Date() }; offlineEvents.push(event); await eventBus.publish<TestEvent>( 'test.offline.event', event, { priority: EventPriority.NORMAL, category: EventCategory.DATA, tags: ['test'] } ); } // Check metrics to verify events are buffered const metrics = eventBus.getMetrics(); expect(metrics.bufferedEvents).toBeGreaterThan(0); // Simulate reconnection if (client) { // Reconnect client.connect(); // Set connected to true // @ts-ignore - Accessing private property for testing adapter.connected = true; // Process offline buffer // @ts-ignore - Accessing private method for testing await (eventBus as any).processOfflineBuffer(); } // Wait for events to be processed await new Promise(resolve => setTimeout(resolve, 2000)); // Verify that all events were received expect(eventsReceived.length).toBe(offlineEvents.length); // Verify that each event was received const receivedIds = eventsReceived.map(e => e.id); const expectedIds = offlineEvents.map(e => e.id); expect(new Set(receivedIds).size).toBe(expectedIds.length); expect(receivedIds.sort()).toEqual(expectedIds.sort()); // Shutdown the event bus await eventBus.shutdown(); }, 15000); // Increase timeout to 15 seconds });