-
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 13 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,237 @@ | ||
const util = require('../util'); | ||
|
||
const _ = require('lodash'), | ||
coditva marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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) => await new Promise((resolve, reject) => 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') return reject(error) // Retry only if the ERROR is ERRCONNECT | ||
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 resolve(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 | ||
return reject(`Retrying server error: ${body.message}`); // Change this after discussion with Harsh | ||
} | ||
|
||
return reject(`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(){} // e | ||
}); | ||
} | ||
|
||
/** | ||
* @private | ||
*/ | ||
_buildRequestObject = (request)=> { | ||
return { | ||
url: request.url, // buildUrl(request.url), // @TODO : Ask utkarsh if we have a function that combines a url object to | ||
method: request.method, | ||
path: '/ Collections / Create Collections / Create a collection into personal workspace' // TODO - Find out where is this used | ||
} | ||
} | ||
|
||
/** | ||
* @private | ||
* @param {Object} response | ||
* @param {Boolean} skipResponse | ||
* | ||
* @returns {Object} | ||
*/ | ||
_buildResponseObject = (response, skipResponse) => { | ||
if(skipResponse) return | ||
|
||
return { | ||
code: response.code, | ||
name: response.status, | ||
time: response.responseTime, | ||
size: response.responseSize, | ||
} | ||
} | ||
|
||
/** | ||
* @private | ||
* @param {Array} assertions | ||
* | ||
* @returns {Array} | ||
*/ | ||
_buildTestObject = (assertions) => { | ||
const tests = [] | ||
|
||
assertions && assertions.forEach(assert => { | ||
tests.push({ | ||
name: assert.assertion, | ||
error: assert.error || {}, // @TODO - Understand This is the entire object with params - name , index, test, message , stack | ||
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, | ||
ref: 'uuid4', // @TODO : Figure out how is this used ?? | ||
request: _buildRequestObject(req.request), | ||
response: req.response ? _buildResponseObject(req.response, skipResponse): null, | ||
requestError: req.requestError || '',// @TODO - 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) => { | ||
const run = { | ||
name: runOptions.collection.name || '', | ||
status:runSummary.run.error ? 'failed': '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.collectionUID || '', | ||
environment: runOptions.environmentUID || '', | ||
iterations: _executionToIterationConverter(runSummary.run.executions, runOptions.iterationCount , runOptions.publishSkipResponse), | ||
delay: runOptions.delayRequest, | ||
persist: false, | ||
saveResponse: !runOptions.publishSkipResponse, | ||
dataFile: runOptions.collectionUID && runOptions.environmentUID ? runOptions.iterationData : null, | ||
workspace: runOptions.publish, | ||
currentIteration: runOptions.iterationCount,// Tells how many iterations where there , | ||
folder: '' // @TODO - What is this ? | ||
} | ||
return JSON.stringify(run); | ||
} | ||
|
||
/** | ||
* @public | ||
* Starts the run upload process | ||
* | ||
* @param {*} runSummary | ||
* @param {*} runOptions | ||
* | ||
* @returns { Promise } | ||
*/ | ||
uploadRunToPostman = (runSummary, runOptions) => { | ||
|
||
let runPayload; | ||
|
||
runOptions.collectionUID = util.extractResourceUID(runOptions.collection); | ||
runOptions.environmentUID = util.extractResourceUID(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. | ||
_executionToIterationConverter, | ||
_buildPostmanUploadPayload, | ||
_buildRequestObject, | ||
_buildResponseObject, | ||
_buildTestObject, | ||
_uploadWithRetry, | ||
_upload, | ||
|
||
}; |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -284,6 +284,54 @@ 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} | ||||||
*/ | ||||||
extractResourceUID: function (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. Let's call it |
||||||
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 ''; | ||||||
} | ||||||
|
||||||
const uidExtractionPattern = /https:\/\/api\.getpostman\.com\/[collections|environments]+\/(\w+)\?apikey=\w+/g; | ||||||
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 regex seems to be incorrect. It works for cases like:
And it doesn't work for cases:
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. Do we even need to check for |
||||||
|
||||||
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.