UNPKG

node-red-contrib-testmonitor

Version:

A comprehensive Node-RED wrapper for TestMonitor API providing test case management, test runs, milestones, and test result operations for test automation workflows

442 lines (389 loc) 16.4 kB
module.exports = function (RED) { "use strict"; const NodeCache = require('node-cache'); const axios = require('axios'); const fs = require('fs'); function TestMonitorTestResultNode(config) { RED.nodes.createNode(this, config); const node = this; // Configuration node.operation = config.operation || "get"; node.testResultId = config.testResultId || ""; node.testCaseId = config.testCaseId || ""; node.testRunId = config.testRunId || ""; node.statusId = config.statusId || 1; // Default to passed node.field = config.field || "payload"; node.fieldType = config.fieldType || "msg"; node.enableCaching = config.enableCaching !== false; node.cacheDuration = parseInt(config.cacheDuration) || 300; // 5 minutes default node.timeout = parseInt(config.timeout) || 30000; node.credentials = RED.nodes.getNode(config.credentials); // Initialize cache const cache = node.enableCaching ? new NodeCache({ stdTTL: node.cacheDuration, checkperiod: 60 }) : null; // Status tracking function updateStatus(text, color = "grey") { node.status({ fill: color, shape: "dot", text: text }); } // Generate cache key function generateCacheKey(operation, testResultId, additionalParams = {}) { const keyData = { operation, testResultId, ...additionalParams }; return `testresult_${JSON.stringify(keyData)}`; } // Validate required credentials if (!node.credentials) { updateStatus("Missing credentials", "red"); node.error("TestMonitor credentials configuration is required"); return; } // Test Result Status IDs (common TestMonitor values) const testResultStatuses = { 'passed': 1, 'failed': 3, 'blocked': 2, 'not_tested': 4, 'retest': 5 }; // TestResult operations const testResultOperations = { get: async (params) => { if (!params.testResultId) { throw new Error("Test Result ID is required for get operation"); } const endpoint = `/test-results/${params.testResultId}`; return new Promise((resolve, reject) => { node.credentials.get(endpoint, {}, (error, result) => { if (error) { reject(error); } else { resolve(result.data); } }); }); }, list: async (params) => { const queryParams = { project_id: node.credentials.projectId }; if (params.testRunId) { queryParams['test_run_id'] = params.testRunId; } if (params.testCaseId) { queryParams['test_case_id'] = params.testCaseId; } const endpoint = '/test-results'; return new Promise((resolve, reject) => { node.credentials.get(endpoint, queryParams, (error, result) => { if (error) { reject(error); } else { resolve(result.data); } }); }); }, create: async (params) => { if (!params.testCaseId) { throw new Error("Test Case ID is required for create operation"); } if (!params.testRunId) { throw new Error("Test Run ID is required for create operation"); } // Convert status string to ID if needed let statusId = params.statusId || params.test_result_status_id || 1; if (typeof statusId === 'string') { statusId = testResultStatuses[statusId.toLowerCase()] || 1; } const testResultData = { test_case_id: params.testCaseId, test_run_id: params.testRunId, test_result_status_id: statusId, description: params.description || "Test result", draft: params.draft !== undefined ? params.draft : false }; // Remove null/undefined values Object.keys(testResultData).forEach(key => { if (testResultData[key] === null || testResultData[key] === undefined) { delete testResultData[key]; } }); const endpoint = '/test-results'; return new Promise((resolve, reject) => { node.credentials.post(endpoint, testResultData, (error, result) => { if (error) { reject(error); } else { resolve(result.data); } }); }); }, update: async (params) => { if (!params.testResultId) { throw new Error("Test Result ID is required for update operation"); } const updateData = {}; // Only include fields that are provided if (params.testCaseId !== undefined) updateData.test_case_id = params.testCaseId; if (params.testRunId !== undefined) updateData.test_run_id = params.testRunId; if (params.description !== undefined) updateData.description = params.description; if (params.draft !== undefined) updateData.draft = params.draft; if (params.viewed !== undefined) updateData.viewed = params.viewed; // Handle status ID conversion if (params.statusId !== undefined || params.test_result_status_id !== undefined) { let statusId = params.statusId || params.test_result_status_id; if (typeof statusId === 'string') { statusId = testResultStatuses[statusId.toLowerCase()] || statusId; } updateData.test_result_status_id = statusId; } const endpoint = `/test-results/${params.testResultId}`; return new Promise((resolve, reject) => { node.credentials.put(endpoint, updateData, (error, result) => { if (error) { reject(error); } else { resolve(result.data); } }); }); }, delete: async (params) => { if (!params.testResultId) { throw new Error("Test Result ID is required for delete operation"); } const endpoint = `/test-results/${params.testResultId}`; return new Promise((resolve, reject) => { node.credentials.delete(endpoint, (error, result) => { if (error) { reject(error); } else { resolve({ success: true, message: `Test result ${params.testResultId} deleted successfully` }); } }); }); }, addComment: async (params) => { if (!params.testResultId) { throw new Error("Test Result ID is required for addComment operation"); } if (!params.comment) { throw new Error("Comment is required for addComment operation"); } const endpoint = `/test-result/${params.testResultId}/comments`; const commentData = { message: params.comment }; return new Promise((resolve, reject) => { node.credentials.post(endpoint, commentData, (error, result) => { if (error) { reject(error); } else { resolve(result.data); } }); }); }, addAttachment: async (params) => { if (!params.testResultId) { throw new Error("Test Result ID is required for addAttachment operation"); } if (!params.filePath) { throw new Error("File path is required for addAttachment operation"); } // Check if file exists if (!fs.existsSync(params.filePath)) { throw new Error(`File not found: ${params.filePath}`); } try { const FormData = require('form-data'); const form = new FormData(); form.append('file', fs.createReadStream(params.filePath)); const axiosInstance = node.credentials.getAxiosInstance(); const endpoint = `/test-result/${params.testResultId}/attachments`; const response = await axiosInstance.post(endpoint, form, { headers: { ...form.getHeaders(), 'Authorization': node.credentials.getAuthHeaders()['Authorization'] } }); return response.data; } catch (error) { throw new Error(`Failed to upload attachment: ${error.message}`); } }, ensureExists: async (params) => { if (!params.testCaseId || !params.testRunId) { throw new Error("Test Case ID and Test Run ID are required for ensureExists operation"); } // First try to find existing result try { const existingResults = await testResultOperations.list({ testRunId: params.testRunId, testCaseId: params.testCaseId }); // Look for matching test case in the results const existingResult = existingResults.find(result => result.test_case_id === params.testCaseId ); if (existingResult) { // Update existing result if provided if (params.statusId || params.description) { return await testResultOperations.update({ testResultId: existingResult.id, statusId: params.statusId, description: params.description, draft: params.draft }); } return existingResult; } } catch (error) { // If listing fails, continue to create new } // Create new result if not found return await testResultOperations.create(params); } }; // Main message handler node.on('input', async function (msg, send, done) { send = send || function () { node.send.apply(node, arguments); }; try { updateStatus("Processing...", "blue"); // Extract parameters from message or node configuration const inputData = RED.util.getMessageProperty(msg, node.field); const params = { testResultId: inputData?.testResultId || msg.testResultId || node.testResultId, operation: inputData?.operation || msg.operation || node.operation, testCaseId: inputData?.testCaseId || msg.testCaseId || node.testCaseId, testRunId: inputData?.testRunId || msg.testRunId || node.testRunId, statusId: inputData?.statusId || msg.statusId || inputData?.test_result_status_id || msg.test_result_status_id || node.statusId, description: inputData?.description || msg.description, draft: inputData?.draft !== undefined ? inputData.draft : msg.draft, viewed: inputData?.viewed !== undefined ? inputData.viewed : msg.viewed, comment: inputData?.comment || msg.comment, filePath: inputData?.filePath || msg.filePath }; // Convert status string to ID if needed if (typeof params.statusId === 'string') { params.statusId = testResultStatuses[params.statusId.toLowerCase()] || params.statusId; } // Check cache for read operations const cacheKey = generateCacheKey(params.operation, params.testResultId, params); if (cache && ['get', 'list'].includes(params.operation)) { const cachedResult = cache.get(cacheKey); if (cachedResult) { updateStatus("From cache", "green"); msg.payload = cachedResult; send(msg); done(); return; } } // Validate operation if (!testResultOperations[params.operation]) { throw new Error(`Unknown operation: ${params.operation}`); } // Execute operation const result = await testResultOperations[params.operation](params); // Cache read operations if (cache && ['get', 'list'].includes(params.operation)) { cache.set(cacheKey, result); } // Set result in message msg.payload = result; msg.testMonitor = { operation: params.operation, testResultId: params.testResultId, testCaseId: params.testCaseId, testRunId: params.testRunId, timestamp: new Date().toISOString() }; updateStatus(`${params.operation} completed`, "green"); send(msg); done(); } catch (error) { updateStatus(`Error: ${error.message}`, "red"); node.error(error.message, msg); done(error); } }); updateStatus("Ready"); } RED.nodes.registerType("testmonitor-testresult", TestMonitorTestResultNode); };