Files
nest/packages/microservices/test/client/client-mqtt.spec.ts
2024-11-26 09:39:20 +01:00

455 lines
15 KiB
TypeScript

import { expect } from 'chai';
import { EMPTY } from 'rxjs';
import * as sinon from 'sinon';
import { ClientMqtt } from '../../client/client-mqtt';
import { MqttEventsMap } from '../../events/mqtt.events';
import { ReadPacket } from '../../interfaces';
import { MqttRecord } from '../../record-builders';
describe('ClientMqtt', () => {
const test = 'test';
let client: ClientMqtt = new ClientMqtt({});
let untypedClient = client as any;
describe('getRequestPattern', () => {
it(`should leave pattern as it is`, () => {
expect(client.getRequestPattern(test)).to.equal(test);
});
});
describe('getResponsePattern', () => {
it(`should append "/reply" to string`, () => {
const expectedResult = test + '/reply';
expect(client.getResponsePattern(test)).to.equal(expectedResult);
});
});
describe('publish', () => {
const pattern = 'test';
let msg: ReadPacket;
let subscribeSpy: sinon.SinonSpy,
publishSpy: sinon.SinonSpy,
onSpy: sinon.SinonSpy,
removeListenerSpy: sinon.SinonSpy,
unsubscribeSpy: sinon.SinonSpy,
connectSpy: sinon.SinonStub,
assignStub: sinon.SinonStub,
mqttClient;
const id = '1';
beforeEach(() => {
client = new ClientMqtt({});
untypedClient = client as any;
msg = { pattern, data: 'data' };
subscribeSpy = sinon.spy((name, fn) => fn());
publishSpy = sinon.spy();
onSpy = sinon.spy();
removeListenerSpy = sinon.spy();
unsubscribeSpy = sinon.spy();
mqttClient = {
subscribe: subscribeSpy,
on: (type, handler) => (type === 'subscribe' ? handler() : onSpy()),
removeListener: removeListenerSpy,
unsubscribe: unsubscribeSpy,
publish: publishSpy,
addListener: () => ({}),
};
untypedClient.mqttClient = mqttClient;
connectSpy = sinon.stub(client, 'connect');
assignStub = sinon
.stub(client, 'assignPacketId' as any)
.callsFake(packet => Object.assign(packet, { id }));
});
afterEach(() => {
connectSpy.restore();
assignStub.restore();
});
it('should subscribe to response pattern name', async () => {
await client['publish'](msg, () => {});
expect(subscribeSpy.calledWith(`${pattern}/reply`)).to.be.true;
});
it('should publish stringified message to request pattern name', async () => {
await client['publish'](msg, () => {});
expect(publishSpy.calledWith(pattern, JSON.stringify(msg))).to.be.true;
});
it('should add callback to routing map', async () => {
await client['publish'](msg, () => {});
expect(client['routingMap'].has(id)).to.be.true;
});
describe('on error', () => {
beforeEach(() => {
assignStub.callsFake(() => {
throw new Error();
});
});
it('should call callback', () => {
const callback = sinon.spy();
client['publish'](msg, callback);
expect(callback.called).to.be.true;
expect(callback.getCall(0).args[0].err).to.be.instanceof(Error);
});
});
describe('dispose callback', () => {
let getResponsePatternStub: sinon.SinonStub;
let callback: sinon.SinonSpy, subscription;
const channel = 'channel';
beforeEach(async () => {
callback = sinon.spy();
getResponsePatternStub = sinon
.stub(client, 'getResponsePattern')
.callsFake(() => channel);
subscription = await client['publish'](msg, callback);
subscription(channel, JSON.stringify({ isDisposed: true, id }));
});
afterEach(() => {
getResponsePatternStub.restore();
});
it('should unsubscribe to response pattern name', () => {
expect(unsubscribeSpy.calledWith(channel)).to.be.true;
});
it('should remove callback from routing map', () => {
expect(client['routingMap'].has(id)).to.be.false;
});
});
describe('headers', () => {
it('should not generate headers if none are configured', async () => {
await client['publish'](msg, () => {});
expect(publishSpy.getCall(0).args[2]).to.be.undefined;
});
it('should send packet headers', async () => {
const requestHeaders = { '1': '123' };
msg.data = new MqttRecord('data', {
properties: { userProperties: requestHeaders },
});
await client['publish'](msg, () => {});
expect(publishSpy.getCall(0).args[2].properties.userProperties).to.eql(
requestHeaders,
);
});
it('should combine packet and static headers', async () => {
const staticHeaders = { 'client-id': 'some-client-id' };
untypedClient.options.userProperties = staticHeaders;
const requestHeaders = { '1': '123' };
msg.data = new MqttRecord('data', {
properties: { userProperties: requestHeaders },
});
await client['publish'](msg, () => {});
expect(publishSpy.getCall(0).args[2].properties.userProperties).to.eql({
...staticHeaders,
...requestHeaders,
});
});
it('should prefer packet headers over static headers', async () => {
const staticHeaders = { 'client-id': 'some-client-id' };
untypedClient.options.headers = staticHeaders;
const requestHeaders = { 'client-id': 'override-client-id' };
msg.data = new MqttRecord('data', {
properties: { userProperties: requestHeaders },
});
await client['publish'](msg, () => {});
expect(publishSpy.getCall(0).args[2].properties.userProperties).to.eql(
requestHeaders,
);
});
});
});
describe('createResponseCallback', () => {
let callback: sinon.SinonSpy, subscription;
const responseMessage = {
response: 'test',
id: '1',
};
describe('not completed', () => {
beforeEach(async () => {
callback = sinon.spy();
subscription = client.createResponseCallback();
client['routingMap'].set(responseMessage.id, callback);
subscription('channel', Buffer.from(JSON.stringify(responseMessage)));
});
it('should call callback with expected arguments', () => {
expect(
callback.calledWith({
err: undefined,
response: responseMessage.response,
}),
).to.be.true;
});
});
describe('disposed and "id" is correct', () => {
beforeEach(async () => {
callback = sinon.spy();
subscription = client.createResponseCallback();
client['routingMap'].set(responseMessage.id, callback);
subscription(
'channel',
Buffer.from(
JSON.stringify({
...responseMessage,
isDisposed: true,
}),
),
);
});
it('should call callback with dispose param', () => {
expect(callback.called).to.be.true;
expect(
callback.calledWith({
isDisposed: true,
response: responseMessage.response,
err: undefined,
}),
).to.be.true;
});
});
describe('disposed and "id" is incorrect', () => {
beforeEach(async () => {
callback = sinon.spy();
subscription = client.createResponseCallback();
client['routingMap'].set('3', callback);
subscription('channel', Buffer.from(JSON.stringify(responseMessage)));
});
it('should not call callback', () => {
expect(callback.called).to.be.false;
});
});
});
describe('close', () => {
let endSpy: sinon.SinonSpy;
beforeEach(() => {
endSpy = sinon.spy();
untypedClient.mqttClient = { end: endSpy };
});
it('should close "pub" when it is not null', () => {
client.close();
expect(endSpy.called).to.be.true;
});
it('should not close "pub" when it is null', () => {
untypedClient.mqttClient = null;
client.close();
expect(endSpy.called).to.be.false;
});
});
describe('connect', () => {
let createClientStub: sinon.SinonStub;
let registerErrorListenerSpy: sinon.SinonSpy;
let connect$Stub: sinon.SinonStub;
let mergeCloseEvent: sinon.SinonStub;
beforeEach(async () => {
createClientStub = sinon.stub(client, 'createClient').callsFake(
() =>
({
addListener: () => ({}),
removeListener: () => ({}),
on: () => ({}),
}) as any,
);
registerErrorListenerSpy = sinon.spy(client, 'registerErrorListener');
connect$Stub = sinon.stub(client, 'connect$' as any).callsFake(() => ({
subscribe: ({ complete }) => complete(),
pipe() {
return this;
},
}));
mergeCloseEvent = sinon
.stub(client, 'mergeCloseEvent')
.callsFake((_, source) => source);
});
afterEach(() => {
createClientStub.restore();
registerErrorListenerSpy.restore();
connect$Stub.restore();
mergeCloseEvent.restore();
});
describe('when is not connected', () => {
beforeEach(async () => {
client['mqttClient'] = null;
await client.connect();
});
it('should call "registerErrorListener" once', async () => {
expect(registerErrorListenerSpy.called).to.be.true;
});
it('should call "createClient" once', async () => {
expect(createClientStub.called).to.be.true;
});
it('should call "connect$" once', async () => {
expect(connect$Stub.called).to.be.true;
});
});
describe('when is connected', () => {
beforeEach(() => {
client['mqttClient'] = { test: true } as any;
});
it('should not call "createClient"', () => {
expect(createClientStub.called).to.be.false;
});
it('should not call "registerErrorListener"', () => {
expect(registerErrorListenerSpy.called).to.be.false;
});
it('should not call "connect$"', () => {
expect(connect$Stub.called).to.be.false;
});
});
});
describe('mergeCloseEvent', () => {
it('should merge close event', () => {
const error = new Error();
const instance: any = {
on: (ev, callback) => callback(error),
off: () => ({}),
};
client.mergeCloseEvent(instance, EMPTY).subscribe({
error: (err: any) => expect(err).to.be.eql(error),
});
});
});
describe('registerErrorListener', () => {
it('should bind error event handler', () => {
const callback = sinon.stub().callsFake((_, fn) => fn({ code: 'test' }));
const emitter = {
on: callback,
};
client.registerErrorListener(emitter as any);
expect(callback.getCall(0).args[0]).to.be.eql(MqttEventsMap.ERROR);
});
});
describe('registerConnectListener', () => {
it('should bind connect event handler', () => {
const callback = sinon.stub().callsFake((_, fn) => fn({ code: 'test' }));
const emitter = {
on: callback,
};
client.registerConnectListener(emitter as any);
expect(callback.getCall(0).args[0]).to.be.eql(MqttEventsMap.CONNECT);
});
});
describe('registerDisconnectListener', () => {
it('should bind disconnect event handler', () => {
const callback = sinon.stub().callsFake((_, fn) => fn({ code: 'test' }));
const emitter = {
on: callback,
};
client.registerDisconnectListener(emitter as any);
expect(callback.getCall(0).args[0]).to.be.eql(MqttEventsMap.DISCONNECT);
});
});
describe('registerOfflineListener', () => {
it('should bind offline event handler', () => {
const callback = sinon.stub().callsFake((_, fn) => fn({ code: 'test' }));
const emitter = {
on: callback,
};
client.registerOfflineListener(emitter as any);
expect(callback.getCall(0).args[0]).to.be.eql(MqttEventsMap.OFFLINE);
});
});
describe('registerCloseListener', () => {
it('should bind close event handler', () => {
const callback = sinon.stub().callsFake((_, fn) => fn({ code: 'test' }));
const emitter = {
on: callback,
};
client.registerCloseListener(emitter as any);
expect(callback.getCall(0).args[0]).to.be.eql(MqttEventsMap.CLOSE);
});
});
describe('dispatchEvent', () => {
let msg: ReadPacket;
let publishStub: sinon.SinonStub, mqttClient;
beforeEach(() => {
client = new ClientMqtt({});
untypedClient = client as any;
msg = { pattern: 'pattern', data: 'data' };
publishStub = sinon.stub();
mqttClient = {
publish: publishStub,
};
untypedClient.mqttClient = mqttClient;
});
it('should publish packet', async () => {
publishStub.callsFake((a, b, c, d) => d());
await client['dispatchEvent'](msg);
expect(publishStub.called).to.be.true;
});
it('should throw error', async () => {
publishStub.callsFake((a, b, c, d) => d(new Error()));
client['dispatchEvent'](msg).catch(err =>
expect(err).to.be.instanceOf(Error),
);
});
describe('headers', () => {
it('should not generate headers if none are configured', async () => {
publishStub.callsFake((a, b, c, d) => d());
await client['dispatchEvent'](msg);
expect(publishStub.getCall(0).args[2]).to.be.undefined;
});
it('should send packet headers', async () => {
publishStub.callsFake((a, b, c, d) => d());
const requestHeaders = { '1': '123' };
msg.data = new MqttRecord('data', {
properties: { userProperties: requestHeaders },
});
await client['dispatchEvent'](msg);
expect(publishStub.getCall(0).args[2].properties.userProperties).to.eql(
requestHeaders,
);
});
it('should combine packet and static headers', async () => {
publishStub.callsFake((a, b, c, d) => d());
const staticHeaders = { 'client-id': 'some-client-id' };
untypedClient.options.userProperties = staticHeaders;
const requestHeaders = { '1': '123' };
msg.data = new MqttRecord('data', {
properties: { userProperties: requestHeaders },
});
await client['dispatchEvent'](msg);
expect(publishStub.getCall(0).args[2].properties.userProperties).to.eql(
{
...staticHeaders,
...requestHeaders,
},
);
});
it('should prefer packet headers over static headers', async () => {
publishStub.callsFake((a, b, c, d) => d());
const staticHeaders = { 'client-id': 'some-client-id' };
untypedClient.options.headers = staticHeaders;
const requestHeaders = { 'client-id': 'override-client-id' };
msg.data = new MqttRecord('data', {
properties: { userProperties: requestHeaders },
});
await client['dispatchEvent'](msg);
expect(publishStub.getCall(0).args[2].properties.userProperties).to.eql(
requestHeaders,
);
});
});
});
});