@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
text/typescript
/**
* 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
});