From ff9be4de1f702a47acd05a82ff28a9d0f48a99f3 Mon Sep 17 00:00:00 2001 From: Karan Shah <64479353+karanshah-browserstack@users.noreply.github.com> Date: Fri, 12 Mar 2021 21:10:17 +0530 Subject: [PATCH] Improve Local Testing experience (#114) * Added local_mode and cli args. Calculate local_identifier based on local_mode. * Adding Local Binary start and stop logic * Adding demand message for local-identifier and local-mode options * Adding log messages for sync and local mode * upgrading yargs and putting requiredArgs * passed already failing 12 cases * added new test cases for the modified code * added new test cases and passed already failing test cases * removed unwanted files * Checking local identifier running in always on mode * local inferred, local mode inferred, sync inferred. - Adding checks and params for local, local mode, and sync inferred. * passed all failing tests and added new specs * Adding local start and stop error * Updated validation messages for local cli * Send the value of local_mode not the true/false * Bug fixes for callback stop and start binary * Removing bshost. * Adding local_config_file for passing configuration for local binary * Updating inferred event logic - If the user passed anything explicitly, then it is not inferred. - This is to understand which of the "auto-fallback" flows does CLI fall in. - Inverted the logic of the *_inferred events. * updating user agent to 1.7.2 * written extra specs and passed failing spec * removed unwanted log files * covered new functions under test * Modified local unit tests * Fixing Unit test cases * removed local object creation * Indentation and minor null pointer fixes. * Adding placeholders for local_mode and local_config_file in default json * Local Start fail error. * Fix Local Inferred logic * Fixed local_mode not set to always-on when local_identifier is supplied in browserstack.json * Send local_identifier_error when local_identifier not running in stopBinary * Local mode and id validation * Added test cases for local testing bug fixes * Warning message in case of local_mode is invalid * Updated message for invalid local config file * Fixed cli args precendence on-demand bug * Updating version to 1.8.0 Co-authored-by: Surya Tripathi Co-authored-by: roshan --- bin/commands/runs.js | 39 ++- bin/helpers/capabilityHelper.js | 49 ++- bin/helpers/config.json | 3 +- bin/helpers/constants.js | 15 +- bin/helpers/usageReporting.js | 5 +- bin/helpers/utils.js | 260 ++++++++++++++- bin/runner.js | 16 + bin/templates/configTemplate.js | 4 +- package.json | 5 +- test/unit/bin/commands/runs.js | 84 ++++- test/unit/bin/helpers/utils.js | 559 +++++++++++++++++++++++++++++++- 11 files changed, 970 insertions(+), 69 deletions(-) diff --git a/bin/commands/runs.js b/bin/commands/runs.js index d1ec0202..fb9af132 100644 --- a/bin/commands/runs.js +++ b/bin/commands/runs.js @@ -40,10 +40,16 @@ module.exports = function run(args) { utils.setTestEnvs(bsConfig, args); //accept the local from env variable if provided - utils.setLocal(bsConfig); + utils.setLocal(bsConfig, args); + + // set Local Mode (on-demand/ always-on) + utils.setLocalMode(bsConfig, args); //accept the local identifier from env variable if provided - utils.setLocalIdentifier(bsConfig); + utils.setLocalIdentifier(bsConfig, args); + + // set Local Config File + utils.setLocalConfigFile(bsConfig, args); // run test in headed mode utils.setHeaded(bsConfig, args); @@ -61,8 +67,12 @@ module.exports = function run(args) { return archiver.archive(bsConfig.run_settings, config.fileName, args.exclude).then(function (data) { // Uploaded zip file - return zipUploader.zipUpload(bsConfig, config.fileName).then(function (zip) { + return zipUploader.zipUpload(bsConfig, config.fileName).then(async function (zip) { // Create build + + //setup Local Testing + let bs_local = await utils.setupLocalTesting(bsConfig, args); + return build.createBuild(bsConfig, zip).then(function (data) { let message = `${data.message}! ${Constants.userMessages.BUILD_CREATED} with build id: ${data.build_id}`; let dashboardLink = `${Constants.userMessages.VISIT_DASHBOARD} ${data.dashboard_url}`; @@ -82,7 +92,11 @@ module.exports = function run(args) { } if (args.sync) { - syncRunner.pollBuildStatus(bsConfig, data).then((exitCode) => { + syncRunner.pollBuildStatus(bsConfig, data).then(async (exitCode) => { + + // stop the Local instance + await utils.stopLocalBinary(bsConfig, bs_local, args); + // Generate custom report! reportGenerator(bsConfig, data.build_id, args, function(){ utils.sendUsageReport(bsConfig, args, `${message}\n${dashboardLink}`, Constants.messageTypes.SUCCESS, null); @@ -96,17 +110,24 @@ module.exports = function run(args) { if(!args.sync) logger.info(Constants.userMessages.EXIT_SYNC_CLI_MESSAGE.replace("",data.build_id)); utils.sendUsageReport(bsConfig, args, `${message}\n${dashboardLink}`, Constants.messageTypes.SUCCESS, null); return; - }).catch(function (err) { + }).catch(async function (err) { // Build creation failed logger.error(err); + // stop the Local instance + await utils.stopLocalBinary(bsConfig, bs_local, args); + utils.sendUsageReport(bsConfig, args, err, Constants.messageTypes.ERROR, 'build_failed'); }); }).catch(function (err) { - // Zip Upload failed + // Zip Upload failed | Local Start failed logger.error(err); - logger.error(Constants.userMessages.ZIP_UPLOAD_FAILED); - fileHelpers.deleteZip(); - utils.sendUsageReport(bsConfig, args, `${err}\n${Constants.userMessages.ZIP_UPLOAD_FAILED}`, Constants.messageTypes.ERROR, 'zip_upload_failed'); + if(err === Constants.userMessages.LOCAL_START_FAILED){ + utils.sendUsageReport(bsConfig, args, `${err}\n${Constants.userMessages.LOCAL_START_FAILED}`, Constants.messageTypes.ERROR, 'local_start_failed'); + } else { + logger.error(Constants.userMessages.ZIP_UPLOAD_FAILED); + fileHelpers.deleteZip(); + utils.sendUsageReport(bsConfig, args, `${err}\n${Constants.userMessages.ZIP_UPLOAD_FAILED}`, Constants.messageTypes.ERROR, 'zip_upload_failed'); + } }); }).catch(function (err) { // Zipping failed diff --git a/bin/helpers/capabilityHelper.js b/bin/helpers/capabilityHelper.js index 5d314bdd..0872f6b8 100644 --- a/bin/helpers/capabilityHelper.js +++ b/bin/helpers/capabilityHelper.js @@ -42,18 +42,47 @@ const caps = (bsConfig, zip) => { reject("Test suite is empty"); } + // Inferred settings + if(bsConfig.connection_settings){ + if (bsConfig.connection_settings.local_mode_inferred) { + obj.local_mode_inferred = bsConfig.connection_settings.local_mode_inferred; + } + + if (bsConfig.connection_settings.local_inferred) { + obj.local_inferred = bsConfig.connection_settings.local_inferred; + } + + if (bsConfig.connection_settings.sync_inferred) { + obj.sync_inferred = bsConfig.connection_settings.sync_inferred; + logger.info('Setting "sync" mode to enable Local testing.'); + } + } + // Local obj.local = false; - if (bsConfig.connection_settings && bsConfig.connection_settings.local === true) obj.local = true; - logger.info(`Local is set to: ${obj.local} (${obj.local ? Constants.userMessages.LOCAL_TRUE : Constants.userMessages.LOCAL_FALSE})`); + if (bsConfig.connection_settings && bsConfig.connection_settings.local === true) { + obj.local = true; + } + + obj.localMode = null; + // Local Mode + if (obj.local === true && bsConfig.connection_settings.local_mode) { + obj.localMode = bsConfig.connection_settings.local_mode; + if (bsConfig.connection_settings.user_defined_local_mode_warning) { + logger.warn(Constants.userMessages.INVALID_LOCAL_MODE_WARNING); + } + logger.info(`Local testing set up in ${obj.localMode} mode.`); + } // Local Identifier obj.localIdentifier = null; if (obj.local === true && (bsConfig.connection_settings.localIdentifier || bsConfig.connection_settings.local_identifier)) { obj.localIdentifier = bsConfig.connection_settings.localIdentifier || bsConfig.connection_settings.local_identifier; - logger.log(`Local Identifier is set to: ${obj.localIdentifier}`); + logger.info(`Local testing identifier: ${obj.localIdentifier}`); } + logger.info(`Local is set to: ${obj.local} (${obj.local ? Constants.userMessages.LOCAL_TRUE : Constants.userMessages.LOCAL_FALSE})`); + // Project name obj.project = "project-name"; // Build name @@ -94,9 +123,9 @@ const caps = (bsConfig, zip) => { if(obj.parallels === Constants.cliMessages.RUN.DEFAULT_PARALLEL_MESSAGE) obj.parallels = undefined - if (obj.project) logger.log(`Project name is: ${obj.project}`); + if (obj.project) logger.info(`Project name is: ${obj.project}`); - if (obj.customBuildName) logger.log(`Build name is: ${obj.customBuildName}`); + if (obj.customBuildName) logger.info(`Build name is: ${obj.customBuildName}`); if (obj.callbackURL) logger.info(`callback url is : ${obj.callbackURL}`); @@ -132,6 +161,16 @@ const validate = (bsConfig, args) => { // if parallels specified via arguments validate only arguments if (!Utils.isUndefined(args) && !Utils.isUndefined(args.parallels) && !Utils.isParallelValid(args.parallels)) reject(Constants.validationMessages.INVALID_PARALLELS_CONFIGURATION); + // validate local args i.e --local-mode and --local-identifier + + if( Utils.searchForOption('--local-identifier') && (Utils.isUndefined(args.localIdentifier) || (!Utils.isUndefined(args.localIdentifier) && !args.localIdentifier.trim()))) reject(Constants.validationMessages.INVALID_CLI_LOCAL_IDENTIFIER); + + if( Utils.getLocalFlag(bsConfig.connection_settings) && (Utils.isUndefined(bsConfig["connection_settings"]["local_identifier"]) || ( !Utils.isUndefined(bsConfig["connection_settings"]["local_identifier"]) && !bsConfig["connection_settings"]["local_identifier"].trim()))) reject(Constants.validationMessages.INVALID_LOCAL_IDENTIFIER); + + if( Utils.searchForOption('--local-mode') && ( Utils.isUndefined(args.localMode) || (!Utils.isUndefined(args.localMode) && !["always-on","on-demand"].includes(args.localMode)))) reject(Constants.validationMessages.INVALID_LOCAL_MODE); + + 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); + // 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/config.json b/bin/helpers/config.json index dbd6b7a3..83596c99 100644 --- a/bin/helpers/config.json +++ b/bin/helpers/config.json @@ -2,5 +2,6 @@ "uploadUrl": "https://api-cloud.browserstack.com/automate-frameworks/cypress/upload", "rails_host": "https://api.browserstack.com", "dashboardUrl": "https://automate.browserstack.com/dashboard/v2/builds/", - "usageReportingUrl": "https://eds.browserstack.com:443/send_event_cy_internal" + "usageReportingUrl": "https://eds.browserstack.com:443/send_event_cy_internal", + "localTestingListUrl": "https://www.browserstack.com/local/v1/list" } diff --git a/bin/helpers/constants.js b/bin/helpers/constants.js index d029440b..17eb1afc 100644 --- a/bin/helpers/constants.js +++ b/bin/helpers/constants.js @@ -37,7 +37,10 @@ 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" }; const validationMessages = { @@ -57,7 +60,11 @@ const validationMessages = { INVALID_CYPRESS_JSON: "cypress.json is not a valid json", INVALID_DEFAULT_AUTH_PARAMS: "Your username and access key are required to run your tests on BrowserStack. Learn more at https://www.browserstack.com/docs/automate/cypress/authentication", LOCAL_NOT_SET: "To test on BrowserStack, you will have to set up Local testing. Read more here: https://www.browserstack.com/docs/automate/cypress/local-testing", - INCORRECT_DIRECTORY_STRUCTURE: "No tests to run. Note that your Cypress tests should be in the same directory where the cypress.json exists." + INCORRECT_DIRECTORY_STRUCTURE: "No tests to run. Note that your Cypress tests should be in the same directory where the cypress.json exists.", + INVALID_CLI_LOCAL_IDENTIFIER: "When using --local-identifier, a value needs to be supplied. \n--local-identifier .\nFor more info, check out https://www.browserstack.com/docs/automate/cypress/cli-reference", + 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" }; const cliMessages = { @@ -94,6 +101,10 @@ const cliMessages = { SYNC_DESCRIPTION: "Makes the run command in sync", 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", + LOCAL_MODE: 'Accepted values: ("always-on" | "on-demand") - if you choose to keep the binary "always-on", it will speed up your tests by keeping the Local connection warmed up in the background; otherwise, you can choose to have it spawn and killed for every build', + LOCAL_IDENTIFIER: "Accepted values: String - assign an identifier to your Local process instance", + LOCAL_CONFIG_FILE: "Accepted values: String - path to local config-file to your Local process instance. Learn more at https://www.browserstack.com/local-testing/binary-params" }, COMMON: { DISABLE_USAGE_REPORTING: "Disable usage reporting", diff --git a/bin/helpers/usageReporting.js b/bin/helpers/usageReporting.js index 174f66f3..2001487e 100644 --- a/bin/helpers/usageReporting.js +++ b/bin/helpers/usageReporting.js @@ -231,5 +231,6 @@ function send(args) { } module.exports = { - send -} + send, + cli_version_and_path, +}; diff --git a/bin/helpers/utils.js b/bin/helpers/utils.js index 9c979796..c59c9785 100644 --- a/bin/helpers/utils.js +++ b/bin/helpers/utils.js @@ -3,6 +3,9 @@ const os = require("os"); const path = require("path"); const fs = require("fs"); const glob = require('glob'); +const getmac = require('getmac').default; +const { v4: uuidv4 } = require('uuid'); +const browserstack = require('browserstack-local'); const usageReporting = require("./usageReporting"), logger = require("./logger").winstonLogger, @@ -11,6 +14,8 @@ const usageReporting = require("./usageReporting"), syncCliLogger = require("../helpers/logger").syncCliLogger, config = require("../helpers/config"); +const request = require('request'); + exports.validateBstackJson = (bsConfigPath) => { return new Promise(function (resolve, reject) { try { @@ -33,34 +38,46 @@ exports.getErrorCodeFromMsg = (errMsg) => { let errorCode = null; switch (errMsg) { case Constants.validationMessages.EMPTY_BROWSERSTACK_JSON: - errorCode = "bstack_json_invalid_empty"; + errorCode = 'bstack_json_invalid_empty'; break; case Constants.validationMessages.INCORRECT_AUTH_PARAMS: - errorCode = "bstack_json_invalid_missing_keys"; + errorCode = 'bstack_json_invalid_missing_keys'; break; case Constants.validationMessages.EMPTY_BROWSER_LIST: - errorCode = "bstack_json_invalid_no_browsers"; + errorCode = 'bstack_json_invalid_no_browsers'; break; case Constants.validationMessages.EMPTY_RUN_SETTINGS: - errorCode = "bstack_json_invalid_no_run_settings"; + errorCode = 'bstack_json_invalid_no_run_settings'; break; case Constants.validationMessages.EMPTY_CYPRESS_PROJ_DIR: - errorCode = "bstack_json_invalid_no_cypress_proj_dir"; + errorCode = 'bstack_json_invalid_no_cypress_proj_dir'; break; case Constants.validationMessages.INVALID_DEFAULT_AUTH_PARAMS: - errorCode = "bstack_json_default_auth_keys"; + errorCode = 'bstack_json_default_auth_keys'; break; case Constants.validationMessages.INVALID_PARALLELS_CONFIGURATION: - errorCode = "invalid_parallels_specified"; + errorCode = 'invalid_parallels_specified'; + break; + case Constants.validationMessages.INVALID_LOCAL_IDENTIFIER: + errorCode = 'invalid_local_identifier'; + break; + case Constants.validationMessages.INVALID_CLI_LOCAL_IDENTIFIER: + errorCode = 'invalid_local_identifier'; + break; + case Constants.validationMessages.INVALID_LOCAL_MODE: + errorCode = 'invalid_local_mode'; + break; + case Constants.validationMessages.INVALID_LOCAL_CONFIG_FILE: + errorCode = 'invalid_local_config_file'; break; case Constants.validationMessages.LOCAL_NOT_SET: - errorCode = "cypress_json_base_url_no_local"; + errorCode = 'cypress_json_base_url_no_local'; break; case Constants.validationMessages.INCORRECT_DIRECTORY_STRUCTURE: - errorCode = "invalid_directory_structure"; + errorCode = 'invalid_directory_structure'; break; case Constants.validationMessages.INVALID_CYPRESS_CONFIG_FILE: - errorCode = "invalid_cypress_config_file"; + errorCode = 'invalid_cypress_config_file'; break; } if ( @@ -247,7 +264,7 @@ exports.isParallelValid = (value) => { } exports.getUserAgent = () => { - return `BStack-Cypress-CLI/1.5.1 (${os.arch()}/${os.platform()}/${os.release()})`; + return `BStack-Cypress-CLI/1.8.0 (${os.arch()}/${os.platform()}/${os.release()})`; }; exports.isAbsolute = (configPath) => { @@ -311,25 +328,228 @@ exports.getLocalFlag = (connectionSettings) => { ); }; -exports.setLocal = (bsConfig) => { - if (!this.isUndefined(process.env.BROWSERSTACK_LOCAL)) { +exports.setLocal = (bsConfig, args) => { + let localInferred = !(this.searchForOption('--local-mode')); + if (!this.isUndefined(args.local)) { + let local = false; + if (String(args.local).toLowerCase() === 'true') { + local = true; + } + bsConfig['connection_settings']['local'] = local; + } else if (!this.isUndefined(process.env.BROWSERSTACK_LOCAL)) { let local = false; - if (String(process.env.BROWSERSTACK_LOCAL).toLowerCase() === "true") + if (String(process.env.BROWSERSTACK_LOCAL).toLowerCase() === 'true') { local = true; - bsConfig["connection_settings"]["local"] = local; + } + bsConfig['connection_settings']['local'] = local; logger.info( - "Reading local setting from the environment variable BROWSERSTACK_LOCAL" + 'Reading local setting from the environment variable BROWSERSTACK_LOCAL' ); + } else if ( + this.isUndefined(bsConfig['connection_settings']['local']) && + ( !this.isUndefined(args.localMode) || !this.isUndefined(bsConfig['connection_settings']['local_mode']) ) + ) { + bsConfig['connection_settings']['local'] = true; + bsConfig.connection_settings.local_inferred = localInferred; } }; -exports.setLocalIdentifier = (bsConfig) => { - if (!this.isUndefined(process.env.BROWSERSTACK_LOCAL_IDENTIFIER)) { +exports.setLocalIdentifier = (bsConfig, args) => { + if (!this.isUndefined(args.localIdentifier)){ + bsConfig["connection_settings"]["local_identifier"] = args.localIdentifier; + bsConfig['connection_settings']['local_mode'] = "always-on"; + } else if (!this.isUndefined(process.env.BROWSERSTACK_LOCAL_IDENTIFIER)) { bsConfig["connection_settings"]["local_identifier"] = process.env.BROWSERSTACK_LOCAL_IDENTIFIER; logger.info( "Reading local identifier from the environment variable BROWSERSTACK_LOCAL_IDENTIFIER" ); + bsConfig['connection_settings']['local_mode'] = 'always-on'; + } else if ( + bsConfig['connection_settings']['local'] && + !this.isUndefined(bsConfig["connection_settings"]["local_identifier"]) + ){ + bsConfig['connection_settings']['local_mode'] = 'always-on'; + } else if ( + bsConfig['connection_settings']['local'] && + this.isUndefined(bsConfig["connection_settings"]["local_identifier"]) + ){ + bsConfig["connection_settings"]["local_identifier"] = this.generateLocalIdentifier(bsConfig['connection_settings']['local_mode']); + } +}; + +exports.setLocalMode = (bsConfig, args) => { + if(String(bsConfig["connection_settings"]["local"]).toLowerCase() === "true"){ + let local_mode = 'on-demand'; + + let localModeUndefined= this.isUndefined(bsConfig["connection_settings"]["local_mode"]); + + if (!this.isUndefined(args.localMode)) { + if(String(args.localMode) === "always-on"){ + local_mode = 'always-on'; + } + } else if (!localModeUndefined && !["always-on", "on-demand"].includes(bsConfig['connection_settings']['local_mode'])) { + bsConfig.connection_settings.user_defined_local_mode_warning = bsConfig['connection_settings']['local_mode']; + } else if ( + !this.isUndefined(bsConfig['connection_settings']['local_mode']) && + String(bsConfig['connection_settings']['local_mode']).toLowerCase() === + 'always-on' + ) { + local_mode = 'always-on'; + } + bsConfig['connection_settings']['local_mode'] = local_mode; + if (this.isUndefined(args.sync) || !args.sync ){ + bsConfig['connection_settings']['sync_inferred'] = true; + } + args.sync = true; + + let localModeInferred = !(this.searchForOption('--local-mode')); + + if (localModeInferred && localModeUndefined) { + bsConfig.connection_settings.local_mode_inferred = local_mode; + } + } +}; + +exports.setupLocalTesting = (bsConfig, args) => { + return new Promise(async (resolve, reject) => { + if( bsConfig['connection_settings'] && bsConfig['connection_settings']['local'] && String(bsConfig['connection_settings']['local']) === "true" ){ + let localIdentifierRunning = await this.checkLocalIdentifierRunning( + bsConfig, bsConfig['connection_settings']['local_identifier'] + ); + if (!localIdentifierRunning){ + var bs_local = this.getLocalBinary(); + var bs_local_args = this.setLocalArgs(bsConfig, args); + let that = this; + logger.info('Setting up Local testing...'); + bs_local.start(bs_local_args, function (localStartError) { + if (that.isUndefined(localStartError)) { + resolve(bs_local); + } else { + let message = `name: ${localStartError.name}, message: ${localStartError.message}, extra: ${localStartError.extra}`, + errorCode = "local_start_error"; + that.sendUsageReport( + bsConfig, + args, + message, + Constants.messageTypes.ERROR, + errorCode + ); + reject(Constants.userMessages.LOCAL_START_FAILED); + } + }); + } else { + resolve(); + } + } else { + resolve(); + } + }); +}; + +exports.stopLocalBinary = (bsConfig, bs_local, args) => { + return new Promise(async (resolve, reject) => { + if(bsConfig['connection_settings'] && bsConfig['connection_settings']['local']){ + let localIdentifierRunning = await this.checkLocalIdentifierRunning(bsConfig,bsConfig["connection_settings"]["local_identifier"]); + if(!localIdentifierRunning){ + let message = `Local Binary not running.`, + errorCode = 'local_identifier_error'; + this.sendUsageReport( + bsConfig, + args, + message, + Constants.messageTypes.ERROR, + errorCode + ); + } + } + if (!this.isUndefined(bs_local) && bs_local.isRunning() && bsConfig['connection_settings'] && bsConfig['connection_settings']['local_mode'].toLowerCase() != "always-on") { + let that = this; + bs_local.stop(function (localStopError) { + if (that.isUndefined(localStopError)) { + resolve(); + } else { + let message = `name: ${localStopError.name}, message: ${localStopError.message}, extra: ${localStopError.extra}`, + errorCode = 'local_stop_error'; + that.sendUsageReport( + bsConfig, + args, + message, + Constants.messageTypes.ERROR, + errorCode + ); + resolve(Constants.userMessages.LOCAL_STOP_FAILED); + } + }); + } else { + resolve(); + } + }); +}; + +exports.getLocalBinary = () => { + return new browserstack.Local(); +}; + +exports.setLocalArgs = (bsConfig, args) => { + let local_args = {} + local_args['key'] = bsConfig['auth']['access_key']; + local_args['localIdentifier'] = bsConfig["connection_settings"]["local_identifier"]; + local_args['daemon'] = true; + local_args['enable-logging-for-api'] = true + local_args['source'] = `cypress:${usageReporting.cli_version_and_path(bsConfig).version}`; + if(!this.isUndefined(bsConfig["connection_settings"]["local_config_file"])){ + local_args['config-file'] = path.resolve(bsConfig["connection_settings"]["local_config_file"]); + } + return local_args; +}; + +exports.generateLocalIdentifier = (mode) => { + let local_identifier = undefined; + if(mode == "always-on"){ + local_identifier = getmac(); + } else { + local_identifier = uuidv4(); + } + return Buffer.from(local_identifier).toString("base64"); +}; + +exports.checkLocalIdentifierRunning = (bsConfig, localIdentifier) => { + let options = { + url: `${config.localTestingListUrl}?auth_token=${bsConfig.auth.access_key}&state=running`, + auth: { + user: bsConfig.auth.username, + password: bsConfig.auth.access_key, + }, + headers: { + 'User-Agent': this.getUserAgent(), + }, + }; + let that = this; + return new Promise ( function(resolve, reject) { + request.get(options, function (err, resp, body) { + if(err){ + reject(err); + } + let response = JSON.parse(body); + let localInstances = []; + if(!that.isUndefined(response['instances'])){ + localInstances = response['instances']; + } + let localIdentifiers = []; + + localInstances.forEach(function(instance){ + localIdentifiers.push(instance['localIdentifier']); + }); + + resolve(localIdentifiers.includes(localIdentifier)); + }); + }); +}; + +exports.setLocalConfigFile = (bsConfig, args) => { + if(!this.isUndefined(args.localConfigFile)){ + bsConfig['connection_settings']['local_config_file'] = args.localConfigFile; } }; @@ -402,6 +622,10 @@ exports.isJSONInvalid = (err, args) => { return false } + if( err === Constants.validationMessages.INVALID_CLI_LOCAL_IDENTIFIER || err === Constants.validationMessages.INVALID_LOCAL_MODE ){ + return false + } + return invalid } diff --git a/bin/runner.js b/bin/runner.js index 35a5c11b..30586293 100755 --- a/bin/runner.js +++ b/bin/runner.js @@ -205,6 +205,22 @@ var argv = yargs default: false, describe: Constants.cliMessages.RUN.HEADED, type: "boolean" + }, + 'local': { + describe: Constants.cliMessages.RUN.LOCAL, + type: "boolean" + }, + 'local-identifier': { + describe: Constants.cliMessages.RUN.LOCAL_IDENTIFIER, + type: "string" + }, + 'local-mode': { + describe: Constants.cliMessages.RUN.LOCAL_MODE, + type: "string" + }, + 'local-config-file': { + describe: Constants.cliMessages.RUN.LOCAL_CONFIG_FILE, + type: "string" } }) .help('help') diff --git a/bin/templates/configTemplate.js b/bin/templates/configTemplate.js index 8593abc6..192983d4 100644 --- a/bin/templates/configTemplate.js +++ b/bin/templates/configTemplate.js @@ -65,7 +65,9 @@ module.exports = function () { }, "connection_settings": { "local": false, - "local_identifier": null + "local_identifier": null, + "local_mode": null, + "local_config_file": null }, "disable_usage_reporting": false } diff --git a/package.json b/package.json index 05330af0..d68bcd8c 100644 --- a/package.json +++ b/package.json @@ -14,15 +14,18 @@ "dependencies": { "archiver": "^5.2.0", "async": "^3.2.0", + "browserstack-local": "^1.4.8", "chalk": "^4.1.0", "fs-extra": "^8.1.0", + "getmac": "^5.17.0", "glob": "^7.1.6", "mkdirp": "^1.0.3", "request": "^2.88.0", "requestretry": "^4.1.0", "table": "^5.4.6", + "uuid": "^8.3.2", "winston": "^2.3.1", - "yargs": "^14.2.2" + "yargs": "^14.2.3" }, "repository": { "type": "git", diff --git a/test/unit/bin/commands/runs.js b/test/unit/bin/commands/runs.js index f4d4de7a..e1a17365 100644 --- a/test/unit/bin/commands/runs.js +++ b/test/unit/bin/commands/runs.js @@ -5,7 +5,7 @@ const chai = require("chai"), const Constants = require("../../../../bin/helpers/constants"), logger = require("../../../../bin/helpers/logger").winstonLogger, testObjects = require("../../support/fixtures/testObjects"); -const { setHeaded } = require("../../../../bin/helpers/utils"); +const { setHeaded, setupLocalTesting, stopLocalBinary, setUserSpecs, setLocalConfigFile } = require("../../../../bin/helpers/utils"); const proxyquire = require("proxyquire").noCallThru(); @@ -91,6 +91,7 @@ describe("runs", () => { setUserSpecsStub = sandbox.stub(); setTestEnvsStub = sandbox.stub(); getConfigPathStub = sandbox.stub(); + setupLocalTestingStub = sandbox.stub(); setUsageReportingFlagStub = sandbox.stub().returns(undefined); sendUsageReportStub = sandbox.stub().callsFake(function () { return "end"; @@ -102,6 +103,8 @@ describe("runs", () => { setHeadedStub = sandbox.stub(); deleteResultsStub = sandbox.stub(); setDefaultsStub = sandbox.stub(); + setLocalModeStub = sandbox.stub(); + setLocalConfigFileStub = sandbox.stub(); }); afterEach(() => { @@ -132,14 +135,18 @@ describe("runs", () => { setHeaded: setHeadedStub, deleteResults: deleteResultsStub, setDefaults: setDefaultsStub, - isJSONInvalid: isJSONInvalidStub + setupLocalTesting: setupLocalTestingStub, + isJSONInvalid: isJSONInvalidStub, + setLocalMode: setLocalModeStub, + setLocalConfigFile: setLocalConfigFileStub }, '../helpers/capabilityHelper': { - validate: capabilityValidatorStub, + validate: capabilityValidatorStub }, }); validateBstackJsonStub.returns(Promise.resolve(bsConfig)); + setupLocalTestingStub.returns(Promise.resolve("return nothing")); capabilityValidatorStub.returns(Promise.reject("random-error")); return runs(args) @@ -149,15 +156,23 @@ describe("runs", () => { .catch((error) => { sinon.assert.calledOnce(getConfigPathStub); sinon.assert.calledOnce(getConfigPathStub); + sinon.assert.calledOnce(deleteResultsStub); sinon.assert.calledOnce(validateBstackJsonStub); + sinon.assert.calledOnce(setDefaultsStub); + sinon.assert.calledOnce(setUsernameStub); + sinon.assert.calledOnce(setAccessKeyStub); + sinon.assert.calledOnce(setBuildNameStub); + sinon.assert.calledOnce(setCypressConfigFilenameStub); + sinon.assert.calledOnce(setUserSpecsStub); + sinon.assert.calledOnce(setTestEnvsStub); + sinon.assert.calledOnce(setLocalStub); + sinon.assert.calledOnce(setLocalModeStub); + sinon.assert.calledOnce(setLocalConfigFileStub); + sinon.assert.calledOnce(setHeadedStub); sinon.assert.calledOnce(capabilityValidatorStub); - sinon.assert.calledOnce(setUsageReportingFlagStub); sinon.assert.calledOnce(getErrorCodeFromMsgStub); - sinon.assert.calledOnce(setLocalStub); sinon.assert.calledOnce(setLocalIdentifierStub); - sinon.assert.calledOnce(setHeadedStub); - sinon.assert.calledOnce(deleteResultsStub); - sinon.assert.calledOnce(setDefaultsStub); + sinon.assert.calledOnce(setUsageReportingFlagStub); sinon.assert.calledOnceWithExactly( sendUsageReportStub, bsConfig, @@ -192,11 +207,14 @@ describe("runs", () => { archiverStub = sandbox.stub(); deleteZipStub = sandbox.stub(); setLocalStub = sandbox.stub(); + setLocalModeStub = sandbox.stub(); + setupLocalTestingStub = sandbox.stub(); setLocalIdentifierStub = sandbox.stub(); setHeadedStub = sandbox.stub(); deleteResultsStub = sandbox.stub(); getNumberOfSpecFilesStub = sandbox.stub().returns([]); setDefaultsStub = sandbox.stub(); + setLocalConfigFileStub = sandbox.stub(); }); afterEach(() => { @@ -223,11 +241,14 @@ describe("runs", () => { setUsageReportingFlag: setUsageReportingFlagStub, getConfigPath: getConfigPathStub, setLocal: setLocalStub, + setLocalMode: setLocalModeStub, + setupLocalTesting: setupLocalTestingStub, setLocalIdentifier: setLocalIdentifierStub, setHeaded: setHeadedStub, deleteResults: deleteResultsStub, setDefaults: setDefaultsStub, - getNumberOfSpecFiles: getNumberOfSpecFilesStub + getNumberOfSpecFiles: getNumberOfSpecFilesStub, + setLocalConfigFile: setLocalConfigFileStub }, '../helpers/capabilityHelper': { validate: capabilityValidatorStub, @@ -241,6 +262,7 @@ describe("runs", () => { }); validateBstackJsonStub.returns(Promise.resolve(bsConfig)); + setupLocalTestingStub.returns(Promise.resolve("nothing")) capabilityValidatorStub.returns(Promise.resolve(Constants.validationMessages.VALIDATED)); archiverStub.returns(Promise.reject("random-error")); @@ -251,6 +273,12 @@ describe("runs", () => { .catch((error) => { sinon.assert.calledOnce(getConfigPathStub); sinon.assert.calledOnce(getConfigPathStub); + sinon.assert.calledOnce(setLocalModeStub); + sinon.assert.calledOnce(setUsernameStub); + sinon.assert.calledOnce(setAccessKeyStub); + sinon.assert.calledOnce(setBuildNameStub); + sinon.assert.calledOnce(setLocalConfigFileStub); + sinon.assert.calledOnce(setCypressConfigFilenameStub); sinon.assert.calledOnce(getNumberOfSpecFilesStub); sinon.assert.calledOnce(setParallelsStub); sinon.assert.calledOnce(setLocalStub); @@ -298,11 +326,14 @@ describe("runs", () => { zipUploadStub = sandbox.stub(); deleteZipStub = sandbox.stub(); setLocalStub = sandbox.stub(); + setLocalModeStub = sandbox.stub(); + setupLocalTestingStub = sandbox.stub(); setLocalIdentifierStub = sandbox.stub(); setHeadedStub = sandbox.stub(); deleteResultsStub = sandbox.stub(); getNumberOfSpecFilesStub = sandbox.stub().returns([]); setDefaultsStub = sandbox.stub(); + setLocalConfigFileStub = sandbox.stub(); }); afterEach(() => { @@ -329,11 +360,14 @@ describe("runs", () => { setUsageReportingFlag: setUsageReportingFlagStub, getConfigPath: getConfigPathStub, setLocal: setLocalStub, + setLocalMode: setLocalModeStub, + setupLocalTesting: setupLocalTestingStub, setLocalIdentifier: setLocalIdentifierStub, setHeaded: setHeadedStub, deleteResults: deleteResultsStub, getNumberOfSpecFiles: getNumberOfSpecFilesStub, - setDefaults: setDefaultsStub + setDefaults: setDefaultsStub, + setLocalConfigFile: setLocalConfigFileStub }, '../helpers/capabilityHelper': { validate: capabilityValidatorStub, @@ -351,6 +385,7 @@ describe("runs", () => { validateBstackJsonStub.returns(Promise.resolve(bsConfig)); capabilityValidatorStub.returns(Promise.resolve(Constants.validationMessages.VALIDATED)); + setupLocalTestingStub.returns(Promise.resolve("nothing")); archiverStub.returns(Promise.resolve("Zipping completed")); zipUploadStub.returns(Promise.reject("random-error")); @@ -361,6 +396,8 @@ describe("runs", () => { .catch((error) => { sinon.assert.calledOnce(getConfigPathStub); sinon.assert.calledOnce(getConfigPathStub); + sinon.assert.calledOnce(setLocalModeStub); + sinon.assert.calledOnce(setLocalConfigFileStub); sinon.assert.calledOnce(getNumberOfSpecFilesStub); sinon.assert.calledOnce(setParallelsStub); sinon.assert.calledOnce(setLocalStub); @@ -412,11 +449,15 @@ describe("runs", () => { createBuildStub = sandbox.stub(); deleteZipStub = sandbox.stub(); setLocalStub = sandbox.stub(); + setLocalModeStub = sandbox.stub(); + setupLocalTestingStub = sandbox.stub(); setLocalIdentifierStub = sandbox.stub(); setHeadedStub = sandbox.stub(); deleteResultsStub = sandbox.stub(); getNumberOfSpecFilesStub = sandbox.stub().returns([]); setDefaultsStub = sandbox.stub(); + stopLocalBinaryStub = sandbox.stub(); + setLocalConfigFileStub = sandbox.stub(); }); afterEach(() => { @@ -443,11 +484,15 @@ describe("runs", () => { setUsageReportingFlag: setUsageReportingFlagStub, getConfigPath: getConfigPathStub, setLocal: setLocalStub, + setLocalMode: setLocalModeStub, + setupLocalTesting: setupLocalTestingStub, setLocalIdentifier: setLocalIdentifierStub, setHeaded: setHeadedStub, deleteResults: deleteResultsStub, getNumberOfSpecFiles: getNumberOfSpecFilesStub, - setDefaults: setDefaultsStub + setDefaults: setDefaultsStub, + stopLocalBinary: stopLocalBinaryStub, + setLocalConfigFile: setLocalConfigFileStub }, '../helpers/capabilityHelper': { validate: capabilityValidatorStub, @@ -467,11 +512,13 @@ describe("runs", () => { }); validateBstackJsonStub.returns(Promise.resolve(bsConfig)); + setupLocalTestingStub.returns(Promise.resolve("nothing")); capabilityValidatorStub.returns( Promise.resolve(Constants.validationMessages.VALIDATED) ); archiverStub.returns(Promise.resolve("Zipping completed")); zipUploadStub.returns(Promise.resolve("zip uploaded")); + stopLocalBinaryStub.returns(Promise.resolve("nothing")); createBuildStub.returns(Promise.reject("random-error")); return runs(args) @@ -481,6 +528,9 @@ describe("runs", () => { .catch((error) => { sinon.assert.calledOnce(getConfigPathStub); sinon.assert.calledOnce(getConfigPathStub); + sinon.assert.calledOnce(setLocalConfigFileStub); + sinon.assert.calledOnce(setLocalModeStub); + sinon.assert.calledOnce(setupLocalTestingStub); sinon.assert.calledOnce(validateBstackJsonStub); sinon.assert.calledOnce(capabilityValidatorStub); sinon.assert.calledOnce(getNumberOfSpecFilesStub); @@ -539,9 +589,12 @@ describe("runs", () => { setDefaultsStub = sandbox.stub(); isUndefinedStub = sandbox.stub(); setLocalStub = sandbox.stub(); + setLocalModeStub = sandbox.stub(); + setupLocalTestingStub = sandbox.stub(); setLocalIdentifierStub = sandbox.stub(); setHeadedStub = sandbox.stub(); getNumberOfSpecFilesStub = sandbox.stub().returns([]); + setLocalConfigFileStub = sandbox.stub(); }); afterEach(() => { @@ -569,13 +622,16 @@ describe("runs", () => { setParallels: setParallelsStub, getConfigPath: getConfigPathStub, setLocal: setLocalStub, + setLocalMode: setLocalModeStub, + setupLocalTesting: setupLocalTestingStub, setLocalIdentifier: setLocalIdentifierStub, setHeaded: setHeadedStub, exportResults: exportResultsStub, deleteResults: deleteResultsStub, setDefaults: setDefaultsStub, isUndefined: isUndefinedStub, - getNumberOfSpecFiles: getNumberOfSpecFilesStub + getNumberOfSpecFiles: getNumberOfSpecFilesStub, + setLocalConfigFile: setLocalConfigFileStub }, '../helpers/capabilityHelper': { validate: capabilityValidatorStub, @@ -598,6 +654,7 @@ describe("runs", () => { }); validateBstackJsonStub.returns(Promise.resolve(bsConfig)); + setupLocalTestingStub.returns(Promise.resolve("nothing")); capabilityValidatorStub.returns( Promise.resolve(Constants.validationMessages.VALIDATED) ); @@ -613,10 +670,13 @@ describe("runs", () => { sinon.assert.calledOnce(getConfigPathStub); sinon.assert.calledOnce(getConfigPathStub); sinon.assert.calledOnce(validateBstackJsonStub); + sinon.assert.calledOnce(setLocalConfigFileStub); sinon.assert.calledOnce(capabilityValidatorStub); sinon.assert.calledOnce(getNumberOfSpecFilesStub); sinon.assert.calledOnce(setParallelsStub); sinon.assert.calledOnce(setLocalStub); + sinon.assert.calledOnce(setLocalModeStub); + sinon.assert.calledOnce(setupLocalTestingStub); sinon.assert.calledOnce(setLocalIdentifierStub); sinon.assert.calledOnce(setHeadedStub); sinon.assert.calledOnce(archiverStub); diff --git a/test/unit/bin/helpers/utils.js b/test/unit/bin/helpers/utils.js index f5e943cf..84bb8997 100644 --- a/test/unit/bin/helpers/utils.js +++ b/test/unit/bin/helpers/utils.js @@ -1,6 +1,8 @@ 'use strict'; const path = require('path'); +var sandbox = require('sinon').createSandbox(); +const request = require('request'); const chai = require('chai'), expect = chai.expect, sinon = require('sinon'), @@ -8,13 +10,14 @@ const chai = require('chai'), glob = require('glob'), chalk = require('chalk'), fs = require('fs'); - +const getmac = require('getmac').default; +const usageReporting = require('../../../../bin/helpers/usageReporting'); const utils = require('../../../../bin/helpers/utils'), constant = require('../../../../bin/helpers/constants'), logger = require('../../../../bin/helpers/logger').winstonLogger, testObjects = require('../../support/fixtures/testObjects'), syncLogger = require("../../../../bin/helpers/logger").syncCliLogger; - +const browserstack = require('browserstack-local'); chai.use(chaiAsPromised); logger.transports['console.info'].silent = true; @@ -73,6 +76,31 @@ describe('utils', () => { 'Please use --config-file .' ) ).to.eq('bstack_json_path_invalid'); + expect( + utils.getErrorCodeFromMsg( + constant.validationMessages.INVALID_LOCAL_IDENTIFIER + ) + ).to.eq('invalid_local_identifier'); + expect( + utils.getErrorCodeFromMsg( + constant.validationMessages.INVALID_CLI_LOCAL_IDENTIFIER + ) + ).to.eq('invalid_local_identifier'); + expect( + utils.getErrorCodeFromMsg( + constant.validationMessages.INVALID_LOCAL_MODE + ) + ).to.eq('invalid_local_mode'); + expect( + utils.getErrorCodeFromMsg( + constant.validationMessages.INVALID_LOCAL_CONFIG_FILE + ) + ).to.eq('invalid_local_config_file'); + expect( + utils.getErrorCodeFromMsg( + "Invalid browserstack.json file." + ) + ).to.eq("bstack_json_invalid"); }); }); @@ -330,7 +358,6 @@ describe('utils', () => { let args = testObjects.initSampleArgs; it('should call sendUsageReport', () => { - let sandbox = sinon.createSandbox(); sendUsageReportStub = sandbox .stub(utils, 'sendUsageReport') .callsFake(function () { @@ -338,6 +365,8 @@ describe('utils', () => { }); utils.configCreated(args); sinon.assert.calledOnce(sendUsageReportStub); + sandbox.restore(); + sinon.restore(); }); }); @@ -642,12 +671,23 @@ describe('utils', () => { }); describe('setLocal', () => { - beforeEach(function () { + afterEach(function () { + sinon.restore(); delete process.env.BROWSERSTACK_LOCAL; }); - afterEach(function () { - delete process.env.BROWSERSTACK_LOCAL; + it('bsconfig connection_settings local_inferred as true if serachforOption returns false with args local-mode true', () => { + let bsConfig = { + connection_settings: { + } + }; + let args = { + localMode: "always-on" + }; + let searchForOptionStub = sinon.stub(utils,"searchForOption"); + searchForOptionStub.returns(false); + utils.setLocal(bsConfig,args); + expect(bsConfig.connection_settings.local_inferred).to.be.eq(true); }); it('should not change local in bsConfig if process.env.BROWSERSTACK_LOCAL is undefined', () => { @@ -656,7 +696,21 @@ describe('utils', () => { local: true, }, }; - utils.setLocal(bsConfig); + let args = {}; + utils.setLocal(bsConfig,args); + expect(bsConfig.connection_settings.local).to.be.eq(true); + }); + + it('should change local to true in bsConfig if process.env.BROWSERSTACK_LOCAL is set to true', () => { + let bsConfig = { + connection_settings: { + local: false, + }, + }; + let args = { + }; + process.env.BROWSERSTACK_LOCAL = true; + utils.setLocal(bsConfig,args); expect(bsConfig.connection_settings.local).to.be.eq(true); }); @@ -666,8 +720,9 @@ describe('utils', () => { local: true, }, }; + let args = {}; process.env.BROWSERSTACK_LOCAL = false; - utils.setLocal(bsConfig); + utils.setLocal(bsConfig,args); expect(bsConfig.connection_settings.local).to.be.eq(false); }); @@ -677,21 +732,347 @@ describe('utils', () => { local: false, }, }; + let args = {}; process.env.BROWSERSTACK_LOCAL = true; - utils.setLocal(bsConfig); + utils.setLocal(bsConfig,args); expect(bsConfig.connection_settings.local).to.be.eq(true); }); - it('should set local to true in bsConfig if process.env.BROWSERSTACK_LOCAL is set to true & local is not set in bsConfig', () => { + it('should set local to true in bsConfig if args is set to true & local is not set in bsConfig', () => { let bsConfig = { connection_settings: {}, }; - process.env.BROWSERSTACK_LOCAL = true; - utils.setLocal(bsConfig); + let args = { + local: true + } + utils.setLocal(bsConfig,args); expect(bsConfig.connection_settings.local).to.be.eq(true); }); }); + describe('setLocalMode', () => { + + afterEach(() =>{ + sinon.restore(); + }) + + it('if bsconfig local is true and args localMode is always-on then local_mode should be always-on' , () => { + let bsConfig = { + connection_settings: { + local: true, + local_mode: "on-demand" + }, + }; + let args = { + localMode: "always-on" + }; + utils.setLocalMode(bsConfig,args); + expect(bsConfig['connection_settings']['local_mode']).to.be.eq("always-on"); + }); + + it('if bsconfig local mode is not always-on then local_mode should be on-demand', () => { + let bsConfig = { + connection_settings: { + local: true, + }, + }; + let args = {}; + utils.setLocalMode(bsConfig,args); + expect(bsConfig['connection_settings']['local_mode']).to.be.eq("on-demand"); + }); + + it('setLocalMode should end up setting args.sync and sync_inferred as true', () => { + let bsConfig = { + connection_settings: { + local: true + }, + }; + let args = { + localMode: "always-on" + } + utils.setLocalMode(bsConfig,args); + expect(args.sync).to.be.eq(true); + expect(bsConfig.connection_settings.sync_inferred).to.be.eq(true); + }); + + it('if local_mode is not provided then the bsConfig local_mode_inferred changes to local_mode', () => { + let bsConfig = { + connection_settings: { + local: true + }, + }; + let args = { + } + let searchForOptionStub = sinon.stub(utils,"searchForOption"); + searchForOptionStub.returns(false); + utils.setLocalMode(bsConfig,args); + expect(bsConfig.connection_settings.local_mode_inferred).to.be.eq("on-demand"); + }); + + it('if local_mode is provided then the bsConfig local_mode_inferred remains unchanged', () => { + let bsConfig = { + connection_settings: { + local: true, + local_mode: "always-on" + }, + }; + let args = { + } + let searchForOptionStub = sinon.stub(utils,"searchForOption"); + searchForOptionStub.returns(true); + utils.setLocalMode(bsConfig,args); + expect(bsConfig.connection_settings.local_mode_inferred).to.be.undefined; + }); + }); + + describe('setupLocalTesting' ,() => { + + beforeEach(function () { + sinon.restore(); + sandbox.restore(); + }); + + afterEach(function () { + sinon.restore(); + sandbox.restore(); + }); + + it('if local is true and localIdentifier is not running and start error is raised', () => { + let bsConfig = { + auth: { + access_key: "xyz" + }, + connection_settings: { + local: true, + local_identifier: "xyz" + }, + }; + let args = {}; + let checkLocalIdentifierRunningStub = sinon.stub(utils, "checkLocalIdentifierRunning"); + checkLocalIdentifierRunningStub.returns(Promise.resolve(false)); + let setLocalArgsStub = sinon.stub(utils,"setLocalArgs"); + setLocalArgsStub.returns({}); + let localBinaryStartStub = sandbox.stub().yields( + 'Key is required to start local testing!' + ); + let getLocalBinaryStub = sandbox.stub(utils, 'getLocalBinary').returns({ + start: localBinaryStartStub + }); + let sendUsageReportStub = sandbox + .stub(utils, 'sendUsageReport') + .callsFake(function () { + return 'end'; + }); + utils.setupLocalTesting(bsConfig,args).catch((error) => { + expect(error).to.eq(constant.userMessages.LOCAL_START_FAILED); + sinon.assert.calledOnce(sendUsageReportStub); + sinon.assert.calledOnce(getLocalBinaryStub); + }); + }); + + it('if local is true and localIdentifier is not running and start error is not raised', () => { + let bsConfig = { + auth: { + access_key: 'xyz', + }, + connection_settings: { + local: true, + local_identifier: 'xyz', + }, + }; + let args = {}; + let localArgs = { + key: 'abc', + localIdentifier: 'abc', + daemon: true, + }; + let checkLocalIdentifierRunningStub = sinon.stub( + utils, + 'checkLocalIdentifierRunning' + ); + checkLocalIdentifierRunningStub.returns(Promise.resolve(false)); + let setLocalArgsStub = sinon.stub(utils, 'setLocalArgs'); + setLocalArgsStub.returns(localArgs); + + let localBinaryStartStub = sandbox.stub().yields(undefined); + + let getLocalBinaryStub = sandbox.stub(utils, 'getLocalBinary').returns({ + start: localBinaryStartStub, + }); + + let sendUsageReportStub = sandbox + .stub(utils, 'sendUsageReport') + .callsFake(function () { + return 'end'; + }); + utils.setupLocalTesting(bsConfig, args).catch((result) => { + expect(result).to.eq(undefined); + sinon.assert.calledOnce(getLocalBinaryStub); + }); + }); + + it('if bsconfig local is true then promise should return a browserstack local object', () => { + let bsConfig = { + auth: { + access_key: "xyz" + }, + connection_settings: { + local: true, + local_identifier: "xyz" + }, + }; + let args = {}; + let checkLocalIdentifierRunningStub = sinon.stub(utils, "checkLocalIdentifierRunning"); + checkLocalIdentifierRunningStub.returns(Promise.resolve(true)); + return utils.setupLocalTesting(bsConfig,args).then((result) => { + expect(result).to.be.eq(undefined); + }); + }); + }); + + describe('setLocalArgs', () => { + it('setting up local args and returning a local_args hash', () => { + let bsConfig = { + auth: { + access_key: "xyz" + }, + connection_settings: { + local: true, + local_identifier: "on-demand", + local_config_file: "./local.yml" + }, + }; + let args = {}; + let cliVersionPathStub = sinon.stub(usageReporting, "cli_version_and_path").withArgs(bsConfig); + cliVersionPathStub.returns("abc"); + let local_args = utils.setLocalArgs(bsConfig, args); + expect(local_args["key"]).to.be.eq(bsConfig['auth']['access_key']); + expect(local_args["localIdentifier"]).to.be.eq(bsConfig["connection_settings"]["local_identifier"]); + expect(local_args["daemon"]).to.be.eq(true); + expect(local_args["enable-logging-for-api"]).to.be.eq(true); + expect(local_args['config-file']).to.be.eq(path.resolve('./local.yml')); + sinon.restore(); + }); + }); + + describe('stopLocalBinary' , () => { + afterEach(function () { + sinon.restore(); + sandbox.restore(); + }); + it('stopLocalBinary promise gets resolve with undefined' ,() => { + let bsConfig = { + connection_settings: { + local: true + } + }; + let checkLocalIdentifierRunningStub = sinon.stub(utils, "checkLocalIdentifierRunning"); + checkLocalIdentifierRunningStub.returns(Promise.resolve(false)); + let sendUsageReportStub = sandbox + .stub(utils, 'sendUsageReport') + .callsFake(function () { + return 'end'; + }); + return utils.stopLocalBinary(bsConfig).then((result) => { + expect(result).to.be.eq(undefined); + sinon.assert.calledOnce(sendUsageReportStub); + }); + }); + + it('stopLocalBinary promise resolves with undefined if the bs_local isRunning is false' ,() => { + let bsConfig = { + connection_settings: { + local_mode: "on-demand" + } + }; + let isRunningStub = sandbox.stub().returns(false); + let bs_local = { + isRunning: isRunningStub, + }; + let checkLocalIdentifierRunningStub = sinon.stub(utils, "checkLocalIdentifierRunning"); + checkLocalIdentifierRunningStub.returns(Promise.resolve(true)); + return utils.stopLocalBinary(bsConfig, bs_local).then((result) => { + expect(result).to.be.eq(undefined); + }); + }); + + it('if the bs_local isRunning is true and local_mode is always-on, then gets resolve with undefined' ,() => { + let bsConfig = { + connection_settings: { + local_mode: "always-on" + } + }; + let isRunningStub = sandbox.stub().returns(true); + let bs_local = { + isRunning: isRunningStub, + } + let checkLocalIdentifierRunningStub = sinon.stub(utils, "checkLocalIdentifierRunning"); + checkLocalIdentifierRunningStub.returns(Promise.resolve(true)); + return utils.stopLocalBinary(bsConfig, bs_local).then((result) => { + expect(result).to.be.eq(undefined); + }); + }); + + it('if the bs_local isRunning is true and local_mode is not always-on and there is no stop error, then gets resolve with undefined' ,() => { + let bsConfig = { + connection_settings: { + local_mode: "on-demand" + } + }; + let isRunningStub = sandbox.stub().returns(true); + let stopStub = sandbox.stub().yields(undefined); + let bs_local = { + isRunning: isRunningStub, + stop: stopStub + } + let checkLocalIdentifierRunningStub = sinon.stub(utils, "checkLocalIdentifierRunning"); + checkLocalIdentifierRunningStub.returns(Promise.resolve(true)); + return utils.stopLocalBinary(bsConfig, bs_local).then((result) => { + expect(result).to.be.eq(undefined); + }); + }); + + it('if the bs_local isRunning is true and local_mode is not always-on and there is stop error, then gets resolve with stop error' ,() => { + let bsConfig = { + connection_settings: { + local_mode: "on-demand" + } + }; + let isRunningStub = sandbox.stub().returns(true); + let error = new Error('Local Stop Error'); + let stopStub = sandbox.stub().yields(error); + let checkLocalIdentifierRunningStub = sinon.stub(utils, "checkLocalIdentifierRunning"); + checkLocalIdentifierRunningStub.returns(Promise.resolve(true)); + let bs_local = { + isRunning: isRunningStub, + stop: stopStub + } + let sendUsageReportStub = sandbox + .stub(utils, 'sendUsageReport') + .callsFake(function () { + return 'end'; + }); + return utils.stopLocalBinary(bsConfig, bs_local, {}).then((result) => { + expect(result).to.be.eq(constant.userMessages.LOCAL_STOP_FAILED); + sinon.assert.calledOnce(sendUsageReportStub); + sinon.assert.calledOnce(stopStub); + }); + }); + + }); + + describe('generateLocalIdentifier', () => { + + it('if the mode is always-on it returns getmac() as local-identifier', () => { + expect(utils.generateLocalIdentifier("always-on")).to.be.eq(Buffer.from(getmac()).toString("base64")); + }); + it('if the mode is not always-on it returns random uuidv4 as local-identifier', () => { + let uuidv41 = utils.generateLocalIdentifier("abc"); + let uuidv42 = utils.generateLocalIdentifier("abc"); + expect(uuidv41 != uuidv42).to.be.eq(true); + }); + }); + describe('setLocalIdentifier', () => { beforeEach(function () { delete process.env.BROWSERSTACK_LOCAL_IDENTIFIER; @@ -700,15 +1081,18 @@ describe('utils', () => { afterEach(function () { delete process.env.BROWSERSTACK_LOCAL_IDENTIFIER; }); - it('should not change local identifier in bsConfig if process.env.BROWSERSTACK_LOCAL_IDENTIFIER is undefined', () => { + it('should generate local_identifier if args.localIdentifier & process.env.BROWSERSTACK_LOCAL_IDENTIFIER is undefined', () => { let bsConfig = { connection_settings: { - local_identifier: 'local_identifier', + local: true }, }; - utils.setLocalIdentifier(bsConfig); + let args = {}; + let generateLocalIdentifierStub = sinon.stub(utils,"generateLocalIdentifier"); + generateLocalIdentifierStub.returns("abc"); + utils.setLocalIdentifier(bsConfig,args); expect(bsConfig.connection_settings.local_identifier).to.be.eq( - 'local_identifier' + "abc" ); }); @@ -718,8 +1102,9 @@ describe('utils', () => { local_identifier: 'test', }, }; + let args = {}; process.env.BROWSERSTACK_LOCAL_IDENTIFIER = 'local_identifier'; - utils.setLocalIdentifier(bsConfig); + utils.setLocalIdentifier(bsConfig,args); expect(bsConfig.connection_settings.local_identifier).to.be.eq( 'local_identifier' ); @@ -729,12 +1114,41 @@ describe('utils', () => { let bsConfig = { connection_settings: {}, }; + let args = {}; process.env.BROWSERSTACK_LOCAL_IDENTIFIER = 'local_identifier'; - utils.setLocalIdentifier(bsConfig); + utils.setLocalIdentifier(bsConfig,args); expect(bsConfig.connection_settings.local_identifier).to.be.eq( 'local_identifier' ); }); + + it('if args localIdentifier is defined then it gets assigned to bsConfig connection_settings local_identifier' , () => { + let bsConfig = { + local: true, + connection_settings: { + local_identifier: "abc" + } + }; + let args = { + localIdentifier: "xyz" + }; + utils.setLocalIdentifier(bsConfig, args); + expect(bsConfig.connection_settings.local_identifier).to.be.eq("xyz"); + expect(bsConfig.connection_settings.local_mode).to.be.eq('always-on'); + }); + + it('if localIdentifier is defined then local_mode is set to always-on' , () => { + let bsConfig = { + connection_settings: { + local: true, + local_identifier: 'abc', + }, + }; + let args = {} + utils.setLocalIdentifier(bsConfig, args); + expect(bsConfig.connection_settings.local_identifier).to.be.eq("abc"); + expect(bsConfig['connection_settings']['local_mode']).to.be.eq('always-on'); + }); }); describe('setUsername', () => { @@ -906,6 +1320,10 @@ describe('utils', () => { }; }); + afterEach(function(){ + sinon.restore(); + }); + it('has user provided ccf flag', () => { ccfBool = true; @@ -1204,6 +1622,16 @@ describe('utils', () => { let error = constant.validationMessages.INCORRECT_AUTH_PARAMS; expect(utils.isJSONInvalid(error, {})).to.eq(true) }); + + it('JSON is invalid if local identifier is invalid', () =>{ + let error = constant.validationMessages.INVALID_CLI_LOCAL_IDENTIFIER; + expect(utils.isJSONInvalid(error,{})).to.eq(false); + }); + + it('JSON is invalid if local mode is invalid', () =>{ + let error = constant.validationMessages.INVALID_LOCAL_MODE; + expect(utils.isJSONInvalid(error,{})).to.eq(false); + }); }) describe('#deleteBaseUrlFromError', () => { @@ -1217,4 +1645,99 @@ describe('utils', () => { expect(utils.deleteBaseUrlFromError(error)).not.to.match(/To test on BrowserStack/) }); }); + + describe('#checkLocalIdentifierRunning', () => { + afterEach(() =>{ + sinon.restore(); + }); + it('if the bsConfig localIdentifier is not present within the response body then function should resolve with false' , () => { + const responseObject = { + statusCode: 200, + headers: { + 'content-type': 'application/json' + } + }; + const responseBody = { + status: 'success', + instances: [ + { + localIdentifier: 'abcdef', + }, + { + localIdentifier: 'ghij', + }, + { + localIdentifier: 'lmno', + } + ] + }; + sinon.stub(request, 'get') + .yields(undefined, responseObject, JSON.stringify(responseBody)); + + let bsConfig = { + auth: { + access_key: "abcd", + username: "abcd" + } + }; + + let localIdentifier = "abcd"; + return utils.checkLocalIdentifierRunning(bsConfig, localIdentifier).then((result) => { + expect(result).to.be.eq(false); + }); + }); + + it('if the bsConfig localIdentifier if present within the response body then the function should resolve with true' , () => { + const responseObject = { + statusCode: 200, + headers: { + 'content-type': 'application/json' + } + }; + const responseBody = { + status: 'success', + instances: [ + { + localIdentifier: 'abcdef', + }, + { + localIdentifier: 'ghij', + }, + { + localIdentifier: 'lmno', + } + ] + }; + sinon.stub(request, 'get') + .yields(undefined, responseObject, JSON.stringify(responseBody)); + + let bsConfig = { + auth: { + access_key: "abcd", + username: "abcd" + } + }; + + let localIdentifier = "lmno"; + return utils.checkLocalIdentifierRunning(bsConfig, localIdentifier).then((result) => { + expect(result).to.be.eq(true); + }); + }); + }); + + describe('setLocalConfigFile', () => { + it('the args localConfigfile should be assigned to bsconfig connection_settigs local_config_file', () => { + let bsConfig = { + connection_settings: { + local_config_file: "efgh" + } + }; + let args = { + localConfigFile: "abcd" + }; + utils.setLocalConfigFile(bsConfig, args); + expect(args.localConfigFile).to.be.eql(bsConfig.connection_settings.local_config_file); + }); + }); + });