UNPKG

@getanthill/datastore

Version:

Event-Sourced Datastore

406 lines (328 loc) 11.9 kB
import Core from './Core'; import Streams from './Streams'; import { EventEmitter } from 'events'; describe('sdk/Stream', () => { let core; let client: Streams; let eventSource; beforeEach(async () => { core = new Core(); client = new Streams( { token: 'token', telemetry: { logger: { debug: jest.fn(), info: jest.fn(), warning: jest.fn(), error: jest.fn(), }, }, }, core, ); client.getEventSource = jest.fn().mockImplementation(() => { eventSource = new EventEmitter(); eventSource.addEventListener = eventSource.on; eventSource.close = jest.fn(); setTimeout(() => eventSource.emit('open'), 10); return eventSource; }); }); afterEach(() => { jest.restoreAllMocks(); }); describe('constructor', () => { it('creates a new client with default configuration', () => { client = new Streams({}, core); expect(client.config).toEqual({ baseUrl: 'http://localhost:3001', token: 'token', debug: false, connector: 'http', }); }); it('creates a new client with configuration defined in the signature', () => { client = new Streams( { baseUrl: 'https://datastore.org', token: 'private_token' }, core, ); expect(client.config).toMatchObject({ baseUrl: 'https://datastore.org', token: 'private_token', }); }); it('creates a new client with core instanciated', () => { client = new Streams({}, core); expect(client._core).toEqual(core); }); it('creates a new client with telemetry enabled', () => { client = new Streams({ telemetry: { logger: true } }, core); expect(client._telemetry).toEqual({ logger: true }); }); }); describe('#stream', () => { let close; afterEach(() => { jest.restoreAllMocks(); close(); }); it('calls the stream route with default parameters (deprecated)', async () => { const handler = jest.fn(); close = await client.stream(handler); expect(client.getEventSource).toHaveBeenCalledWith( 'http://localhost:3001/api/stream/all/entities/sse?pipeline=[]', { authorization: 'token' }, ); }); it('calls the stream route with default parameters', async () => { const handler = jest.fn(); close = await client.streamHTTP(handler); expect(client.getEventSource).toHaveBeenCalledWith( 'http://localhost:3001/api/stream/all/entities/sse?pipeline=[]', { authorization: 'token' }, ); }); it('calls the stream route for a given model', async () => { const handler = jest.fn(); close = await client.streamHTTP(handler, 'users'); expect(client.getEventSource).toHaveBeenCalledWith( 'http://localhost:3001/api/stream/users/entities/sse?pipeline=[]', { authorization: 'token' }, ); }); it('calls the stream route for a given model and for events', async () => { const handler = jest.fn(); close = await client.streamHTTP(handler, 'users', 'events'); expect(client.getEventSource).toHaveBeenCalledWith( 'http://localhost:3001/api/stream/users/events/sse?pipeline=[]', { authorization: 'token' }, ); }); it('calls the stream route for a given aggregation pipeline', async () => { const handler = jest.fn(); close = await client.streamHTTP(handler, 'users', 'entities', { email: 'john', }); expect(client.getEventSource).toHaveBeenCalledWith( 'http://localhost:3001/api/stream/users/entities/sse?pipeline=[{"$match":{"fullDocument.email":"john"}}]', { authorization: 'token' }, ); }); it('invokes the handler on JSON object reception', async () => { const handler = jest.fn(); close = await client.streamHTTP(handler); eventSource.emit('message', { data: JSON.stringify({ a: 1 }) }); expect(handler).toHaveBeenCalledWith({ a: 1 }); }); it('closes the connection on the invokation of the close returned handler', async () => { const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); const handler = jest.fn(); close = await client.streamHTTP(handler); close(); expect(eventSource.close).toHaveBeenCalledTimes(1); }); }); describe('#listen / #close', () => { let close; beforeEach(() => { close = jest.fn(); }); afterEach(() => { close(); }); it('allows to listen events from the Datastore', async () => { client.streamHTTP = jest.fn().mockImplementation(() => close); await client.listen('all', 'events'); const firstCallArgumentsExceptHandler = client.streamHTTP.mock.calls[0]; firstCallArgumentsExceptHandler.shift(); expect(firstCallArgumentsExceptHandler).toEqual([ 'all', 'events', undefined, undefined, ]); }); it('allows to listen events based on a customized projection', async () => { client.streamHTTP = jest.fn().mockImplementation(() => close); await client.listen('all', 'events', { state: 'created' }); const firstCallArgumentsExceptHandler = client.streamHTTP.mock.calls[0]; firstCallArgumentsExceptHandler.shift(); expect(firstCallArgumentsExceptHandler).toEqual([ 'all', 'events', { state: 'created' }, undefined, ]); }); it('allows to listen events from AMQP based on a custom JSON Schema built from query', async () => { const amqp = { connect: jest.fn(), init: jest.fn(), on: jest.fn(), subscribe: jest.fn(), end: jest.fn(), }; client.getAMQPClient = jest.fn().mockImplementation(() => amqp); client.config.connector = 'amqp'; await client.listen('all', 'events', { state: 'created' }); expect(amqp.connect).toHaveBeenCalledTimes(1); expect(amqp.on.mock.calls[0][0]).toEqual('*/*/events/*'); expect(amqp.subscribe).toHaveBeenCalledWith('*/*/events/*', { required: ['state'], properties: { state: { enum: ['created'], type: 'string' } }, type: 'object', }); }); it('allows to listen events from AMQP based on a custom JSON Schema built with empty query', async () => { const amqp = { connect: jest.fn(), init: jest.fn(), on: jest.fn(), subscribe: jest.fn(), end: jest.fn(), }; client.getAMQPClient = jest.fn().mockImplementation(() => amqp); client.config.connector = 'amqp'; await client.listen('all', 'events'); expect(amqp.connect).toHaveBeenCalledTimes(1); expect(amqp.on.mock.calls[0][0]).toEqual('*/*/events/*'); expect(amqp.subscribe).toHaveBeenCalledWith('*/*/events/*', { properties: {}, type: 'object', }); }); it('allows to listen events from AMQP based on a custom JSON Schema given as query', async () => { const amqp = { connect: jest.fn(), init: jest.fn(), on: jest.fn(), subscribe: jest.fn(), end: jest.fn(), }; client.getAMQPClient = jest.fn().mockImplementation(() => amqp); client.config.connector = 'amqp'; await client.listen( 'all', 'events', { type: 'object', required: ['state'], properties: { state: { type: 'string', enum: ['created', 'canceled'] }, }, }, { queryAsJSONSchema: true }, ); expect(amqp.connect).toHaveBeenCalledTimes(1); expect(amqp.on.mock.calls[0][0]).toEqual('*/*/events/*'); expect(amqp.subscribe).toHaveBeenCalledWith('*/*/events/*', { required: ['state'], properties: { state: { enum: ['created', 'canceled'], type: 'string' }, }, type: 'object', }); }); it('binds only one stream per streamId', async () => { client.streamHTTP = jest.fn().mockImplementation(() => close); await client.listen('all', 'events'); await client.listen('all', 'events'); expect(client.streamHTTP).toHaveBeenCalledTimes(1); }); it('registers a stream with a streamId defined with query JSON stringified', async () => { client.streamHTTP = jest.fn().mockImplementation(() => close); await client.listen('all', 'events', { test: 1 }); expect(Array.from(client._streams.keys())).toEqual([ 'all:events:{"test":1}', ]); expect(client._streams.get('all:events:{"test":1}')).toEqual(close); }); it('registers a new stream in the Datastore client', async () => { client.streamHTTP = jest.fn().mockImplementation(() => close); await client.listen('all', 'events'); expect(client._streams.get('all:events:{}')).toEqual(close); }); it('closes a stream connection if it exists on close invokation', async () => { client.streamHTTP = jest.fn().mockImplementation(() => close); await client.listen('all', 'events'); close('all', 'events'); expect(close).toHaveBeenCalledTimes(1); }); it('closes all stream connections on closeAll invokation', async () => { client.streamHTTP = jest.fn().mockImplementation(() => close); await client.listen('all', 'events'); await client.listen('all', 'events', { test: 1 }); expect(Array.from(client._streams.keys())).toEqual([ 'all:events:{}', 'all:events:{"test":1}', ]); client.closeAll(); expect(close).toHaveBeenCalledTimes(2); expect(client._streams.size).toEqual(0); }); it('emits a message on the streamId ', async () => { await client.listen('all', 'events'); let message; client.on('all:events:{}', (e) => (message = e)); eventSource.emit('message', { data: JSON.stringify({ a: 1 }) }); expect(message).toEqual({ a: 1 }); }); it('closes an AMQP stream connection if it exists on close invokation', async () => { const amqp = { connect: jest.fn(), init: jest.fn(), on: jest.fn(), subscribe: jest.fn(), end: jest.fn().mockResolvedValue(null), }; client.getAMQPClient = jest.fn().mockImplementation(() => amqp); client.config.connector = 'amqp'; close = await client.listen( 'all', 'events', { type: 'object', required: ['state'], properties: { state: { type: 'string', enum: ['created', 'canceled'] }, }, }, { queryAsJSONSchema: true }, ); close('all', 'events'); expect(amqp.end).toHaveBeenCalledTimes(1); }); it('logs an error in case of an AMQP stream connection error arises', async () => { const error = new Error('Ooops'); const amqp = { connect: jest.fn(), init: jest.fn(), on: jest.fn(), subscribe: jest.fn(), end: jest.fn().mockRejectedValue(error), }; client.getAMQPClient = jest.fn().mockImplementation(() => amqp); client.config.connector = 'amqp'; close = await client.listen( 'all', 'events', { type: 'object', required: ['state'], properties: { state: { type: 'string', enum: ['created', 'canceled'] }, }, }, { queryAsJSONSchema: true }, ); close('all', 'events'); await new Promise((resolve) => setTimeout(resolve, 500)); expect(amqp.end).toHaveBeenCalledTimes(1); expect(client._telemetry?.logger.error).toHaveBeenCalledWith( '[Streams#streamAMQP] Error on closing', { err: error }, ); }); }); });