diff --git a/README.md b/README.md index 8715266..66fd9ce 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,10 @@ When a config file is present, it is possible to override the values using the c There is currently no support for encrypting the config file but normal file system security should make it possible to restrict reading it to the user running the tool. +TestEngine-CLI will return with exit status code 0 if it successfully carried out the operation. It will return with +exit status code 1 if it failed to execute the command or parts of, including if a test job fails when it tries to run +a project or some user couldn't be added while importing. + ## Test Jobs The tool can submit test jobs, list jobs which has been submitted (only admins can see other users' jobs) and purge old jobs from the server. diff --git a/bin/auditlog_functions.js b/bin/auditlog_functions.js index 66b34c8..88f453d 100644 --- a/bin/auditlog_functions.js +++ b/bin/auditlog_functions.js @@ -4,15 +4,19 @@ const request = require('superagent'); const config = require('./config').config; const sprintf = require('sprintf-js').sprintf; const util = require('./shared_utils'); +const process = require('process'); module.exports.dispatcher = function (args) { - if (args.length === 0) - return printModuleHelp(); + if (args.length === 0) { + printModuleHelp(); + process.exit(1); + } switch (args[0].toLowerCase()) { case 'dump': if (args.length < 1) { printModuleHelp(); + process.exit(1); } else { let options = util.optionsFromArgs(args.splice(1), [ 'format', 'date', 'user', 'limit', '=iso']); @@ -24,8 +28,7 @@ module.exports.dispatcher = function (args) { printModuleHelp(); break; default: - util.error("Unknown operatation"); - break; + util.printErrorAndExit("Unknown operation"); } }; @@ -61,18 +64,7 @@ function dumpAuditLog(options) { .type('application/json') .send() .end((err, result) => { - if (err !== null) { - if ('code' in err) { - if (err.code === 'ECONNREFUSED') { - util.error(sprintf("Connection refused: %s:%d", err.address, err.port)); - } else { - util.error(sprintf("Error: %s:%s", err.code, err.message)); - } - } else { - util.output(err.status + ': ' + err.message); - } - return 1 - } + util.handleError(err); if (format === 'json') { if ('limit' in options) { util.output(JSON.stringify(result.body.slice(0, options['limit']))); @@ -82,7 +74,7 @@ function dumpAuditLog(options) { } else { printAuditLogHeader(format); let counter = 0; - let maxItems = 'limit' in options?options['limit']:1e10; + let maxItems = 'limit' in options ? options['limit'] : 1e10; let isoTime = 'iso' in options; for (let line of result.body) { printAuditLogLine(line, format, isoTime); diff --git a/bin/jobs_functions.js b/bin/jobs_functions.js index 1220d4e..f1e132b 100644 --- a/bin/jobs_functions.js +++ b/bin/jobs_functions.js @@ -5,11 +5,14 @@ const request = require('superagent'); const config = require('./config').config; const sprintf = require('sprintf-js').sprintf; const fs = require('fs'); +const process = require('process'); module.exports = { dispatcher: function (args) { - if (args.length === 0) - return printModuleHelp(); + if (args.length === 0) { + printModuleHelp(); + process.exit(1); + } switch (args[0].toLowerCase()) { case 'list': { @@ -21,6 +24,7 @@ module.exports = { case 'cancel': { if (args.length < 2) { printModuleHelp(); + process.exit(1); } else { terminateTestJob(args[1]); } @@ -29,6 +33,7 @@ module.exports = { case 'delete': { if (args.length < 2) { printModuleHelp(); + process.exit(1); } else { deleteTestJob(args[1]); } @@ -37,14 +42,16 @@ module.exports = { case 'status': { if (args.length < 2) { printModuleHelp(); + process.exit(1); } else { reportForTestJob(args[1]); } break; } case 'report': { - if (args.length < 3) { + if (args.length < 4) { printModuleHelp(); + process.exit(1); } else { let jobId = args[args.length - 1]; let options = util.optionsFromArgs(args.splice(1), [ @@ -56,8 +63,9 @@ module.exports = { case 'printreport': { if (args.length < 2) { printModuleHelp(); + process.exit(1); } else { - const testJobId = args[ args.length - 1 ]; + const testJobId = args[args.length - 1]; printReport(testJobId); } break; @@ -76,8 +84,7 @@ module.exports = { printModuleHelp(); break; default: - util.error("Unknown operatation"); - break; + util.printErrorAndExit("Unknown operation"); } }, reportForTestJob: reportForTestJob @@ -96,8 +103,8 @@ function printModuleHelp() { } function terminateTestJob(testjobId) { - let url = config.server + '/api/v1/testjobs'+ '/' + testjobId; - util.output('Canceling job: '+testjobId); + let url = config.server + '/api/v1/testjobs' + '/' + testjobId; + util.output('Canceling job: ' + testjobId); request.delete(url) .auth(config.username, config.password) .accept('application/junit+xml') @@ -105,16 +112,9 @@ function terminateTestJob(testjobId) { .end((err, result) => { if (err !== null) { if (('status' in err) && ('message' in result.body)) { - switch (err['status']) { - case 403: - util.error(err['status'] + ': ' + result.body['message']); - break; - default: - util.error(err['status'] + ': ' + result.body['message']); - return; - } + util.printErrorAndExit(err['status'] + ': ' + result.body['message']); } else { - util.error(err); + util.printErrorAndExit(err); } } else { util.output('Successfully canceled job'); @@ -123,8 +123,8 @@ function terminateTestJob(testjobId) { } function deleteTestJob(testjobId) { - let url = config.server + '/api/v1/testjobs'+ '/' + testjobId + '/delete'; - util.output('Deleting job: '+testjobId); + let url = config.server + '/api/v1/testjobs' + '/' + testjobId + '/delete'; + util.output('Deleting job: ' + testjobId); request.delete(url) .auth(config.username, config.password) .accept('application/junit+xml') @@ -133,18 +133,14 @@ function deleteTestJob(testjobId) { if (err !== null) { if (('status' in err) && ('message' in result.body)) { switch (err['status']) { - case 403: - util.error(err['status'] + ': ' + result.body['message']); - break; case 404: - util.output(`${err['status']}: Testjob not found`); + util.printErrorAndExit(`${err['status']}: Testjob not found`); break; default: - util.error(err['status'] + ': ' + result.body['message']); - return; + util.printErrorAndExit(err['status'] + ': ' + result.body['message']); } } else { - util.error(err); + util.printErrorAndExit(err); } } else { util.output('Successfully deleted job'); @@ -152,7 +148,7 @@ function deleteTestJob(testjobId) { }) } -function printReport (testjobId) { +function printReport(testjobId) { const endPoint = config.server + '/api/v1/testjobs'; const url = endPoint + '/' + testjobId + '/report'; util.output(`Printing report for ${testjobId} ...`); @@ -161,16 +157,17 @@ function printReport (testjobId) { .accept('application/json') .send() .end((err, res) => { - const jsonReport = res.body; if (err !== null) { if (err.status === 404) { - util.output(`Testjob with id ${testjobId} not found`); + util.printErrorAndExit(`Testjob with id ${testjobId} not found`); } else { - util.output(err.status + ': ' + err.message); + util.printErrorAndExit(err.status + ': ' + err.message); } - return 1 } - util.output(utility.inspect(jsonReport, { showHidden: false, depth: null})); + if (res) { + const jsonReport = res.body; + util.output(utility.inspect(jsonReport, {showHidden: false, depth: null})); + } }); } @@ -208,17 +205,13 @@ function reportForTestJob(testjobId, outputFolder, fileName, format) { reportFilename += '.pdf'; break; default: - util.error("Invalid format: " + format); - contentType = ''; - break; + util.printErrorAndExit("Invalid format: " + format); } } else { - util.error("Output folder exists but is not a directory"); - return; + util.printErrorAndExit("Output folder exists but is not a directory"); } } if (contentType !== '') { - let success = true; let url = endPoint + '/' + testjobId + '/report'; let stream; let reportFileName; @@ -231,10 +224,13 @@ function reportForTestJob(testjobId, outputFolder, fileName, format) { .accept(contentType) .on('response', function (response) { if (response.status !== 200) { - success = false; if (reportFileName) { fs.unlinkSync(reportFileName) } + util.printErrorAndExit(`Status code: ${response.status}`); + } else { + util.output('Report created successfully'); + process.exit(0); } }).send(); if (stream) { @@ -245,16 +241,9 @@ function reportForTestJob(testjobId, outputFolder, fileName, format) { util.output('Status of job ' + testjobId + ': ' + result.body.status); } else { if (('status' in err) && ('message' in result.body)) { - switch (err['status']) { - case 403: - util.error(err['status'] + ': ' + result.body['message']); - break; - default: - util.error(err['status'] + ': ' + result.body['message']); - return; - } + util.printErrorAndExit(err['status'] + ': ' + result.body['message']); } else { - util.error(err); + util.printErrorAndExit(err); } } }) @@ -271,8 +260,7 @@ function listJobs(options) { .send() .end((err, res) => { if (err !== null) { - util.output(err.status + ': ' + err.message); - return 1 + util.printErrorAndExit(err.status + ': ' + err.message); } let dataFromServer = res.body; if (Array.isArray(dataFromServer) && 'status' in options) { @@ -293,8 +281,7 @@ function listJobs(options) { dumpArrayAsJson(dataFromServer); break; default: - util.error('Unrecognized format'); - break; + util.printErrorAndExit('Unrecognized format'); } }); } @@ -308,7 +295,7 @@ function pruneJobs(options) { let date = new Date(parseInt(matchResult[1]), parseInt(matchResult[2]) - 1, parseInt(matchResult[3])); let tmpDate = date.toISOString(); tmpDate = tmpDate.replace('.000Z', 'Z'); - url += '?before='+encodeURIComponent(tmpDate); + url += '?before=' + encodeURIComponent(tmpDate); } } request.delete(url) @@ -319,17 +306,16 @@ function pruneJobs(options) { if (err !== null) { if ('code' in err) { if (err.code === 'ECONNREFUSED') { - util.error(sprintf("Connection refused: %s:%d", err.address, err.port)); + util.printErrorAndExit(sprintf("Connection refused: %s:%d", err.address, err.port)); } else { - util.error(sprintf("Error: %s:%s", err.code, err.message)); + util.printErrorAndExit(sprintf("Error: %s:%s", err.code, err.message)); } } else { if ('message' in res.body) - util.output(res.body['message']); + util.printErrorAndExit(res.body['message']); else - util.output(err.status + ': ' + err.message); + util.printErrorAndExit(err.status + ': ' + err.message); } - return 1 } let jobsPruned = JSON.parse(res.request.response.body); util.output("Pruned " + jobsPruned + " jobs from the database.") diff --git a/bin/license_functions.js b/bin/license_functions.js index e3f3940..0fcbc82 100644 --- a/bin/license_functions.js +++ b/bin/license_functions.js @@ -5,10 +5,13 @@ const config = require('./config').config; const util = require('./shared_utils'); const fs = require('fs'); const path = require('path'); +const process = require('process'); module.exports.dispatcher = function (args) { - if (args.length === 0) - return printModuleHelp(); + if (args.length === 0) { + printModuleHelp(); + process.exit(1); + } switch (args[0].toLowerCase()) { case 'install': { @@ -20,6 +23,7 @@ module.exports.dispatcher = function (args) { 'email']); if (args.length < 2) { printModuleHelp(); + process.exit(1); } else { installLicense(options, args[args.length - 1]); } @@ -28,6 +32,7 @@ module.exports.dispatcher = function (args) { case 'uninstall': if (args.length < 1) { printModuleHelp(); + process.exit(1); } else { uninstallLicense() } @@ -40,8 +45,7 @@ module.exports.dispatcher = function (args) { printModuleHelp(); break; default: - util.error("Unknown operatation"); - break; + util.printErrorAndExit("Unknown operation"); } }; @@ -68,9 +72,7 @@ function installLicense(options, licenseOrLicenseServer) { installFixedLicense(options, licenseOrLicenseServer); break; default: - util.error("Error: Specifying fixed or floating license is mandatory"); - return; - + util.printErrorAndExit("Error: Specifying fixed or floating license is mandatory"); } } @@ -88,11 +90,10 @@ function uninstallLicense() { if ('status' in err) { switch (err['status']) { case 403: - util.error("User doesn't have enough credentials to uninstall a license"); + util.printErrorAndExit("User doesn't have permission to uninstall a license"); break; default: - util.error(err['status'] + ': ' + result.body['message']); - return; + util.printErrorAndExit(err['status'] + ': ' + result.body['message']); } } } @@ -124,21 +125,21 @@ function installFixedLicense(options, licenseFile) { if ('status' in err) { switch (err['status']) { case 403: - util.error("User doesn't have enough credentials to install a license"); + util.printErrorAndExit("User doesn't have permission to install a license"); break; case 400: - util.error("Failed to install license, error: " + result.body['message']); + util.printErrorAndExit("Failed to install license, error: " + result.body['message']); break; default: if ('message' in result.body) { - util.error(err['status'] + ': ' + result.body['message']); + util.printErrorAndExit(err['status'] + ': ' + result.body['message']); } else { - util.error(err['status'] + ': ' + err['message']); + util.printErrorAndExit(err['status'] + ': ' + err['message']); } return; } } else { - util.error(err); + util.printErrorAndExit(err); } } }); @@ -146,8 +147,10 @@ function installFixedLicense(options, licenseFile) { readStream.on('error', function (err) { util.error("Error: " + err.message); let ext = path.extname(licenseFile).toLowerCase(); - if ((ext !== '.key') && (ext !== '.zip')) + if ((ext !== '.key') && (ext !== '.zip')) { util.error('"' + licenseFile + '" does not seem to be a .zip or .key file'); + } + process.exit(1); }); } @@ -171,17 +174,16 @@ function installFloatingLicense(licenseServerHost, licenseServerPort) { if ('status' in err) { switch (err['status']) { case 403: - util.error("User doesn't have enough credentials to install a license"); + util.printErrorAndExit("User doesn't have enough credentials to install a license"); break; case 400: - util.error("Failed to install license, error: " + result.body['message']); + util.printErrorAndExit("Failed to install license, error: " + result.body['message']); break; default: - util.error(err['status'] + ': ' + err['message']); - return; + util.printErrorAndExit(err['status'] + ': ' + err['message']); } } else { - util.error(err); + util.printErrorAndExit(err); } } }); @@ -203,17 +205,16 @@ function showLicenseInfo() { switch (err['status']) { case 401: case 403: - util.error("User doesn't have credentials to show license"); + util.printErrorAndExit("User doesn't have credentials to show license"); break; case 404: - util.error("No license installed"); + util.printErrorAndExit("No license installed"); break; default: - util.error(err['status'] + ': ' + err['message']); - return; + util.printErrorAndExit(err['status'] + ': ' + err['message']); } } else { - util.error(err); + util.printErrorAndExit(err); } } }); @@ -226,4 +227,4 @@ function licenseInfoToString(licenseInfo) { "Organization: " + licenseInfo['organization'] + "\n" + "Expires: " + licenseInfo['expireDate'] + "\n" + "Max Concurrent TestJobs: " + licenseInfo['maxConcurrentJobs']; -} \ No newline at end of file +} diff --git a/bin/readyapi_project.js b/bin/readyapi_project.js index e5713a8..7af487f 100644 --- a/bin/readyapi_project.js +++ b/bin/readyapi_project.js @@ -26,10 +26,10 @@ module.exports.parse = function (filename) { let jsonProject = parser.parse(fs.readFileSync(filename, {encoding: 'utf8'}), xmlParserOptions); if (!('con:soapui-project' in jsonProject)) { - throw "'" + filename + "' does not seem to be a ReadyAPI project file"; + util.printErrorAndExit(`${filename} does not seem to be a ReadyAPI project file`); } - if ('con:encryptedContent' in jsonProject['con:soapui-project']) { - throw "'" + filename + "' is encrypted and may have to be sent to the server as a zip file"; + if ('con:encryptedContent' in jsonProject[ 'con:soapui-project' ]) { + util.printErrorAndExit(`${filename} is encrypted and may have to be sent to the server as a zip file`); } if ("con:soapui-project" in jsonProject) { result = postProcessStructure(jsonProject); @@ -41,29 +41,26 @@ module.exports.parse = function (filename) { result['resourceRoot'] = resourceRoot; result['projectFiles'] = [filename]; } else { - util.error("File doesn't seem to be a ReadyAPI project"); - return null; + util.printErrorAndExit("File doesn't seem to be a ReadyAPI project"); } return result; }; module.exports.parseComposite = function (pathname) { - let result; - let jsonProject; if (!fs.lstatSync(pathname).isDirectory()) { - throw pathname + ' doesn\'t point to a directory'; + util.printErrorAndExit(`${pathname} doesn't point to a directory`); } if (!fs.existsSync(pathname + path.sep + 'settings.xml')) { - throw pathname + ' doesn\'t point to a composite project (settings.xml missing)'; + util.printErrorAndExit(`${pathname} doesn't point to a composite project (settings.xml missing)`); } if (!fs.existsSync(pathname + path.sep + 'element.order')) { - throw pathname + ' doesn\'t point to a composite project (element.order missing)'; + util.printErrorAndExit(`${pathname} doesn't point to a composite project (element.order missing)`); } if (!fs.existsSync(pathname + path.sep + 'project.content')) { - throw pathname + ' doesn\'t point to a composite project (project.content missing)'; + util.printErrorAndExit(`${pathname} doesn't point to a composite project (project.content missing)`); } let filename = pathname + path.sep + 'settings.xml'; jsonProject = parser.parse(fs.readFileSync(filename, {encoding: 'utf8'}), xmlParserOptions); diff --git a/bin/run_functions.js b/bin/run_functions.js index 13eb01e..c8f8c3c 100644 --- a/bin/run_functions.js +++ b/bin/run_functions.js @@ -17,7 +17,8 @@ const utility = require('util'); module.exports.dispatcher = function (args) { if (args.length === 0) { - return printModuleHelp(); + printModuleHelp(); + process.exit(1); } let argsWithoutFilename = args.splice(1, args.length - 2); let options = util.optionsFromArgs(argsWithoutFilename, [ @@ -40,9 +41,7 @@ module.exports.dispatcher = function (args) { 'proxyPassword', 'projectPassword']); - if (conflictingOptionsCheck(options) === false) { - return; - } + conflictingOptionsCheck(options); switch (args[0].toLowerCase()) { case 'project': runProject(args[args.length - 1], options); @@ -51,8 +50,7 @@ module.exports.dispatcher = function (args) { printModuleHelp(); break; default: - util.error("Unknown operation"); - break; + util.printErrorAndExit("Unknown operation"); } }; @@ -67,22 +65,17 @@ function printModuleHelp() { function conflictingOptionsCheck(options) { if (('securitytest' in options) && (('testcase' in options) || ('testsuite' in options))) { - util.error('Error: Parameters testsuite and testcase are not allowed when securitytest is used'); - return false; + util.printErrorAndExit('Error: Parameters testsuite and testcase are not allowed when securitytest is used'); } if ('tags' in options) { if (('testsuite' in options) || ('testcase' in options)) { - util.error('Error: tags cannot be used together with testcase/testsuite '); - return false; + util.printErrorAndExit('Error: tags cannot be used together with testcase/testsuite '); } } if (('testcase' in options) && !('testsuite' in options)) { util.error('Warning: Specifying testscase without testsuite can cause unpredictable results'); - return true; } - - return true; } function extractFilesFromJsonRepresentation(data, options) { @@ -149,7 +142,7 @@ function runProject(filename, options) { if (typeof err === 'string') { if (err.match(/is encrypted/)) { if (!('projectPassword' in options)) - util.error('Error: Submitting encrypted projects without projectPassword will not work'); + util.printErrorAndExit('Error: Submitting encrypted projects without projectPassword will not work'); else executeProject(filename, null, options); } @@ -157,7 +150,7 @@ function runProject(filename, options) { } } else { util.error("Cannot open file: " + filename); - + process.exit(1) } } @@ -249,7 +242,7 @@ function executeProject(filename, project, options) { async.series([ // First create a zip file, if needed. // - function (callback) { + function (callback) { if (!isZipFile && (project !== null) && ((files.length > 0) || (project['projectFiles'].length > 1))) { // We depend on files, we need to create and send a zip file let projectRootPath = ''; @@ -395,25 +388,25 @@ function executeProject(filename, project, options) { if (Array.isArray(result.body)) { util.error("Project cannot be accepted, files missing:"); for (let missingFile of result.body) { - util.error(' ' + missingFile['fileName']); + util.printErrorAndExit(' ' + missingFile['fileName']); } } break; case 400: - util.error('Error: ' + result.body['message']); + util.printErrorAndExit('Error: ' + result.body[ 'message' ]); break; default: callback(err); - return; } } else { util.error(err); + process.exit(100); } } callback(); }); }, - function (callback) { + function (callback) { if ('async' in options) { callback(); return; @@ -438,7 +431,6 @@ function executeProject(filename, project, options) { await util.sleep(200); }, function () { - // callback(); if ((websocket !== null) && (websocket.readyState !== 0)) { websocket.close(); } @@ -455,18 +447,19 @@ function executeProject(filename, project, options) { if (res) { if ('code' in res) { if (res.code === 'ECONNREFUSED') { - util.error(sprintf("Connection refused: %s:%d", res.address, res.port)); + util.printErrorAndExit(sprintf("Connection refused: %s:%d", res.address, res.port)); + } else if (res.code === 'ENOTFOUND') { + const { host, port } = res; + util.printErrorAndExit(`Host ${host}:${port} does not exist.`); } else { - util.error(res); - process.exit(1); + util.printErrorAndExit(res); } } else if ('status' in res) { if ('message' in res.response.body) { - util.error("Error: " + res.response.body['message']); + util.printErrorAndExit("Error: " + res.response.body['message']); } else { - util.error(res.response.text); + util.printErrorAndExit(res.response.text); } - process.exit(); } } else { if (!options.async || !('async' in options)) { @@ -476,6 +469,9 @@ function executeProject(filename, project, options) { if (config.showProgress) util.output(''); util.output("Result: " + status); + if(status === 'FAILED') { + process.exit(1); + } if ((jobId !== null) && ((status !== 'CANCELED') diff --git a/bin/shared_utils.js b/bin/shared_utils.js index 6991751..b0d033e 100644 --- a/bin/shared_utils.js +++ b/bin/shared_utils.js @@ -2,6 +2,7 @@ const config = require('./config').config; const process = require('process'); +const sprintf = require('sprintf-js').sprintf; module.exports.csvQuoteQuotes = function (stringValue) { return stringValue.replace(/["]/g, '""'); @@ -16,7 +17,7 @@ module.exports.booleanValue = function (unknownValue) { }; -module.exports.output = function (message, appendNewLine=true) { +module.exports.output = function (message, appendNewLine = true) { if (!config.quiet) { process.stdout.write(message); if (appendNewLine) @@ -118,3 +119,22 @@ module.exports.optionsFromArgs = function (args, validArguments = null) { return ret; }; +module.exports.handleError = function (err) { + if (err !== null) { + if ('code' in err) { + if (err.code === 'ECONNREFUSED') { + module.exports.printErrorAndExit(sprintf("Connection refused: %s:%d", err.address, err.port)); + } else { + module.exports.printErrorAndExit(sprintf("Error: %s:%s", err.code, err.message)); + } + } else { + module.exports.printErrorAndExit(err.status + ': ' + err.message); + } + } +} + +module.exports.printErrorAndExit = function (errorMessage) { + module.exports.error(errorMessage); + process.exit(1); +} + diff --git a/bin/testengine.js b/bin/testengine.js index 35d9aa7..690bf76 100755 --- a/bin/testengine.js +++ b/bin/testengine.js @@ -61,6 +61,7 @@ if (program.args.length > 0) { break; default: program.outputHelp(); + process.exit(1); break; } } diff --git a/bin/user_functions.js b/bin/user_functions.js index 3388d86..3bb7450 100644 --- a/bin/user_functions.js +++ b/bin/user_functions.js @@ -6,15 +6,18 @@ const sprintf = require('sprintf-js').sprintf; const csv = require('csvtojson'); const fs = require('fs'); const util = require('./shared_utils'); +const process = require('process'); -module.exports.dispatcher = function(args) { - if (args.length === 0) - return printModuleHelp(); +module.exports.dispatcher = function (args) { + if (args.length === 0) { + printModuleHelp(); + process.exit(1); + } switch (args[0].toLowerCase()) { case 'add': if (args.length < 3) { - util.error("Usage: testengine user add "); + util.printErrorAndExit("Usage: testengine user add "); } else { addUser(args[1], args[2]) } @@ -22,7 +25,7 @@ module.exports.dispatcher = function(args) { case 'import': if (args.length < 2) { - util.error("Usage: testengine user import "); + util.printErrorAndExit("Usage: testengine user import "); } else { importUsers(args[1]) } @@ -30,7 +33,7 @@ module.exports.dispatcher = function(args) { case 'edit': if (args.length < 3) { - util.error("Usage: testengine user edit [password=newpassword] [admin=true/false]"); + util.printErrorAndExit("Usage: testengine user edit [password=newpassword] [admin=true/false]"); } else { let options = util.optionsFromArgs(args.splice(2), [ 'password', 'admin']); @@ -50,7 +53,7 @@ module.exports.dispatcher = function(args) { case 'del': case 'delete': if (args.length < 2) { - return util.error("Usage: testengine user delete "); + util.printErrorAndExit("Usage: testengine user delete "); } else { deleteUser(args[1]); } @@ -59,8 +62,7 @@ module.exports.dispatcher = function(args) { printModuleHelp(); break; default: - util.error("Unknown operatation"); - break; + util.printErrorAndExit("Unknown operation"); } }; @@ -88,31 +90,31 @@ function addUser(username, password, isAdmin = false, silent = false, callback = .type('application/json') .send(payload) .end((err) => { - if (!silent) + if (!silent) { if (err !== null) { util.error('Failed to create ' + (isAdmin ? 'admin ' : '') + 'user "' + username + '"'); if ('code' in err) { if (err.code === 'ECONNREFUSED') { - util.error(sprintf("Connection refused: %s:%d", err.address, err.port)); + util.printErrorAndExit(sprintf("Connection refused: %s:%d", err.address, err.port)); } else { - util.error(sprintf("Error: %s:%s", err.code, err.message)); + util.printErrorAndExit(sprintf("Error: %s:%s", err.code, err.message)); } - return 1 } else { switch (err.status) { case 422: - util.error("Username or password is incorrect"); + util.printErrorAndExit(err.response.body.message); break; case 409: - util.error('User "' + username + '" already exists.'); + util.printErrorAndExit('User "' + username + '" already exists.'); break; default: - util.output(err.status + ': ' + err.message); + util.printErrorAndExit(err.status + ': ' + err.message); } } } else { util.output('Created ' + (isAdmin ? 'admin ' : '') + 'user "' + username + '"'); } + } if (callback) { callback(err) } @@ -121,6 +123,7 @@ function addUser(username, password, isAdmin = false, silent = false, callback = function importUsers(fileOrURL) { let urlRegExp = /^[a-z]{1,5}[:][/]{2}/; + let failedImport = false; let stream = null; if (fileOrURL.match(urlRegExp) !== null) { @@ -140,29 +143,37 @@ function importUsers(fileOrURL) { } } } - }).fromStream(stream) - .then((jsonObj) => { - for (let user of jsonObj) { - let isAdmin = false; - if ('admin' in user) { - isAdmin = user['admin']; - } - if (!('password' in user)) { - user['password'] = createRandomPassword(); - } - addUser(user['username'], user['password'], isAdmin, true, (err) => { - if (err == null) { - util.output(sprintf("%s,%s,%d", - util.csvQuoteQuotes(user['username']), - util.csvQuoteQuotes(user['password']), - user['admin'] ? 1 : 0)); + }).fromStream(stream).then((jsonObj) => { + for (let i = 0; i < jsonObj.length; ++i) { + let user = jsonObj[i]; + let isAdmin = false; + if ('admin' in user) { + isAdmin = user['admin']; + } + if (!('password' in user)) { + user['password'] = createRandomPassword(); + } + addUser(user['username'], user['password'], isAdmin, true, (err) => { + if (err == null) { + util.output(sprintf("%s,%s,%d", + util.csvQuoteQuotes(user['username']), + util.csvQuoteQuotes(user['password']), + user['admin'] ? 1 : 0)); + } else { + util.error('User ' + user['username'] + ' could not be imported'); + if (err.response) { + util.error(err.status + ': ' + err.response.body.message); } else { - util.error('User ' + user['username'] + ' could not be imported'); - util.error(err.status + ': ' + err.message); + util.error(err); } - }); - } - }) + failedImport = true; + } + if (failedImport && i === jsonObj.length - 1) { + process.exit(1); + } + }); + } + }); } function updateUser(username, options) { @@ -182,19 +193,8 @@ function updateUser(username, options) { .type('application/json') .send(payload) .end((err) => { - if (err !== null) { - if ('code' in err) { - if (err.code === 'ECONNREFUSED') { - util.error(sprintf("Connection refused: %s:%d", err.address, err.port)); - } else { - util.error(sprintf("Error: %s:%s", err.code, err.message)); - } - } else { - util.output(err.status + ': ' + err.message); - } - return 1 - } - util.output('User "' + username + '"successfully updated'); + util.handleError(err); + util.output('User "' + username + '" successfully updated'); }); } @@ -205,10 +205,9 @@ function deleteUser(username) { .send() .end((err) => { if (err !== null) { - util.output(err.status + ': ' + err.message); - return 1 + util.printErrorAndExit(err.status + ': ' + err.message); } - util.output('User "' + username + '"successfully deleted'); + util.output('User "' + username + '" successfully deleted'); }); } @@ -219,18 +218,7 @@ function listUsers(options) { .accept('application/json') .send() .end((err, res) => { - if (err !== null) { - if ('code' in err) { - if (err.code === 'ECONNREFUSED') { - util.error(sprintf("Connection refused: %s:%d", err.address, err.port)); - } else { - util.error(sprintf("Error: %s:%s", err.code, err.message)); - } - } else { - util.output(err.status + ': ' + err.message); - } - return 1 - } + util.handleError(err); switch (format) { case 'csv': dumpArrayAsCSV(res.body); @@ -240,7 +228,7 @@ function listUsers(options) { break; default: util.error('Unrecognized format'); - break; + process.exit(1) } }); } @@ -263,4 +251,4 @@ function dumpArrayAsCSV(array) { function createRandomPassword(length = 8) { return Math.random().toString(36).slice(-1 * length); -} \ No newline at end of file +} diff --git a/test/admin.config b/test/admin.config new file mode 100644 index 0000000..c93a43a --- /dev/null +++ b/test/admin.config @@ -0,0 +1,5 @@ +{ + "username": "admin", + "password": "password", + "host": "http://localhost:8080" +} diff --git a/test/regularUser.config b/test/regularUser.config new file mode 100644 index 0000000..0b24807 --- /dev/null +++ b/test/regularUser.config @@ -0,0 +1,5 @@ +{ + "username": "regular", + "password": "asdasd", + "host": "http://localhost:8080" +} diff --git a/test/runtimeerror.xml b/test/runtimeerror.xml new file mode 100644 index 0000000..6e8e79a --- /dev/null +++ b/test/runtimeerror.xml @@ -0,0 +1,62 @@ + + + + + + SEQUENTIAL + + + + + + + + + + + + + + + + + + + + + + + + + + // Sample event script to add custom HTTP header to all outgoing REST, SOAP and HTTP(S) calls +// This code is often used for adding custom authentication to ReadyAPI functional tests + +// If hardcoding the token, uncomment and change line 5 +// token = '4567' + +// If your token is parameterized in Project level custom property, uncomment line 8 +// token = request.parent.testCase.testSuite.project.getProperty('auth_token').getValue() + +// To modify all outgoing calls, remove comments from lines 11 to 16 +// headers = request.requestHeaders +// if (headers.containsKey('auth_token2') == false) { +// headers.put('auth_token2', token) +// request.requestHeaders = headers +// } + + + // Save all test step results into files +// Change the directory path in line 5 to a location where you want to store details +// then uncomment lines 5 to 10 + +// filePath = 'C:\\tempOutputDirectory\\' +// fos = new java.io.FileOutputStream(filePath + testStepResult.testStep.label + '.txt', true) +// pw = new java.io.PrintWriter(fos) +// testStepResult.writeTo(pw) +// pw.close() +// fos.close() + + + + diff --git a/test/slow.xml b/test/slow.xml new file mode 100644 index 0000000..1889298 --- /dev/null +++ b/test/slow.xml @@ -0,0 +1,62 @@ + + + + + + SEQUENTIAL + + + + + + 5000 + + + + + + + + + + + + + + + + + + + + // Sample event script to add custom HTTP header to all outgoing REST, SOAP and HTTP(S) calls +// This code is often used for adding custom authentication to ReadyAPI functional tests + +// If hardcoding the token, uncomment and change line 5 +// token = '4567' + +// If your token is parameterized in Project level custom property, uncomment line 8 +// token = request.parent.testCase.testSuite.project.getProperty('auth_token').getValue() + +// To modify all outgoing calls, remove comments from lines 11 to 16 +// headers = request.requestHeaders +// if (headers.containsKey('auth_token2') == false) { +// headers.put('auth_token2', token) +// request.requestHeaders = headers +// } + + + // Save all test step results into files +// Change the directory path in line 5 to a location where you want to store details +// then uncomment lines 5 to 10 + +// filePath = 'C:\\tempOutputDirectory\\' +// fos = new java.io.FileOutputStream(filePath + testStepResult.testStep.label + '.txt', true) +// pw = new java.io.PrintWriter(fos) +// testStepResult.writeTo(pw) +// pw.close() +// fos.close() + + + + diff --git a/test/successful.xml b/test/successful.xml new file mode 100644 index 0000000..279d826 --- /dev/null +++ b/test/successful.xml @@ -0,0 +1,74 @@ + + + + + + 1 + SEQUENTIAL + + + 1 + + + + + <xml-fragment/> + + http://localhost:8080 + + + No Authorization + No Authorization + + + + + + + + + + + + + + + + + + + + + + + // Sample event script to add custom HTTP header to all outgoing REST, SOAP and HTTP(S) calls +// This code is often used for adding custom authentication to ReadyAPI functional tests + +// If hardcoding the token, uncomment and change line 5 +// token = '4567' + +// If your token is parameterized in Project level custom property, uncomment line 8 +// token = request.parent.testCase.testSuite.project.getProperty('auth_token').getValue() + +// To modify all outgoing calls, remove comments from lines 11 to 16 +// headers = request.requestHeaders +// if (headers.containsKey('auth_token2') == false) { +// headers.put('auth_token2', token) +// request.requestHeaders = headers +// } + + + // Save all test step results into files +// Change the directory path in line 5 to a location where you want to store details +// then uncomment lines 5 to 10 + +// filePath = 'C:\\tempOutputDirectory\\' +// fos = new java.io.FileOutputStream(filePath + testStepResult.testStep.label + '.txt', true) +// pw = new java.io.PrintWriter(fos) +// testStepResult.writeTo(pw) +// pw.close() +// fos.close() + + + + diff --git a/test/testScript.js b/test/testScript.js new file mode 100644 index 0000000..9e13eb6 --- /dev/null +++ b/test/testScript.js @@ -0,0 +1,134 @@ +/** This script is used to test all commands in testengine. It will run most operations in testengine-cli with a few flag + * combinations and print it to screen. A tester can then read the output to see how the cli behaves. Together with the + * script there are a few readyapi-projects and login configurations for testengine. Assumes the testengine instance is + * running at localhost:8080 and has the logins admin/password, regularUser/asdasd. + * To run the script you need node.js. Ensure the working directory is the test-folder and run: + * node testScript.js + */ + +const {execSync, exec} = require('child_process'); + +const baseCli = 'node ../bin/testengine.js'; +const licenseServer = 'localhost:1194'; + +function startJob(projectPath) { + let startJob = [baseCli, 'run project', projectPath, '-c admin.config'].join(' '); + let extractTestJobId = baseCli + " jobs list -c admin.config -c admin.config|sed -n 3p |sed 's/ *$//g'|rev|cut -d ' ' -f 2|rev"; + exec(startJob); + let jobId = execSync(extractTestJobId).toString().trim(); + return jobId; +} + +function runAllCombinations(commands, flags, fn) { + for (let i = 0; i < commands.length; ++i) { + for (let j = 0; j < flags.length; ++j) { + fn(commands[i], flags[j]); + } + } +} + +function runCli(command, flag, jobId = '') { + let cli = [baseCli, command, jobId, flag].join(' '); + console.log('\n' + cli); + try { + console.log(execSync(cli).toString() + 'exit status 0'); + } catch (error) { + console.log('' + error.stdout + 'exit status ' + error.status); + } +} + +function startSlowJobAndThenRunCli(command, flag) { + let testJobId = startJob('slow.xml'); + runCli(command, flag, testJobId); +} + +let commands = ['auditlog', + 'auditlog dump', + 'auditlog help', + + 'user list', + 'user add', + 'user add hej', + 'user add hej pw', + 'user add hej password', + 'user add hej password moar', + 'user edit hej2', + 'user edit hej admin=true', + 'user edit hej admin=false', + 'user edit hej password=pw', + 'user edit hej nope=asda', + 'user edit nope', + 'user delete', + 'user delete nope', + 'user delete hej', + 'user import users.csv', + + 'run', + 'run help', + 'run project', + 'run project help', + 'run project noneExisting.xml', + 'run project runtimeerror.xml', + 'run project validationerror.xml', + 'run project successful.xml', + 'run project successful.xml testsuite="TestSuite 1"', + 'run project successful.xml testsuite="TestSuite 1" securitytest="blah"', + 'run project successful.xml testsuite="TestSuite 1" testcase="TestCase 1"', + 'run project successful.xml testcase="TestCase 1"', + 'run project successful.xml testcase="TestCase 1" securitytest="blah"', + 'run project testsuite="TestSuite 1" successful.xml', + 'run project testsuite="TestSuite 1" securitytest="blah" successful.xml', + 'run project testsuite="TestSuite 1" testcase="TestCase 1" successful.xml', + 'run project testcase="TestCase 1" successful.xml', + 'run project testcase="TestCase 1" securitytest="blah" successful.xml', + 'run project successful.xml printReport', + 'run project successful.xml printReport async', + + 'jobs list user=lol', + 'jobs list user=admin', + 'jobs list user=regular', + 'jobs help', + 'jobs prune before=2018-01-01', + 'jobs prune before=dasdasdas', + 'license install', + 'license install type=floating', + 'license install type=fixed', + 'license install type=noneExisting', + 'license install type=floating', + 'license install type=floating', + 'license uninstall', + 'license install type=floating ' + licenseServer, + 'license install type=floating noneExisting:1234', + 'license install type=floating file.txt', + 'license install type=floating noneExisting=text ' + licenseServer, + 'license install type=floating email=notMail ' + licenseServer, + 'license install type=floating firstName=oskar ' + licenseServer, + 'license install type=floating lastName=oskarsson ' + licenseServer, + 'license install type=floating email=oskar@oskarsson.com ' + licenseServer, + 'license install type=floating firstName=oskar lastName=oskarsson email=oskar@oskarsson.com ' + licenseServer]; + +let flags = ['-C', + '-c', + '-C admin.config', + '-H localhost:1231', + '-H localhost:8080', + '-c regularUser.config', + '-c admin.config']; + +let jobCommands = ['jobs status', + 'jobs printReport', + 'jobs report', + 'jobs report output=.', + 'jobs report output=noneExisting', + 'jobs report output=. reportFileName=report', + 'jobs report output=output noneExisting=report.txt', + 'jobs report output=output reportFileName=report format=junit', + 'jobs report output=output reportFileName=report format=excel', + 'jobs report output=output reportFileName=report format=json', + 'jobs report output=output reportFileName=report format=pdf', + 'jobs report output=. reportFileName=report format=noneExisting']; + +runAllCombinations(commands, flags, (command, flag) => runCli(command, flag)); +let testJobId = startJob('successful.xml'); +runAllCombinations(jobCommands, flags, (command, flag) => runCli(command, flag, testJobId)); +runAllCombinations(['jobs cancel'], flags, startSlowJobAndThenRunCli); diff --git a/test/users.csv b/test/users.csv new file mode 100644 index 0000000..e21915c --- /dev/null +++ b/test/users.csv @@ -0,0 +1,10 @@ +username,password,admin +correctAdmin,thepassword,true +correctRegularUser,thepassword,false +longrow,password,false,more +shortrow,password +shortpassword,pw,true +,nousername,false +nopassword,,false +incorrectAdminString,password,blargh +noadminstring,password, diff --git a/test/validationerror.xml b/test/validationerror.xml new file mode 100644 index 0000000..2a5c152 --- /dev/null +++ b/test/validationerror.xml @@ -0,0 +1,65 @@ + + + + + + SEQUENTIAL + + + + + + /home/jonathan/thefile.txt + 0 + 0 + false + + + + + + + + + + + + + + + + + + + + // Sample event script to add custom HTTP header to all outgoing REST, SOAP and HTTP(S) calls +// This code is often used for adding custom authentication to ReadyAPI functional tests + +// If hardcoding the token, uncomment and change line 5 +// token = '4567' + +// If your token is parameterized in Project level custom property, uncomment line 8 +// token = request.parent.testCase.testSuite.project.getProperty('auth_token').getValue() + +// To modify all outgoing calls, remove comments from lines 11 to 16 +// headers = request.requestHeaders +// if (headers.containsKey('auth_token2') == false) { +// headers.put('auth_token2', token) +// request.requestHeaders = headers +// } + + + // Save all test step results into files +// Change the directory path in line 5 to a location where you want to store details +// then uncomment lines 5 to 10 + +// filePath = 'C:\\tempOutputDirectory\\' +// fos = new java.io.FileOutputStream(filePath + testStepResult.testStep.label + '.txt', true) +// pw = new java.io.PrintWriter(fos) +// testStepResult.writeTo(pw) +// pw.close() +// fos.close() + + + +