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
JavaScript
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);
};