diff --git a/bin/commands/runs.js b/bin/commands/runs.js index 47fc70e7..1eccaee1 100644 --- a/bin/commands/runs.js +++ b/bin/commands/runs.js @@ -12,7 +12,9 @@ const archiver = require("../helpers/archiver"), checkUploaded = require("../helpers/checkUploaded"), reportGenerator = require('../helpers/reporterHTML').reportGenerator, {initTimeComponents, instrumentEventTime, markBlockStart, markBlockEnd, getTimeComponents} = require('../helpers/timeComponents'), - downloadBuildArtifacts = require('../helpers/buildArtifacts').downloadBuildArtifacts; + downloadBuildArtifacts = require('../helpers/buildArtifacts').downloadBuildArtifacts, + updateNotifier = require('update-notifier'), + pkg = require('../../package.json'); module.exports = function run(args) { let bsConfigPath = utils.getConfigPath(args.cf); @@ -75,6 +77,10 @@ module.exports = function run(args) { //set config (--config) utils.setConfig(bsConfig, args); + + // set sync/async mode (--async/--sync) + utils.setCLIMode(bsConfig, args); + // set other cypress configs e.g. reporter and reporter-options utils.setOtherConfigs(bsConfig, args); markBlockEnd('setConfig'); @@ -118,19 +124,19 @@ module.exports = function run(args) { return build.createBuild(bsConfig, zip).then(function (data) { markBlockEnd('createBuild'); markBlockEnd('total'); + utils.setProcessHooks(data.build_id, bsConfig, bs_local, args); let message = `${data.message}! ${Constants.userMessages.BUILD_CREATED} with build id: ${data.build_id}`; let dashboardLink = `${Constants.userMessages.VISIT_DASHBOARD} ${data.dashboard_url}`; utils.exportResults(data.build_id, `${config.dashboardUrl}${data.build_id}`); if ((utils.isUndefined(bsConfig.run_settings.parallels) && utils.isUndefined(args.parallels)) || (!utils.isUndefined(bsConfig.run_settings.parallels) && bsConfig.run_settings.parallels == Constants.cliMessages.RUN.DEFAULT_PARALLEL_MESSAGE)) { logger.warn(Constants.userMessages.NO_PARALLELS); } - if (bsConfig.run_settings.cypress_version && bsConfig.run_settings.cypress_version !== data.cypress_version) { if (bsConfig.run_settings.cypress_version.toString().match(Constants.LATEST_VERSION_SYNTAX_REGEX)) { - let versionMessage = utils.latestSyntaxToActualVersionMessage(bsConfig.run_settings.cypress_version, data.cypress_version); + let versionMessage = utils.latestSyntaxToActualVersionMessage(bsConfig.run_settings.cypress_version, data.cypress_version, data.framework_upgrade_message); logger.info(versionMessage); } else { - let versionMessage = utils.versionChangedMessage(bsConfig.run_settings.cypress_version, data.cypress_version); + let versionMessage = utils.versionChangedMessage(bsConfig.run_settings.cypress_version, data.cypress_version, data.framework_upgrade_message); logger.warn(versionMessage); } } @@ -241,5 +247,10 @@ module.exports = function run(args) { utils.setUsageReportingFlag(null, args.disableUsageReporting); utils.sendUsageReport(null, args, err.message, Constants.messageTypes.ERROR, utils.getErrorCodeFromErr(err)); process.exitCode = Constants.ERROR_EXIT_CODE; + }).finally(function(){ + updateNotifier({ + pkg, + updateCheckInterval: 1000 * 60 * 60 * 24 * 7, + }).notify({isGlobal: true}); }); } diff --git a/bin/commands/stop.js b/bin/commands/stop.js index 736b4517..2125ad00 100644 --- a/bin/commands/stop.js +++ b/bin/commands/stop.js @@ -9,7 +9,7 @@ const config = require("../helpers/config"), module.exports = function stop(args) { let bsConfigPath = utils.getConfigPath(args.cf); - return utils.validateBstackJson(bsConfigPath).then(function (bsConfig) { + return utils.validateBstackJson(bsConfigPath).then(async function (bsConfig) { utils.setDefaults(bsConfig, args); // accept the username from command line if provided @@ -25,69 +25,8 @@ module.exports = function stop(args) { let buildId = args._[1]; - let options = { - url: config.buildStopUrl + buildId, - auth: { - user: bsConfig.auth.username, - password: bsConfig.auth.access_key, - }, - headers: { - 'User-Agent': utils.getUserAgent(), - }, - }; + await utils.stopBrowserStackBuild(bsConfig, args, buildId); - request.post(options, function (err, resp, body) { - let message = null; - let messageType = null; - let errorCode = null; - - if (err) { - message = Constants.userMessages.BUILD_STOP_FAILED; - messageType = Constants.messageTypes.ERROR; - errorCode = 'api_failed_build_stop'; - - logger.info(message); - } else { - let build = null; - try { - build = JSON.parse(body); - } catch (error) { - build = null; - } - - if (resp.statusCode == 299) { - messageType = Constants.messageTypes.INFO; - errorCode = 'api_deprecated'; - - if (build) { - message = build.message; - logger.info(message); - } else { - message = Constants.userMessages.API_DEPRECATED; - logger.info(message); - } - } else if (resp.statusCode != 200) { - messageType = Constants.messageTypes.ERROR; - errorCode = 'api_failed_build_stop'; - - if (build) { - message = `${ - Constants.userMessages.BUILD_STOP_FAILED - } with error: \n${JSON.stringify(build, null, 2)}`; - logger.error(message); - if (build.message === 'Unauthorized') errorCode = 'api_auth_failed'; - } else { - message = Constants.userMessages.BUILD_STOP_FAILED; - logger.error(message); - } - } else { - messageType = Constants.messageTypes.SUCCESS; - message = `${JSON.stringify(build, null, 2)}`; - logger.info(message); - } - } - utils.sendUsageReport(bsConfig, args, message, messageType, errorCode); - }); }).catch(function (err) { logger.error(err); utils.setUsageReportingFlag(null, args.disableUsageReporting); diff --git a/bin/helpers/capabilityHelper.js b/bin/helpers/capabilityHelper.js index 02f2344f..b0aac7a2 100644 --- a/bin/helpers/capabilityHelper.js +++ b/bin/helpers/capabilityHelper.js @@ -156,6 +156,8 @@ const validate = (bsConfig, args) => { if( Utils.searchForOption('--local-config-file') && ( Utils.isUndefined(args.localConfigFile) || (!Utils.isUndefined(args.localConfigFile) && !fs.existsSync(args.localConfigFile)))) reject(Constants.validationMessages.INVALID_LOCAL_CONFIG_FILE); + if( Utils.searchForOption('--async') && ( !Utils.isUndefined(args.async) && bsConfig["connection_settings"]["local"])) reject(Constants.validationMessages.INVALID_LOCAL_ASYNC_ARGS); + // validate if config file provided exists or not when cypress_config_file provided // validate the cypressProjectDir key otherwise. let cypressConfigFilePath = bsConfig.run_settings.cypressConfigFilePath; diff --git a/bin/helpers/constants.js b/bin/helpers/constants.js index 99a6285e..48851393 100644 --- a/bin/helpers/constants.js +++ b/bin/helpers/constants.js @@ -7,6 +7,7 @@ const syncCLI = { }, INITIAL_DELAY_MULTIPLIER: 10, DEFAULT_LINE_SEP: "\n--------------------------------------------------------------------------------", + STARTUP_MESSAGE: "BrowserStack machines are now setting up Cypress with the specified npm dependencies for running your tests. It might take some time before your tests start runnning and showing up below..." }; const userMessages = { @@ -41,7 +42,7 @@ const userMessages = { FATAL_NETWORK_ERROR: `fatal: unable to access '${config.buildUrl}': Could not resolve host: ${config.rails_host}`, RETRY_LIMIT_EXCEEDED: `Max retries exceeded trying to connect to the host (retries: ${config.retries})`, CHECK_DASHBOARD_AT: "Please check the build status at: ", - CYPRESS_VERSION_CHANGED: "Your build will run using Cypress instead of Cypress . Read more about supported versions here: http://browserstack.com/docs/automate/cypress/supported-versions", + CYPRESS_VERSION_CHANGED: "Your build will run using Cypress instead of Cypress . Read more about supported versions here: http://browserstack.com/docs/automate/cypress/supported-versions", LOCAL_START_FAILED: "Local Testing setup failed.", LOCAL_STOP_FAILED: "Local Binary stop failed.", INVALID_LOCAL_MODE_WARNING: "Invalid value specified for local_mode. local_mode: (\"always-on\" | \"on-demand\"). For more info, check out https://www.browserstack.com/docs/automate/cypress/cli-reference", @@ -49,7 +50,8 @@ const userMessages = { DOWNLOAD_BUILD_ARTIFACTS_FAILED: "Downloading build artifacts for the build failed for machines.", ASYNC_DOWNLOADS: "Test artifacts as specified under 'downloads' can be downloaded after the build has completed its run, using 'browserstack-cypress generate-downloads '", DOWNLOAD_BUILD_ARTIFACTS_SUCCESS: "Your build artifact(s) have been successfully downloaded in '/build_artifacts/' directory", - LATEST_SYNTAX_TO_ACTUAL_VERSION_MESSAGE: "Your build will run using Cypress as you had specified . Read more about supported versions here: http://browserstack.com/docs/automate/cypress/supported-versions" + LATEST_SYNTAX_TO_ACTUAL_VERSION_MESSAGE: "Your build will run using Cypress as you had specified . Read more about supported versions here: http://browserstack.com/docs/automate/cypress/supported-versions", + PROCESS_KILL_MESSAGE: "Stopping the CLI and the execution of the build on BrowserStack", }; const validationMessages = { @@ -74,7 +76,8 @@ const validationMessages = { INVALID_LOCAL_MODE: "When using --local-mode, a value needs to be supplied. \n--local-mode (\"always-on\" | \"on-demand\").\nFor more info, check out https://www.browserstack.com/docs/automate/cypress/cli-reference", INVALID_LOCAL_CONFIG_FILE: "Using --local-config-file requires an input of the form /path/to/config-file.yml.\nFor more info, check out https://www.browserstack.com/docs/automate/cypress/cli-reference", INVALID_LOCAL_IDENTIFIER: "Invalid value specified for local_identifier. For more info, check out https://www.browserstack.com/docs/automate/cypress/cli-reference", - INVALID_BROWSER_ARGS: "Aborting as an unacceptable value was passed for --browser. Read more at https://www.browserstack.com/docs/automate/cypress/cli-reference" + INVALID_BROWSER_ARGS: "Aborting as an unacceptable value was passed for --browser. Read more at https://www.browserstack.com/docs/automate/cypress/cli-reference", + INVALID_LOCAL_ASYNC_ARGS: "Cannot run in --async mode when local is set to true. Please run the build after removing --async", }; const cliMessages = { @@ -96,7 +99,7 @@ const cliMessages = { }, RUN: { PARALLEL_DESC: "The maximum number of parallels to use to run your test suite", - INFO: "Run your tests on BrowserStack.", + INFO: "Run your tests on BrowserStack. For more help: `browserstack-cypress run --help`.", CYPRESS_DESC: "Path to Cypress config file", CYPRESS_CONFIG_DEMAND: "Cypress config file is required", BUILD_NAME: "The build name you want to use to name your test runs", @@ -105,6 +108,7 @@ const cliMessages = { SPECS_DESCRIPTION: "Specify the spec files to run", ENV_DESCRIPTION: "Specify the environment variables for your spec files", SYNC_DESCRIPTION: "Makes the run command in sync", + ASYNC_DESCRIPTION: "Makes the run command in async", BUILD_REPORT_MESSAGE: "See the entire build report here", HEADED: "Run your tests in a headed browser instead of a headless browser", LOCAL: "Accepted values: (true | false) - create a local testing connection to let you test staging and localhost websites, or sites behind proxies; learn more at browserstack.com/local-testing", @@ -143,7 +147,7 @@ const messageTypes = { NULL: null } -const allowedFileTypes = ['js', 'json', 'txt', 'ts', 'feature', 'features', 'pdf', 'jpg', 'jpeg', 'png', 'zip', 'npmrc', 'xml', 'doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx', 'jsx', 'coffee', 'cjsx', 'csv', 'tsv', 'yml', 'yaml', 'env']; +const allowedFileTypes = ['js', 'json', 'txt', 'ts', 'feature', 'features', 'pdf', 'jpg', 'jpeg', 'png', 'zip', 'npmrc', 'xml', 'doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx', 'jsx', 'coffee', 'cjsx', 'csv', 'tsv', 'yml', 'yaml', 'env', 'mov', 'mp4', 'mp3', 'wav']; const filesToIgnoreWhileUploading = [ '**/node_modules/**', diff --git a/bin/helpers/sync/syncSpecsLogs.js b/bin/helpers/sync/syncSpecsLogs.js index abb14f25..44b38d50 100644 --- a/bin/helpers/sync/syncSpecsLogs.js +++ b/bin/helpers/sync/syncSpecsLogs.js @@ -3,6 +3,7 @@ const request = require("request"), config = require("../config"), utils = require("../utils"), logger = require("../logger").syncCliLogger, + winstonLogger = require("../logger").winstonLogger, async = require('async'), Constants = require("../constants"), tableStream = require('table').createStream, @@ -164,6 +165,7 @@ let showSpecsStatus = (data) => { } let printInitialLog = () => { + winstonLogger.info(Constants.syncCLI.STARTUP_MESSAGE); logger.info(`\n${Constants.syncCLI.LOGS.INIT_LOG}`) logger.info(lineSeparator); n = Constants.syncCLI.INITIAL_DELAY_MULTIPLIER diff --git a/bin/helpers/utils.js b/bin/helpers/utils.js index b72968d4..73f67a9b 100644 --- a/bin/helpers/utils.js +++ b/bin/helpers/utils.js @@ -16,6 +16,7 @@ const usageReporting = require("./usageReporting"), config = require("../helpers/config"); const request = require('request'); +const axios = require("axios"); exports.validateBstackJson = (bsConfigPath) => { return new Promise(function (resolve, reject) { @@ -80,6 +81,9 @@ exports.getErrorCodeFromMsg = (errMsg) => { case Constants.validationMessages.INVALID_CYPRESS_CONFIG_FILE: errorCode = 'invalid_cypress_config_file'; break; + case Constants.validationMessages.INVALID_LOCAL_ASYNC_ARGS: + errorCode = 'invalid_local_async_args'; + break; } if ( errMsg.includes("Please use --config-file .") @@ -733,15 +737,18 @@ exports.getNetworkErrorMessage = (dashboard_url) => { return chalk.red(message) } -exports.versionChangedMessage = (preferredVersion, actualVersion) => { +exports.versionChangedMessage = (preferredVersion, actualVersion, frameworkUpgradeMessage = '') => { let message = Constants.userMessages.CYPRESS_VERSION_CHANGED.replace("", preferredVersion); message = message.replace("", actualVersion); + frameworkUpgradeMessage = frameworkUpgradeMessage.replace('.latest', ""); + message = message.replace('', frameworkUpgradeMessage); return message } -exports.latestSyntaxToActualVersionMessage = (latestSyntaxVersion, actualVersion) => { +exports.latestSyntaxToActualVersionMessage = (latestSyntaxVersion, actualVersion, frameworkUpgradeMessage = '') => { let message = Constants.userMessages.LATEST_SYNTAX_TO_ACTUAL_VERSION_MESSAGE.replace("", latestSyntaxVersion); message = message.replace("", actualVersion); + message = message.replace('', frameworkUpgradeMessage) return message } @@ -760,6 +767,10 @@ exports.isJSONInvalid = (err, args) => { return false } + if( err === Constants.validationMessages.INVALID_LOCAL_ASYNC_ARGS && !this.isUndefined(args.async)) { + return false + } + return invalid } @@ -820,3 +831,94 @@ exports.getCypressJSON = (bsConfig) => { } return cypressJSON; } + +exports.setCLIMode = (bsConfig, args) => { + args.sync = true; + if(!this.isUndefined(args.async) && args.async){ + args.sync = false; + } +} + +exports.stopBrowserStackBuild = async (bsConfig, args, buildId ) => { + let url = config.buildStopUrl + buildId; + let options = { + url: url, + auth: { + username: bsConfig["auth"]["username"], + password: bsConfig["auth"]["access_key"], + }, + headers: { + 'User-Agent': this.getUserAgent(), + }, + }; + + let message = null; + let messageType = null; + let errorCode = null; + try{ + let resp = await axios.post(url, {} , options); + let build = null; + if(resp.data){ + build = resp.data; + } + + if (resp.status == 299) { + messageType = Constants.messageTypes.INFO; + errorCode = 'api_deprecated'; + + if (build) { + message = build.message; + logger.info(message); + } else { + message = Constants.userMessages.API_DEPRECATED; + logger.info(message); + } + } else if (resp.status != 200) { + messageType = Constants.messageTypes.ERROR; + errorCode = 'api_failed_build_stop'; + + if (build) { + message = `${ + Constants.userMessages.BUILD_STOP_FAILED + } with error: \n${JSON.stringify(build, null, 2)}`; + logger.error(message); + if (build.message === 'Unauthorized') errorCode = 'api_auth_failed'; + } else { + message = Constants.userMessages.BUILD_STOP_FAILED; + logger.error(message); + } + } else { + messageType = Constants.messageTypes.SUCCESS; + message = `${JSON.stringify(build, null, 2)}`; + logger.info(message); + } + } catch(err){ + console.log(err); + message = Constants.userMessages.BUILD_STOP_FAILED; + messageType = Constants.messageTypes.ERROR; + errorCode = 'api_failed_build_stop'; + logger.info(message); + } finally { + this.sendUsageReport(bsConfig, args, message, messageType, errorCode); + } +} + +exports.setProcessHooks = (buildId, bsConfig, bsLocal, args) => { + let bindData = { + buildId: buildId, + bsConfig: bsConfig, + bsLocalInstance: bsLocal, + args: args + } + process.on('SIGINT', processExitHandler.bind(this, bindData)); + process.on('SIGTERM', processExitHandler.bind(this, bindData)); + process.on('SIGBREAK', processExitHandler.bind(this, bindData)); + process.on('uncaughtException', processExitHandler.bind(this, bindData)); +} + +async function processExitHandler(exitData){ + logger.warn(Constants.userMessages.PROCESS_KILL_MESSAGE); + await this.stopBrowserStackBuild(exitData.bsConfig, exitData.args, exitData.buildId); + await this.stopLocalBinary(exitData.bsConfig, exitData.bsLocalInstance, exitData.args); + process.exit(0); +} diff --git a/bin/runner.js b/bin/runner.js index ae4acd1e..6c0ef772 100755 --- a/bin/runner.js +++ b/bin/runner.js @@ -164,10 +164,15 @@ var argv = yargs type: "boolean" }, 'sync': { - default: false, + default: true, describe: Constants.cliMessages.RUN.SYNC_DESCRIPTION, type: "boolean" }, + 'async': { + default: false, + describe: Constants.cliMessages.RUN.ASYNC_DESCRIPTION, + type: "boolean" + }, 'force-upload': { default: false, describe: Constants.cliMessages.COMMON.FORCE_UPLOAD, diff --git a/package.json b/package.json index 91bb68ef..d8cf00fe 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ "winston": "^2.3.1", "yargs": "^14.2.3", "axios": "^0.21.1", - "unzipper": "^0.10.11" + "unzipper": "^0.10.11", + "update-notifier": "^5.1.0" }, "repository": { "type": "git", diff --git a/test/unit/bin/commands/runs.js b/test/unit/bin/commands/runs.js index ffca3fef..039f4c5a 100644 --- a/test/unit/bin/commands/runs.js +++ b/test/unit/bin/commands/runs.js @@ -109,6 +109,7 @@ describe("runs", () => { setLocalConfigFileStub = sandbox.stub(); setBrowsersStub = sandbox.stub(); setConfigStub = sandbox.stub(); + setCLIModeStub = sandbox.stub(); }); afterEach(() => { @@ -147,7 +148,8 @@ describe("runs", () => { setLocalConfigFile: setLocalConfigFileStub, setSystemEnvs: setSystemEnvsStub, setBrowsers: setBrowsersStub, - setConfig: setConfigStub + setConfig: setConfigStub, + setCLIMode: setCLIModeStub }, '../helpers/capabilityHelper': { validate: capabilityValidatorStub @@ -181,6 +183,7 @@ describe("runs", () => { sinon.assert.calledOnce(setHeadedStub); sinon.assert.calledOnce(setNoWrapStub); sinon.assert.calledOnce(setConfigStub); + sinon.assert.calledOnce(setCLIModeStub); sinon.assert.calledOnce(setOtherConfigsStub); sinon.assert.calledOnce(capabilityValidatorStub); sinon.assert.calledOnce(getErrorCodeFromMsgStub); @@ -235,6 +238,7 @@ describe("runs", () => { setLocalConfigFileStub = sandbox.stub(); setBrowsersStub = sandbox.stub(); setConfigStub = sandbox.stub(); + setCLIModeStub = sandbox.stub(); }); afterEach(() => { @@ -274,7 +278,8 @@ describe("runs", () => { setLocalConfigFile: setLocalConfigFileStub, setSystemEnvs: setSystemEnvsStub, setBrowsers: setBrowsersStub, - setConfig: setConfigStub + setConfig: setConfigStub, + setCLIMode: setCLIModeStub }, '../helpers/capabilityHelper': { validate: capabilityValidatorStub, @@ -316,6 +321,7 @@ describe("runs", () => { sinon.assert.calledOnce(setLocalIdentifierStub); sinon.assert.calledOnce(setHeadedStub); sinon.assert.calledOnce(setNoWrapStub); + sinon.assert.calledOnce(setCLIModeStub); sinon.assert.calledOnce(setOtherConfigsStub); sinon.assert.calledOnce(validateBstackJsonStub); sinon.assert.calledOnce(capabilityValidatorStub); @@ -375,6 +381,7 @@ describe("runs", () => { setLocalConfigFileStub = sandbox.stub(); setConfigStub = sandbox.stub(); setBrowsersStub = sandbox.stub(); + setCLIModeStub = sandbox.stub(); }); afterEach(() => { @@ -414,7 +421,8 @@ describe("runs", () => { setDefaults: setDefaultsStub, setLocalConfigFile: setLocalConfigFileStub, setBrowsers: setBrowsersStub, - setConfig: setConfigStub + setConfig: setConfigStub, + setCLIMode: setCLIModeStub }, '../helpers/capabilityHelper': { validate: capabilityValidatorStub, @@ -456,6 +464,7 @@ describe("runs", () => { sinon.assert.calledOnce(setLocalIdentifierStub); sinon.assert.calledOnce(setHeadedStub); sinon.assert.calledOnce(setNoWrapStub); + sinon.assert.calledOnce(setCLIModeStub); sinon.assert.calledOnce(setOtherConfigsStub); sinon.assert.calledOnce(validateBstackJsonStub); sinon.assert.calledOnce(capabilityValidatorStub); @@ -520,6 +529,7 @@ describe("runs", () => { setLocalConfigFileStub = sandbox.stub(); setConfigStub = sandbox.stub(); setBrowsersStub = sandbox.stub(); + setCLIModeStub = sandbox.stub(); }); afterEach(() => { @@ -560,7 +570,8 @@ describe("runs", () => { stopLocalBinary: stopLocalBinaryStub, setLocalConfigFile: setLocalConfigFileStub, setBrowsers: setBrowsersStub, - setConfig: setConfigStub + setConfig: setConfigStub, + setCLIMode: setCLIModeStub }, '../helpers/capabilityHelper': { validate: capabilityValidatorStub, @@ -612,6 +623,7 @@ describe("runs", () => { sinon.assert.calledOnce(setLocalIdentifierStub); sinon.assert.calledOnce(setHeadedStub); sinon.assert.calledOnce(setNoWrapStub); + sinon.assert.calledOnce(setCLIModeStub); sinon.assert.calledOnce(setOtherConfigsStub); sinon.assert.calledOnce(archiverStub); sinon.assert.calledOnce(setUsageReportingFlagStub); @@ -686,6 +698,8 @@ describe("runs", () => { setBrowsersStub = sandbox.stub(); stopLocalBinaryStub = sandbox.stub(); nonEmptyArrayStub = sandbox.stub(); + setCLIModeStub = sandbox.stub(); + setProcessHooksStub = sandbox.stub(); }); afterEach(() => { @@ -733,6 +747,8 @@ describe("runs", () => { setConfig: setConfigStub, stopLocalBinary: stopLocalBinaryStub, nonEmptyArray: nonEmptyArrayStub, + setCLIMode: setCLIModeStub, + setProcessHooks: setProcessHooksStub }, '../helpers/capabilityHelper': { validate: capabilityValidatorStub, @@ -795,6 +811,8 @@ describe("runs", () => { sinon.assert.calledOnce(setLocalIdentifierStub); sinon.assert.calledOnce(setHeadedStub); sinon.assert.calledOnce(setNoWrapStub); + sinon.assert.calledOnce(setCLIModeStub); + sinon.assert.calledOnce(setProcessHooksStub); sinon.assert.calledOnce(setOtherConfigsStub); sinon.assert.calledOnce(generateUniqueHashStub); sinon.assert.calledOnce(archiverStub); diff --git a/test/unit/bin/commands/stop.js b/test/unit/bin/commands/stop.js index 161352b4..ea340319 100644 --- a/test/unit/bin/commands/stop.js +++ b/test/unit/bin/commands/stop.js @@ -17,7 +17,7 @@ describe("buildStop", () => { let body = testObjects.buildStopSampleBody; let bsConfig = testObjects.sampleBsConfig; - describe("Handle API deprecated", () => { + describe("Handle API success", () => { var sandbox; beforeEach(() => { @@ -34,6 +34,7 @@ describe("buildStop", () => { }); getErrorCodeFromErrStub = sandbox.stub().returns("random-error"); setDefaultsStub = sandbox.stub(); + stopBrowserStackBuildStub = sandbox.stub().returns(Promise.reject(true)); }); afterEach(() => { @@ -41,289 +42,28 @@ describe("buildStop", () => { sinon.restore(); }); - it("message thrown if API deprecated", () => { - let message = Constants.userMessages.API_DEPRECATED; - let messageType = Constants.messageTypes.INFO; - let errorCode = "api_deprecated"; - - let requestStub = sandbox - .stub(request, "post") - .yields(null, { statusCode: 299 }, null); - - const stop = proxyquire('../../../../bin/commands/stop', { - '../helpers/utils': { - validateBstackJson: validateBstackJsonStub, - setUsername: setUsernameStub, - setAccessKey: setAccessKeyStub, - getErrorCodeFromErr: getErrorCodeFromErrStub, - sendUsageReport: sendUsageReportStub, - setUsageReportingFlag: setUsageReportingFlagStub, - setCypressConfigFilename: setCypressConfigFilenameStub, - getUserAgent: getUserAgentStub, - getConfigPath: getConfigPathStub, - setDefaults: setDefaultsStub - }, - request: {post: requestStub}, - }); - - validateBstackJsonStub.returns(Promise.resolve(bsConfig)); - - return stop(args) - .then(function (_bsConfig) { - sinon.assert.calledOnce(requestStub); - sinon.assert.calledOnce(getUserAgentStub); - sinon.assert.calledOnceWithExactly(sendUsageReportStub, bsConfig, args, message, messageType, errorCode); - }) - .catch((error) => { - chai.assert.isNotOk(error, "Promise error"); - }); - }); - - it("message thrown if build returned", () => { - let message = body.message; - let messageType = Constants.messageTypes.INFO; - let errorCode = "api_deprecated"; - - let requestStub = sandbox - .stub(request, "post") - .yields(null, { statusCode: 299 }, JSON.stringify(body)); - - const stop = proxyquire('../../../../bin/commands/stop', { - '../helpers/utils': { - validateBstackJson: validateBstackJsonStub, - getErrorCodeFromErr: getErrorCodeFromErrStub, - setUsername: setUsernameStub, - setAccessKey: setAccessKeyStub, - sendUsageReport: sendUsageReportStub, - setUsageReportingFlag: setUsageReportingFlagStub, - setCypressConfigFilename: setCypressConfigFilenameStub, - getUserAgent: getUserAgentStub, - getConfigPath: getConfigPathStub, - setDefaults: setDefaultsStub - }, - request: {post: requestStub}, - }); - - validateBstackJsonStub.returns(Promise.resolve(bsConfig)); - - return stop(args) - .then(function (_bsConfig) { - sinon.assert.calledOnce(requestStub); - sinon.assert.calledOnce(getUserAgentStub); - sinon.assert.calledOnceWithExactly(sendUsageReportStub, bsConfig, args, message, messageType, errorCode); - }) - .catch((error) => { - chai.assert.isNotOk(error, "Promise error"); - }); - }); - }); - - describe("Handle statusCode != 200", () => { - beforeEach(() => { - sandbox = sinon.createSandbox(); - setUsernameStub = sandbox.stub(); - setAccessKeyStub = sandbox.stub(); - validateBstackJsonStub = sandbox.stub(); - getConfigPathStub = sandbox.stub(); - setUsageReportingFlagStub = sandbox.stub().returns(undefined); - setCypressConfigFilenameStub = sandbox.stub().returns(undefined); - getUserAgentStub = sandbox.stub().returns("random user-agent"); - sendUsageReportStub = sandbox.stub().callsFake(function () { - return "end"; - }); - getErrorCodeFromErrStub = sandbox.stub().returns("random-error"); - setDefaultsStub = sandbox.stub(); - }); - - afterEach(() => { - sandbox.restore(); - sinon.restore(); - }); - - it("message thrown if statusCode != 200", () => { - let message = Constants.userMessages.BUILD_STOP_FAILED; - let messageType = Constants.messageTypes.ERROR; - let errorCode = "api_failed_build_stop"; - - let requestStub = sinon - .stub(request, "post") - .yields(null, { statusCode: 400 }, null); - - const stop = proxyquire('../../../../bin/commands/stop', { - '../helpers/utils': { - validateBstackJson: validateBstackJsonStub, - getErrorCodeFromErr: getErrorCodeFromErrStub, - sendUsageReport: sendUsageReportStub, - setUsername: setUsernameStub, - setAccessKey: setAccessKeyStub, - setUsageReportingFlag: setUsageReportingFlagStub, - setCypressConfigFilename: setCypressConfigFilenameStub, - getUserAgent: getUserAgentStub, - getConfigPath: getConfigPathStub, - setDefaults: setDefaultsStub - }, - request: {post: requestStub}, - }); - - validateBstackJsonStub.returns(Promise.resolve(bsConfig)); - - return stop(args) - .then(function (_bsConfig) { - sinon.assert.calledOnce(requestStub); - sinon.assert.calledOnce(getUserAgentStub); - sinon.assert.calledOnceWithExactly(sendUsageReportStub, bsConfig, args, message, messageType, errorCode); - }) - .catch((error) => { - chai.assert.isNotOk(error, "Promise error"); - }); - }); - - it("message thrown if statusCode != 200 and user unauthorized", () => { - let body_with_message = { - ...body, - message: "Unauthorized", - }; - - let message = `${ - Constants.userMessages.BUILD_STOP_FAILED - } with error: \n${JSON.stringify(body_with_message, null, 2)}`; - let messageType = Constants.messageTypes.ERROR; - let errorCode = "api_auth_failed"; - - let requestStub = sinon - .stub(request, "post") - .yields(null, { statusCode: 401 }, JSON.stringify(body_with_message)); - + it("should call stopBrowserStackBuild method", () => { const stop = proxyquire('../../../../bin/commands/stop', { '../helpers/utils': { validateBstackJson: validateBstackJsonStub, - getErrorCodeFromErr: getErrorCodeFromErrStub, - sendUsageReport: sendUsageReportStub, setUsername: setUsernameStub, setAccessKey: setAccessKeyStub, - setUsageReportingFlag: setUsageReportingFlagStub, - setCypressConfigFilename: setCypressConfigFilenameStub, - getUserAgent: getUserAgentStub, - getConfigPath: getConfigPathStub, - setDefaults: setDefaultsStub - }, - request: {post: requestStub}, - }); - - validateBstackJsonStub.returns(Promise.resolve(bsConfig)); - - return stop(args) - .then(function (_bsConfig) { - sinon.assert.calledOnce(requestStub); - sinon.assert.calledOnce(getUserAgentStub); - sinon.assert.calledOnceWithExactly(sendUsageReportStub, bsConfig, args, message, messageType, errorCode); - }) - .catch((error) => { - chai.assert.isNotOk(error, "Promise error"); - }); - }); - - it("message thrown if statusCode != 200 and build is present", () => { - let message = `${ - Constants.userMessages.BUILD_STOP_FAILED - } with error: \n${JSON.stringify(body, null, 2)}`; - let messageType = Constants.messageTypes.ERROR; - let errorCode = "api_failed_build_stop"; - - let requestStub = sinon - .stub(request, "post") - .yields(null, { statusCode: 402 }, JSON.stringify(body)); - - const stop = proxyquire('../../../../bin/commands/stop', { - '../helpers/utils': { - validateBstackJson: validateBstackJsonStub, - getErrorCodeFromErr: getErrorCodeFromErrStub, - sendUsageReport: sendUsageReportStub, - setUsername: setUsernameStub, - setAccessKey: setAccessKeyStub, - setUsageReportingFlag: setUsageReportingFlagStub, - setCypressConfigFilename: setCypressConfigFilenameStub, - getUserAgent: getUserAgentStub, - getConfigPath: getConfigPathStub, - setDefaults: setDefaultsStub - }, - request: {post: requestStub}, - }); - - validateBstackJsonStub.returns(Promise.resolve(bsConfig)); - - return stop(args) - .then(function (_bsConfig) { - sinon.assert.calledOnce(requestStub); - sinon.assert.calledOnce(getUserAgentStub); - sinon.assert.calledOnceWithExactly(sendUsageReportStub, bsConfig, args, message, messageType, errorCode); - }) - .catch((error) => { - chai.assert.isNotOk(error, "Promise error"); - }); - }); - }); - - describe("Handle API success", () => { - var sandbox; - - beforeEach(() => { - sandbox = sinon.createSandbox(); - setUsernameStub = sandbox.stub(); - setAccessKeyStub = sandbox.stub(); - validateBstackJsonStub = sandbox.stub(); - getConfigPathStub = sandbox.stub(); - setUsageReportingFlagStub = sandbox.stub().returns(undefined); - setCypressConfigFilenameStub = sandbox.stub().returns(undefined); - getUserAgentStub = sandbox.stub().returns("random user-agent"); - sendUsageReportStub = sandbox.stub().callsFake(function () { - return "end"; - }); - getErrorCodeFromErrStub = sandbox.stub().returns("random-error"); - setDefaultsStub = sandbox.stub(); - }); - - afterEach(() => { - sandbox.restore(); - sinon.restore(); - }); - - it("message thrown if API success", () => { - let message = `${JSON.stringify(body, null, 2)}`; - let messageType = Constants.messageTypes.SUCCESS; - let errorCode = null; - - let requestStub = sandbox - .stub(request, "post") - .yields(null, { statusCode: 200 }, JSON.stringify(body)); - - const stop = proxyquire('../../../../bin/commands/stop', { - '../helpers/utils': { - validateBstackJson: validateBstackJsonStub, getErrorCodeFromErr: getErrorCodeFromErrStub, sendUsageReport: sendUsageReportStub, - setUsername: setUsernameStub, - setAccessKey: setAccessKeyStub, setUsageReportingFlag: setUsageReportingFlagStub, setCypressConfigFilename: setCypressConfigFilenameStub, - getUserAgent: getUserAgentStub, getConfigPath: getConfigPathStub, - setDefaults: setDefaultsStub - }, - request: {post: requestStub}, + setDefaults: setDefaultsStub, + stopBrowserStackBuild: stopBrowserStackBuildStub + } }); validateBstackJsonStub.returns(Promise.resolve(bsConfig)); return stop(args) .then(function (_bsConfig) { - sinon.assert.calledOnce(requestStub); - sinon.assert.calledOnce(getUserAgentStub); - sinon.assert.calledOnceWithExactly(sendUsageReportStub, bsConfig, args, message, messageType, errorCode); + sinon.assert.calledOnce(stopBrowserStackBuildStub); }) - .catch((error) => { - chai.assert.isNotOk(error, "Promise error"); - }); }); }); diff --git a/test/unit/bin/helpers/utils.js b/test/unit/bin/helpers/utils.js index a75b8c9e..fafd02be 100644 --- a/test/unit/bin/helpers/utils.js +++ b/test/unit/bin/helpers/utils.js @@ -11,7 +11,8 @@ const chai = require('chai'), chalk = require('chalk'), os = require("os"), crypto = require('crypto'), - fs = require('fs'); + fs = require('fs'), + axios = require('axios'); const getmac = require('getmac').default; const usageReporting = require('../../../../bin/helpers/usageReporting'); const utils = require('../../../../bin/helpers/utils'), @@ -98,6 +99,11 @@ describe('utils', () => { constant.validationMessages.INVALID_LOCAL_CONFIG_FILE ) ).to.eq('invalid_local_config_file'); + expect( + utils.getErrorCodeFromMsg( + constant.validationMessages.INVALID_LOCAL_ASYNC_ARGS + ) + ).to.eq('invalid_local_async_args'); expect( utils.getErrorCodeFromMsg('Invalid browserstack.json file.') ).to.eq('bstack_json_invalid'); @@ -1989,13 +1995,14 @@ describe('utils', () => { describe('#versionChangedMessage', () => { it('should return proper error message with placeholders replaced', () => { let preferredVersion = 'v1', - actualVersion = 'v2'; + actualVersion = 'v2', + frameworkUpgradeMessage = 'framework_upgrade_message'; let message = constant.userMessages.CYPRESS_VERSION_CHANGED.replace( '', preferredVersion - ).replace('', actualVersion); + ).replace('', actualVersion).replace('', 'framework_upgrade_message'); expect( - utils.versionChangedMessage(preferredVersion, actualVersion) + utils.versionChangedMessage(preferredVersion, actualVersion, frameworkUpgradeMessage) ).to.eq(message); }); }); @@ -2003,16 +2010,18 @@ describe('utils', () => { describe('#latestSyntaxToActualVersionMessage', () => { it('should return proper info message with placeholders replaced', () => { let latestSyntaxVersion = '7.latest', - actualVersion = '7.6.0'; + actualVersion = '7.6.0', + frameworkUpgradeMessage = 'framework_upgrade_message'; let message = constant.userMessages.LATEST_SYNTAX_TO_ACTUAL_VERSION_MESSAGE.replace( '', latestSyntaxVersion - ).replace('', actualVersion); + ).replace('', actualVersion).replace('', 'framework_upgrade_message'); expect( utils.latestSyntaxToActualVersionMessage( latestSyntaxVersion, - actualVersion + actualVersion, + frameworkUpgradeMessage ) ).to.eq(message); }); @@ -2370,4 +2379,167 @@ describe('utils', () => { } }); }); + + describe('setCLIMode', () => { + it('should set sync mode to false when async is set', () => { + let args = { + sync: true, + async: true + } + let bsConfig = {} + utils.setCLIMode(bsConfig, args); + expect(args.sync).to.be.eql(false) + }); + + it('should set sync mode to true by default', () => { + let args = { + sync: true + } + let bsConfig = {} + utils.setCLIMode(bsConfig, args); + expect(args.sync).to.be.eql(true) + }); + }); + + describe('stopBrowserStackBuild', () => { + let axiosPostStub, getUserAgentStub, sendUsageReportStub, message, messageType, errorCode; + let bsConfig = testObjects.sampleBsConfig; + let args = {} + let buildId = 'build_id'; + let body = testObjects.buildStopSampleBody; + + beforeEach(() => { + axiosPostStub = sandbox.stub(axios, "post"); + getUserAgentStub = sinon.stub(utils, 'getUserAgent').returns('user-agent'); + sendUsageReportStub = sinon.stub(utils, 'sendUsageReport'); + }); + afterEach(()=>{ + axiosPostStub.restore(); + getUserAgentStub.restore(); + sendUsageReportStub.restore(); + sandbox.restore(); + }) + + it('message thrown if API deprecated', async () => { + let api_deprecated_response = { + status: 299 + } + message = constant.userMessages.API_DEPRECATED; + messageType = constant.messageTypes.INFO; + errorCode = 'api_deprecated'; + axiosPostStub.resolves(api_deprecated_response); + await utils.stopBrowserStackBuild(bsConfig, args, buildId); + sinon.assert.calledOnce(axiosPostStub); + sinon.assert.calledOnce(getUserAgentStub); + sinon.assert.calledOnceWithExactly(sendUsageReportStub, bsConfig, args, message, messageType, errorCode); + }); + + it('message thrown if build returned', async () => { + let api_deprecated_response = { + status: 299, + data: body + } + message = body.message; + messageType = constant.messageTypes.INFO; + errorCode = 'api_deprecated'; + axiosPostStub.resolves(api_deprecated_response); + await utils.stopBrowserStackBuild(bsConfig, args, buildId); + sinon.assert.calledOnce(axiosPostStub); + sinon.assert.calledOnce(getUserAgentStub); + sinon.assert.calledOnceWithExactly(sendUsageReportStub, bsConfig, args, message, messageType, errorCode); + }); + + it('message thrown if statusCode != 200', async () => { + let non_200_status_response = { + status: 400 + } + message = constant.userMessages.BUILD_STOP_FAILED; + messageType = constant.messageTypes.ERROR; + errorCode = 'api_failed_build_stop'; + axiosPostStub.resolves(non_200_status_response); + await utils.stopBrowserStackBuild(bsConfig, args, buildId); + sinon.assert.calledOnce(axiosPostStub); + sinon.assert.calledOnce(getUserAgentStub); + sinon.assert.calledOnceWithExactly(sendUsageReportStub, bsConfig, args, message, messageType, errorCode); + }); + + it('message thrown if statusCode != 200 and user unauthorized', async () => { + let body_with_message = { + ...body, + message: "Unauthorized", + }; + let non_200_status_response = { + status: 401, + data: body_with_message + } + + message = `${ + constant.userMessages.BUILD_STOP_FAILED + } with error: \n${JSON.stringify(body_with_message, null, 2)}`; + messageType = constant.messageTypes.ERROR; + errorCode = 'api_auth_failed'; + axiosPostStub.resolves(non_200_status_response); + await utils.stopBrowserStackBuild(bsConfig, args, buildId); + sinon.assert.calledOnce(axiosPostStub); + sinon.assert.calledOnce(getUserAgentStub); + sinon.assert.calledOnceWithExactly(sendUsageReportStub, bsConfig, args, message, messageType, errorCode); + }); + + it('message thrown if statusCode != 200 and build is present', async () => { + let non_200_status_response = { + status: 402, + data: body + } + + message = `${ + constant.userMessages.BUILD_STOP_FAILED + } with error: \n${JSON.stringify(body, null, 2)}`; + messageType = constant.messageTypes.ERROR; + errorCode = 'api_failed_build_stop'; + axiosPostStub.resolves(non_200_status_response); + await utils.stopBrowserStackBuild(bsConfig, args, buildId); + sinon.assert.calledOnce(axiosPostStub); + sinon.assert.calledOnce(getUserAgentStub); + sinon.assert.calledOnceWithExactly(sendUsageReportStub, bsConfig, args, message, messageType, errorCode); + }); + + it('message thrown if API success', async () => { + let success_response = { + status: 200, + data: body + } + + message = `${JSON.stringify(body, null, 2)}`; + messageType = constant.messageTypes.SUCCESS; + errorCode = null; + axiosPostStub.resolves(success_response); + await utils.stopBrowserStackBuild(bsConfig, args, buildId); + sinon.assert.calledOnce(axiosPostStub); + sinon.assert.calledOnce(getUserAgentStub); + sinon.assert.calledOnceWithExactly(sendUsageReportStub, bsConfig, args, message, messageType, errorCode); + }); + }); + + describe('setProcessHooks', () => { + it('should handle "SIGINT" event', (done) => { + let buildId = 'build_id'; + let bsConfig = testObjects.sampleBsConfig; + let bsLocalStub = sinon.stub(); + let args= {} + + let warnLogSpy = sinon.spy(logger, 'warn') + let stopBrowserStackBuildStub= sinon.stub(utils, 'stopBrowserStackBuild').returns(Promise.resolve(true)); + sinon.stub(utils, 'stopLocalBinary').returns(Promise.resolve(true)); + sinon.stub(process, 'exit').returns({}); + utils.setProcessHooks(buildId, bsConfig, bsLocalStub, args); + process.on('SIGINT', () => { + sinon.assert.calledWith(warnLogSpy, constant.userMessages.PROCESS_KILL_MESSAGE); + sinon.assert.calledOnce(stopBrowserStackBuildStub); + done(); + }); + process.emit('SIGINT'); + sinon.stub.restore(); + process.exit.restore(); + }); + }); });