-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Upload newman runs to Postman #2849
base: develop
Are you sure you want to change the base?
Changes from 16 commits
db0ddfb
f814e5f
02c0270
e956289
c299ab9
63e3988
06a597a
0de68dd
79d4f87
94578ec
76530b9
afa582a
cadd6d9
53078af
96bd6d4
506cb77
7a1b370
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,257 @@ | ||||||
const _ = require('lodash'), | ||||||
coditva marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
sdk = require('postman-collection'), | ||||||
util = require('../util'), | ||||||
print = require('../print'), | ||||||
retry = require('async-retry'), | ||||||
request = require('postman-request'), | ||||||
|
||||||
POSTMAN_UPLOAD_ERROR_LINK = '<TBA>', | ||||||
UPLOAD_RUN_API_URL = '<TBA>', | ||||||
NEWMAN_STRING = 'newman'; | ||||||
|
||||||
/** | ||||||
* @private | ||||||
* Internal upload call | ||||||
* | ||||||
* @param {Object} uploadOptions | ||||||
* @returns {function} Returns an async function which can be used by async-retry library to have retries | ||||||
*/ | ||||||
_upload = (uploadOptions) => { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this is going to return a function, it should be called something like |
||||||
return async (bail) => request.post(uploadOptions , (error, response, body) => { | ||||||
if(error){ | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indentation looks weird here. |
||||||
if(error.code === 'ECONNREFUSED') { // Retry only if the ERROR is ERRCONNECT | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nitpick: |
||||||
throw new Error(error.message); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the error message is same, why not just throw the error itself?
Suggested change
|
||||||
} | ||||||
return bail(error); // For other errors , dont retry | ||||||
} | ||||||
|
||||||
// Handle exact status codes | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't seem to be handling the exact status codes. 😅 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should use a |
||||||
|
||||||
if(200 <= response.statusCode && response.statusCode <= 299){ | ||||||
return body; | ||||||
} | ||||||
|
||||||
if( 400 <= response.statusCode && response.statusCode<= 499 ){ | ||||||
|
||||||
if(response.statusCode === 404){ | ||||||
return bail(new Error('Couldn\'t find the postman server route')) | ||||||
} | ||||||
|
||||||
return bail(new Error(body.message)); // Avoid retry if there is client side error ( API key error / Workspace ID / permission error) | ||||||
|
||||||
} | ||||||
|
||||||
if( 500 <= response.statusCode && response.statusCode <= 599){ // Perform Retry if Server side Error | ||||||
throw new Error(`Retrying server error: ${body.message}`); // Change this after discussion with Harsh | ||||||
} | ||||||
|
||||||
throw new Error(`Recieved an unexpected Response status code while uploading Newman run`); // This should not be activated ( Discuss with Harsh , how to handle 3xx ) | ||||||
}); | ||||||
} | ||||||
|
||||||
/** | ||||||
* @private | ||||||
* Internal upload function which handles the retry | ||||||
* | ||||||
* @param {*} uploadOptions | ||||||
* @param {*} retryOptions | ||||||
* | ||||||
* @returns {Promise} | ||||||
*/ | ||||||
_uploadWithRetry = (upload, retryOptions) => { | ||||||
return retry(upload,{ | ||||||
retries: retryOptions.maxRetries, | ||||||
factor: retryOptions.retryDelayMultiplier, | ||||||
randomize: retryOptions.addJitter || false, | ||||||
maxRetryTime: retryOptions.maxRetryDelay * 1000 , // converting to ms | ||||||
maxTimeout: retryOptions.totalTimeout * 1000, // converting to ms | ||||||
onRetry: retryOptions.onRetry || function(){} | ||||||
}); | ||||||
} | ||||||
|
||||||
/** | ||||||
* @private | ||||||
*/ | ||||||
_buildRequestObject = (request)=> { | ||||||
|
||||||
if (!request) { | ||||||
return {}; | ||||||
} | ||||||
|
||||||
request = new sdk.Request(request); | ||||||
|
||||||
return { | ||||||
url: _.invoke(request, 'url.toString', ''), | ||||||
method: _.get(request, 'method', ''), | ||||||
headers: request.getHeaders({ enabled: false }), | ||||||
body: request.toJSON().body, | ||||||
path: ''// '/ Collections / Create Collections / Create a collection into personal workspace' // TODO - Find out where is this used , ask giri if we can skip this ? | ||||||
}; | ||||||
|
||||||
} | ||||||
|
||||||
/** | ||||||
* @private | ||||||
* @param {Object} response | ||||||
* @param {Boolean} skipResponse | ||||||
* | ||||||
* @returns {Object} | ||||||
*/ | ||||||
_buildResponseObject = (response, skipResponse) => { | ||||||
|
||||||
if(!response) { | ||||||
return {} | ||||||
} | ||||||
|
||||||
return { | ||||||
code: response.code, | ||||||
name: response.status, | ||||||
time: response.responseTime, | ||||||
size: response.responseSize, | ||||||
headers: response.header, | ||||||
body: (!skipResponse && response.stream.data ) ? new TextDecoder('utf-8').decode(new Uint8Array(response.stream.data)) : null | ||||||
} | ||||||
} | ||||||
|
||||||
/** | ||||||
* @private | ||||||
* @param {Array} assertions | ||||||
* | ||||||
* @returns {Array} | ||||||
*/ | ||||||
_buildTestObject = (assertions) => { | ||||||
const tests = [] | ||||||
|
||||||
assertions && assertions.forEach(assert => { | ||||||
|
||||||
tests.push({ | ||||||
name: assert.assertion, | ||||||
error: assert.error ? _.pick(assert.error, ['name','message','stack']) : null, | ||||||
status: assert.error ? 'fail' : 'pass', | ||||||
}) | ||||||
}); | ||||||
|
||||||
return tests | ||||||
} | ||||||
|
||||||
/** | ||||||
* @private | ||||||
* @param {Array} executions | ||||||
* @param {Number} iterationCount | ||||||
* @param {Boolean} skipResponse | ||||||
* | ||||||
* @returns {Array} | ||||||
*/ | ||||||
_executionToIterationConverter = (executions, iterationCount, skipResponse) => { | ||||||
|
||||||
const iterations = [] | ||||||
executions = util.partition(executions, iterationCount); | ||||||
|
||||||
executions.forEach( iter => { | ||||||
const iteration = [] | ||||||
|
||||||
iter.forEach(req => { | ||||||
iteration.push({ | ||||||
id: req.item.id, | ||||||
name: req.item.name || '', | ||||||
request: _buildRequestObject(req.request), | ||||||
response: req.response ? _buildResponseObject(req.response, skipResponse): null, | ||||||
error: req.requestError || null,// @TODO - In RUnner, we are storing true for error / undefined if no error. Check for this with Harsh | ||||||
tests: _buildTestObject(req.assertions), // What's the value if tests are not present for a request | ||||||
}); | ||||||
}); | ||||||
|
||||||
iterations.push(iteration); | ||||||
|
||||||
}); | ||||||
return iterations | ||||||
} | ||||||
|
||||||
/** | ||||||
* @private | ||||||
* @param {Object} runOptions | ||||||
* @param {Object} runOptions | ||||||
* | ||||||
* @returns {String} | ||||||
*/ | ||||||
_buildPostmanUploadPayload = (runOptions, runSummary) => { | ||||||
if(!runOptions || !runSummary) { | ||||||
throw new Error('Cannot Build Run Payload without runOptions or RunSummary'); | ||||||
} | ||||||
|
||||||
const run = { | ||||||
name: runOptions.collection.name || '', | ||||||
status:'finished', // THis can be finished or failed - Check with Harsh about all the possible values | ||||||
source: NEWMAN_STRING, | ||||||
failedTestCount: runSummary.run.stats.assertions.failed || 0, | ||||||
totalTestCount: runSummary.run.stats.assertions.total || 0, | ||||||
collection: runOptions.collectionID || '', | ||||||
environment: runOptions.environmentID || '', | ||||||
iterations: _executionToIterationConverter(runSummary.run.executions, runOptions.iterationCount , runOptions.publishSkipResponse), | ||||||
delay: runOptions.delayRequest || 0, | ||||||
persist: false, | ||||||
saveResponse: !runOptions.publishSkipResponse, | ||||||
dataFile: runOptions.collectionUID && runOptions.environmentUID ? runOptions.iterationData : null, | ||||||
workspace: runOptions.publish, | ||||||
currentIteration: runOptions.iterationCount, | ||||||
folder: null // This is always null as you cannot run a folder via newman | ||||||
} | ||||||
return JSON.stringify(run); | ||||||
} | ||||||
|
||||||
/** | ||||||
* @public | ||||||
* Starts the run upload process | ||||||
* | ||||||
* @param {*} runSummary | ||||||
* @param {*} runOptions | ||||||
* | ||||||
* @returns { Promise } | ||||||
*/ | ||||||
uploadRunToPostman = (runSummary, runOptions) => { | ||||||
let runPayload; | ||||||
|
||||||
runOptions.collectionID = util.extractResourceID(runOptions.collection); | ||||||
runOptions.environmentID = util.extractResourceID(runOptions.environment); | ||||||
|
||||||
try{ | ||||||
runPayload = _buildPostmanUploadPayload(runOptions, runSummary) | ||||||
}catch(error){ | ||||||
throw new Error(`Unable to serialize the run - ${error}`); | ||||||
} | ||||||
|
||||||
print.lf('Uploading newman run to Postman'); | ||||||
|
||||||
const uploadOptions = { | ||||||
url: UPLOAD_RUN_API_URL, | ||||||
body: runPayload, | ||||||
headers: { | ||||||
'Content-Type': 'application/json', | ||||||
'X-Api-Key': runOptions.postmanApiKey | ||||||
} | ||||||
}, | ||||||
retryOptions = { | ||||||
maxRetries: runOptions.publishRetries, | ||||||
totalTimeout: runOptions.publishTimeout, | ||||||
retryDelayMultiplier: 2, | ||||||
maxRetryDelay : 64, | ||||||
addJitter: true | ||||||
}; | ||||||
|
||||||
return _uploadWithRetry(_upload(uploadOptions), retryOptions); | ||||||
} | ||||||
|
||||||
module.exports = { | ||||||
uploadRunToPostman, | ||||||
POSTMAN_UPLOAD_ERROR_LINK, | ||||||
|
||||||
// Exporting following functions for testing ONLY. | ||||||
_buildPostmanUploadPayload, | ||||||
_executionToIterationConverter, | ||||||
_buildRequestObject, | ||||||
_buildResponseObject, | ||||||
_buildTestObject, | ||||||
_uploadWithRetry, | ||||||
_upload, | ||||||
|
||||||
}; |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -284,6 +284,55 @@ util = { | |||||
*/ | ||||||
isFloat: function (value) { | ||||||
return (value - parseFloat(value) + 1) >= 0; | ||||||
}, | ||||||
|
||||||
/** | ||||||
* Helper to get resource ID from URL | ||||||
* | ||||||
* If resourceUrl = https://api.getpostman.com/collections/<collection-UID>?apikey=<api_key> | ||||||
* This method extracts <collection-UID> | ||||||
* | ||||||
* @param {String} resource - url for collection\environment | ||||||
* @param resourceURL | ||||||
* @returns {String} | ||||||
*/ | ||||||
extractResourceID: function (resourceURL) { | ||||||
if (_.isObject(resourceURL) || !resourceURL || resourceURL === '') { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems like a weird check. If we want the
Suggested change
|
||||||
return ''; | ||||||
} | ||||||
|
||||||
// eslint-disable-next-line max-len | ||||||
const uidExtractionPattern = /https:\/\/api\.getpostman\.com\/(?:(?:collections)|(?:environments))\/([A-Za-z0-9-]+)\?\w*/g; | ||||||
|
||||||
let matches, | ||||||
results = []; | ||||||
|
||||||
while ((matches = uidExtractionPattern.exec(resourceURL)) !== null) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why are we looping over all the matches? It should be a single match, no? You would have the match present in the first call to |
||||||
if (matches.index === uidExtractionPattern.lastIndex) { | ||||||
uidExtractionPattern.lastIndex++; | ||||||
} | ||||||
|
||||||
matches.forEach((match) => { return results.push(match); }); | ||||||
} | ||||||
|
||||||
return results.length === 0 ? '' : results[1]; | ||||||
}, | ||||||
|
||||||
partition: function (arr, n) { | ||||||
const validPartitionArg = Number.isSafeInteger(n) && n > 0, | ||||||
partitions = []; | ||||||
|
||||||
if (!validPartitionArg) { | ||||||
throw new Error('n must be positive integer'); | ||||||
} | ||||||
|
||||||
for (let i = 0; i < arr.length; i += n) { | ||||||
const partition = arr.slice(i, i + n); | ||||||
|
||||||
partitions.push(partition); | ||||||
} | ||||||
|
||||||
return partitions; | ||||||
} | ||||||
}; | ||||||
|
||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@UmedJadhav This error is raised from the
postman-runtime
library if the collection run did not finish successfully.