diff --git a/bin/commands/runs.js b/bin/commands/runs.js index e7aff508..57dfa7b1 100644 --- a/bin/commands/runs.js +++ b/bin/commands/runs.js @@ -48,6 +48,9 @@ module.exports = function run(args, rawArgs) { // set cypress config filename utils.setCypressConfigFilename(bsConfig, args); + // set cypress geo location + utils.setGeolocation(bsConfig, args); + // accept the specs list from command line if provided utils.setUserSpecs(bsConfig, args); @@ -236,6 +239,9 @@ module.exports = function run(args, rawArgs) { utils.sendUsageReport(bsConfig, args, `${message}\n${dashboardLink}`, Constants.messageTypes.SUCCESS, null, buildReportData, rawArgs); return; }).catch(async function (err) { + if (err && err.includes('browserstack.geoLocation')) { + err = err.replace(/browserstack.geoLocation/g, 'geolocation'); + } // Build creation failed logger.error(err); // stop the Local instance diff --git a/bin/helpers/capabilityHelper.js b/bin/helpers/capabilityHelper.js index 4d194723..067f2afa 100644 --- a/bin/helpers/capabilityHelper.js +++ b/bin/helpers/capabilityHelper.js @@ -173,6 +173,10 @@ const validate = (bsConfig, args) => { if( Utils.searchForOption('--async') && ( !Utils.isUndefined(args.async) && bsConfig["connection_settings"]["local"])) reject(Constants.validationMessages.INVALID_LOCAL_ASYNC_ARGS); + if (bsConfig.run_settings.userProvidedGeolocation && !bsConfig.run_settings.geolocation.match(/^[A-Z]{2}$/g)) reject(Constants.validationMessages.INVALID_GEO_LOCATION); + + if (bsConfig["connection_settings"]["local"] && bsConfig.run_settings.userProvidedGeolocation) reject(Constants.validationMessages.NOT_ALLOWED_GEO_LOCATION_AND_LOCAL_MODE); + // 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 3d0cfcc2..fb6e0824 100644 --- a/bin/helpers/constants.js +++ b/bin/helpers/constants.js @@ -87,6 +87,11 @@ const validationMessages = { 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_LOCAL_ASYNC_ARGS: "Cannot run in --async mode when local is set to true. Please run the build after removing --async", + INVALID_GEO_LOCATION: "[BROWSERSTACK_INVALID_COUNTRY_CODE] The country code specified for 'geolocation' is invalid. For list of supported countries, refer to - https://www.browserstack.com/ip-geolocation", + NOT_SUPPORTED_GEO_LOCATION: "The country code you have passed for IP Geolocation is currently not supported. Please refer the link https://www.browserstack.com/ip-geolocation for a list of supported countries.", + NOT_AVAILABLE_GEO_LOCATION: "The country code you have passed for IP Geolocation is not available at the moment. Please try again in a few hours.", + ACCESS_DENIED_GEO_LOCATION: "'geolocation' (IP Geolocation feature) capability is not supported in your account. It is only available under Enterprise plans, refer https://www.browserstack.com/ip-geolocation for more details.", + NOT_ALLOWED_GEO_LOCATION_AND_LOCAL_MODE: "IP Geolocation feature is not available in conjunction with BrowserStack Local.", HOME_DIRECTORY_NOT_FOUND: "Specified home directory could not be found. Please make sure the path of the home directory is correct.", HOME_DIRECTORY_NOT_A_DIRECTORY: "Specified home directory is not a directory. The home directory can only be a directory and not a file.", CYPRESS_CONFIG_FILE_NOT_PART_OF_HOME_DIRECTORY: "Could not find cypress.json within the specified home directory. Please make sure cypress.json resides within the home directory." @@ -132,6 +137,7 @@ const cliMessages = { CONFIG_DESCRIPTION: "Set configuration values. Separate multiple values with a comma. The values set here override any values set in your configuration file.", REPORTER: "Specify the custom reporter to use", REPORTER_OPTIONS: "Specify reporter options for custom reporter", + CYPRESS_GEO_LOCATION: "Enterprise feature to simulate website and mobile behavior from different locations." }, COMMON: { DISABLE_USAGE_REPORTING: "Disable usage reporting", diff --git a/bin/helpers/utils.js b/bin/helpers/utils.js index 01620399..610e6b95 100644 --- a/bin/helpers/utils.js +++ b/bin/helpers/utils.js @@ -85,6 +85,12 @@ exports.getErrorCodeFromMsg = (errMsg) => { case Constants.validationMessages.INVALID_LOCAL_ASYNC_ARGS: errorCode = 'invalid_local_async_args'; break; + case Constants.validationMessages.INVALID_GEO_LOCATION: + errorCode = 'invalid_geo_location'; + break; + case Constants.validationMessages.NOT_ALLOWED_GEO_LOCATION_AND_LOCAL_MODE: + errorCode = 'not_allowed_geo_location_and_local_mode'; + break; case Constants.validationMessages.HOME_DIRECTORY_NOT_FOUND: errorCode = 'home_directory_not_found'; break; @@ -281,6 +287,28 @@ exports.setCypressConfigFilename = (bsConfig, args) => { } } +exports.verifyGeolocationOption = () => { + let glOptionsSet = (this.searchForOption('-gl') || this.searchForOption('--gl')); + let geoHyphenLocationOptionsSet = (this.searchForOption('-geo-location') || this.searchForOption('--geo-location')); + let geoLocationOptionsSet = (this.searchForOption('-geolocation') || this.searchForOption('--geolocation')); + return (glOptionsSet || geoHyphenLocationOptionsSet || geoLocationOptionsSet); +} + +exports.setGeolocation = (bsConfig, args) => { + let userProvidedGeolocation = this.verifyGeolocationOption(); + bsConfig.run_settings.userProvidedGeolocation = (userProvidedGeolocation || (!this.isUndefined(bsConfig.run_settings.geolocation))); + + if (userProvidedGeolocation && !this.isUndefined(args.geolocation)) { + bsConfig.run_settings.geolocation = args.geolocation; + } + + if (this.isUndefined(bsConfig.run_settings.geolocation)){ + bsConfig.run_settings.geolocation = null; + } else { + bsConfig.run_settings.geolocation = bsConfig.run_settings.geolocation.toUpperCase(); + } +} + // specs can be passed from bstack configuration file // specs can be passed via command line args as a string // command line args takes precedence over config diff --git a/bin/runner.js b/bin/runner.js index 97046493..275a2c30 100755 --- a/bin/runner.js +++ b/bin/runner.js @@ -133,6 +133,12 @@ var argv = yargs demand: true, demand: Constants.cliMessages.RUN.CYPRESS_CONFIG_DEMAND }, + 'gl': { + alias: 'geolocation', + describe: Constants.cliMessages.RUN.CYPRESS_GEO_LOCATION, + default: undefined, + type: 'string' + }, 'p': { alias: ['parallels', 'parallel'], describe: Constants.cliMessages.RUN.PARALLEL_DESC, diff --git a/test/unit/bin/commands/runs.js b/test/unit/bin/commands/runs.js index ba62a9b6..09bc3d3b 100644 --- a/test/unit/bin/commands/runs.js +++ b/test/unit/bin/commands/runs.js @@ -115,6 +115,7 @@ describe("runs", () => { setBrowsersStub = sandbox.stub(); setConfigStub = sandbox.stub(); setCLIModeStub = sandbox.stub(); + setGeolocationStub = sandbox.stub(); }); afterEach(() => { @@ -154,7 +155,8 @@ describe("runs", () => { setSystemEnvs: setSystemEnvsStub, setBrowsers: setBrowsersStub, setConfig: setConfigStub, - setCLIMode: setCLIModeStub + setCLIMode: setCLIModeStub, + setGeolocation: setGeolocationStub }, '../helpers/capabilityHelper': { validate: capabilityValidatorStub @@ -194,6 +196,7 @@ describe("runs", () => { sinon.assert.calledOnce(getErrorCodeFromMsgStub); sinon.assert.calledOnce(setLocalIdentifierStub); sinon.assert.calledOnce(setUsageReportingFlagStub); + sinon.assert.calledOnce(setGeolocationStub); sinon.assert.calledOnceWithExactly( sendUsageReportStub, bsConfig, @@ -249,6 +252,7 @@ describe("runs", () => { setBrowsersStub = sandbox.stub(); setConfigStub = sandbox.stub(); setCLIModeStub = sandbox.stub(); + setGeolocationStub = sandbox.stub(); getVideoConfigStub = sandbox.stub(); }); @@ -292,6 +296,7 @@ describe("runs", () => { setBrowsers: setBrowsersStub, setConfig: setConfigStub, setCLIMode: setCLIModeStub, + setGeolocation: setGeolocationStub, getVideoConfig: getVideoConfigStub, }, '../helpers/capabilityHelper': { @@ -351,6 +356,7 @@ describe("runs", () => { sinon.assert.calledOnce(deleteResultsStub); sinon.assert.calledOnce(setDefaultsStub); sinon.assert.calledOnce(setSystemEnvsStub); + sinon.assert.calledOnce(setGeolocationStub); sinon.assert.calledOnceWithExactly( sendUsageReportStub, bsConfig, @@ -408,6 +414,7 @@ describe("runs", () => { setBrowsersStub = sandbox.stub(); setCLIModeStub = sandbox.stub(); fetchZipSizeStub = sandbox.stub(); + setGeolocationStub = sandbox.stub(); getVideoConfigStub = sandbox.stub(); }); @@ -452,6 +459,7 @@ describe("runs", () => { setConfig: setConfigStub, setCLIMode: setCLIModeStub, fetchZipSize: fetchZipSizeStub, + setGeolocation: setGeolocationStub, getVideoConfig: getVideoConfigStub, }, '../helpers/capabilityHelper': { @@ -513,6 +521,7 @@ describe("runs", () => { sinon.assert.calledOnce(deleteResultsStub); sinon.assert.calledOnce(setDefaultsStub); sinon.assert.calledOnce(setSystemEnvsStub); + sinon.assert.calledOnce(setGeolocationStub); sinon.assert.calledOnceWithExactly( sendUsageReportStub, bsConfig, @@ -575,6 +584,7 @@ describe("runs", () => { setBrowsersStub = sandbox.stub(); setCLIModeStub = sandbox.stub(); fetchZipSizeStub = sandbox.stub(); + setGeolocationStub = sandbox.stub(); getVideoConfigStub = sandbox.stub(); }); @@ -620,6 +630,7 @@ describe("runs", () => { setConfig: setConfigStub, setCLIMode: setCLIModeStub, fetchZipSize: fetchZipSizeStub, + setGeolocation: setGeolocationStub, getVideoConfig: getVideoConfigStub, }, '../helpers/capabilityHelper': { @@ -692,6 +703,7 @@ describe("runs", () => { sinon.assert.calledOnce(deleteResultsStub); sinon.assert.calledOnce(setDefaultsStub); sinon.assert.calledOnce(setSystemEnvsStub); + sinon.assert.calledOnce(setGeolocationStub); sinon.assert.calledOnceWithExactly( sendUsageReportStub, @@ -768,6 +780,7 @@ describe("runs", () => { setCLIModeStub = sandbox.stub(); setProcessHooksStub = sandbox.stub(); fetchZipSizeStub = sandbox.stub(); + setGeolocationStub = sandbox.stub(); getVideoConfigStub = sandbox.stub(); }); @@ -821,6 +834,7 @@ describe("runs", () => { setCLIMode: setCLIModeStub, setProcessHooks: setProcessHooksStub, fetchZipSize: fetchZipSizeStub, + setGeolocation: setGeolocationStub, getVideoConfig: getVideoConfigStub, }, '../helpers/capabilityHelper': { @@ -910,6 +924,7 @@ describe("runs", () => { sinon.assert.calledOnce(deleteResultsStub); sinon.assert.calledOnce(setDefaultsStub); sinon.assert.calledOnce(setSystemEnvsStub); + sinon.assert.calledOnce(setGeolocationStub); sinon.assert.match( sendUsageReportStub.getCall(0).args, [ diff --git a/test/unit/bin/helpers/capabilityHelper.js b/test/unit/bin/helpers/capabilityHelper.js index 552e656b..1bd8ef4f 100644 --- a/test/unit/bin/helpers/capabilityHelper.js +++ b/test/unit/bin/helpers/capabilityHelper.js @@ -586,6 +586,7 @@ describe("capabilityHelper.js", () => { cypress_proj_dir: "random path", cypressConfigFilePath: "random path" }, + connection_settings: {local: false} }; }); @@ -952,6 +953,7 @@ describe("capabilityHelper.js", () => { cypressConfigFilePath: "random path", cypressProjectDir: "random path" }, + connection_settings: {local: false} }; }); it("validate cypress json is present", () => { @@ -1064,6 +1066,99 @@ describe("capabilityHelper.js", () => { }) }); }); + + describe("validate ip geolocation", () => { + beforeEach(() => { + bsConfig = { + auth: {}, + browsers: [ + { + browser: "chrome", + os: "Windows 10", + versions: ["78", "77"], + }, + ], + run_settings: { + cypress_proj_dir: "random path", + cypressConfigFilePath: "random path", + cypressProjectDir: "random path" + }, + connection_settings: {} + }; + }); + + it("should throw an error if both local and geolocation are used", () => { + bsConfig.run_settings.geolocation = "US"; + bsConfig.run_settings.userProvidedGeolocation = true; + bsConfig.connection_settings.local = true; + bsConfig.connection_settings.local_identifier = "some text"; + + return capabilityHelper + .validate(bsConfig, {}) + .then(function (data) { + chai.assert.fail("Promise error"); + }) + .catch((error) => { + chai.assert.equal(error, Constants.validationMessages.NOT_ALLOWED_GEO_LOCATION_AND_LOCAL_MODE); + }); + }); + + it("should throw an error if incorrect format for geolocation code is used (valid country name but incorrect code)", () => { + bsConfig.run_settings.geolocation = "USA"; + bsConfig.run_settings.userProvidedGeolocation = true; + + return capabilityHelper + .validate(bsConfig, {}) + .then(function (data) { + chai.assert.fail("Promise error"); + }) + .catch((error) => { + chai.assert.equal(error, Constants.validationMessages.INVALID_GEO_LOCATION); + }); + }); + + it("should throw an error if incorrect format for geolocation code is used (random value)", () => { + bsConfig.run_settings.geolocation = "RANDOM"; + bsConfig.run_settings.userProvidedGeolocation = true; + + return capabilityHelper + .validate(bsConfig, {}) + .then(function (data) { + chai.assert.fail("Promise error"); + }) + .catch((error) => { + chai.assert.equal(error, Constants.validationMessages.INVALID_GEO_LOCATION); + }); + }); + + it("should throw an error if incorrect format for geolocation code is used (special chars)", () => { + bsConfig.run_settings.geolocation = "$USA$!&@*)()"; + bsConfig.run_settings.userProvidedGeolocation = true; + + return capabilityHelper + .validate(bsConfig, {}) + .then(function (data) { + chai.assert.fail("Promise error"); + }) + .catch((error) => { + chai.assert.equal(error, Constants.validationMessages.INVALID_GEO_LOCATION); + }); + }); + + it("should throw an error if incorrect format for geolocation code is used (small caps)", () => { + bsConfig.run_settings.geolocation = "us"; + bsConfig.run_settings.userProvidedGeolocation = true; + + return capabilityHelper + .validate(bsConfig, {}) + .then(function (data) { + chai.assert.fail("Promise error"); + }) + .catch((error) => { + chai.assert.equal(error, Constants.validationMessages.INVALID_GEO_LOCATION); + }); + }); + }); describe("validate home directory", () => { beforeEach(() => { @@ -1081,6 +1176,7 @@ describe("capabilityHelper.js", () => { cypressConfigFilePath: "random path", cypressProjectDir: "random path" }, + connection_settings: {local: false} }; }); diff --git a/test/unit/bin/helpers/utils.js b/test/unit/bin/helpers/utils.js index 3a9ae66a..5e980d21 100644 --- a/test/unit/bin/helpers/utils.js +++ b/test/unit/bin/helpers/utils.js @@ -1823,6 +1823,157 @@ describe('utils', () => { }); }); + describe('verifyGeolocationOption', () => { + let utilsearchForOptionGeolocationStub, userOption, testOption; + + beforeEach(function () { + utilsearchForOptionGeolocationStub = sinon + .stub(utils, 'searchForOption') + .callsFake((...userOption) => { + return userOption == testOption; + }); + }); + + afterEach(function () { + utilsearchForOptionGeolocationStub.restore(); + }); + + it('-gl user option', () => { + testOption = '-gl'; + expect(utils.verifyGeolocationOption()).to.be.true; + sinon.assert.calledWithExactly( + utilsearchForOptionGeolocationStub, + testOption + ); + }); + + it('--gl user option', () => { + testOption = '--gl'; + expect(utils.verifyGeolocationOption()).to.be.true; + sinon.assert.calledWithExactly( + utilsearchForOptionGeolocationStub, + testOption + ); + }); + + it('-geo-location user option', () => { + testOption = '-geo-location'; + expect(utils.verifyGeolocationOption()).to.be.true; + sinon.assert.calledWithExactly( + utilsearchForOptionGeolocationStub, + testOption + ); + }); + + it('--geo-location user option', () => { + testOption = '--geo-location'; + expect(utils.verifyGeolocationOption()).to.be.true; + sinon.assert.calledWithExactly( + utilsearchForOptionGeolocationStub, + testOption + ); + }); + + it('-geolocation user option', () => { + testOption = '-geolocation'; + expect(utils.verifyGeolocationOption()).to.be.true; + sinon.assert.calledWithExactly( + utilsearchForOptionGeolocationStub, + testOption + ); + }); + + it('--geolocation user option', () => { + testOption = '--geolocation'; + expect(utils.verifyGeolocationOption()).to.be.true; + sinon.assert.calledWithExactly( + utilsearchForOptionGeolocationStub, + testOption + ); + }); + }); + + describe('setGeolocation', () => { + let verifyGeolocationOptionStub, + glBool, + args, + bsConfig, + geolocation; + + beforeEach(function () { + verifyGeolocationOptionStub = sinon + .stub(utils, 'verifyGeolocationOption') + .callsFake(() => glBool); + + args = { + geolocation: 'IN', + }; + }); + + afterEach(function () { + sinon.restore(); + }); + + it('has user provided gl flag', () => { + glBool = true; + + bsConfig = { + run_settings: { + geolocation: 'IN', + }, + }; + + utils.setGeolocation(bsConfig, args); + + expect(bsConfig.run_settings.geolocation).to.be.eq( + args.geolocation + ); + expect(bsConfig.run_settings.userProvidedGeolocation).to.be.true; + }); + + it('does not have user provided gl flag, sets the value from bsConfig', () => { + glBool = false; + args = { + geolocation: null + }; + bsConfig = { + run_settings: { + geolocation: 'IN', + }, + }; + + utils.setGeolocation(bsConfig, args); + + expect(bsConfig.run_settings.geolocation).to.not.be.eq( + args.geolocation + ); + expect(bsConfig.run_settings.geolocation).to.be.eq('IN'); + expect(bsConfig.run_settings.userProvidedGeolocation).to.be.true; + }); + + it('does not have user provided gl flag and config value, sets geolocation to be null', () => { + geolocation = 'run_settings_geolocation'; + glBool = false; + args = { + geolocation: null + }; + bsConfig = { + run_settings: { + geolocation: null, + }, + }; + + utils.setGeolocation(bsConfig, args); + + expect(bsConfig.run_settings.geolocation).to.be.eq(null); + expect(bsConfig.run_settings.userProvidedGeolocation).to.be.false; + }); + + afterEach(function () { + verifyGeolocationOptionStub.restore(); + }); + }); + describe('setDefaults', () => { beforeEach(function () { delete process.env.BROWSERSTACK_USERNAME;