diff --git a/bin/accessibility-automation/constants.js b/bin/accessibility-automation/constants.js index 496667a9..7d5480ff 100644 --- a/bin/accessibility-automation/constants.js +++ b/bin/accessibility-automation/constants.js @@ -1 +1,4 @@ -exports.API_URL = 'https://accessibility.browserstack.com/api'; +const { getEnvironmentUrl } = require('../helpers/environmentUrl'); + +// BROWSERSTACK_ENV=preprod|staging (internal only) swaps the host; production when unset. +exports.API_URL = getEnvironmentUrl('https://accessibility.browserstack.com/api'); diff --git a/bin/helpers/config.js b/bin/helpers/config.js index 991f8201..6c640d7c 100644 --- a/bin/helpers/config.js +++ b/bin/helpers/config.js @@ -1,4 +1,5 @@ var config = require('./config.json'); +const { getEnvironmentUrl } = require('./environmentUrl'); config.env = process.env.BSTACK_CYPRESS_NODE_ENV || "production"; @@ -10,6 +11,11 @@ if(config.env !== "production") { config.rails_host = process.env.RAILS_HOST; config.dashboardUrl = process.env.DASHBOARD_URL; config.usageReportingUrl = process.env.USAGE_REPORTING_URL; +} else { + // BROWSERSTACK_ENV=preprod|staging (internal only); a no-op when unset. + ['uploadUrl', 'rails_host', 'dashboardUrl', 'turboScaleUrl', 'turboScaleAPIUrl'].forEach((key) => { + config[key] = getEnvironmentUrl(config[key]); + }); } config.cypress_v1 = `${config.rails_host}/automate/cypress/v1`; diff --git a/bin/helpers/environmentUrl.js b/bin/helpers/environmentUrl.js new file mode 100644 index 00000000..02fa9871 --- /dev/null +++ b/bin/helpers/environmentUrl.js @@ -0,0 +1,55 @@ +'use strict'; + +// Internal, non-production only: BROWSERSTACK_ENV=preprod|staging points the cli at that +// environment's hosts, the same switch browserstack-binary (core/utils/environment-url.js) and +// browserstack-node-agent use. Unset, or any other value, keeps the production hosts. +const ENV_VAR = 'BROWSERSTACK_ENV'; + +const PREPROD_HOSTS = [ + ['api-cloud.browserstack.com', 'api-cloud-preprod.bsstag.com'], + ['api.browserstack.com', 'api-preprod.bsstag.com'], + ['automate.browserstack.com', 'automate-preprod.bsstag.com'], + ['automation.browserstack.com', 'automation-preprod.bsstag.com'], + ['collector-observability.browserstack.com', 'collector-observability-preprod.bsstag.com'], + ['accessibility.browserstack.com', 'accessibility-preprod.bsstag.com'], + ['grid.browserstack.com', 'grid-preprod.bsstag.com'], +]; + +const STAGING_HOSTS = [ + ['api-cloud.browserstack.com', 'api-cloud-devtestops.bsstag.com'], + ['api.browserstack.com', 'apidevtestops.bsstag.com'], + ['automate.browserstack.com', 'automate-devtestops.bsstag.com'], + ['automation.browserstack.com', 'automation.bsstag.com'], + ['collector-observability.browserstack.com', 'collector-observability-devtestops.bsstag.com'], + ['accessibility.browserstack.com', 'accessibility.bsstag.com'], + ['grid.browserstack.com', 'grid-devhst.bsstag.com'], +]; + +const getEnvironment = () => { + const value = (process.env[ENV_VAR] || '').trim().toLowerCase(); + if (value === 'preprod' || value === 'pre-prod') return 'preprod'; + if (value === 'staging' || value === 'stag') return 'staging'; + return undefined; +}; + +// Swaps the host only (exact match), so paths, ports and query strings are kept and a host that +// merely contains a mapped name (e.g. app-automate.browserstack.com) is left alone. +const getEnvironmentUrl = (url) => { + const env = getEnvironment(); + if (!env || typeof url !== 'string') return url; + const hosts = env === 'preprod' ? PREPROD_HOSTS : STAGING_HOSTS; + let parsed; + try { + parsed = new URL(url); + } catch (err) { + return url; + } + const match = hosts.find(([from]) => parsed.hostname === from); + if (!match) return url; + parsed.hostname = match[1]; + const result = parsed.toString(); + // URL.toString() adds a trailing slash to a bare origin; keep the caller's shape. + return url.endsWith('/') ? result : result.replace(/\/$/, ''); +}; + +module.exports = { ENV_VAR, getEnvironment, getEnvironmentUrl }; diff --git a/bin/testObservability/helper/constants.js b/bin/testObservability/helper/constants.js index 19e7517c..8701e3f8 100644 --- a/bin/testObservability/helper/constants.js +++ b/bin/testObservability/helper/constants.js @@ -1,4 +1,5 @@ const path = require('path'); +const { getEnvironmentUrl } = require('../../helpers/environmentUrl'); exports.consoleHolder = Object.assign({},console); exports.BATCH_SIZE = 1000; @@ -37,7 +38,9 @@ const REMOTE_API_URL_ENV = 'BROWSERSTACK_TEST_OBSERVABILITY_REMOTE_API_URL'; exports.API_URL_ENV = API_URL_ENV; exports.REMOTE_API_URL_ENV = REMOTE_API_URL_ENV; -exports.apiUrl = () => httpsOverride(API_URL_ENV) || DEFAULT_API_URL; +// An explicit override wins; otherwise BROWSERSTACK_ENV=preprod|staging picks that environment's +// collector. setSystemEnvs forwards the result to the remote reporter, so both sides agree. +exports.apiUrl = () => httpsOverride(API_URL_ENV) || getEnvironmentUrl(DEFAULT_API_URL); exports.remoteReporterApiUrl = () => httpsOverride(REMOTE_API_URL_ENV) || exports.apiUrl(); exports.IPC_EVENTS = { diff --git a/bin/testObservability/helper/helper.js b/bin/testObservability/helper/helper.js index ddd2e8c7..3ce5c276 100644 --- a/bin/testObservability/helper/helper.js +++ b/bin/testObservability/helper/helper.js @@ -15,6 +15,7 @@ const { runOptions } = require('../../helpers/runnerArgs') const pGitconfig = promisify(gitconfig); +const { getEnvironmentUrl } = require('../../helpers/environmentUrl'); const logger = require("../../helpers/logger").winstonLogger; const utils = require('../../helpers/utils'); const helper = require('../../helpers/helper'); @@ -114,7 +115,7 @@ exports.printBuildLink = async (shouldStopSession, exitCode = null) => { && process.env.BS_TESTOPS_BUILD_HASHED_ID != "null" && process.env.BS_TESTOPS_BUILD_HASHED_ID != "undefined") { console.log(); - logger.info(`Visit https://automation.browserstack.com/builds/${process.env.BS_TESTOPS_BUILD_HASHED_ID} to view build report, insights, and many more debugging information all at one place!\n`); + logger.info(`Visit ${getEnvironmentUrl('https://automation.browserstack.com')}/builds/${process.env.BS_TESTOPS_BUILD_HASHED_ID} to view build report, insights, and many more debugging information all at one place!\n`); } } catch(err) { exports.debug('Build Not Found'); diff --git a/test/unit/bin/helpers/environmentUrl.js b/test/unit/bin/helpers/environmentUrl.js new file mode 100644 index 00000000..f296fd2f --- /dev/null +++ b/test/unit/bin/helpers/environmentUrl.js @@ -0,0 +1,66 @@ +const { expect } = require('chai'); + +const { getEnvironment, getEnvironmentUrl } = require('../../../../bin/helpers/environmentUrl'); + +describe('environmentUrl (BROWSERSTACK_ENV)', () => { + const original = process.env.BROWSERSTACK_ENV; + afterEach(() => { + if (original === undefined) delete process.env.BROWSERSTACK_ENV; + else process.env.BROWSERSTACK_ENV = original; + }); + + it('leaves every url unchanged when unset', () => { + delete process.env.BROWSERSTACK_ENV; + expect(getEnvironment()).to.equal(undefined); + expect(getEnvironmentUrl('https://api.browserstack.com')).to.equal('https://api.browserstack.com'); + }); + + it('leaves urls unchanged for an unknown value', () => { + process.env.BROWSERSTACK_ENV = 'production'; + expect(getEnvironment()).to.equal(undefined); + expect(getEnvironmentUrl('https://accessibility.browserstack.com/api')).to.equal('https://accessibility.browserstack.com/api'); + }); + + it('maps every preprod host the cli uses, keeping paths', () => { + process.env.BROWSERSTACK_ENV = 'Preprod'; + expect(getEnvironmentUrl('https://api.browserstack.com')).to.equal('https://api-preprod.bsstag.com'); + expect(getEnvironmentUrl('https://api-cloud.browserstack.com/automate-frameworks/cypress/upload')) + .to.equal('https://api-cloud-preprod.bsstag.com/automate-frameworks/cypress/upload'); + expect(getEnvironmentUrl('https://automate.browserstack.com/dashboard/v2/builds/')) + .to.equal('https://automate-preprod.bsstag.com/dashboard/v2/builds/'); + expect(getEnvironmentUrl('https://automation.browserstack.com')).to.equal('https://automation-preprod.bsstag.com'); + expect(getEnvironmentUrl('https://collector-observability.browserstack.com')) + .to.equal('https://collector-observability-preprod.bsstag.com'); + expect(getEnvironmentUrl('https://accessibility.browserstack.com/api')).to.equal('https://accessibility-preprod.bsstag.com/api'); + }); + + it('accepts pre-prod and maps staging', () => { + process.env.BROWSERSTACK_ENV = 'pre-prod'; + expect(getEnvironment()).to.equal('preprod'); + process.env.BROWSERSTACK_ENV = 'stag'; + expect(getEnvironmentUrl('https://api.browserstack.com')).to.equal('https://apidevtestops.bsstag.com'); + }); + + it('matches whole hosts only', () => { + process.env.BROWSERSTACK_ENV = 'preprod'; + expect(getEnvironmentUrl('https://app-automate.browserstack.com/x')).to.equal('https://app-automate.browserstack.com/x'); + expect(getEnvironmentUrl('https://eds.browserstack.com:443/send_event_cy_internal')) + .to.equal('https://eds.browserstack.com:443/send_event_cy_internal'); + expect(getEnvironmentUrl('not a url')).to.equal('not a url'); + }); + + it('feeds the observability collector default, while an explicit override still wins', () => { + const { apiUrl } = require('../../../../bin/testObservability/helper/constants'); + const savedOverride = process.env.BROWSERSTACK_TEST_OBSERVABILITY_API_URL; + delete process.env.BROWSERSTACK_TEST_OBSERVABILITY_API_URL; + try { + process.env.BROWSERSTACK_ENV = 'preprod'; + expect(apiUrl()).to.equal('https://collector-observability-preprod.bsstag.com'); + process.env.BROWSERSTACK_TEST_OBSERVABILITY_API_URL = 'https://collector.example.test'; + expect(apiUrl()).to.equal('https://collector.example.test'); + } finally { + if (savedOverride === undefined) delete process.env.BROWSERSTACK_TEST_OBSERVABILITY_API_URL; + else process.env.BROWSERSTACK_TEST_OBSERVABILITY_API_URL = savedOverride; + } + }); +});