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 (392 loc) • 16.3 kB
JavaScript
module.exports = function (RED)
{
"use strict";
const NodeCache = require('node-cache');
function TestMonitorTestRunNode(config)
{
RED.nodes.createNode(this, config);
const node = this;
// Configuration
node.operation = config.operation || "get";
node.testRunId = config.testRunId || "";
node.milestoneId = config.milestoneId || "";
node.name = config.name || "";
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, testRunId, additionalParams = {})
{
const keyData = { operation, testRunId, ...additionalParams };
return `testrun_${JSON.stringify(keyData)}`;
}
// Validate required credentials
if (!node.credentials)
{
updateStatus("Missing credentials", "red");
node.error("TestMonitor credentials configuration is required");
return;
}
// TestRun operations
const testRunOperations = {
get: async (params) =>
{
if (!params.testRunId)
{
throw new Error("Test Run ID is required for get operation");
}
const endpoint = `/test-runs/${params.testRunId}`;
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.milestoneId)
{
queryParams['milestone'] = params.milestoneId;
}
const endpoint = '/test-runs';
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.milestoneId)
{
throw new Error("Milestone ID is required for create operation");
}
if (!params.name)
{
throw new Error("Name is required for create operation");
}
const testRunData = {
milestone_id: params.milestoneId,
name: params.name,
draft: params.draft !== undefined ? params.draft : true,
priority: params.priority,
starts_at: params.starts_at,
ends_at: params.ends_at,
users: params.users || [],
test_cases: params.test_cases || [],
tags: params.tags || [],
subscribers: params.subscribers || [],
test_environment_id: params.test_environment_id,
custom_fields: params.custom_fields || []
};
// Remove null/undefined values
Object.keys(testRunData).forEach(key =>
{
if (testRunData[key] === null || testRunData[key] === undefined)
{
delete testRunData[key];
}
});
const endpoint = '/test-runs';
return new Promise((resolve, reject) =>
{
node.credentials.post(endpoint, testRunData, (error, result) =>
{
if (error)
{
reject(error);
} else
{
resolve(result.data);
}
});
});
},
update: async (params) =>
{
if (!params.testRunId)
{
throw new Error("Test Run ID is required for update operation");
}
const updateData = {};
// Only include fields that are provided
if (params.name !== undefined) updateData.name = params.name;
if (params.milestoneId !== undefined) updateData.milestone_id = params.milestoneId;
if (params.draft !== undefined) updateData.draft = params.draft;
if (params.priority !== undefined) updateData.priority = params.priority;
if (params.starts_at !== undefined) updateData.starts_at = params.starts_at;
if (params.ends_at !== undefined) updateData.ends_at = params.ends_at;
if (params.users !== undefined) updateData.users = params.users;
if (params.test_cases !== undefined) updateData.test_cases = params.test_cases;
if (params.tags !== undefined) updateData.tags = params.tags;
if (params.subscribers !== undefined) updateData.subscribers = params.subscribers;
if (params.test_environment_id !== undefined) updateData.test_environment_id = params.test_environment_id;
if (params.custom_fields !== undefined) updateData.custom_fields = params.custom_fields;
const endpoint = `/test-runs/${params.testRunId}`;
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.testRunId)
{
throw new Error("Test Run ID is required for delete operation");
}
const endpoint = `/test-runs/${params.testRunId}`;
return new Promise((resolve, reject) =>
{
node.credentials.delete(endpoint, (error, result) =>
{
if (error)
{
reject(error);
} else
{
resolve({ success: true, message: `Test run ${params.testRunId} deleted successfully` });
}
});
});
},
getTestCases: async (params) =>
{
if (!params.testRunId)
{
throw new Error("Test Run ID is required for getTestCases operation");
}
const endpoint = `/test-run/${params.testRunId}/test-cases`;
const queryParams = {
project_id: node.credentials.projectId,
'filter[has_test_cases]': true
};
return new Promise((resolve, reject) =>
{
node.credentials.get(endpoint, queryParams, (error, result) =>
{
if (error)
{
reject(error);
} else
{
resolve(result.data);
}
});
});
},
getTestResults: async (params) =>
{
if (!params.testRunId)
{
throw new Error("Test Run ID is required for getTestResults operation");
}
const endpoint = `/test-run/${params.testRunId}/test-results`;
const queryParams = {
project_id: node.credentials.projectId
};
return new Promise((resolve, reject) =>
{
node.credentials.get(endpoint, queryParams, (error, result) =>
{
if (error)
{
reject(error);
} else
{
resolve(result.data);
}
});
});
},
addTestCases: async (params) =>
{
if (!params.testRunId)
{
throw new Error("Test Run ID is required for addTestCases operation");
}
if (!params.test_case_ids || !Array.isArray(params.test_case_ids))
{
throw new Error("test_case_ids array is required for addTestCases operation");
}
const endpoint = `/test-runs/${params.testRunId}`;
const updateData = {
test_case_ids: params.test_case_ids
};
return new Promise((resolve, reject) =>
{
node.credentials.put(endpoint, updateData, (error, result) =>
{
if (error)
{
reject(error);
} else
{
resolve(result.data);
}
});
});
},
openTestRun: async (params) =>
{
if (!params.testRunId)
{
throw new Error("Test Run ID is required for openTestRun operation");
}
const endpoint = `/test-runs/${params.testRunId}`;
const updateData = { draft: false };
return new Promise((resolve, reject) =>
{
node.credentials.put(endpoint, updateData, (error, result) =>
{
if (error)
{
reject(error);
} else
{
resolve(result.data);
}
});
});
},
closeTestRun: async (params) =>
{
if (!params.testRunId)
{
throw new Error("Test Run ID is required for closeTestRun operation");
}
const endpoint = `/test-runs/${params.testRunId}`;
const updateData = { draft: true };
return new Promise((resolve, reject) =>
{
node.credentials.put(endpoint, updateData, (error, result) =>
{
if (error)
{
reject(error);
} else
{
resolve(result.data);
}
});
});
}
};
// 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 = {
testRunId: inputData?.testRunId || msg.testRunId || node.testRunId,
operation: inputData?.operation || msg.operation || node.operation,
milestoneId: inputData?.milestoneId || msg.milestoneId || node.milestoneId,
name: inputData?.name || msg.name || node.name,
draft: inputData?.draft !== undefined ? inputData.draft : msg.draft,
priority: inputData?.priority !== undefined ? inputData.priority : msg.priority,
starts_at: inputData?.starts_at || msg.starts_at,
ends_at: inputData?.ends_at || msg.ends_at,
users: inputData?.users || msg.users,
test_cases: inputData?.test_cases || msg.test_cases,
test_case_ids: inputData?.test_case_ids || msg.test_case_ids,
tags: inputData?.tags || msg.tags,
subscribers: inputData?.subscribers || msg.subscribers,
test_environment_id: inputData?.test_environment_id || msg.test_environment_id,
custom_fields: inputData?.custom_fields || msg.custom_fields
};
// Check cache for read operations
const cacheKey = generateCacheKey(params.operation, params.testRunId, params);
if (cache && ['get', 'list', 'getTestCases', 'getTestResults'].includes(params.operation))
{
const cachedResult = cache.get(cacheKey);
if (cachedResult)
{
updateStatus("From cache", "green");
msg.payload = cachedResult;
send(msg);
done();
return;
}
}
// Validate operation
if (!testRunOperations[params.operation])
{
throw new Error(`Unknown operation: ${params.operation}`);
}
// Execute operation
const result = await testRunOperations[params.operation](params);
// Cache read operations
if (cache && ['get', 'list', 'getTestCases', 'getTestResults'].includes(params.operation))
{
cache.set(cacheKey, result);
}
// Set result in message
msg.payload = result;
msg.testMonitor = {
operation: params.operation,
testRunId: params.testRunId,
milestoneId: params.milestoneId,
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-testrun", TestMonitorTestRunNode);
};