UNPKG

@getanthill/datastore

Version:

Event-Sourced Datastore

365 lines (279 loc) 9.25 kB
import MQTTClient from './mqtt'; import setup from '../../test/setup'; describe('services/mqtt', () => { let mqtt; let app; beforeEach(async () => { app = await setup.build(); mqtt = new MQTTClient(app.services.config.mqtt, { // @ts-ignore logger: { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn(), }, }); await mqtt.connect(); }); afterEach(async () => { await mqtt.end(); await setup.teardownDb(app.services.mongodb); }); describe('#connect / #end', () => { it('throws an exception if not connected yet', async () => { await mqtt.end(); let error; try { const _client = mqtt.client; } catch (err) { error = err; } expect(error).toEqual(MQTTClient.ERRORS.NOT_CONNECTED); }); it('allows to connect to MQTT', async () => { const message = mqtt.next('topic'); await mqtt.subscribe('topic', {}); await mqtt.publish('topic', { hello: 'mqtt' }); expect(await message).toEqual({ hello: 'mqtt' }); }); it('allows to connect to MQTT with a namespace configured', async () => { mqtt.config.namespace = 'ds'; const message = mqtt.next('topic'); await mqtt.subscribe('topic', {}); await mqtt.publish('topic', { hello: 'mqtt' }); expect(await message).toEqual({ hello: 'mqtt' }); }); it('does noop if already connected', async () => { await mqtt.connect(); const message = mqtt.next('topic'); await mqtt.subscribe('topic', {}); await mqtt.publish('topic', { hello: 'mqtt' }); expect(await message).toEqual({ hello: 'mqtt' }); }); it('allows to disconnect from MQTT', async () => { await mqtt.end(); expect(mqtt._client).toEqual(null); }); it('does noop if already disconnected', async () => { await mqtt.end(); await mqtt.end(); expect(mqtt._client).toEqual(null); }); }); describe('#authenticate', () => { it('authenticates events based on userProperties on client connection', async () => { await mqtt.end(); mqtt = new MQTTClient( { ...app.services.config.mqtt, options: { ...app.services.config.mqtt.options, properties: { userProperties: { authorization: 'token' } }, }, }, { // @ts-ignore logger: { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn(), }, }, ); await mqtt.connect(); let message; mqtt.on( 'topic', mqtt.authenticate( [{ id: 'token', level: 'read', token: 'token' }], (m) => (message = m), ), ); const wait = mqtt.next('topic'); await mqtt.subscribe('topic', {}); await mqtt.publish('topic', { hello: 'mqtt' }); await wait; expect(message).toEqual({ hello: 'mqtt' }); }); it('authenticates events based on userProperties', async () => { let message; mqtt.on( 'topic', mqtt.authenticate( [{ id: 'token', level: 'read', token: 'token' }], (m) => (message = m), ), ); const wait = mqtt.next('topic'); await mqtt.subscribe('topic', {}); await mqtt.publish( 'topic', { hello: 'mqtt' }, { properties: { userProperties: { authorization: 'token' } } }, ); await wait; expect(message).toEqual({ hello: 'mqtt' }); }); it('invokes the handler if the event has valid token', () => { const handler = jest.fn(); const middleware = mqtt.authenticate( [{ id: 'token', level: 'read', token: 'token' }], handler, ); const event = {}; middleware(event, 'topic', { authorization: 'token' }); expect(handler).toHaveBeenCalledWith( event, 'topic', { authorization: 'token' }, undefined, ); }); it('noops on events without authorization token', () => { const handler = jest.fn(); const middleware = mqtt.authenticate( [{ id: 'token', level: 'read', token: 'token' }], handler, ); const event = {}; middleware(event, 'topic', {}); expect(handler).toHaveBeenCalledTimes(0); }); it('noops on events without valid authorization token', () => { const handler = jest.fn(); const middleware = mqtt.authenticate( [{ id: 'token', level: 'read', token: 'token' }], handler, ); const event = {}; middleware(event, 'topic', { authorization: 'invalid' }); expect(handler).toHaveBeenCalledTimes(0); }); }); describe('#mapTopic', () => { it('returns the parsed topic without parameter', () => { expect(mqtt.mapTopic('my/topic', {})).toMatchObject({ original: 'my/topic', regexp: /my\/topic/, topic: 'my/topic', paramNames: [], }); }); it('returns the parsed topic with parameter', () => { expect(mqtt.mapTopic('my/topic/{topic_id}', {})).toMatchObject({ original: 'my/topic/{topic_id}', regexp: /my\/topic\/([^\/]+)/, topic: 'my/topic/+', paramNames: ['topic_id'], }); }); it('returns the parsed topic with multiple parameters', () => { expect( mqtt.mapTopic('my/topic/{topic_id}/{topic_name}', {}), ).toMatchObject({ original: 'my/topic/{topic_id}/{topic_name}', regexp: /my\/topic\/([^\/]+)\/([^\/]+)/, topic: 'my/topic/+/+', paramNames: ['topic_id', 'topic_name'], }); }); }); describe('#getRoute', () => { it('returns null if no route has been registered yet', () => { expect(mqtt.getRoute('unknown')).toEqual(null); }); it('returns null if no route is matching the topic', () => { mqtt.subscribe('my/topic', {}); expect(mqtt.getRoute('unknown')).toEqual(null); }); it('returns the route if one route is matching the topic', () => { mqtt.subscribe('my/topic', {}); expect(mqtt.getRoute('my/topic')).toMatchObject({ original: 'my/topic', regexp: /my\/topic/, topic: 'my/topic', }); }); }); describe('#next', () => { it('triggers a timeout error if no message is received', async () => { let error; try { await mqtt.next('topic'); } catch (err) { error = err; } expect(error.message).toEqual('[mqtt#next] Message timeout'); }); }); describe('#subscribe', () => { it('allows to subscribe OpenAPI like topics', async () => { await mqtt.subscribe('ds/accounts/updated/{account_id}', {}); expect(Array.from(mqtt.topics.keys())).toEqual(['ds/accounts/updated/+']); }); it('calls the subscribe method on the mqtt client instance', async () => { jest.spyOn(mqtt.client, 'subscribe'); await mqtt.subscribe('ds/accounts/updated/{account_id}', {}); expect(mqtt.client.subscribe).toHaveBeenCalledWith( '$share/datastore/ds/accounts/updated/+', ); }); }); describe('#onMessage', () => { it('noops on no matching route', async () => { let message; mqtt.on('topic', (m) => (message = m)); mqtt.client.emit('message', 'topic', JSON.stringify({ hello: 'world' })); expect(message).toEqual(undefined); }); it('emits the message to all subscribers', async () => { let message; mqtt.on('topic', (m) => (message = m)); await mqtt.subscribe('topic', {}); mqtt.client.emit('message', 'topic', JSON.stringify({ hello: 'world' })); expect(message).toEqual({ hello: 'world' }); }); it('emits the message to all subscribers with route params', async () => { let event; mqtt.on( 'topic/{topic_name}', (m, r, h) => (event = { message: m, route: r, headers: h }), ); await mqtt.subscribe('topic/{topic_name}', {}); mqtt.client.emit( 'message', 'topic/paris', JSON.stringify({ hello: 'world' }), ); expect(event).toMatchObject({ headers: {}, route: { params: { topic_name: 'paris' } }, message: { hello: 'world' }, }); }); it('logs an error message in case of invalid message contract', async () => { let message; mqtt.on('topic', (m) => (message = m)); jest.spyOn(mqtt.telemetry.logger, 'error'); await mqtt.subscribe('topic', { type: 'number' }); mqtt.client.emit('message', 'topic', JSON.stringify({ hello: 'world' })); expect(message).toEqual(undefined); expect(mqtt.telemetry.logger.error).toHaveBeenCalledTimes(1); expect(mqtt.telemetry.logger.error.mock.calls[0][1]).toMatchObject({ event: { hello: 'world' }, schema: { type: 'number' }, errors: [ { instancePath: '', keyword: 'type', message: 'must be number', params: { type: 'number' }, schemaPath: '#/type', }, ], }); }); }); });