matterbridge-roborock-vacuum-plugin
Version:
Matterbridge Roborock Vacuum Plugin
584 lines • 28.7 kB
JavaScript
import mqtt from 'mqtt';
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { MessageContext, RequestMessage } from '../../../../roborockCommunication/models/index.js';
import { MQTTClient } from '../../../../roborockCommunication/mqtt/mqttClient.js';
import { V1ResponseBroadcaster } from '../../../../roborockCommunication/routing/listeners/v1ResponseBroadcaster.js';
import { V1PendingResponseTracker } from '../../../../roborockCommunication/routing/services/v1PendingResponseTracker.js';
import { asPartial, asType, createMockLogger } from '../../../helpers/testUtils.js';
function makeUserdata() {
return asPartial({ rriot: { r: { m: 'mqtt://broker.example' }, u: 'testuser', k: 'key123', s: 'secret' } });
}
function makeLogger() {
return asPartial({ debug: vi.fn(), info: vi.fn(), notice: vi.fn(), error: vi.fn() });
}
describe('MQTTClient (additional)', () => {
let userdata;
let context;
let logger;
let responseBroadcaster;
let responseTracker;
beforeEach(() => {
vi.restoreAllMocks();
userdata = makeUserdata();
context = new MessageContext(userdata);
logger = makeLogger();
responseTracker = new V1PendingResponseTracker(logger);
responseBroadcaster = new V1ResponseBroadcaster(responseTracker, logger);
});
it('isReady/isConnected reflect internal state', () => {
const client = new MQTTClient(logger, context, userdata, responseBroadcaster, responseTracker);
expect(client.isConnected()).toBe(false);
expect(client.isReady()).toBe(false);
client['connected'] = true;
client['mqttClient'] = asPartial({});
expect(client.isConnected()).toBe(true);
expect(client.isReady()).toBe(true);
// isReady should be false if mqttClient is undefined even when connected is true
client['mqttClient'] = undefined;
expect(client.isReady()).toBe(false);
});
it('connect calls mqtt.connect and registers event handlers', () => {
const mockMqttClient = { on: vi.fn(), end: vi.fn(), reconnect: vi.fn(), publish: vi.fn(), subscribe: vi.fn() };
// prevent keepAlive timer from running
const spyConnect = vi.spyOn(mqtt, 'connect').mockImplementation(() => asType(mockMqttClient));
const client = new MQTTClient(logger, context, userdata, responseBroadcaster, responseTracker);
client['keepConnectionAlive'] = vi.fn();
client.connect();
expect(spyConnect).toHaveBeenCalledWith(userdata.rriot.r.m, expect.objectContaining({
clientId: expect.any(String),
username: expect.any(String),
password: expect.any(String),
}));
// ensure handlers were attached
expect(mockMqttClient.on).toHaveBeenCalled();
});
it('sendInternal logs error when not connected', async () => {
const client = new MQTTClient(logger, context, userdata, responseBroadcaster, responseTracker);
const req = new RequestMessage({ method: 'test' });
await asType(client).sendInternal('duid-1', req);
expect(logger.error).toHaveBeenCalled();
});
it('sendInternal publishes when connected', async () => {
const mockMqttClient = { on: vi.fn(), end: vi.fn(), reconnect: vi.fn(), publish: vi.fn(), subscribe: vi.fn() };
vi.spyOn(mqtt, 'connect').mockImplementation(() => asType(mockMqttClient));
const client = new MQTTClient(logger, context, userdata, responseBroadcaster, responseTracker);
client['keepConnectionAlive'] = vi.fn();
client['mqttClient'] = mockMqttClient;
client['connected'] = true;
Object.defineProperty(client, 'serializer', {
value: asPartial({ serialize: vi.fn().mockReturnValue({ buffer: Buffer.from('payload') }) }),
writable: true,
});
const req = new RequestMessage({ method: 'test' });
await asType(client).sendInternal('my-duid', req);
expect(mockMqttClient.publish).toHaveBeenCalledTimes(1);
const topicArg = mockMqttClient.publish.mock.calls[0][0];
expect(topicArg).toContain(userdata.rriot.u);
expect(topicArg).toContain('my-duid');
});
afterEach(() => {
vi.restoreAllMocks();
});
});
vi.mock('mqtt', async () => {
const actual = await vi.importActual('mqtt');
globalThis.mockConnect = vi.fn();
return {
...actual,
connect: globalThis.mockConnect,
};
});
describe('MQTTClient', () => {
let logger;
let context;
let userdata;
let client;
let serializer;
let deserializer;
let responseBroadcaster;
let responseTracker;
const createdClients = [];
beforeEach(() => {
logger = createMockLogger();
context = { getMQTTProtocolVersion: vi.fn().mockReturnValue('1.0') };
userdata = {
rriot: {
u: 'user',
k: 'key',
s: 'secret',
r: { m: 'mqtt://broker' },
},
};
serializer = { serialize: vi.fn(() => ({ buffer: Buffer.from('msg') })) };
deserializer = { deserialize: vi.fn(() => 'deserialized') };
responseTracker = new V1PendingResponseTracker(logger);
responseBroadcaster = new V1ResponseBroadcaster(responseTracker, logger);
// Mock mqtt client instance
client = {
on: vi.fn(),
end: vi.fn(),
publish: vi.fn(),
subscribe: vi.fn(),
reconnect: vi.fn(),
};
globalThis.mockConnect.mockReturnValue(client);
});
function createMQTTClient() {
class TestMQTTClient extends MQTTClient {
constructor() {
super(logger, context, userdata, responseBroadcaster, responseTracker);
}
}
const mqttClient = new TestMQTTClient();
Object.defineProperty(mqttClient, 'connectionBroadcaster', {
value: asPartial({
onConnected: vi.fn(),
onDisconnected: vi.fn(),
onOffline: vi.fn(),
onClose: vi.fn(),
onError: vi.fn(),
onReconnect: vi.fn(),
}),
writable: true,
});
Object.defineProperty(mqttClient, 'responseBroadcaster', {
value: asPartial({
onMessage: vi.fn(),
tryResolve: vi.fn(),
}),
writable: true,
});
Object.defineProperty(mqttClient, 'deserializer', {
value: deserializer,
writable: true,
});
Object.defineProperty(mqttClient, 'serializer', {
value: serializer,
writable: true,
});
createdClients.push(mqttClient);
return mqttClient;
}
afterEach(async () => {
// Clean up any MQTT clients to prevent timer leaks
for (const mqttClient of createdClients) {
try {
if (mqttClient.keepConnectionAliveInterval) {
clearInterval(mqttClient.keepConnectionAliveInterval);
}
if (mqttClient.mqttClient) {
// Force the client to null to prevent reconnect attempts
const mc = mqttClient.mqttClient;
mqttClient.mqttClient = null;
if (mc && typeof mc.end === 'function') {
mc.end(true); // Force close
}
}
}
catch (_e) {
// Ignore cleanup errors
}
}
createdClients.length = 0;
vi.clearAllMocks();
vi.clearAllTimers();
vi.useRealTimers();
});
afterAll(() => {
// Final cleanup
vi.clearAllTimers();
});
it('should generate username and password in constructor', () => {
const mqttClient = createMQTTClient();
expect(mqttClient['mqttUsername']).toBe('c6d6afb9');
expect(mqttClient['mqttPassword']).toBe('938f62d6603bde9c');
});
it('should not connect if already connected', () => {
const mqttClient = createMQTTClient();
mqttClient['mqttClient'] = client;
mqttClient.connect();
expect(globalThis.mockConnect).not.toHaveBeenCalled();
});
it('should disconnect if connected', async () => {
const mqttClient = createMQTTClient();
mqttClient['mqttClient'] = client;
mqttClient['connected'] = true;
await mqttClient.disconnect();
expect(client.end).toHaveBeenCalled();
});
it('should not disconnect if not connected', async () => {
const mqttClient = createMQTTClient();
mqttClient['mqttClient'] = undefined;
mqttClient['connected'] = false;
await mqttClient.disconnect();
expect(client.end).not.toHaveBeenCalled();
});
it('should log error if disconnect throws', async () => {
const mqttClient = createMQTTClient();
mqttClient['mqttClient'] = asPartial({
end: vi.fn(() => {
throw new Error('fail');
}),
});
mqttClient['connected'] = true;
await mqttClient.disconnect();
expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('[MQTTClient] client failed to disconnect with error:'));
});
it('should publish message if connected', async () => {
const mqttClient = createMQTTClient();
mqttClient['mqttClient'] = client;
mqttClient['connected'] = true;
const request = {
toMqttRequest: vi.fn().mockReturnThis(),
method: 'test',
version: '1.0',
};
await mqttClient['sendInternal']('duid1', asType(request));
expect(serializer.serialize).toHaveBeenCalledWith('duid1', expect.objectContaining({ version: '1.0' }));
expect(client.publish).toHaveBeenCalledWith('rr/m/i/user/c6d6afb9/duid1', Buffer.from('msg'), { qos: 1 });
});
it('should log error if send called when not connected', async () => {
const mqttClient = createMQTTClient();
mqttClient['mqttClient'] = undefined;
mqttClient['connected'] = false;
const request = { toMqttRequest: vi.fn(), method: 'test' };
await mqttClient.send('duid1', asType(request));
expect(logger.error).toHaveBeenCalled();
expect(client.publish).not.toHaveBeenCalled();
});
it('onConnect should set connected, call onConnected, and subscribeToQueue', async () => {
const mqttClient = createMQTTClient();
mqttClient['mqttClient'] = client;
mqttClient['subscribeToQueue'] = vi.fn();
await mqttClient['onConnect'](asType({}));
expect(mqttClient['connected']).toBe(true);
expect(mqttClient['connectionBroadcaster'].onConnected).toHaveBeenCalled();
expect(mqttClient['subscribeToQueue']).toHaveBeenCalled();
});
it('subscribeToQueue should call client.subscribe with correct topic', () => {
const mqttClient = createMQTTClient();
mqttClient['mqttClient'] = client;
mqttClient['connected'] = true;
mqttClient['subscribeToQueue']();
expect(client.subscribe).toHaveBeenCalledWith('rr/m/o/user/c6d6afb9/#', expect.any(Function));
});
it('onSubscribe should log error and call onDisconnected if error', async () => {
const mqttClient = createMQTTClient();
await mqttClient['onSubscribe'](new Error('fail'), undefined);
expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('[MQTTClient] Failed to subscribe: Error: fail'));
expect(mqttClient['connected']).toBe(false);
expect(mqttClient['connectionBroadcaster'].onDisconnected).toHaveBeenCalled();
});
it('onSubscribe should do nothing if no error', async () => {
const mqttClient = createMQTTClient();
await mqttClient['onSubscribe'](null, undefined);
expect(logger.error).not.toHaveBeenCalled();
expect(mqttClient['connectionBroadcaster'].onDisconnected).not.toHaveBeenCalled();
});
it('onDisconnect should call onDisconnected', async () => {
const mqttClient = createMQTTClient();
await mqttClient['onDisconnect']();
expect(mqttClient['connectionBroadcaster'].onDisconnected).toHaveBeenCalled();
});
it('onError should log error and call onError', async () => {
const mqttClient = createMQTTClient();
mqttClient['connected'] = true;
await mqttClient['onError'](new Error('fail'));
expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('MQTT connection error'));
expect(mqttClient['connectionBroadcaster'].onError).toHaveBeenCalledWith('mqtt-c6d6afb9', expect.stringContaining('MQTT connection error'));
});
it('onReconnect should NOT call subscribeToQueue (subscribe happens in onConnect)', () => {
const mqttClient = createMQTTClient();
mqttClient['subscribeToQueue'] = vi.fn();
mqttClient['onReconnect']();
// subscribeToQueue should NOT be called in onReconnect - it's called by onConnect
expect(mqttClient['subscribeToQueue']).not.toHaveBeenCalled();
});
it('onMessage should call deserializer and chainedMessageListener.onMessage if message', async () => {
const mqttClient = createMQTTClient();
await mqttClient['onMessage']('rr/m/o/user/c6d6afb9/duid1', Buffer.from('msg'));
expect(deserializer.deserialize).toHaveBeenCalledWith('duid1', Buffer.from('msg'), 'MQTTClient');
expect(mqttClient['responseBroadcaster'].onMessage).toHaveBeenCalledWith('deserialized');
});
it('onMessage should log notice if message is falsy', async () => {
const mqttClient = createMQTTClient();
await mqttClient['onMessage']('topic', asType(null));
expect(logger.notice).toHaveBeenCalledWith(expect.stringContaining('received empty message'));
});
it('onMessage should log error if deserializer throws', async () => {
const mqttClient = createMQTTClient();
deserializer.deserialize.mockImplementation(() => {
throw new Error('fail');
});
await mqttClient['onMessage']('topic/duid', Buffer.from('msg'));
expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('unable to process message'));
});
it('connect should setup mqtt client with event handlers', () => {
const mqttClient = createMQTTClient();
// Reset the mock before calling connect
globalThis.mockConnect.mockClear();
mqttClient.connect();
// Verify the client was created and event handlers were registered
expect(mqttClient['mqttClient']).toBeDefined();
// Get the actual client instance
const actualClient = mqttClient['mqttClient'];
// Verify event handlers were registered on the actual client
// Since we're using the real mqtt library, just verify the client exists and is set up
expect(actualClient).toBeTruthy();
expect(typeof actualClient?.on).toBe('function');
expect(typeof actualClient?.publish).toBe('function');
expect(typeof actualClient?.subscribe).toBe('function');
});
it('keepConnectionAlive should setup interval that reconnects client', () => {
vi.useFakeTimers();
const mqttClient = createMQTTClient();
mqttClient['mqttClient'] = client;
mqttClient['connected'] = true;
mqttClient['keepConnectionAlive']();
expect(mqttClient['keepConnectionAliveInterval']).toBeDefined();
// Fast-forward time by 60 minutes to trigger the interval callback
vi.advanceTimersByTime(60 * 60 * 1000);
expect(logger.debug).toHaveBeenCalledWith('[MQTTClient] Force reconnecting to ensure fresh connection');
// Clean up
clearInterval(mqttClient['keepConnectionAliveInterval']);
vi.useRealTimers();
});
it('keepConnectionAlive should call connect if mqttClient is undefined', () => {
vi.useFakeTimers();
const mqttClient = createMQTTClient();
const connectSpy = vi.spyOn(mqttClient, 'connect');
mqttClient['mqttClient'] = undefined;
mqttClient['connected'] = false;
mqttClient['keepConnectionAlive']();
// Fast-forward time by 60 minutes to trigger the interval callback
vi.advanceTimersByTime(60 * 60 * 1000);
expect(connectSpy).toHaveBeenCalled();
// Clean up
clearInterval(mqttClient['keepConnectionAliveInterval']);
vi.useRealTimers();
});
it('keepConnectionAlive should clear existing interval before setting new one', () => {
vi.useFakeTimers();
const mqttClient = createMQTTClient();
const clearIntervalSpy = vi.spyOn(global, 'clearInterval');
// Set initial interval
mqttClient['keepConnectionAlive']();
const firstInterval = mqttClient['keepConnectionAliveInterval'];
// Call again to clear and reset
mqttClient['keepConnectionAlive']();
expect(clearIntervalSpy).toHaveBeenCalledWith(firstInterval);
// Clean up
clearInterval(mqttClient['keepConnectionAliveInterval']);
vi.useRealTimers();
});
it('disconnect should return early if not connected', async () => {
const mqttClient = createMQTTClient();
mqttClient['mqttClient'] = client;
mqttClient['connected'] = false;
await mqttClient.disconnect();
expect(client.end).not.toHaveBeenCalled();
});
it('onClose should call onClose only if connected', async () => {
const mqttClient = createMQTTClient();
mqttClient['connected'] = true;
await mqttClient['onClose']();
expect(mqttClient['connectionBroadcaster'].onClose).toHaveBeenCalledWith('mqtt-c6d6afb9');
expect(mqttClient['connected']).toBe(false);
});
it('onClose should not call onClose if already disconnected', async () => {
const mqttClient = createMQTTClient();
mqttClient['connected'] = false;
await mqttClient['onClose']();
expect(mqttClient['connectionBroadcaster'].onClose).not.toHaveBeenCalled();
});
it('should broadcast onClose and onConnected when MQTT disconnects unexpectedly (real disconnect)', async () => {
const mqttClient = createMQTTClient();
mqttClient['mqttClient'] = client;
mqttClient['connected'] = true;
mqttClient['isForceReconnecting'] = false;
mqttClient['subscribeToQueue'] = vi.fn();
await mqttClient['onClose']();
expect(mqttClient['connectionBroadcaster'].onClose).toHaveBeenCalledWith('mqtt-c6d6afb9');
expect(mqttClient['connected']).toBe(false);
await mqttClient['onConnect'](asType({}));
expect(mqttClient['connectionBroadcaster'].onConnected).toHaveBeenCalledWith('mqtt-c6d6afb9');
});
it('should NOT broadcast onClose or onConnected when keepConnectionAlive forces reconnect', async () => {
vi.useFakeTimers();
const mqttClient = createMQTTClient();
mqttClient['mqttClient'] = client;
mqttClient['connected'] = true;
mqttClient['subscribeToQueue'] = vi.fn();
mqttClient['keepConnectionAlive']();
vi.advanceTimersByTime(60 * 60 * 1000);
expect(mqttClient['isForceReconnecting']).toBe(true);
await mqttClient['onClose']();
expect(mqttClient['connectionBroadcaster'].onClose).not.toHaveBeenCalled();
await mqttClient['onConnect'](asType({}));
expect(mqttClient['connectionBroadcaster'].onConnected).not.toHaveBeenCalled();
expect(mqttClient['isForceReconnecting']).toBe(false);
clearInterval(mqttClient['keepConnectionAliveInterval']);
vi.useRealTimers();
});
it('onOffline should set connected to false and call onOffline', async () => {
const mqttClient = createMQTTClient();
mqttClient['connected'] = true;
await mqttClient['onOffline']();
expect(mqttClient['connected']).toBe(false);
expect(mqttClient['connectionBroadcaster'].onOffline).toHaveBeenCalledWith('mqtt-c6d6afb9');
});
it('onReconnect should call onReconnect on connectionBroadcaster', () => {
const mqttClient = createMQTTClient();
mqttClient['onReconnect']();
expect(mqttClient['connectionBroadcaster'].onReconnect).toHaveBeenCalledWith('mqtt-c6d6afb9', 'Attempting to reconnect to MQTT broker');
});
it('onError should translate error code 5 to "Connection refused: Not authorized"', async () => {
const mqttClient = createMQTTClient();
await mqttClient['onError'](asPartial({ code: 5 }));
expect(logger.error).toHaveBeenCalledWith('MQTT connection error: Connection refused: Not authorized');
expect(mqttClient['connectionBroadcaster'].onError).toHaveBeenCalledWith('mqtt-c6d6afb9', 'MQTT connection error: Connection refused: Not authorized');
});
it('onError should increment consecutiveAuthErrors for auth errors', async () => {
const mqttClient = createMQTTClient();
expect(mqttClient['consecutiveAuthErrors']).toBe(0);
await mqttClient['onError'](asPartial({ code: 5 }));
expect(mqttClient['consecutiveAuthErrors']).toBe(1);
await mqttClient['onError'](asPartial({ code: 5 }));
expect(mqttClient['consecutiveAuthErrors']).toBe(2);
});
it('onError should trigger backoff after 5 consecutive auth errors', async () => {
vi.useFakeTimers();
const mqttClient = createMQTTClient();
mqttClient['terminateConnection'] = vi.fn();
const connectSpy = vi.spyOn(mqttClient, 'connect');
// Trigger 5 consecutive auth errors
for (let i = 0; i < 5; i++) {
await mqttClient['onError'](asPartial({ code: 5 }));
}
expect(logger.error).toHaveBeenCalledWith('[MQTTClient] Auth error threshold reached, entering 60-minute backoff');
expect(mqttClient['terminateConnection']).toHaveBeenCalled();
expect(mqttClient['authErrorBackoffTimeout']).toBeDefined();
// Fast-forward time by 60 minutes
vi.advanceTimersByTime(60 * 60 * 1000);
expect(connectSpy).toHaveBeenCalled();
expect(mqttClient['consecutiveAuthErrors']).toBe(0);
expect(logger.info).toHaveBeenCalledWith('[MQTTClient] Auth error backoff period ended, attempting reconnection');
vi.useRealTimers();
});
it('onError should not trigger backoff for fewer than 5 auth errors', async () => {
const mqttClient = createMQTTClient();
mqttClient['terminateConnection'] = vi.fn();
// Trigger 4 auth errors (below threshold)
for (let i = 0; i < 4; i++) {
await mqttClient['onError'](asPartial({ code: 5 }));
}
expect(mqttClient['terminateConnection']).not.toHaveBeenCalled();
expect(mqttClient['authErrorBackoffTimeout']).toBeUndefined();
});
it('onError should reset consecutiveAuthErrors on successful connection', async () => {
const mqttClient = createMQTTClient();
mqttClient['consecutiveAuthErrors'] = 3;
await mqttClient['onConnect'](asType({}));
expect(mqttClient['consecutiveAuthErrors']).toBe(0);
});
it('terminateConnection should clear intervals and close client', () => {
const mqttClient = createMQTTClient();
mqttClient['mqttClient'] = client;
mqttClient['connected'] = true;
mqttClient['keepConnectionAliveInterval'] = setInterval(() => { }, 1000);
mqttClient['authErrorBackoffTimeout'] = setTimeout(() => { }, 1000);
mqttClient['terminateConnection']();
expect(mqttClient['keepConnectionAliveInterval']).toBeUndefined();
expect(mqttClient['authErrorBackoffTimeout']).toBeUndefined();
expect(client.end).toHaveBeenCalledWith(true);
expect(mqttClient['mqttClient']).toBeUndefined();
expect(mqttClient['connected']).toBe(false);
});
it('terminateConnection should handle missing timers gracefully', () => {
const mqttClient = createMQTTClient();
mqttClient['mqttClient'] = client;
mqttClient['connected'] = true;
mqttClient['keepConnectionAliveInterval'] = undefined;
mqttClient['authErrorBackoffTimeout'] = undefined;
mqttClient['terminateConnection']();
expect(client.end).toHaveBeenCalledWith(true);
expect(mqttClient['connected']).toBe(false);
});
it('terminateConnection should handle missing mqttClient', () => {
const mqttClient = createMQTTClient();
mqttClient['mqttClient'] = undefined;
mqttClient['connected'] = true;
mqttClient['terminateConnection']();
expect(mqttClient['connected']).toBe(false);
});
it('keepConnectionAlive should reconnect when client does not exists', () => {
vi.useFakeTimers();
const mqttClient = createMQTTClient();
const connectSpy = vi.spyOn(mqttClient, 'connect');
const originalClient = mqttClient['mqttClient'];
mqttClient['mqttClient'] = undefined;
mqttClient['connected'] = false;
mqttClient['keepConnectionAlive']();
// Fast-forward time by 60 minutes to trigger the interval callback
vi.advanceTimersByTime(60 * 60 * 1000);
expect(connectSpy).toHaveBeenCalled();
expect(logger.info).toHaveBeenCalledWith('[MQTTClient] Force reconnecting to ensure fresh connection (new connection)');
// Clean up
clearInterval(mqttClient['keepConnectionAliveInterval']);
vi.useRealTimers();
});
it('onConnect should return early if result is falsy', async () => {
const mqttClient = createMQTTClient();
mqttClient['subscribeToQueue'] = vi.fn();
await mqttClient['onConnect'](asType(null));
expect(logger.error).toHaveBeenCalledWith('[MQTTClient] onConnect called with no result');
expect(mqttClient['connected']).toBe(false);
expect(mqttClient['subscribeToQueue']).not.toHaveBeenCalled();
});
it('subscribeToQueue should log error when not connected', () => {
const mqttClient = createMQTTClient();
mqttClient['mqttClient'] = client;
mqttClient['connected'] = false;
mqttClient['subscribeToQueue']();
expect(logger.error).toHaveBeenCalledWith('[MQTTClient] cannot subscribe, client not connected');
expect(client.subscribe).not.toHaveBeenCalled();
});
it('subscribeToQueue should log error when mqttClient is undefined', () => {
const mqttClient = createMQTTClient();
mqttClient['mqttClient'] = undefined;
mqttClient['connected'] = true;
mqttClient['subscribeToQueue']();
expect(logger.error).toHaveBeenCalledWith('[MQTTClient] cannot subscribe, client not connected');
});
it('disconnect should clear keepConnectionAliveInterval', async () => {
const mqttClient = createMQTTClient();
mqttClient['mqttClient'] = client;
mqttClient['connected'] = true;
mqttClient['keepConnectionAliveInterval'] = setInterval(() => { }, 1000);
await mqttClient.disconnect();
expect(mqttClient['keepConnectionAliveInterval']).toBeUndefined();
});
it('disconnect should clear authErrorBackoffTimeout', async () => {
const mqttClient = createMQTTClient();
mqttClient['mqttClient'] = client;
mqttClient['connected'] = true;
mqttClient['authErrorBackoffTimeout'] = setTimeout(() => { }, 1000);
await mqttClient.disconnect();
expect(mqttClient['authErrorBackoffTimeout']).toBeUndefined();
});
it('onMessage should call both onResponse and onMessage on responseBroadcaster', async () => {
const mqttClient = createMQTTClient();
await mqttClient['onMessage']('rr/m/o/user/c6d6afb9/duid1', Buffer.from('msg'));
expect(mqttClient['responseBroadcaster'].tryResolve).toHaveBeenCalledWith('deserialized');
expect(mqttClient['responseBroadcaster'].onMessage).toHaveBeenCalledWith('deserialized');
});
it('onMessage should handle non-Error objects', async () => {
const mqttClient = createMQTTClient();
deserializer.deserialize.mockImplementation(() => {
throw Error('string error');
});
await mqttClient['onMessage']('topic/duid', Buffer.from('msg'));
expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('unable to process message'));
});
});
//# sourceMappingURL=MQTTClient.test.js.map