UNPKG

@cuppet/core

Version:

Core testing framework components for Cuppet - BDD framework based on Cucumber and Puppeteer

319 lines (286 loc) 11.7 kB
const mqtt = require('mqtt'); const config = require('config'); /** * MqttManager class for managing MQTT client lifecycle * Follows the same pattern as BrowserManager and AppiumManager */ class MqttManager { constructor(brokerUrl = null, options = {}) { this.client = null; this.brokerUrl = brokerUrl || (config.has('mqtt.brokerUrl') ? config.get('mqtt.brokerUrl') : 'mqtt://localhost:1883'); this.options = this.prepareOptions(options); this.messageBuffer = new Map(); // Store messages by topic this.subscriptions = new Set(); // Track active subscriptions } /** * Prepare MQTT connection options from config or provided options * @param {Object} customOptions - Custom options to override config * @returns {Object} - MQTT connection options */ prepareOptions(customOptions) { const defaultOptions = { clientId: config.has('mqtt.clientId') ? config.get('mqtt.clientId') : `cuppet-test-${Math.random().toString(16).slice(2, 8)}`, clean: config.has('mqtt.cleanSession') ? config.get('mqtt.cleanSession') : true, connectTimeout: config.has('mqtt.connectTimeout') ? config.get('mqtt.connectTimeout') : 5000, keepalive: config.has('mqtt.keepalive') ? config.get('mqtt.keepalive') : 60, }; // Add username and password if provided in config if (config.has('mqtt.username')) { defaultOptions.username = config.get('mqtt.username'); } if (config.has('mqtt.password')) { defaultOptions.password = config.get('mqtt.password'); } // Merge with custom options return { ...defaultOptions, ...customOptions }; } /** * Initialize MQTT client and connect to broker * @returns {Promise<void>} */ async initialize() { return new Promise((resolve, reject) => { try { console.log(`Connecting to MQTT broker: ${this.brokerUrl}`); this.client = mqtt.connect(this.brokerUrl, this.options); let settled = false; // Guard against multiple resolve/reject calls // Centralized cleanup function const cleanup = () => { clearTimeout(timeoutId); this.client.removeListener('connect', onConnect); this.client.removeListener('error', onError); }; // Declare timeout first const timeoutId = setTimeout(() => { if (!settled) { settled = true; cleanup(); this.client.end(true); reject(new Error(`MQTT connection timeout after ${this.options.connectTimeout}ms`)); } }, this.options.connectTimeout); const onConnect = () => { if (!settled) { settled = true; cleanup(); console.log(`Successfully connected to MQTT broker: ${this.brokerUrl}`); this.setupMessageHandler(); resolve(); } }; const onError = (error) => { if (!settled) { settled = true; cleanup(); console.error(`MQTT connection error: ${error.message}`); this.client.end(true); reject(error); } }; this.client.once('connect', onConnect); this.client.once('error', onError); } catch (error) { if (this.client) { this.client.end(true); } reject(error); } }); } /** * Setup message handler to buffer incoming messages * @private */ setupMessageHandler() { this.client.on('message', (topic, message) => { console.log(`Received message on topic: ${topic}`); // Store message in buffer for the specific topic if (!this.messageBuffer.has(topic)) { this.messageBuffer.set(topic, []); } const messageData = { topic: topic, message: message.toString(), timestamp: Date.now(), raw: message, }; this.messageBuffer.get(topic).push(messageData); }); } /** * Subscribe to a topic or topics * @param {string|string[]} topic - Topic or array of topics to subscribe to * @param {Object} options - Subscription options (qos, etc.) * @returns {Promise<void>} */ async subscribe(topic, options = { qos: 0 }) { return new Promise((resolve, reject) => { this.client.subscribe(topic, options, (error, granted) => { if (error) { console.error(`Failed to subscribe to ${topic}: ${error.message}`); reject(error); } else { const topics = Array.isArray(topic) ? topic : [topic]; topics.forEach((t) => this.subscriptions.add(t)); console.log(`Successfully subscribed to: ${JSON.stringify(granted)}`); resolve(granted); } }); }); } /** * Unsubscribe from a topic or topics * @param {string|string[]} topic - Topic or array of topics to unsubscribe from * @returns {Promise<void>} */ async unsubscribe(topic) { return new Promise((resolve, reject) => { this.client.unsubscribe(topic, (error) => { if (error) { console.error(`Failed to unsubscribe from ${topic}: ${error.message}`); reject(error); } else { const topics = Array.isArray(topic) ? topic : [topic]; topics.forEach((t) => this.subscriptions.delete(t)); console.log(`Successfully unsubscribed from: ${topic}`); resolve(); } }); }); } /** * Publish a message to a topic * @param {string} topic - Topic to publish to * @param {string|Buffer} message - Message to publish * @param {Object} options - Publish options (qos, retain, etc.) * @returns {Promise<void>} */ async publish(topic, message, options = { qos: 0, retain: false }) { return new Promise((resolve, reject) => { this.client.publish(topic, message, options, (error) => { if (error) { console.error(`Failed to publish to ${topic}: ${error.message}`); reject(error); } else { console.log(`Successfully published to: ${topic}`); resolve(); } }); }); } /** * Get messages from buffer for a specific topic * @param {string} topic - Topic to get messages for * @returns {Array} - Array of messages */ getMessages(topic) { return this.messageBuffer.get(topic) || []; } /** * Get the latest message from buffer for a specific topic * @param {string} topic - Topic to get latest message for * @returns {Object|null} - Latest message or null */ getLatestMessage(topic) { const messages = this.getMessages(topic); return messages.length > 0 ? messages[messages.length - 1] : null; } /** * Clear message buffer for a specific topic or all topics * @param {string|null} topic - Topic to clear or null to clear all * @returns {<void>} */ clearMessageBuffer(topic = null) { if (topic) { this.messageBuffer.delete(topic); console.log(`Cleared message buffer for topic: ${topic}`); } else { this.messageBuffer.clear(); console.log('Cleared all message buffers'); } } /** * Wait for a message on a specific topic with timeout * @param {string} topic - Topic to wait for message on * @param {number} timeoutSeconds - Timeout in seconds * @returns {Promise<Object>} - Resolves with message when received */ async waitForMessage(topic, timeoutSeconds = 10) { const timeoutMs = timeoutSeconds * 1000; const startTime = Date.now(); return new Promise((resolve, reject) => { const checkInterval = setInterval(() => { const latestMessage = this.getLatestMessage(topic); if (latestMessage && latestMessage.timestamp >= startTime) { clearInterval(checkInterval); resolve(latestMessage); } else if (Date.now() - startTime > timeoutMs) { clearInterval(checkInterval); reject(new Error(`Timeout waiting for message on topic: ${topic} after ${timeoutSeconds} seconds`)); } }, 100); // Check every 100ms }); } /** Wait for specific message on a topic with timeout. IMPORTANT: Message is position,case and whitespace sensitive. * @param {string} topic - Topic to wait for message on * @param {string} message - Message to wait for * @param {number} timeoutSeconds - Timeout in seconds * @returns {Promise<Object>} - Resolves with message when received */ async waitForSpecificMessage(topic, message, timeoutSeconds = 10) { const timeoutMs = timeoutSeconds * 1000; const startTime = Date.now(); let latestMessageString = null; while (Date.now() - startTime < timeoutMs) { const latestMessage = this.getLatestMessage(topic); if (latestMessage) { latestMessageString = JSON.stringify(latestMessage.message); const expectedMessageString = JSON.stringify(message); if (latestMessageString === expectedMessageString) { return latestMessage; } } await new Promise((resolve) => setTimeout(resolve, 100)); } throw new Error( `Timeout waiting for message on topic: ${topic} after ${timeoutSeconds} seconds. Latest message received: ${latestMessageString}` ); } /** * Check if client is connected * @returns {boolean} */ isConnected() { return this.client && this.client.connected; } /** * Stop the MQTT client and cleanup * @returns {Promise<void>} */ async stop() { return new Promise((resolve) => { if (this.client && this.client.connected) { console.log('Disconnecting from MQTT broker...'); // Unsubscribe from all topics if (this.subscriptions.size > 0) { const topicsArray = Array.from(this.subscriptions); this.client.unsubscribe(topicsArray); } // Clear buffers this.messageBuffer.clear(); this.subscriptions.clear(); // End connection this.client.end(false, () => { console.log('MQTT client disconnected'); resolve(); }); } else { resolve(); } }); } } module.exports = MqttManager;