diff --git a/package-lock.json b/package-lock.json index a8bc90a..ad1ea8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@mojaloop/ml-testing-toolkit-client-lib", - "version": "1.12.0", + "version": "1.12.1-snapshot.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@mojaloop/ml-testing-toolkit-client-lib", - "version": "1.12.0", + "version": "1.12.1-snapshot.13", "license": "Apache-2.0", "dependencies": { "@mojaloop/central-services-logger": "11.10.1", @@ -15,9 +15,9 @@ "@slack/webhook": "7.0.6", "atob": "2.1.2", "aws-sdk": "2.1692.0", - "axios": "1.12.2", + "axios": "1.13.2", "cli-table3": "0.6.5", - "commander": "14.0.1", + "commander": "14.0.2", "dotenv": "17.2.3", "fs": "0.0.1-security", "lodash": "4.17.21", @@ -36,7 +36,7 @@ "audit-ci": "7.1.0", "jest": "30.2.0", "jest-junit": "16.0.0", - "npm-check-updates": "19.1.1", + "npm-check-updates": "19.1.2", "nyc": "17.1.0", "parse-strings-in-object": "1.6.0", "pre-commit": "1.2.2", @@ -2095,6 +2095,17 @@ } } }, + "node_modules/@mojaloop/central-services-shared/node_modules/axios": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/@mojaloop/central-services-shared/node_modules/dotenv": { "version": "17.2.2", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.2.tgz", @@ -2333,6 +2344,17 @@ "jws": "4.0.0" } }, + "node_modules/@mojaloop/sdk-standard-components/node_modules/axios": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -3897,9 +3919,9 @@ } }, "node_modules/axios": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", - "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -4722,9 +4744,9 @@ } }, "node_modules/commander": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.1.tgz", - "integrity": "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==", + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", "license": "MIT", "engines": { "node": ">=20" @@ -11353,9 +11375,9 @@ } }, "node_modules/npm-check-updates": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-19.1.1.tgz", - "integrity": "sha512-vy/uNbaK6Xfj/QzM8OXeALZak67E0uHjUlbdT1YGy4bdj0xlBU6AVd+8bscY8vlDpyzL6Y7mxcrX8kzEDeEpNg==", + "version": "19.1.2", + "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-19.1.2.tgz", + "integrity": "sha512-FNeFCVgPOj0fz89hOpGtxP2rnnRHR7hD2E8qNU8SMWfkyDZXA/xpgjsL3UMLSo3F/K13QvJDnbxPngulNDDo/g==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index d637887..012d779 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@mojaloop/ml-testing-toolkit-client-lib", "description": "Testing Toolkit Client Library", - "version": "1.12.0", + "version": "1.12.1-snapshot.13", "license": "Apache-2.0", "author": "Vijaya Kumar Guthi, ModusBox Inc. ", "contributors": [ @@ -75,9 +75,9 @@ "@slack/webhook": "7.0.6", "atob": "2.1.2", "aws-sdk": "2.1692.0", - "axios": "1.12.2", + "axios": "1.13.2", "cli-table3": "0.6.5", - "commander": "14.0.1", + "commander": "14.0.2", "dotenv": "17.2.3", "fs": "0.0.1-security", "lodash": "4.17.21", @@ -93,7 +93,7 @@ "audit-ci": "7.1.0", "jest": "30.2.0", "jest-junit": "16.0.0", - "npm-check-updates": "19.1.1", + "npm-check-updates": "19.1.2", "nyc": "17.1.0", "parse-strings-in-object": "1.6.0", "pre-commit": "1.2.2", diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 0000000..954720f --- /dev/null +++ b/src/constants.js @@ -0,0 +1,14 @@ +const { env } = require('node:process') + +const TESTS_EXECUTION_TIMEOUT = parseInt(env.TESTS_EXECUTION_TIMEOUT, 10) || 1000 * 60 * 15 + +const EXIT_CODES = Object.freeze({ + success: 0, + failure: 1, + timeout: 2 +}) + +module.exports = { + EXIT_CODES, + TESTS_EXECUTION_TIMEOUT +} diff --git a/src/extras/slack-broadcast.js b/src/extras/slack-broadcast.js index 507046c..49c12de 100644 --- a/src/extras/slack-broadcast.js +++ b/src/extras/slack-broadcast.js @@ -44,15 +44,17 @@ const millisecondsToTime = (milliseconds) => { */ const generateSlackBlocks = (progress, reportURL) => { const slackBlocks = [] + const failedTestCases = [] let totalAssertionsCount = 0 let totalPassedAssertionsCount = 0 let totalRequestsCount = 0 - const failedTestCases = [] + progress.test_cases.forEach(testCase => { // console.log(fStr.yellow(testCase.name)) totalRequestsCount += testCase.requests.length let testCaseAssertionsCount = 0 let testCasePassedAssertionsCount = 0 + testCase.requests.forEach(req => { const passedAssertionsCount = req.request.tests && req.request.tests.passedAssertionsCount ? req.request.tests.passedAssertionsCount : 0 const assertionsCount = req.request.tests && req.request.tests.assertions && req.request.tests.assertions.length ? req.request.tests.assertions.length : 0 @@ -61,6 +63,7 @@ const generateSlackBlocks = (progress, reportURL) => { testCaseAssertionsCount += assertionsCount testCasePassedAssertionsCount += passedAssertionsCount }) + if (testCaseAssertionsCount !== testCasePassedAssertionsCount) { failedTestCases.push({ name: testCase.name, @@ -83,6 +86,8 @@ const generateSlackBlocks = (progress, reportURL) => { // totalAssertionsCount = totalPassedAssertionsCount // failedTestCases.length = 0 + const isPassed = (totalAssertionsCount === totalPassedAssertionsCount) && (totalPassedAssertionsCount > 0) + if (config.briefSummaryPrefix) { const top5FailedTestCases = failedTestCases.sort((a, b) => b.failedAssertions - a.failedAssertions).slice(0, 5) return [{ @@ -90,7 +95,7 @@ const generateSlackBlocks = (progress, reportURL) => { elements: [{ type: 'rich_text_section', elements: [ - { type: 'text', text: `${totalAssertionsCount === totalPassedAssertionsCount ? '✅' : '⚠️'}${config.briefSummaryPrefix || ''} ` }, + { type: 'text', text: `${isPassed ? '✅' : (progress.isTimeout ? '⌛' : '⚠️')}${config.briefSummaryPrefix || ''} ` }, reportURL ? { type: 'link', url: reportURL, text: config.reportName } : { type: 'text', text: config.reportName }, { type: 'text', text: ' tests: ' }, { type: 'text', text: String(progress.test_cases.length), style: { code: true } }, @@ -138,7 +143,7 @@ const generateSlackBlocks = (progress, reportURL) => { summaryText += '>Runtime duration: *' + `${progress.runtimeInformation.runDurationMs} ms` + '*\n' const additionalParams = {} - if (totalAssertionsCount === totalPassedAssertionsCount) { + if (isPassed) { if (config.slackPassedImage) { additionalParams.accessory = { type: 'image', @@ -155,6 +160,7 @@ const generateSlackBlocks = (progress, reportURL) => { } } } + let extramSummaryText = '' if (config.extraSummaryInformation) { const extraSummaryInformationArr = config.extraSummaryInformation.split(',') @@ -171,6 +177,7 @@ const generateSlackBlocks = (progress, reportURL) => { }, ...additionalParams }) + if (reportURL) { slackBlocks.push({ type: 'section', @@ -183,6 +190,7 @@ const generateSlackBlocks = (progress, reportURL) => { slackBlocks.push({ type: 'divider' }) + return slackBlocks } @@ -208,6 +216,20 @@ const sendSlackNotification = async (progress, reportURL = 'http://localhost/') } } +/* istanbul ignore next */ +const sendTimeoutSlackNotification = async (progress, reportURL = 'http://localhost/') => { + const text = 'Timeout Tests Report' + const blocks = generateSlackBlocks(progress, reportURL) + + if (config.slackWebhookUrl) { + await sendWebhook(config.slackWebhookUrl, text, blocks) + } + + if (config.slackWebhookUrlForFailed) { + await sendWebhook(config.slackWebhookUrlForFailed, text, blocks) + } +} + const sendWebhook = async (url, text, blocks) => { const webhook = new IncomingWebhook(url) try { @@ -232,6 +254,7 @@ const needToNotifyFailed = (webhookUrl, progress) => { module.exports = { sendSlackNotification, + sendTimeoutSlackNotification, sendWebhook, needToNotifyFailed } diff --git a/src/modes/outbound.js b/src/modes/outbound.js index 555c89a..d19560b 100644 --- a/src/modes/outbound.js +++ b/src/modes/outbound.js @@ -37,6 +37,7 @@ const slackBroadcast = require('../extras/slack-broadcast') const releaseCd = require('../extras/release-cd') const TemplateGenerator = require('../utils/templateGenerator') const { TraceHeaderUtils } = require('@mojaloop/ml-testing-toolkit-shared-lib') +const { TESTS_EXECUTION_TIMEOUT } = require('../constants') const totalProgress = { totalTestCases: 0, @@ -188,6 +189,7 @@ const sendTemplate = async (sessionId) => { * @property {string} status * @property {Object} totalResult * @property {Object} saveReportStatus + * @property {Boolean} [isTimeout] * @property {unknown} [otherFields] - see ml-testing-toolkit repo. */ @@ -212,6 +214,7 @@ const sendTemplate = async (sessionId) => { */ const handleIncomingProgress = async (progress) => { const config = objectStore.get('config') + if (progress.status === 'FINISHED') { let passed try { @@ -256,8 +259,38 @@ const handleIncomingProgress = async (progress) => { } } +// istanbul ignore next +const handleTimeout = async () => { + try { + console.log('Tests execution timed out....') + const config = objectStore.get('config') + const now = Date.now() + + const timeoutReport = { + name: config.reportName || determineTemplateName(config.inputFiles.split(',')), + runtimeInformation: { + testReportId: `timeout-${now}`, + startedTime: new Date(now - TESTS_EXECUTION_TIMEOUT).toUTCString(), + completedTime: new Date(now).toUTCString(), + runDurationMs: TESTS_EXECUTION_TIMEOUT, + totalAssertions: totalProgress.totalAssertions, + totalPassedAssertions: totalProgress.passedAssertions + }, + test_cases: [], // think if we need to reconstruct passed test cases + status: 'TERMINATED', + isTimeout: true + } + console.log(fStr.yellow(`⚠️ Summary (timeout): ${totalProgress.passedAssertions}/${totalProgress.totalAssertions} assertions passed`)) + + await slackBroadcast.sendTimeoutSlackNotification(timeoutReport) + } catch (err) { + console.log(fStr.red(`Error on handling tests timeout: ${err?.message}`)) + } +} + module.exports = { sendTemplate, handleIncomingProgress, + handleTimeout, determineTemplateName } diff --git a/src/router.js b/src/router.js index 9dbf806..f43935f 100644 --- a/src/router.js +++ b/src/router.js @@ -26,12 +26,12 @@ * Georgi Logodazhki (Original Author) -------------- ******/ -const fs = require('fs') + +const fs = require('node:fs') const _ = require('lodash') -const objectStore = require('./objectStore') const { TraceHeaderUtils } = require('@mojaloop/ml-testing-toolkit-shared-lib') - -const TESTS_EXECUTION_TIMEOUT = 1000 * 60 * 15 // 15min timout +const { EXIT_CODES, TESTS_EXECUTION_TIMEOUT } = require('./constants') +const objectStore = require('./objectStore') const cli = (commanderOptions) => { const configFile = { @@ -87,10 +87,11 @@ const cli = (commanderOptions) => { // Generate a session ID const sessionId = TraceHeaderUtils.generateSessionId() require('./utils/listeners').outbound(sessionId) - require('./modes/outbound').sendTemplate(sessionId) - setTimeout(() => { - console.log('Tests execution timed out....') - process.exit(1) + const { sendTemplate, handleTimeout } = require('./modes/outbound') + sendTemplate(sessionId) + setTimeout(async () => { + await handleTimeout() + process.exit(EXIT_CODES.timeout) }, TESTS_EXECUTION_TIMEOUT) } else { console.log('error: required option \'-e, --environment-file \' not specified') diff --git a/src/utils/report.js b/src/utils/report.js index cee0b98..68af56b 100644 --- a/src/utils/report.js +++ b/src/utils/report.js @@ -48,6 +48,7 @@ const report = async (data, reportType) => { const config = objectStore.get('config') let reportData let reportFilename + switch (config.reportFormat) { case 'none': return returnInfo @@ -86,6 +87,7 @@ const report = async (data, reportType) => { console.log('reportFormat is not supported') return } + if (config.reportTarget) { const reportTargetRe = /(.*):\/\/(.*)/g const reportTargetArr = reportTargetRe.exec(config.reportTarget) diff --git a/test/unit/router.test.js b/test/unit/router.test.js index 34b7363..7ed1b3c 100644 --- a/test/unit/router.test.js +++ b/test/unit/router.test.js @@ -32,6 +32,7 @@ const spyExit = jest.spyOn(process, 'exit').mockImplementation(() => {}) const { cli } = require('../../src/router') +const { EXIT_CODES, TESTS_EXECUTION_TIMEOUT } = require('../../src/constants'); jest.mock('../../src/utils/listeners') jest.mock('../../src/modes/outbound') @@ -82,8 +83,8 @@ describe('Cli client', () => { expect(() => { cli(config) }).not.toThrow() - jest.advanceTimersByTime(1000 * 60 * 15) - expect(spyExit).toHaveBeenCalledWith(1) + await jest.advanceTimersByTime(TESTS_EXECUTION_TIMEOUT) + expect(spyExit).toHaveBeenCalledWith(EXIT_CODES.timeout) }) it('when mode is outbound and inputFile was not provided should not throw an error', async () => { const config = {