From 0e4de0ac01a5d998f670507c7974cd0bc70ec816 Mon Sep 17 00:00:00 2001 From: Anudeep Date: Sat, 17 Aug 2024 15:41:44 +0530 Subject: [PATCH] feat: send errors to targets (#233) --- package-lock.json | 32 ++--- package.json | 2 +- src/commands/publish.command.js | 33 ++++- src/targets/chat.js | 24 ++++ src/targets/index.js | 10 +- src/targets/slack.js | 38 ++++++ src/targets/teams.js | 33 +++++ test/handle-errors.spec.js | 206 ++++++++++++++++++++++++++++++++ test/mocks/chat.mock.js | 31 +++++ test/mocks/slack.mock.js | 39 +++++- test/mocks/teams.mock.js | 44 +++++++ 11 files changed, 471 insertions(+), 21 deletions(-) create mode 100644 test/handle-errors.spec.js diff --git a/package-lock.json b/package-lock.json index 09bd288..9636089 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "testbeats", - "version": "2.0.8", + "version": "2.0.9", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "testbeats", - "version": "2.0.8", + "version": "2.0.9", "license": "ISC", "dependencies": { "async-retry": "^1.3.3", @@ -738,9 +738,9 @@ } }, "node_modules/fast-xml-parser": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.0.tgz", - "integrity": "sha512-kLY3jFlwIYwBNDojclKsNAC12sfD6NwW74QB2CoNGPvtVxjliYehVunB3HYyNi+n4Tt1dAcgwYvmKF/Z18flqg==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", + "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", "funding": [ { "type": "github", @@ -2440,11 +2440,11 @@ } }, "node_modules/test-results-parser": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/test-results-parser/-/test-results-parser-0.1.19.tgz", - "integrity": "sha512-Q3iAZWRz/DvSS+ecPys29WUsMRybsURwYvkuqdCcmYPHUBcyUPrKDQDGRLSFPMI8b44XfDHh67+RVTFg7mOgQQ==", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/test-results-parser/-/test-results-parser-0.2.3.tgz", + "integrity": "sha512-mTiaR6v2g3vODj1m93mZAUaz03MSNlTn5KKZDEdxJGdi/ydgugLsW8UvsNiyxTmA2AOFoO3iVrUED0PYk7nDFg==", "dependencies": { - "fast-xml-parser": "^4.3.2", + "fast-xml-parser": "^4.4.1", "globrex": "^0.1.2", "html-escaper": "^3.0.3", "totalist": "^3.0.1" @@ -3096,9 +3096,9 @@ } }, "fast-xml-parser": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.0.tgz", - "integrity": "sha512-kLY3jFlwIYwBNDojclKsNAC12sfD6NwW74QB2CoNGPvtVxjliYehVunB3HYyNi+n4Tt1dAcgwYvmKF/Z18flqg==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", + "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", "requires": { "strnum": "^1.0.5" } @@ -4163,11 +4163,11 @@ } }, "test-results-parser": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/test-results-parser/-/test-results-parser-0.1.19.tgz", - "integrity": "sha512-Q3iAZWRz/DvSS+ecPys29WUsMRybsURwYvkuqdCcmYPHUBcyUPrKDQDGRLSFPMI8b44XfDHh67+RVTFg7mOgQQ==", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/test-results-parser/-/test-results-parser-0.2.3.tgz", + "integrity": "sha512-mTiaR6v2g3vODj1m93mZAUaz03MSNlTn5KKZDEdxJGdi/ydgugLsW8UvsNiyxTmA2AOFoO3iVrUED0PYk7nDFg==", "requires": { - "fast-xml-parser": "^4.3.2", + "fast-xml-parser": "^4.4.1", "globrex": "^0.1.2", "html-escaper": "^3.0.3", "totalist": "^3.0.1" diff --git a/package.json b/package.json index 148bcae..237ea74 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "testbeats", - "version": "2.0.8", + "version": "2.0.9", "description": "Publish test results to Microsoft Teams, Google Chat, Slack and InfluxDB", "main": "src/index.js", "types": "./src/index.d.ts", diff --git a/src/commands/publish.command.js b/src/commands/publish.command.js index 03c06af..54feac5 100644 --- a/src/commands/publish.command.js +++ b/src/commands/publish.command.js @@ -18,6 +18,7 @@ class PublishCommand { */ constructor(opts) { this.opts = opts; + this.errors = []; } async publish() { @@ -31,7 +32,7 @@ class PublishCommand { this.#validateConfig(); this.#processResults(); await this.#publishResults(); - logger.info('✅ Results published successfully!'); + await this.#publishErrors(); } #validateEnvDetails() { @@ -184,13 +185,24 @@ class PublishCommand { } else if (result_options.type === 'jmeter') { this.results.push(prp.parse(result_options)); } else { - this.results.push(trp.parse(result_options)); + const { result, errors } = trp.parseV2(result_options); + if (result) { + this.results.push(result); + } + if (errors) { + this.errors = this.errors.concat(errors); + } } } } } async #publishResults() { + if (!this.results.length) { + logger.warn('⚠️ No results to publish'); + return; + } + for (const config of this.configs) { for (let i = 0; i < this.results.length; i++) { const result = this.results[i]; @@ -207,6 +219,23 @@ class PublishCommand { } } } + logger.info('✅ Results published successfully!'); + } + + async #publishErrors() { + if (!this.errors.length) { + logger.debug('⚠️ No errors to publish'); + return; + } + logger.info('🛑 Publishing errors...'); + for (const config of this.configs) { + if (config.targets) { + for (const target of config.targets) { + await target_manager.handleErrors({ target, errors: this.errors }); + } + } + } + throw new Error(this.errors.join('\n')); } } diff --git a/src/targets/chat.js b/src/targets/chat.js index 0c7c59d..3f85695 100644 --- a/src/targets/chat.js +++ b/src/targets/chat.js @@ -238,7 +238,31 @@ const default_inputs = { ] }; +async function handleErrors({ target, errors }) { + let title = 'Error: Reporting Test Results'; + title = target.inputs.title ? title + ' - ' + target.inputs.title : title; + + const root_payload = getRootPayload(); + const payload = root_payload.cards[0]; + + payload.sections.push({ + "widgets": [ + { + "textParagraph": { + text: `${title}

Errors:
${errors.join('
')}` + } + } + ] + }); + + return request.post({ + url: target.inputs.url, + body: root_payload + }); +} + module.exports = { run, + handleErrors, default_options } \ No newline at end of file diff --git a/src/targets/index.js b/src/targets/index.js index f344d88..365651b 100644 --- a/src/targets/index.js +++ b/src/targets/index.js @@ -34,6 +34,14 @@ async function run(target, result) { } } +async function handleErrors({ target, errors }) { + const target_runner = getTargetRunner(target); + if (target_runner.handleErrors) { + await target_runner.handleErrors({ target, errors }); + } +} + module.exports = { - run + run, + handleErrors } \ No newline at end of file diff --git a/src/targets/slack.js b/src/targets/slack.js index 78e504b..fc75cfe 100644 --- a/src/targets/slack.js +++ b/src/targets/slack.js @@ -264,7 +264,45 @@ const default_inputs = { ] } +async function handleErrors({ target, errors }) { + let title = 'Error: Reporting Test Results'; + title = target.inputs.title ? title + ' - ' + target.inputs.title : title; + + const blocks = []; + + blocks.push({ + "type": "section", + "text": { + "type": "mrkdwn", + "text": title + } + }); + blocks.push({ + "type": "section", + "text": { + "type": "mrkdwn", + "text": errors.join('\n\n') + } + }); + + const payload = { + "attachments": [ + { + "color": COLORS.DANGER, + "blocks": blocks, + "fallback": title, + } + ] + }; + + return request.post({ + url: target.inputs.url, + body: payload + }); +} + module.exports = { run, + handleErrors, default_options } diff --git a/src/targets/teams.js b/src/targets/teams.js index 293145f..9bd9255 100644 --- a/src/targets/teams.js +++ b/src/targets/teams.js @@ -300,7 +300,40 @@ const default_inputs = { ] } +async function handleErrors({ target, errors }) { + let title = 'Error: Reporting Test Results'; + title = target.inputs.title ? title + ' - ' + target.inputs.title : title; + + const root_payload = getRootPayload(); + const payload = getMainPayload(target); + + payload.body.push({ + "type": "TextBlock", + "text": title, + "size": "medium", + "weight": "bolder", + "wrap": true + }); + + payload.body.push({ + "type": "TextBlock", + "text": errors.join('\n'), + "size": "medium", + "weight": "bolder", + "wrap": true + }); + + setRootPayload(root_payload, payload); + + + return request.post({ + url: target.inputs.url, + body: root_payload + }); +} + module.exports = { run, + handleErrors, default_options } diff --git a/test/handle-errors.spec.js b/test/handle-errors.spec.js new file mode 100644 index 0000000..915b239 --- /dev/null +++ b/test/handle-errors.spec.js @@ -0,0 +1,206 @@ +const { mock } = require('pactum'); +const assert = require('assert'); +const { publish } = require('../src'); + +describe('handle errors', () => { + + afterEach(() => { + mock.clearInteractions(); + }); + + it('should send errors to chat', async () => { + const id = mock.addInteraction('post errors to chat'); + let err; + try { + await publish({ + config: { + "targets": [ + { + "name": "chat", + "inputs": { + "url": "http://localhost:9393/message" + } + } + ], + "results": [ + { + "type": "testng", + "files": [ + "test/data/testng/invalid.xml" + ] + } + ] + } + }); + } catch (e) { + err = e; + } + assert.equal(mock.getInteraction(id).exercised, true); + assert.ok(err.toString().includes('invalid.xml')); + }); + + it('should send errors to teams', async () => { + const id = mock.addInteraction('post errors to teams'); + let err; + try { + await publish({ + config: { + "targets": [ + { + "name": "teams", + "inputs": { + "url": "http://localhost:9393/message" + } + } + ], + "results": [ + { + "type": "testng", + "files": [ + "test/data/testng/invalid.xml" + ] + } + ] + } + }); + } catch (e) { + err = e; + } + assert.equal(mock.getInteraction(id).exercised, true); + assert.ok(err.toString().includes('invalid.xml')); + }); + + it('should send errors to slack', async () => { + const id = mock.addInteraction('post errors to slack'); + let err; + try { + await publish({ + config: { + "targets": [ + { + "name": "slack", + "inputs": { + "url": "http://localhost:9393/message" + } + } + ], + "results": [ + { + "type": "testng", + "files": [ + "test/data/testng/invalid.xml" + ] + } + ] + } + }); + } catch (e) { + err = e; + } + assert.equal(mock.getInteraction(id).exercised, true); + assert.ok(err.toString().includes('invalid.xml')); + }); + + it('should send results and errors to chat', async () => { + const id1 = mock.addInteraction('post test-summary to chat'); + const id2 = mock.addInteraction('post errors to chat'); + let err; + try { + await publish({ + config: { + "targets": [ + { + "name": "chat", + "inputs": { + "url": "http://localhost:9393/message" + } + } + ], + "results": [ + { + "type": "testng", + "files": [ + "test/data/testng/single-suite.xml", + "test/data/testng/invalid.xml" + ] + } + ] + } + }); + } catch (e) { + err = e; + } + assert.equal(mock.getInteraction(id1).exercised, true); + assert.equal(mock.getInteraction(id2).exercised, true); + assert.ok(err.toString().includes('invalid.xml')); + }); + + it('should send results and errors to teams', async () => { + const id1 = mock.addInteraction('post test-summary to teams'); + const id2 = mock.addInteraction('post errors to teams'); + let err; + try { + await publish({ + config: { + "targets": [ + { + "name": "teams", + "inputs": { + "url": "http://localhost:9393/message" + } + } + ], + "results": [ + { + "type": "testng", + "files": [ + "test/data/testng/single-suite.xml", + "test/data/testng/invalid.xml" + ] + } + ] + } + }); + } catch (e) { + err = e; + } + assert.equal(mock.getInteraction(id1).exercised, true); + assert.equal(mock.getInteraction(id2).exercised, true); + assert.ok(err.toString().includes('invalid.xml')); + }); + + it('should send results and errors to slack', async () => { + const id1 = mock.addInteraction('post test-summary to slack'); + const id2 = mock.addInteraction('post errors to slack'); + let err; + try { + await publish({ + config: { + "targets": [ + { + "name": "slack", + "inputs": { + "url": "http://localhost:9393/message" + } + } + ], + "results": [ + { + "type": "testng", + "files": [ + "test/data/testng/single-suite.xml", + "test/data/testng/invalid.xml" + ] + } + ] + } + }); + } catch (e) { + err = e; + } + assert.equal(mock.getInteraction(id1).exercised, true); + assert.equal(mock.getInteraction(id2).exercised, true); + assert.ok(err.toString().includes('invalid.xml')); + }); + +}); \ No newline at end of file diff --git a/test/mocks/chat.mock.js b/test/mocks/chat.mock.js index 1d92aaa..6424aae 100644 --- a/test/mocks/chat.mock.js +++ b/test/mocks/chat.mock.js @@ -1,5 +1,6 @@ const { addInteractionHandler } = require('pactum').handler; const { addDataTemplate } = require('pactum').stash; +const { includes } = require('pactum-matchers'); addDataTemplate({ 'CHAT_RESULT_SINGLE_SUITE': { @@ -580,4 +581,34 @@ addInteractionHandler('post test-summary with ci-info to chat', () => { status: 200 } } +}); + +addInteractionHandler('post errors to chat', () => { + return { + strict: false, + request: { + method: 'POST', + path: '/message', + body: { + "cards": [ + { + "sections": [ + { + "widgets": [ + { + "textParagraph": { + "text": includes('invalid.xml') + } + } + ] + } + ] + } + ] + } + }, + response: { + status: 200 + } + } }); \ No newline at end of file diff --git a/test/mocks/slack.mock.js b/test/mocks/slack.mock.js index ab64f89..a8a3119 100644 --- a/test/mocks/slack.mock.js +++ b/test/mocks/slack.mock.js @@ -1,5 +1,6 @@ const { addInteractionHandler } = require('pactum').handler; const { addDataTemplate } = require('pactum').stash; +const { includes } = require('pactum-matchers'); addDataTemplate({ 'SLACK_ROOT_SINGLE_SUITE': { @@ -396,7 +397,6 @@ addInteractionHandler('post test-summary with mentions to slack', () => { } }); - addInteractionHandler('post test-summary with mentions group name to slack', () => { return { request: { @@ -793,4 +793,41 @@ addInteractionHandler('post test-summary to slack with max suites as 1', () => { status: 200 } } +}); + +addInteractionHandler('post errors to slack', () => { + return { + strict: false, + request: { + method: 'POST', + path: '/message', + body: { + "attachments": [ + { + "color": "#DC143C", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Error: Reporting Test Results" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": includes('invalid.xml') + } + } + ], + "fallback": "Error: Reporting Test Results" + } + ] + } + }, + response: { + status: 200 + } + } }); \ No newline at end of file diff --git a/test/mocks/teams.mock.js b/test/mocks/teams.mock.js index b1d055d..2f986a4 100644 --- a/test/mocks/teams.mock.js +++ b/test/mocks/teams.mock.js @@ -1,5 +1,6 @@ const { addInteractionHandler } = require('pactum').handler; const { addDataTemplate } = require('pactum').stash; +const { includes } = require('pactum-matchers'); addDataTemplate({ 'TEAMS_ROOT_TITLE_SINGLE_SUITE': { @@ -1724,4 +1725,47 @@ addInteractionHandler('post test-summary with metadata and hyperlinks to teams', status: 200 } } +}); + +addInteractionHandler('post errors to teams', () => { + return { + strict: false, + request: { + method: 'POST', + path: '/message', + body: { + "type": "message", + "attachments": [ + { + "contentType": "application/vnd.microsoft.card.adaptive", + "content": { + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.0", + "body": [ + { + "type": "TextBlock", + "text": "Error: Reporting Test Results", + "size": "medium", + "weight": "bolder", + "wrap": true + }, + { + "type": "TextBlock", + "text": includes('invalid.xml'), + "size": "medium", + "weight": "bolder", + "wrap": true + } + ], + "actions": [] + } + } + ] + } + }, + response: { + status: 200 + } + } }); \ No newline at end of file