-
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 10 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 | ||||
---|---|---|---|---|---|---|
|
@@ -61,9 +61,12 @@ program | |||||
.option('--cookie-jar <path>', 'Specify the path to a custom cookie jar (serialized tough-cookie JSON) ') | ||||||
.option('--export-cookie-jar <path>', 'Exports the cookie jar to a file after completing the run') | ||||||
.option('--verbose', 'Show detailed information of collection run and each request sent') | ||||||
.option('-p, --publish-workspace <workspace-id>', 'Publishes to given workspace') | ||||||
.option('--publish-workspace-skip-response', 'Skip responses (headers, body, etc) while uploading newman run to Postman') | ||||||
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. What does "workspace" mean in this option? Could it simply be 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. Changed all the CLI parameter names |
||||||
.option('--publish-retry', 'Number of times newman can try to publish before safely erroring out.') | ||||||
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.
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. Makes sense |
||||||
.option('--publish-upload-timeout', 'Timeout for uploading newman runs to postman', util.cast.integer, 0) | ||||||
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. Could simply be |
||||||
.action((collection, command) => { | ||||||
let options = util.commanderToObject(command), | ||||||
|
||||||
// parse custom reporter options | ||||||
reporterOptions = util.parseNestedOptions(program._originalArgs, '--reporter-', options.reporters); | ||||||
|
||||||
|
@@ -74,14 +77,15 @@ program | |||||
acc[key] = _.assignIn(value, reporterOptions._generic); // overrides reporter options with _generic | ||||||
}, {}); | ||||||
|
||||||
newman.run(options, function (err, summary) { | ||||||
newman.run(options, function (err, summary, newmanStatus) { | ||||||
const runError = err || summary.run.error || summary.run.failures.length; | ||||||
|
||||||
if (err) { | ||||||
console.error(`error: ${err.message || err}\n`); | ||||||
err.friendly && console.error(` ${err.friendly}\n`); | ||||||
} | ||||||
runError && !_.get(options, 'suppressExitCode') && process.exit(1); | ||||||
|
||||||
runError && !newmanStatus.resultUploaded && !_.get(options, 'suppressExitCode') && process.exit(1); | ||||||
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 incorrect. If the 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 should be an 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.
Suggested change
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. Fixed this |
||||||
}); | ||||||
}); | ||||||
|
||||||
|
@@ -139,7 +143,7 @@ function run (argv, callback) { | |||||
|
||||||
// This hack has been added from https://github.com/nodejs/node/issues/6456#issue-151760275 | ||||||
// @todo: remove when https://github.com/nodejs/node/issues/6456 has been fixed | ||||||
(Number(process.version[1]) >= 6) && [process.stdout, process.stderr].forEach((s) => { | ||||||
Number(process.version[1]) >= 6 && [process.stdout, process.stderr].forEach((s) => { | ||||||
UmedJadhav marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
s && s.isTTY && s._handle && s._handle.setBlocking && s._handle.setBlocking(true); | ||||||
}); | ||||||
|
||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -9,6 +9,7 @@ var _ = require('lodash'), | |||||
getOptions = require('./options'), | ||||||
exportFile = require('./export-file'), | ||||||
util = require('../util'), | ||||||
uploadRunToPostman = require('./upload-run'), | ||||||
|
||||||
/** | ||||||
* This object describes the various events raised by Newman, and what each event argument contains. | ||||||
|
@@ -95,7 +96,12 @@ module.exports = function (options, callback) { | |||||
var emitter = new EventEmitter(), // @todo: create a new inherited constructor | ||||||
runner = new runtime.Runner(), | ||||||
stopOnFailure, | ||||||
entrypoint; | ||||||
entrypoint, | ||||||
uploadConfig = _.pick(options, ['postmanApiKey', 'publishWorkspace', 'publishWorkspaceSkipResponse', | ||||||
'publishUploadTimeout', 'publishRetry']); | ||||||
|
||||||
uploadConfig.collectionUID = util.extractResourceUID(options.collection); | ||||||
uploadConfig.environmentUID = util.extractResourceUID(options.environment); | ||||||
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. The concept of |
||||||
|
||||||
// get the configuration from various sources | ||||||
getOptions(options, function (err, options) { | ||||||
|
@@ -302,6 +308,9 @@ module.exports = function (options, callback) { | |||||
// in case runtime faced an error during run, we do not process any other event and emit `done`. | ||||||
// we do it this way since, an error in `done` callback would have anyway skipped any intermediate | ||||||
// events or callbacks | ||||||
|
||||||
// TODO: Add analytics call here | ||||||
|
||||||
if (err) { | ||||||
emitter.emit('done', err, emitter.summary); | ||||||
callback(err, emitter.summary); | ||||||
|
@@ -340,10 +349,13 @@ module.exports = function (options, callback) { | |||||
}); | ||||||
}); | ||||||
|
||||||
asyncEach(emitter.exports, exportFile, function (err) { | ||||||
asyncEach(emitter.exports, exportFile, async function (err) { | ||||||
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. Using callbacks and async/await in the same function seems like an anti-pattern. I would recommend if we stick to callbacks to maintain consistency. Thoughts? |
||||||
// we now trigger actual done event which we had overridden | ||||||
emitter.emit('done', err, emitter.summary); | ||||||
callback(err, emitter.summary); | ||||||
|
||||||
var resultUploaded = await new uploadRunToPostman(uploadConfig, emitter.summary).start(); | ||||||
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 could be just a method call instead if we only ever need one method from the instance of the class. Why create a class for this at all? 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.
Suggested change
|
||||||
|
||||||
callback(err, emitter.summary, { resultUploaded }); | ||||||
}); | ||||||
} | ||||||
}); | ||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,133 @@ | ||||||
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'), | ||||||
ERROR_GO_TO_LINK = '<TBA>', | ||||||
UPLOAD_RUN_API_URL = '<TBA>' | ||||||
|
||||||
class uploadRunToPostman{ | ||||||
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 need to create a class for this. Class is needed only if you want to abstract some data and use the instance in multiple places. In our case, we only need a single method. Sidenote: The name of the class always starts with a capital letter. |
||||||
|
||||||
constructor(uploadConfig, runSummary){ | ||||||
this.uploadConfig = uploadConfig, | ||||||
this.runSummary = runSummary; | ||||||
} | ||||||
|
||||||
/** | ||||||
* TBA after discussing with Harsh | ||||||
* @param {*} runSummary | ||||||
* @param {*} collectionUID | ||||||
* @param {*} environmentUID | ||||||
*/ | ||||||
buildRunObject = ( runSummary, collectionUID , environmentUID ) => {} | ||||||
|
||||||
/** | ||||||
* @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. Why does it need to return a function? Couldn't this function itself be used in 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. Hint: .bind() |
||||||
return async(bail) => await new Promise((resolve, reject) => request.post(uploadOptions , (error, response, body) => { | ||||||
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 you're already returning a Promise, you don't need to
Suggested change
|
||||||
if(error){ | ||||||
if(error.code === 'ECONNREFUSED') return reject(error) // Retry only if the ERROR is ERRCONNECT | ||||||
return bail(error); // For other errors , dont retry | ||||||
} | ||||||
|
||||||
if(200 <= response.statusCode && response.statusCode <= 299){ | ||||||
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 weird. We should know the discreet status codes that are returned by the API and check on those instead of just checking the range. |
||||||
return resolve(body); | ||||||
} | ||||||
|
||||||
if( 400 <= response.statusCode && response.statusCode<= 499){ | ||||||
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. Similar as above:
|
||||||
|
||||||
if(response.statusCode === 404){ | ||||||
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. Inconsistent spacing.
Suggested change
|
||||||
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 | ||||||
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. Similar as above:
|
||||||
return reject(`Retrying because of server error: ${body.message}`); | ||||||
} | ||||||
|
||||||
return reject(); // 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,{ | ||||||
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. Weird spacing.
Suggested change
|
||||||
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 | ||||||
}); | ||||||
} | ||||||
|
||||||
/** | ||||||
* @public | ||||||
* Starts the run upload process | ||||||
* | ||||||
* @returns {Boolean} Indicate if run upload was successful | ||||||
*/ | ||||||
start = async () => { | ||||||
if(!this.uploadConfig.publishWorkspace){ | ||||||
return true; | ||||||
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. Shouldn't this check be made before calling the Also, since this is an async function. It would be better to reject the returned promise by 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. Moved the check to |
||||||
} | ||||||
|
||||||
if(!this.uploadConfig.postmanApiKey){ | ||||||
print.lf('Postman API Key was not provided , cannot upload newman run w/o Postman API Key'); | ||||||
return false; | ||||||
} | ||||||
|
||||||
print.lf('Uploading newman run to Postman'); | ||||||
|
||||||
const run = this.buildRunObject(runSummary, uploadConfig.publishWorkspaceSkipResponse); | ||||||
|
||||||
const uploadOptions = { | ||||||
url: UPLOAD_RUN_API_URL, | ||||||
body: JSON.stringify(run), | ||||||
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.
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. Added a try catch block |
||||||
headers: { | ||||||
'Content-Type': 'application/json', | ||||||
'X-API-Header': uploadConfig.postmanApiKey | ||||||
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. Is this the correct key for key header? Shouldn't it be 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. Fixed |
||||||
} | ||||||
}, | ||||||
retryOptions = { | ||||||
maxRetries: uploadConfig.publishRetry, | ||||||
totalTimeout: uploadConfig.publishUploadTimeout, | ||||||
retryDelayMultiplier: 2, | ||||||
maxRetryDelay : 64, | ||||||
addJitter: true | ||||||
}; | ||||||
|
||||||
try{ | ||||||
const response = await this._uploadWithRetry(this._upload(uploadOptions), retryOptions) | ||||||
|
||||||
print.lf(`Uploaded the newman run to postman. | ||||||
Visit ${response.message} to view the results in postman web`); | ||||||
return true; | ||||||
|
||||||
} catch(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. Let the caller catch the error and show appropriate 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. Reworked the flow |
||||||
print.lf(`Unable to upload the results to Postman: | ||||||
Reason: ${error.message} | ||||||
You can find solutions to common upload errors here: ${ERROR_GO_TO_LINK}`); | ||||||
return false; | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
module.exports = uploadRunToPostman; |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -284,6 +284,37 @@ 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]; | ||||||
} | ||||||
}; | ||||||
|
||||||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -40,6 +40,7 @@ | |
"license": "Apache-2.0", | ||
"dependencies": { | ||
"async": "3.2.1", | ||
"async-retry": "^1.3.3", | ||
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. The |
||
"chardet": "1.3.0", | ||
"cli-progress": "3.9.0", | ||
"cli-table3": "0.6.0", | ||
|
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.
Could it simply be
--publish
? From the perspective of user, I feel if we have a option like--publish-workspace
, we would have another option--publish
which enables publishing. But this option itself is doing that, no?Also,
publish-workspace
feels a little like we're publishing a workspace. 😅Thoughts?
cc @codenirvana
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.
I agree, let's keep it
--publish
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.
and
--publish-skip-response