@mojaloop/ml-testing-toolkit-client-lib 1.6.1 → 1.8.0-snapshot.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [1.7.0](https://github.com/mojaloop/ml-testing-toolkit-client-lib/compare/v1.6.1...v1.7.0) (2025-03-13)
6
+
7
+
8
+ ### Features
9
+
10
+ * enhance release report structure, implement brief summary option for Slack ([#19](https://github.com/mojaloop/ml-testing-toolkit-client-lib/issues/19)) ([8a6dd8b](https://github.com/mojaloop/ml-testing-toolkit-client-lib/commit/8a6dd8b99182f2931fe3b1cbd14aec7a63b2e316))
11
+
5
12
  ### [1.6.1](https://github.com/mojaloop/ml-testing-toolkit-client-lib/compare/v1.6.0...v1.6.1) (2025-02-25)
6
13
 
7
14
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mojaloop/ml-testing-toolkit-client-lib",
3
3
  "description": "Testing Toolkit Client Library",
4
- "version": "1.6.1",
4
+ "version": "1.8.0-snapshot.0",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Vijaya Kumar Guthi, ModusBox Inc. ",
7
7
  "contributors": [
@@ -63,13 +63,13 @@
63
63
  "snapshot": "npx standard-version --no-verify --skip.changelog --prerelease snapshot --releaseCommitMessageFormat 'chore(snapshot): {{currentTag}}'"
64
64
  },
65
65
  "dependencies": {
66
- "@mojaloop/central-services-logger": "11.5.5",
66
+ "@mojaloop/central-services-logger": "11.6.2",
67
67
  "@mojaloop/ml-testing-toolkit-shared-lib": "14.0.4",
68
- "@mojaloop/sdk-standard-components": "19.7.0",
69
- "@slack/webhook": "7.0.4",
68
+ "@mojaloop/sdk-standard-components": "19.10.3",
69
+ "@slack/webhook": "7.0.5",
70
70
  "atob": "2.1.2",
71
71
  "aws-sdk": "2.1692.0",
72
- "axios": "1.7.9",
72
+ "axios": "1.8.3",
73
73
  "cli-table3": "0.6.5",
74
74
  "commander": "13.1.0",
75
75
  "dotenv": "16.4.7",
package/src/client.js CHANGED
@@ -47,7 +47,9 @@ program
47
47
  .option('--report-auto-filename-enable <reportAutoFilenameEnable>', 'default: false, if true the file name will be generated by the backend')
48
48
  .option('--report-target <reportTarget>', 'default: "file://<file_name_genrated_by_backend>" --- supported targets: "file://path_to_file", "s3://<bucket_name>[/<file_path>]"')
49
49
  .option('--slack-webhook-url <slackWebhookUrl>', 'default: "Disabled" --- supported formats: "https://....."')
50
+ .option('--slack-webhook-url-for-failed <slackWebhookUrl>', 'default: "Disabled" --- supported formats: "https://....."')
50
51
  .option('--extra-summary-information <Comma separated values in the format key:value>', 'default: none --- example: "Testcase Name:something,Environment:Dev1"')
52
+ .option('--brief-summary-prefix <Prefix to use for a brief summary in Slack>', 'default: none --- example: "environment name, test name"')
51
53
  .on('--help', () => { // Extra information on help message
52
54
  console.log('')
53
55
  console.log(' *** If the option report-target is set to use AWS S3 service, the variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION) should be passed in environment')
@@ -29,10 +29,21 @@
29
29
  const axios = require('axios').default
30
30
  const config = require('rc')('release_cd', {})
31
31
 
32
- module.exports = async function (name, result) {
32
+ module.exports = async function (name, { runtimeInformation, test_cases: testCases = [] }) {
33
33
  if (!config.reportUrl) return
34
34
  const data = {
35
- [`tests.${name}`]: result
35
+ [`tests.${name}`]: {
36
+ ...runtimeInformation,
37
+ assertions: Object.fromEntries(testCases.map(
38
+ testCase => testCase?.requests?.map(
39
+ request => request?.request?.tests?.assertions
40
+ .filter(assertion => assertion?.assertionId ?? assertion.id)
41
+ .map(
42
+ assertion => [`${testCase.testCaseId ?? testCase.name}.${request.request.requestId ?? request.request.id}.${assertion?.assertionId ?? assertion.id}`, assertion?.resultStatus?.status]
43
+ )
44
+ )
45
+ ).flat(2))
46
+ }
36
47
  }
37
48
  console.log(`Sending report to ${config.reportUrl}`, data)
38
49
  await axios({
@@ -31,24 +31,38 @@ const objectStore = require('../objectStore')
31
31
 
32
32
  const config = objectStore.get('config')
33
33
 
34
- const generateSlackBlocks = (progress) => {
34
+ const millisecondsToTime = (milliseconds) => {
35
+ const seconds = Math.floor(milliseconds / 1000)
36
+ const minutes = Math.floor(seconds / 60)
37
+ const hours = Math.floor(minutes / 60)
38
+ return `${String(hours).padStart(2, '0')}:${String(minutes % 60).padStart(2, '0')}:${String(seconds % 60).padStart(2, '0')}`
39
+ }
40
+
41
+ const generateSlackBlocks = (progress, reportURL) => {
35
42
  const slackBlocks = []
36
43
  let totalAssertionsCount = 0
37
44
  let totalPassedAssertionsCount = 0
38
45
  let totalRequestsCount = 0
46
+ const failedTestCases = []
39
47
  progress.test_cases.forEach(testCase => {
40
48
  // console.log(fStr.yellow(testCase.name))
41
49
  totalRequestsCount += testCase.requests.length
42
- // let testCaseAssertionsCount = 0
43
- // let testCasePassedAssertionsCount = 0
50
+ let testCaseAssertionsCount = 0
51
+ let testCasePassedAssertionsCount = 0
44
52
  testCase.requests.forEach(req => {
45
53
  const passedAssertionsCount = req.request.tests && req.request.tests.passedAssertionsCount ? req.request.tests.passedAssertionsCount : 0
46
54
  const assertionsCount = req.request.tests && req.request.tests.assertions && req.request.tests.assertions.length ? req.request.tests.assertions.length : 0
47
55
  totalAssertionsCount += assertionsCount
48
56
  totalPassedAssertionsCount += passedAssertionsCount
49
- // testCaseAssertionsCount += assertionsCount
50
- // testCasePassedAssertionsCount += passedAssertionsCount
57
+ testCaseAssertionsCount += assertionsCount
58
+ testCasePassedAssertionsCount += passedAssertionsCount
51
59
  })
60
+ if (testCaseAssertionsCount !== testCasePassedAssertionsCount) {
61
+ failedTestCases.push({
62
+ name: testCase.name,
63
+ failedAssertions: testCaseAssertionsCount - testCasePassedAssertionsCount
64
+ })
65
+ }
52
66
  // const passed = testCasePassedAssertionsCount === testCaseAssertionsCount
53
67
  // // TODO: make sure this list should not be more than 40 because we can add only max 50 blocks in a slack message
54
68
  // if(!passed) {
@@ -61,6 +75,40 @@ const generateSlackBlocks = (progress) => {
61
75
  // })
62
76
  // }
63
77
  })
78
+
79
+ // totalAssertionsCount = totalPassedAssertionsCount
80
+ // failedTestCases.length = 0
81
+
82
+ if (config.briefSummaryPrefix) {
83
+ const top5FailedTestCases = failedTestCases.sort((a, b) => b.failedAssertions - a.failedAssertions).slice(0, 5)
84
+ return [{
85
+ type: 'context',
86
+ elements: [{
87
+ type: 'mrkdwn',
88
+ text: [
89
+ `${totalAssertionsCount === totalPassedAssertionsCount ? '🟢' : '🔴'}`,
90
+ reportURL ? `<${reportURL}|${config.briefSummaryPrefix}>` : `${config.briefSummaryPrefix}`,
91
+ `failed: \`${totalAssertionsCount - totalPassedAssertionsCount}/${totalAssertionsCount}`,
92
+ `(${(100 * ((totalAssertionsCount - totalPassedAssertionsCount) / totalAssertionsCount)).toFixed(2)}%)\`,`,
93
+ `requests: \`${totalRequestsCount}\`,`,
94
+ `tests: \`${progress.test_cases.length}\`,`,
95
+ `duration: \`${millisecondsToTime(progress.runtimeInformation.runDurationMs)}\``,
96
+ top5FailedTestCases.length > 0 && '\nTop 5 failed test cases:\n',
97
+ top5FailedTestCases.length > 0 && top5FailedTestCases.map(tc => `• ${tc.name}: \`${tc.failedAssertions}\``).join('\n')
98
+ ].filter(Boolean).join(' ')
99
+ }]
100
+ }]
101
+ }
102
+
103
+ slackBlocks.push({
104
+ type: 'header',
105
+ text: {
106
+ type: 'plain_text',
107
+ text: 'Testing Toolkit Report',
108
+ emoji: true
109
+ }
110
+ })
111
+
64
112
  let summaryText = ''
65
113
 
66
114
  summaryText += '>Total assertions: *' + totalAssertionsCount + '*\n'
@@ -107,40 +155,53 @@ const generateSlackBlocks = (progress) => {
107
155
  },
108
156
  ...additionalParams
109
157
  })
158
+ if (reportURL) {
159
+ slackBlocks.push({
160
+ type: 'section',
161
+ text: {
162
+ type: 'mrkdwn',
163
+ text: '<' + reportURL + '|View Report>'
164
+ }
165
+ })
166
+ }
167
+ slackBlocks.push({
168
+ type: 'divider'
169
+ })
110
170
  return slackBlocks
111
171
  }
112
172
 
113
- const sendSlackNotification = async (progress, reportURL = null) => {
173
+ const sendSlackNotification = async (progress, reportURL = 'http://localhost/') => {
174
+ console.log('runtimeInformation: ', progress.runtimeInformation)
175
+
176
+ let blocks
114
177
  if (config.slackWebhookUrl) {
115
178
  const url = config.slackWebhookUrl
116
179
  const webhook = new IncomingWebhook(url)
117
- let slackBlocks = []
118
- slackBlocks.push({
119
- type: 'header',
120
- text: {
121
- type: 'plain_text',
122
- text: 'Testing Toolkit Report',
123
- emoji: true
124
- }
125
- })
126
- slackBlocks = slackBlocks.concat(generateSlackBlocks(progress))
127
- if (reportURL) {
128
- slackBlocks.push({
129
- type: 'section',
130
- text: {
131
- type: 'mrkdwn',
132
- text: '<' + reportURL + '|View Report>'
133
- }
180
+ blocks = generateSlackBlocks(progress, reportURL)
181
+
182
+ try {
183
+ // console.log(JSON.stringify(slackBlocks, null, 2))
184
+ await webhook.send({
185
+ text: 'Test Report',
186
+ blocks
134
187
  })
188
+ console.log('Slack notification sent.')
189
+ } catch (err) {
190
+ console.log('ERROR: Sending slack notification failed. ', err.message)
135
191
  }
136
- slackBlocks.push({
137
- type: 'divider'
138
- })
192
+ }
193
+
194
+ if (needToNotifyFailed(config, progress)) {
195
+ const url = config.slackWebhookUrlForFailed
196
+ const webhook = new IncomingWebhook(url)
197
+
198
+ if (!blocks) blocks = generateSlackBlocks(progress, reportURL)
199
+
139
200
  try {
140
201
  // console.log(JSON.stringify(slackBlocks, null, 2))
141
202
  await webhook.send({
142
- text: 'Test Report',
143
- blocks: slackBlocks
203
+ text: 'Failed Tests Report',
204
+ blocks
144
205
  })
145
206
  console.log('Slack notification sent.')
146
207
  } catch (err) {
@@ -149,6 +210,13 @@ const sendSlackNotification = async (progress, reportURL = null) => {
149
210
  }
150
211
  }
151
212
 
213
+ const needToNotifyFailed = (conf, totalResult) => {
214
+ return conf.slackWebhookUrlForFailed && (!totalResult?.runtimeInformation?.totalAssertion
215
+ ? true
216
+ : totalResult.runtimeInformation.totalPassedAssertions !== totalResult.runtimeInformation.totalAssertion)
217
+ }
218
+
152
219
  module.exports = {
153
- sendSlackNotification
220
+ sendSlackNotification,
221
+ needToNotifyFailed
154
222
  }
@@ -200,7 +200,7 @@ const handleIncomingProgress = async (progress) => {
200
200
  }
201
201
  }
202
202
  try {
203
- await releaseCd(config.reportName, progress.totalResult.runtimeInformation)
203
+ await releaseCd(config.reportName, progress.totalResult)
204
204
  } catch (err) {
205
205
  console.error(err)
206
206
  }
package/src/router.js CHANGED
@@ -63,10 +63,12 @@ const cli = (commanderOptions) => {
63
63
  reportAutoFilenameEnable: commanderOptions.reportAutoFilenameEnable === 'true' || configFile.reportAutoFilenameEnable === true,
64
64
  reportTarget: commanderOptions.reportTarget || configFile.reportTarget,
65
65
  slackWebhookUrl: commanderOptions.slackWebhookUrl || configFile.slackWebhookUrl,
66
+ slackWebhookUrlForFailed: commanderOptions.slackWebhookUrlForFailed || configFile.slackWebhookUrlForFailed,
66
67
  slackPassedImage: configFile.slackPassedImage,
67
68
  slackFailedImage: configFile.slackFailedImage,
68
69
  baseURL: commanderOptions.baseUrl || configFile.baseURL,
69
70
  extraSummaryInformation: commanderOptions.extraSummaryInformation || configFile.extraSummaryInformation,
71
+ briefSummaryPrefix: commanderOptions.briefSummaryPrefix || configFile.briefSummaryPrefix,
70
72
  labels: commanderOptions.labels || configFile.labels,
71
73
  batchSize: commanderOptions.batchSize || configFile.batchSize || parseInt(process.env.TESTCASES_BATCH_SIZE, 10)
72
74
  }
@@ -42,14 +42,41 @@ describe('Release CD', () => {
42
42
  describe('Post test results', () => {
43
43
  it('Posts test result to configured URL', async () => {
44
44
  const name = 'test'
45
- const result = {}
45
+ const result = {
46
+ test_cases: [
47
+ {
48
+ name: 'test-case',
49
+ requests: [
50
+ {
51
+ request: {
52
+ id: 'request-id',
53
+ tests: {
54
+ assertions: [
55
+ {
56
+ id: 'assertion-id',
57
+ resultStatus: {
58
+ status: 'SUCCESS'
59
+ }
60
+ }
61
+ ]
62
+ }
63
+ }
64
+ }
65
+ ]
66
+ }
67
+ ]
68
+ }
46
69
  config.reportUrl = 'http://example.com'
47
70
  await releaseCd(name, result)
48
71
  expect(axios.default).toHaveBeenCalledWith({
49
72
  method: 'post',
50
73
  url: 'http://example.com',
51
74
  data: {
52
- [`tests.${name}`]: result
75
+ [`tests.${name}`]: {
76
+ assertions: {
77
+ 'test-case.request-id.assertion-id': 'SUCCESS'
78
+ }
79
+ }
53
80
  }
54
81
  })
55
82
  })
@@ -102,6 +102,13 @@ describe('Cli client', () => {
102
102
  text: expect.any(String),
103
103
  blocks: expect.any(Array)
104
104
  }))
105
+ SpySlackSend.mockResolvedValueOnce(null)
106
+ config.briefSummaryPrefix = 'brief'
107
+ await expect(slackBroadCast.sendSlackNotification(sampleProgress)).resolves.toBe(undefined)
108
+ expect(SpySlackSend).toHaveBeenCalledWith(expect.objectContaining({
109
+ text: expect.any(String),
110
+ blocks: expect.any(Array)
111
+ }))
105
112
  })
106
113
  it('When failed case, it should call slack send function', async () => {
107
114
  config.slackWebhookUrl = 'http://some_url'
@@ -127,4 +134,60 @@ describe('Cli client', () => {
127
134
  }))
128
135
  })
129
136
  })
137
+
138
+ describe.skip('needToNotify Tests -->', () => {
139
+ it('should not notify if slackWebhookUrl is not set', () => {
140
+ expect(slackBroadCast.needToNotify({})).toBeFalsy()
141
+ })
142
+
143
+ it('should notify if slackWebhookUrl is set and slackOnlyFailed not', () => {
144
+ const conf = { slackWebhookUrl: 'url' }
145
+ expect(slackBroadCast.needToNotify(conf)).toBe(true)
146
+ })
147
+
148
+ it('should notify if slackWebhookUrl is set and slackOnlyFailed is false', () => {
149
+ const conf = {
150
+ slackWebhookUrl: 'url',
151
+ slackOnlyFailed: false
152
+ }
153
+ expect(slackBroadCast.needToNotify(conf)).toBe(true)
154
+ })
155
+
156
+ it('should notify if slackOnlyFailed=true, and tests failed', () => {
157
+ const conf = {
158
+ slackWebhookUrl: 'url',
159
+ slackOnlyFailed: true
160
+ }
161
+ const totalResult = {
162
+ runtimeInformation: {
163
+ totalPassedAssertions: 0,
164
+ totalAssertion: 1
165
+ }
166
+ }
167
+ expect(slackBroadCast.needToNotify(conf, totalResult)).toBe(true)
168
+ })
169
+
170
+ it('should notify if slackOnlyFailed=true, and tests passed', () => {
171
+ const conf = {
172
+ slackWebhookUrl: 'url',
173
+ slackOnlyFailed: true
174
+ }
175
+ const totalResult = {
176
+ runtimeInformation: {
177
+ totalPassedAssertions: 1,
178
+ totalAssertion: 1
179
+ }
180
+ }
181
+ expect(slackBroadCast.needToNotify(conf, totalResult)).toBe(false)
182
+ })
183
+
184
+ it('should notify if slackOnlyFailed=true, but no totalResult.runtimeInformation', () => {
185
+ const conf = {
186
+ slackWebhookUrl: 'url',
187
+ slackOnlyFailed: true
188
+ }
189
+ const totalResult = {}
190
+ expect(slackBroadCast.needToNotify(conf, totalResult)).toBe(true)
191
+ })
192
+ })
130
193
  })
@@ -30,17 +30,19 @@
30
30
 
31
31
  const spyExit = jest.spyOn(process, 'exit')
32
32
  const { cli } = require('../../src/router')
33
+ // const objectStore = require('../../src/objectStore')
33
34
 
34
35
  jest.mock('../../src/utils/listeners')
35
36
  jest.mock('../../src/modes/outbound')
36
37
  jest.mock('../../src/modes/testcaseDefinitionReport')
38
+ // jest.mock('../../src/objectStore')
37
39
 
38
40
  describe('Cli client', () => {
39
41
  describe('running router', () => {
40
42
  it('when mode is monitoring should not throw an error', async () => {
41
43
  const config = {
42
44
  "mode": "monitoring"
43
- }
45
+ }
44
46
  spyExit.mockImplementationOnce(jest.fn())
45
47
  expect(() => {
46
48
  cli(config)
@@ -111,5 +113,14 @@ describe('Cli client', () => {
111
113
  cli(config)
112
114
  }).not.toThrowError();
113
115
  })
116
+ // it('should have default slackOnlyFailed value', async () => {
117
+ // spyExit.mockImplementationOnce(jest.fn())
118
+ // cli({
119
+ // mode: 'outbound',
120
+ // inputFiles: 'test',
121
+ // environmentFile: 'test'
122
+ // })
123
+ // expect(objectStore.set.mock.lastCall[1].slackOnlyFailed).toBe(false)
124
+ // })
114
125
  })
115
126
  })