@mojaloop/ml-testing-toolkit-client-lib 1.6.1 → 1.7.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 +7 -0
- package/package.json +5 -5
- package/src/client.js +1 -0
- package/src/extras/release-cd.js +13 -2
- package/src/extras/slack-broadcast.js +69 -29
- package/src/modes/outbound.js +1 -1
- package/src/router.js +1 -0
- package/test/unit/extras/release-cd.test.js +29 -2
- package/test/unit/extras/slack-broadcast.test.js +7 -0
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.
|
|
4
|
+
"version": "1.7.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.
|
|
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.
|
|
69
|
-
"@slack/webhook": "7.0.
|
|
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.
|
|
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
|
@@ -48,6 +48,7 @@ program
|
|
|
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
50
|
.option('--extra-summary-information <Comma separated values in the format key:value>', 'default: none --- example: "Testcase Name:something,Environment:Dev1"')
|
|
51
|
+
.option('--brief-summary-prefix <Prefix to use for a brief summary in Slack>', 'default: none --- example: "environment name, test name"')
|
|
51
52
|
.on('--help', () => { // Extra information on help message
|
|
52
53
|
console.log('')
|
|
53
54
|
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')
|
package/src/extras/release-cd.js
CHANGED
|
@@ -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,
|
|
32
|
+
module.exports = async function (name, { runtimeInformation, test_cases: testCases = [] }) {
|
|
33
33
|
if (!config.reportUrl) return
|
|
34
34
|
const data = {
|
|
35
|
-
[`tests.${name}`]:
|
|
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
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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,32 @@ 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 =
|
|
173
|
+
const sendSlackNotification = async (progress, reportURL = 'http://localhost/') => {
|
|
114
174
|
if (config.slackWebhookUrl) {
|
|
115
175
|
const url = config.slackWebhookUrl
|
|
116
176
|
const webhook = new IncomingWebhook(url)
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
}
|
|
134
|
-
})
|
|
135
|
-
}
|
|
136
|
-
slackBlocks.push({
|
|
137
|
-
type: 'divider'
|
|
138
|
-
})
|
|
177
|
+
const blocks = generateSlackBlocks(progress, reportURL)
|
|
178
|
+
|
|
139
179
|
try {
|
|
140
180
|
// console.log(JSON.stringify(slackBlocks, null, 2))
|
|
141
181
|
await webhook.send({
|
|
142
182
|
text: 'Test Report',
|
|
143
|
-
blocks
|
|
183
|
+
blocks
|
|
144
184
|
})
|
|
145
185
|
console.log('Slack notification sent.')
|
|
146
186
|
} catch (err) {
|
package/src/modes/outbound.js
CHANGED
|
@@ -200,7 +200,7 @@ const handleIncomingProgress = async (progress) => {
|
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
202
|
try {
|
|
203
|
-
await releaseCd(config.reportName, progress.totalResult
|
|
203
|
+
await releaseCd(config.reportName, progress.totalResult)
|
|
204
204
|
} catch (err) {
|
|
205
205
|
console.error(err)
|
|
206
206
|
}
|
package/src/router.js
CHANGED
|
@@ -67,6 +67,7 @@ const cli = (commanderOptions) => {
|
|
|
67
67
|
slackFailedImage: configFile.slackFailedImage,
|
|
68
68
|
baseURL: commanderOptions.baseUrl || configFile.baseURL,
|
|
69
69
|
extraSummaryInformation: commanderOptions.extraSummaryInformation || configFile.extraSummaryInformation,
|
|
70
|
+
briefSummaryPrefix: commanderOptions.briefSummaryPrefix || configFile.briefSummaryPrefix,
|
|
70
71
|
labels: commanderOptions.labels || configFile.labels,
|
|
71
72
|
batchSize: commanderOptions.batchSize || configFile.batchSize || parseInt(process.env.TESTCASES_BATCH_SIZE, 10)
|
|
72
73
|
}
|
|
@@ -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}`]:
|
|
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'
|