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