UNPKG

acelga-bus

Version:

An extensible typescript message bus with support for middlewares

371 lines (279 loc) 12.7 kB
/* tslint:disable:no-unused-locals */ import {EventstoreFacade} from '../../eventstore/facade'; import {EventAlreadySubscribed} from '../../corebus/commonErrors'; import {createSpiedMockedEventstoreClient, createBrokenPipelineFactory, noACKDecodedEventstoreResponse, noNACKDecodedEventstoreResponse} from './mocks'; import {ErrorLogger} from '../../'; import {EventFactoryRespository, FactoryNotFoundError} from '../../corebus/eventFactoryRepository'; import BulkDispatcher from '../../corebus/dispatchers/bulk'; import {Dispatcher} from '../../corebus/dispatchers/single'; import Scheduler from '../../corebus/schedulers/parallel'; import {IEventstoreEvent} from '../../eventstore/interfaces'; import {EventProcessor, EventWithoutOriginalEvent, ErrorOnEventReconstruction} from '../../corebus/eventProcessor'; import {isValidDecodedEventStore, decodeEventstoreResponse} from '../../eventstore/utils'; import {pipelineFactory} from '../../corebus/pipeline/factory'; import {originalEventSymbol} from '../../corebus/interfaces'; import {EmptyTracker} from '../../corebus/emptyTracker'; import {HTTPClient} from 'geteventstore-promise'; import {SynchronousClientProcessor} from '../../corebus/synchronousClientProcessor'; class EventA implements IEventstoreEvent { origin = 'aggregateA'; Hola = 'data'; hola(): number { return 2; } } class EventB implements IEventstoreEvent { origin = 'aggregateB'; } function createBus(pipelineFactoryToInject: any = pipelineFactory, eventsToReturn?: any, evDecoder?: any, decodedEventValidator: any = isValidDecodedEventStore){ const subscriptions = [{stream: 'a', subscription: 'a'}]; const { client: client, backoffSummary: backoffSummary, errorLogger: errorLogger, evClient: evClient, backoff: backoff, } = createSpiedMockedEventstoreClient(1, subscriptions, eventsToReturn, evDecoder); const dispatcher = new Dispatcher<IEventstoreEvent>(); const scheduler = new Scheduler<IEventstoreEvent>(); const bulkDispatcher = new BulkDispatcher<IEventstoreEvent>(dispatcher, scheduler, pipelineFactoryToInject, errorLogger); jest.spyOn(bulkDispatcher, 'on'); const eventFactoryRepository = new EventFactoryRespository<any, any>(decodedEventValidator); const recreateEvent = (event: unknown): any => eventFactoryRepository.execute(event); const eventProcessor = new EventProcessor<any, any>(recreateEvent, errorLogger, bulkDispatcher, client); const tracker = new EmptyTracker(); const onEvent = (events: any[]) => eventProcessor.processEvents(events); const synchronousClientProcessor = new SynchronousClientProcessor<any, any, any>(client, onEvent, errorLogger, backoff, decodeEventstoreResponse, subscriptions, tracker, 25000); const bus = new EventstoreFacade(synchronousClientProcessor, client, eventFactoryRepository, bulkDispatcher); return { client, backoffSummary, errorLogger, evClient, backoff, dispatcher, scheduler, bulkDispatcher, eventFactoryRepository, bus, eventProcessor, }; } describe('EventstoreFacade', () => { let evClient: HTTPClient; let bus: EventstoreFacade<any>; let errorLogger: ErrorLogger; let bulkDispatcher: BulkDispatcher<any>; beforeEach(() => { const { errorLogger: _errorLogger, evClient: _evClient, bulkDispatcher: _bulkDispatcher, bus: _bus, } = createBus(); errorLogger = _errorLogger; evClient = _evClient; bulkDispatcher = _bulkDispatcher; bus = _bus; }); it('should add the correct subscription', async () => { await bus.startConsumption(); bus.on(EventA, (event) => { return Promise.resolve(); }); expect(bulkDispatcher.on).toHaveBeenCalled(); }); it('should not allow more than one listeners by event', () => { bus.on(EventA, (event) => { return Promise.resolve(); }); expect(() => bus.on(EventA, () => {})).toThrow(EventAlreadySubscribed); }); it('should not call the handler in the case of a event already subscribed', (done) => { bus.startConsumption(); const handlerA = jest.fn().mockImplementation(() => Promise.resolve()); const handlerB = jest.fn().mockImplementation(() => Promise.resolve()); const factoryA = jest.fn().mockImplementation(() => new EventA()); const factoryB = jest.fn().mockImplementation(() => new EventB()); bus.addEventType(EventA, {build: factoryA}); bus.addEventType(EventB, {build: factoryB}); bus.on(EventA, handlerA); expect(() => bus.on(EventA, handlerB)).toThrow(EventAlreadySubscribed); setTimeout(async () => { await bus.stop(); expect(handlerA).toHaveBeenCalledTimes(1); expect(handlerB).toHaveBeenCalledTimes(0); expect(factoryA).toHaveBeenCalledTimes(1); expect(factoryB).toHaveBeenCalledTimes(1); done(); }, 5); }); it('should receive all events in the case of a correct subscription', (done) => { bus.startConsumption(); const handlerA = jest.fn().mockImplementation(() => Promise.resolve()); const handlerB = jest.fn().mockImplementation(() => Promise.resolve()); const factoryA = jest.fn().mockImplementation(() => new EventA()); const factoryB = jest.fn().mockImplementation(() => new EventB()); bus.addEventType(EventA, {build: factoryA}); bus.addEventType(EventB, {build: factoryB}); bus.on(EventA, handlerA); bus.on(EventB, handlerB); setTimeout(() => { expect(handlerA).toHaveBeenCalledTimes(1); expect(handlerB).toHaveBeenCalledTimes(1); expect(factoryA).toHaveBeenCalledTimes(1); expect(factoryB).toHaveBeenCalledTimes(1); done(); }, 5); }); it('should receive all events in the case of onAny', async (done) => { bus.startConsumption(); const factoryResult = {origin: Math.random() + ''}; const handler = jest.fn().mockImplementation(() => Promise.resolve()); const factory = jest.fn().mockImplementation(() => factoryResult); bus.addEventType(EventA, {build: factory}); bus.onAny(handler); await bus.stop(); expect(handler).toHaveBeenCalledTimes(1); expect(handler).toHaveBeenCalledWith(factoryResult); expect(factory).toHaveBeenCalledTimes(1); done(); }); it('should wait for events to start being consumed when stop', async (done) => { const factoryResult = {origin: Math.random() + ''}; const handler = jest.fn().mockImplementation(() => Promise.resolve()); const factory = jest.fn().mockImplementation(() => factoryResult); bus.addEventType(EventA, {build: factory}); bus.onAny(handler); bus.startConsumption(); await bus.stop(); expect(handler).toHaveBeenCalledTimes(1); expect(handler).toHaveBeenCalledWith(factoryResult); expect(factory).toHaveBeenCalledTimes(1); done(); }); it('should log an error if an event without factory is received', (done) => { bus.startConsumption(); const factory = jest.fn().mockImplementation(() => ({origin: ''})); bus.addEventType(EventA, {build: factory}); setTimeout(async () => { await bus.stop(); expect(errorLogger).toHaveBeenCalledTimes(1); expect(errorLogger).toHaveBeenCalledWith(new ErrorOnEventReconstruction(undefined, new FactoryNotFoundError())); // we add undefined because seems that Jest uses string comparation for comparing nested structures done(); }, 0); }); it('should nack all messages when there is an internal error', async (done) => { const {bus: _bus, client: _client} = createBus(createBrokenPipelineFactory()); const factoryA = jest.fn().mockImplementation(() => new EventA()); const factoryB = jest.fn().mockImplementation(() => new EventB()); _bus.addEventType(EventA, {build: factoryA}); _bus.addEventType(EventB, {build: factoryB}); await _bus.startConsumption(); setTimeout(async () => { await _bus.stop(); expect(_client.nack).toHaveBeenCalledTimes(2); done(); }, 0); }); it('should publish the messages correctly using the client', async (done) => { const event = new EventA(); bus.publish(event); expect(evClient.writeEvent).toHaveBeenCalledWith(event.origin, EventA.name, event); done(); }); it('should nack the mesages that got an error', async (done) => { const {bus: _bus, client: _client} = createBus(); const factoryA = jest.fn().mockImplementation(() => new EventA()); const factoryB = jest.fn().mockImplementation(() => new EventB()); _bus.addEventType(EventA, {build: factoryA}); _bus.addEventType(EventB, {build: factoryB}); _bus.on(EventA, (event) => Promise.resolve()); _bus.on(EventB, (event) => Promise.reject()); await _bus.startConsumption(); setTimeout(async () => { await _bus.stop(); expect(_client.nack).toHaveBeenCalledTimes(1); expect(_client.ack).toHaveBeenCalledTimes(1); done(); }, 0); }); it('should log an error when there is no original event', async (done) => { const {bus: _bus, client: _client, errorLogger: _errorLogger} = createBus(pipelineFactory, undefined, noACKDecodedEventstoreResponse, () => true); const factoryA = jest.fn().mockImplementation(() => new EventA()); const factoryB = jest.fn().mockImplementation(() => new EventB()); _bus.addEventType(EventA, {build: factoryA}); _bus.addEventType(EventB, {build: factoryB}); _bus.on(EventA, (event) => { delete event[originalEventSymbol]; Promise.resolve(); }); _bus.on(EventB, (event) => { delete event[originalEventSymbol]; Promise.resolve(); }); await _bus.startConsumption(); setTimeout(async () => { await _bus.stop(); expect(_client.ack).toHaveBeenCalledTimes(0); expect(_client.nack).toHaveBeenCalledTimes(0); expect(_errorLogger).toHaveBeenCalledTimes(2); expect(_errorLogger.mock.calls[0][0]).toBeInstanceOf(EventWithoutOriginalEvent); expect(_errorLogger.mock.calls[1][0]).toBeInstanceOf(EventWithoutOriginalEvent); done(); }, 0); }); it('should log an error when there is no nack link', async (done) => { const {bus: _bus, client: _client, errorLogger: _errorLogger} = createBus(pipelineFactory, undefined, noNACKDecodedEventstoreResponse, () => true); const factoryA = jest.fn().mockImplementation(() => new EventA()); const factoryB = jest.fn().mockImplementation(() => new EventB()); _bus.addEventType(EventA, {build: factoryA}); _bus.addEventType(EventB, {build: factoryB}); _bus.on(EventA, (event) => { delete event[originalEventSymbol]; return Promise.reject(); }); _bus.on(EventB, (event) => { delete event[originalEventSymbol]; return Promise.resolve(); }); await _bus.startConsumption(); setTimeout(async () => { await _bus.stop(); expect(_client.nack).toHaveBeenCalledTimes(0); expect(_client.ack).toHaveBeenCalledTimes(0); expect(_errorLogger).toHaveBeenCalledTimes(2); expect(_errorLogger.mock.calls[0][0]).toBeInstanceOf(EventWithoutOriginalEvent); expect(_errorLogger.mock.calls[1][0]).toBeInstanceOf(EventWithoutOriginalEvent); done(); }, 0); }); it('should log an error if ack call fails', async (done) => { const {bus: _bus, client: _client, errorLogger: _errorLogger} = createBus(pipelineFactory); const errorReturned = new Error('errorReturned'); jest.spyOn(_client, 'ack').mockImplementation(() => Promise.reject(errorReturned)); const factoryA = jest.fn().mockImplementation(() => new EventA()); const factoryB = jest.fn().mockImplementation(() => new EventB()); _bus.addEventType(EventA, {build: factoryA}); _bus.addEventType(EventB, {build: factoryB}); _bus.on(EventA, (event) => Promise.resolve()); _bus.on(EventB, (event) => Promise.resolve()); await _bus.startConsumption(); setTimeout(async () => { await _bus.stop(); expect(_client.ack).toHaveBeenCalledTimes(2); expect(_client.nack).toHaveBeenCalledTimes(0); expect(_errorLogger).toHaveBeenCalledTimes(2); expect(_errorLogger.mock.calls[0][0]).toEqual(errorReturned); done(); }, 0); }); it('should log an error if ack call fails', async (done) => { const {bus: _bus, client: _client, errorLogger: _errorLogger} = createBus(pipelineFactory); const errorReturned = new Error('errorReturned'); jest.spyOn(_client, 'nack').mockImplementation(() => Promise.reject(errorReturned)); const factoryA = jest.fn().mockImplementation(() => new EventA()); const factoryB = jest.fn().mockImplementation(() => new EventB()); _bus.addEventType(EventA, {build: factoryA}); _bus.addEventType(EventB, {build: factoryB}); _bus.on(EventA, (event) => Promise.reject()); _bus.on(EventB, (event) => Promise.reject()); await _bus.startConsumption(); setTimeout(async () => { await _bus.stop(); expect(_client.ack).toHaveBeenCalledTimes(0); expect(_client.nack).toHaveBeenCalledTimes(2); expect(_errorLogger).toHaveBeenCalledTimes(2); expect(_errorLogger.mock.calls[0][0]).toEqual(errorReturned); done(); }, 0); }); });