diff --git a/package-lock.json b/package-lock.json index cdc2273..27c0bab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "test-results-reporter", - "version": "1.0.19", + "version": "1.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "test-results-reporter", - "version": "1.0.19", + "version": "1.1.0", "license": "ISC", "dependencies": { "async-retry": "^1.3.3", diff --git a/package.json b/package.json index dd08428..bc52f3d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "test-results-reporter", - "version": "1.0.19", + "version": "1.1.0", "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/beats/index.js b/src/beats/index.js new file mode 100644 index 0000000..59fa7d9 --- /dev/null +++ b/src/beats/index.js @@ -0,0 +1,82 @@ +const request = require('phin-retry'); +const TestResult = require('test-results-parser/src/models/TestResult'); + +const BASE_URL = "http://localhost:9393"; + +/** + * @param {import('../index').PublishReport} config + * @param {TestResult} result + */ +async function run(config, result) { + if (config.project && config.build && config.api_key) { + const run_id = await publishTestResults(config, result); + if (run_id) { + attachTestBeatsReportHyperLink(config, run_id); + } + } +} + +/** + * @param {import('../index').PublishReport} config + * @param {TestResult} result + */ +async function publishTestResults(config, result) { + try { + const response = await request.post({ + url: `${BASE_URL}/api/core/v1/test-runs`, + headers: { + 'x-api-key': config.api_key + }, + body: { + project: config.project, + build: config.build, + ...result + } + }); + return response.id; + } catch (error) { + console.log("Unable to publish results to TestBeats"); + console.log(error); + } +} + +/** + * @param {import('../index').PublishReport} config + * @param {string} run_id + */ +function attachTestBeatsReportHyperLink(config, run_id) { + const hyperlink_to_test_beats = getTestBeatsReportHyperLink(run_id); + for (const target of config.targets) { + if (target.name === 'chat' || target.name === 'teams' || target.name === 'slack') { + target.extensions = target.extensions || []; + if (target.extensions.length > 0) { + if (target.extensions[0].name === 'hyperlinks' && target.extensions[0].inputs.links[0].name === 'Test Beats Report') { + target.extensions[0].inputs.links[0].url = `${BASE_URL}/reports/${run_id}`; + continue; + } + } + target.extensions = [hyperlink_to_test_beats, ...target.extensions]; + } + } +} + +/** + * + * @param {string} run_id + * @returns + */ +function getTestBeatsReportHyperLink(run_id) { + return { + "name": "hyperlinks", + "inputs": { + "links": [ + { + "text": "Test Beats Report", + "url": `${BASE_URL}/reports/${run_id}` + } + ] + } + } +} + +module.exports = { run } \ No newline at end of file diff --git a/src/commands/publish.js b/src/commands/publish.js index dcdd7f7..d2a5168 100644 --- a/src/commands/publish.js +++ b/src/commands/publish.js @@ -3,6 +3,7 @@ const trp = require('test-results-parser'); const prp = require('performance-results-parser'); const { processData } = require('../helpers/helper'); +const beats = require('../beats'); const target_manager = require('../targets'); /** @@ -14,22 +15,36 @@ async function run(opts) { opts.config = require(path.join(cwd, opts.config)); } const config = processData(opts.config); - for (const report of config.reports) { - const parsed_results = []; - for (const result_options of report.results) { - if (result_options.type === 'custom') { - parsed_results.push(result_options.result); - } else if (result_options.type === 'jmeter') { - parsed_results.push(prp.parse(result_options)); - } else { - parsed_results.push(trp.parse(result_options)); - } + if (config.reports) { + for (const report of config.reports) { + await processReport(report); } - for (let i = 0; i < parsed_results.length; i++) { - const result = parsed_results[i]; - for (const target of report.targets) { - await target_manager.run(target, result); - } + } else { + await processReport(config); + } +} + +/** + * + * @param {import('../index').PublishReport} report + */ +async function processReport(report) { + const parsed_results = []; + for (const result_options of report.results) { + if (result_options.type === 'custom') { + parsed_results.push(result_options.result); + } else if (result_options.type === 'jmeter') { + parsed_results.push(prp.parse(result_options)); + } else { + parsed_results.push(trp.parse(result_options)); + } + } + + for (let i = 0; i < parsed_results.length; i++) { + const result = parsed_results[i]; + await beats.run(report, result); + for (const target of report.targets) { + await target_manager.run(target, result); } } } diff --git a/src/index.d.ts b/src/index.d.ts index edf755c..1974109 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -210,12 +210,20 @@ export interface CustomResultOptions { } export interface PublishReport { - targets: Target[]; - results: ParseOptions[] | PerformanceParseOptions[] | CustomResultOptions[]; + api_key?: string; + project?: string; + build?: string; + targets?: Target[]; + results?: ParseOptions[] | PerformanceParseOptions[] | CustomResultOptions[]; } export interface PublishConfig { - reports: PublishReport[]; + api_key?: string; + project?: string; + build?: string; + targets?: Target[]; + results?: ParseOptions[] | PerformanceParseOptions[] | CustomResultOptions[]; + reports?: PublishReport[]; } export interface PublishOptions { diff --git a/test/beats.spec.js b/test/beats.spec.js new file mode 100644 index 0000000..81b4f99 --- /dev/null +++ b/test/beats.spec.js @@ -0,0 +1,96 @@ +const { mock } = require('pactum'); +const assert = require('assert'); +const { publish } = require("../src"); + +describe('TestBeats', () => { + + it('should send results to beats', async () => { + const id1 = mock.addInteraction('post test results to testbeats'); + const id2 = mock.addInteraction('post test-summary with testbeats to teams'); + await publish({ + config: { + api_key: 'api-key', + project: 'project-name', + build: 'build-name', + targets: [ + { + name: 'teams', + inputs: { + url: 'http://localhost:9393/message' + } + } + ], + results: [ + { + type: 'testng', + files: [ + 'test/data/testng/single-suite.xml' + ] + } + ] + } + }); + assert.equal(mock.getInteraction(id1).exercised, true); + assert.equal(mock.getInteraction(id2).exercised, true); + }); + + it('should send results to beats with extensions', async () => { + const id1 = mock.addInteraction('post test results to testbeats'); + const id2 = mock.addInteraction('post test-summary with extensions and testbeats to teams'); + await publish({ + config: { + api_key: 'api-key', + project: 'project-name', + build: 'build-name', + targets: [ + { + name: 'teams', + inputs: { + url: 'http://localhost:9393/message' + }, + "extensions": [ + { + "name": "metadata", + "inputs": { + "data": [ + { + "key": "Browser", + "value": "Chrome" + }, + { + "value": "1920*1080" + }, + { + "value": "1920*1080", + "condition": "never" + }, + { + "key": "Pipeline", + "value": "some-url", + "type": "hyperlink" + }, + ] + } + } + ] + } + ], + results: [ + { + type: 'testng', + files: [ + 'test/data/testng/single-suite.xml' + ] + } + ] + } + }); + assert.equal(mock.getInteraction(id1).exercised, true); + assert.equal(mock.getInteraction(id2).exercised, true); + }); + + afterEach(() => { + mock.clearInteractions(); + }); + +}); \ No newline at end of file diff --git a/test/mocks/beats.mock.js b/test/mocks/beats.mock.js new file mode 100644 index 0000000..7bcb0ad --- /dev/null +++ b/test/mocks/beats.mock.js @@ -0,0 +1,17 @@ +const { addInteractionHandler } = require('pactum').handler; + +addInteractionHandler('post test results to testbeats', () => { + return { + strict: false, + request: { + method: 'POST', + path: '/api/core/v1/test-runs' + }, + response: { + status: 200, + body: { + id: 'test-run-id' + } + } + } +}); \ No newline at end of file diff --git a/test/mocks/index.js b/test/mocks/index.js index 32983eb..6a7fe3e 100644 --- a/test/mocks/index.js +++ b/test/mocks/index.js @@ -1,3 +1,4 @@ +require('./beats.mock'); require('./custom.mock'); require('./rp.mock'); require('./slack.mock'); diff --git a/test/mocks/teams.mock.js b/test/mocks/teams.mock.js index 93f25c7..00b30b0 100644 --- a/test/mocks/teams.mock.js +++ b/test/mocks/teams.mock.js @@ -1445,4 +1445,90 @@ addInteractionHandler('post test-summary with metadata to teams', () => { status: 200 } } +}); + +addInteractionHandler('post test-summary with testbeats to teams', () => { + return { + 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": [ + { + "@DATA:TEMPLATE@": "TEAMS_ROOT_TITLE_SINGLE_SUITE" + }, + { + "@DATA:TEMPLATE@": "TEAMS_ROOT_RESULTS_SINGLE_SUITE", + }, + { + "type": "TextBlock", + "text": "[Test Beats Report](http://localhost:9393/reports/test-run-id)", + "wrap": true, + "separator": true + } + ], + "actions": [] + } + } + ] + } + }, + response: { + status: 200 + } + } +}); + +addInteractionHandler('post test-summary with extensions and testbeats to teams', () => { + return { + 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": [ + { + "@DATA:TEMPLATE@": "TEAMS_ROOT_TITLE_SINGLE_SUITE" + }, + { + "@DATA:TEMPLATE@": "TEAMS_ROOT_RESULTS_SINGLE_SUITE", + }, + { + "type": "TextBlock", + "text": "[Test Beats Report](http://localhost:9393/reports/test-run-id)", + "wrap": true, + "separator": true + }, + { + "type": "TextBlock", + "text": "**Browser:** Chrome | 1920*1080 | [Pipeline](some-url)", + "wrap": true, + "separator": true + } + ], + "actions": [] + } + } + ] + } + }, + response: { + status: 200 + } + } }); \ No newline at end of file