From c7a5eba4375b944fdb10c621ebdf9704ebda5e59 Mon Sep 17 00:00:00 2001 From: Aakash Hotchandani Date: Wed, 7 Oct 2026 20:22:36 +0530 Subject: [PATCH 1/5] fix: recognize bsstag hosts and localize session REST host on named staging envs (SDK-6948) With BROWSERSTACK_STAGING_ENV set to a bare token (e.g. k8s) the hub is hub-.bsstag.com. getCloudProvider / isBrowserstackInfra only matched browserstack.com, so the session was classified unknown_grid: session status was never marked and TRA test runs carried no Automate session link. Recognize *.bsstag.com only for a bare-token staging env; dotted value or unset keeps prod behaviour. The service also hardcoded the prod api host for the session REST base URL (session URL print + onReload status marking). Build it from APIUtils, which the CLI bootstrap has already localized to the binary-supplied hosts. Co-Authored-By: Claude Opus 5.5 --- packages/browserstack-service/src/service.ts | 9 +++- packages/browserstack-service/src/util.ts | 20 +++++++-- .../tests/service.test.ts | 18 ++++++++ .../browserstack-service/tests/util.test.ts | 45 +++++++++++++++++++ 4 files changed, 87 insertions(+), 5 deletions(-) diff --git a/packages/browserstack-service/src/service.ts b/packages/browserstack-service/src/service.ts index 8854e15c..6e6a4f86 100644 --- a/packages/browserstack-service/src/service.ts +++ b/packages/browserstack-service/src/service.ts @@ -37,6 +37,7 @@ import { EVENTS } from './instrumentation/performance/constants.js' import { BrowserstackCLI } from './cli/index.js' import { drainSkipReports, markTestStarted, reportSuiteSkipped } from './cli/skipReporter.js' import { CLIUtils } from './cli/cliUtils.js' +import APIUtils from './cli/apiUtils.js' import { _fetch as fetch } from './fetchWrapper.js' import AutomationFramework from './cli/frameworks/automationFramework.js' @@ -246,12 +247,16 @@ export default class BrowserstackService implements Services.ServiceInstance { // Ensure capabilities are not null in case of multiremote + // Resolved here, after beforeSession's CLI bootstrap has applied the binary-supplied + // GRR/staging hosts (APIUtils.updateURLSForGRR); the defaults are the production hosts. + this._sessionBaseUrl = `${APIUtils.BROWSERSTACK_AUTOMATE_API_URL}/automate/sessions` + if (this._isAppAutomate()) { - this._sessionBaseUrl = 'https://api-cloud.browserstack.com/app-automate/sessions' + this._sessionBaseUrl = `${APIUtils.BROWSERSTACK_AA_API_CLOUD_URL}/app-automate/sessions` } if (this._turboScale) { - this._sessionBaseUrl = 'https://api.browserstack.com/automate-turboscale/v1/sessions' + this._sessionBaseUrl = `${APIUtils.BROWSERSTACK_AUTOMATE_API_URL}/automate-turboscale/v1/sessions` } this._scenariosThatRan = [] diff --git a/packages/browserstack-service/src/util.ts b/packages/browserstack-service/src/util.ts index 5aa1e9bb..e5406d33 100644 --- a/packages/browserstack-service/src/util.ts +++ b/packages/browserstack-service/src/util.ts @@ -1142,6 +1142,20 @@ export function getLtsSessionId(): string { return process.env[BROWSERSTACK_LTS_SESSION_ID] || '' } +/** + * SDK-6948: a named internal staging env (`BROWSERSTACK_STAGING_ENV` set to a bare token such as + * `k8s`) serves its hub/cdp from `*.bsstag.com` (e.g. `hub-k8s.bsstag.com`). Treat those hosts as + * BrowserStack so session marking and Automate <-> TRA linking work there. A dotted value (full + * host) or an unset variable leaves the production-only host checks unchanged. + */ +export function isStagingEnvBsstagHost(hostname: string): boolean { + const stagingEnv = process.env.BROWSERSTACK_STAGING_ENV + if (!stagingEnv || stagingEnv.includes('.')) { + return false + } + return hostname === 'bsstag.com' || hostname.endsWith('.bsstag.com') +} + export function getCloudProvider(browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser): string { // NOTE: do NOT branch on isLoadTestingSession() here. getCloudProvider is // shared with Automate-side guards (automateModule onBefore/onAfterTest, @@ -1157,11 +1171,11 @@ export function getCloudProvider(browser: WebdriverIO.Browser | WebdriverIO.Mult // Loop through all instances for (const instanceName of browser.instances) { const instance = (browser as any)[instanceName] as WebdriverIO.Browser - if (instance.options && instance.options.hostname && instance.options.hostname.includes('browserstack')) { + if (instance.options && instance.options.hostname && (instance.options.hostname.includes('browserstack') || isStagingEnvBsstagHost(instance.options.hostname))) { return 'browserstack' } } - } else if (browser.options && browser.options.hostname && browser.options.hostname.includes('browserstack')) { // Single browser instance + } else if (browser.options && browser.options.hostname && (browser.options.hostname.includes('browserstack') || isStagingEnvBsstagHost(browser.options.hostname))) { // Single browser instance return 'browserstack' } return 'unknown_grid' @@ -1270,7 +1284,7 @@ export function isBrowserstackInfra(config: BrowserstackConfig & Options.Testrun // In case hostname is not present anywhere in the config, it returns true by default as hostname is not a mandatory parameter in the config const isBrowserstack = (str: string ): boolean => { - return str === 'browserstack.com' || str.endsWith('.browserstack.com') + return str === 'browserstack.com' || str.endsWith('.browserstack.com') || isStagingEnvBsstagHost(str) } if ((config.hostname) && !isBrowserstack(config.hostname)) { diff --git a/packages/browserstack-service/tests/service.test.ts b/packages/browserstack-service/tests/service.test.ts index aa32efbc..5e015b41 100644 --- a/packages/browserstack-service/tests/service.test.ts +++ b/packages/browserstack-service/tests/service.test.ts @@ -15,6 +15,7 @@ import { TestFrameworkState } from '../src/cli/states/testFrameworkState.js' import { HookState } from '../src/cli/states/hookState.js' import { AutomationFrameworkConstants } from '../src/cli/frameworks/constants/automationFrameworkConstants.js' import { AutomationFrameworkState } from '../src/cli/states/automationFrameworkState.js' +import APIUtils from '../src/cli/apiUtils.js' const jasmineSuiteTitle = 'Jasmine__TopLevel__Suite' const sessionBaseUrl = 'https://api.browserstack.com/automate/sessions' @@ -722,6 +723,23 @@ describe('before', () => { expect(service['_sessionBaseUrl']).toEqual('https://api.browserstack.com/automate-turboscale/v1/sessions') }) + it('uses the GRR/staging api host applied by the CLI for the session base url (SDK-6948)', () => { + const original = APIUtils.BROWSERSTACK_AUTOMATE_API_URL + APIUtils.BROWSERSTACK_AUTOMATE_API_URL = 'https://apik8s.bsstag.com' + try { + const service = new BrowserstackService({} as any, [{}] as any, { + user: 'foo', + key: 'bar', + capabilities: {} + }) + service.before(service['_config'] as any, [], browser) + + expect(service['_sessionBaseUrl']).toEqual('https://apik8s.bsstag.com/automate/sessions') + } finally { + APIUtils.BROWSERSTACK_AUTOMATE_API_URL = original + } + }) + it('should overwrite execute command to route browserstack_executor via executeScript', async () => { (browser as any).isBidi = true const service = new BrowserstackService({} as any, [{}] as any, { user: 'foo', key: 'bar', capabilities: {} }) diff --git a/packages/browserstack-service/tests/util.test.ts b/packages/browserstack-service/tests/util.test.ts index a6e2e6bc..1f06fa63 100644 --- a/packages/browserstack-service/tests/util.test.ts +++ b/packages/browserstack-service/tests/util.test.ts @@ -566,6 +566,51 @@ describe('getCloudProvider', () => { }) }) +describe('SDK-6948 BROWSERSTACK_STAGING_ENV bsstag host recognition', () => { + const originalStagingEnv = process.env.BROWSERSTACK_STAGING_ENV + const stagingConfig = { user: 'foo', key: 'a'.repeat(20), hostname: 'hub-k8s.bsstag.com' } as any + + afterEach(() => { + if (originalStagingEnv === undefined) { + delete process.env.BROWSERSTACK_STAGING_ENV + } else { + process.env.BROWSERSTACK_STAGING_ENV = originalStagingEnv + } + }) + + it('treats *.bsstag.com hosts as browserstack for a named staging env', () => { + process.env.BROWSERSTACK_STAGING_ENV = 'k8s' + expect(utils.isStagingEnvBsstagHost('hub-k8s.bsstag.com')).toBe(true) + expect(getCloudProvider({ options: { hostname: 'hub-k8s.bsstag.com' } } as any)).toEqual('browserstack') + expect(getCloudProvider({ + isMultiremote: true, + instances: ['browserA'], + browserA: { options: { hostname: 'hub-k8s.bsstag.com' } } + } as unknown as WebdriverIO.MultiRemoteBrowser)).toEqual('browserstack') + expect(utils.isBrowserstackInfra(stagingConfig)).toBe(true) + }) + + it('leaves prod behaviour unchanged when BROWSERSTACK_STAGING_ENV is unset', () => { + delete process.env.BROWSERSTACK_STAGING_ENV + expect(utils.isStagingEnvBsstagHost('hub-k8s.bsstag.com')).toBe(false) + expect(getCloudProvider({ options: { hostname: 'hub-k8s.bsstag.com' } } as any)).toEqual('unknown_grid') + expect(getCloudProvider({ options: { hostname: 'hub.browserstack.com' } } as any)).toEqual('browserstack') + expect(utils.isBrowserstackInfra(stagingConfig)).toBe(false) + }) + + it('ignores a dotted (full host) BROWSERSTACK_STAGING_ENV value', () => { + process.env.BROWSERSTACK_STAGING_ENV = 'sdk-k8s.bsstag.com' + expect(utils.isStagingEnvBsstagHost('hub-k8s.bsstag.com')).toBe(false) + expect(getCloudProvider({ options: { hostname: 'hub-k8s.bsstag.com' } } as any)).toEqual('unknown_grid') + }) + + it('does not treat non-bsstag hosts as browserstack for a named staging env', () => { + process.env.BROWSERSTACK_STAGING_ENV = 'k8s' + expect(utils.isStagingEnvBsstagHost('hub.bsstag.com.evil.example')).toBe(false) + expect(getCloudProvider({ options: { hostname: 'anything-saucelabs.com' } } as any)).toEqual('unknown_grid') + }) +}) + describe('isBrowserstackSession', () => { it('return false if run locally', () => { expect(isBrowserstackSession({})).toEqual(false) From 832f42cdf4b8640a5f301684492ce78bbe6d358e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 7 Oct 2026 15:29:22 +0000 Subject: [PATCH 2/5] chore(changeset): auto-generate from PR template (patch) --- .changeset/pr-275.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/pr-275.md diff --git a/.changeset/pr-275.md b/.changeset/pr-275.md new file mode 100644 index 00000000..8a399935 --- /dev/null +++ b/.changeset/pr-275.md @@ -0,0 +1,6 @@ +--- +"@wdio/browserstack-service": patch +--- + +- SDK-6948: with `BROWSERSTACK_STAGING_ENV` set to a bare env name (e.g. `k8s`), `*.bsstag.com` hubs are now treated as BrowserStack, so session status marking and Automate <-> TRA session linking work on internal staging envs. Production behaviour is unchanged. +- SDK-6948: the session REST base URL in `service.ts` now uses the binary-supplied API host (`APIUtils`) instead of a hardcoded production host. From b01c4dc2313470adcdeca4d093df515bbd9b462e Mon Sep 17 00:00:00 2001 From: Aakash Hotchandani Date: Thu, 8 Oct 2026 12:59:39 +0530 Subject: [PATCH 3/5] fix: address review on staging-env host recognition and session URL (SDK-6948) - isStagingEnvBsstagHost trims BROWSERSTACK_STAGING_ENV (blank = unset) and compares the hostname case-insensitively (shared rule across SDKs). - _printSessionURL logs at debug instead of throwing when the session REST reply has no automation_session (e.g. a 401 on the classic flow against a named staging env). - Tests: attack hosts (evilbsstag.com, bsstag.com.attacker.io, bare bsstag.com), dotted-env isBrowserstackInfra, trim/case, App Automate and TurboScale session base URLs, and the _printSessionURL guard. - Comment: document that GRR-enabled accounts follow the regional api host. Co-Authored-By: Claude Opus 5.5 --- packages/browserstack-service/src/service.ts | 13 ++++- packages/browserstack-service/src/util.ts | 8 ++-- .../tests/service.test.ts | 48 +++++++++++++++++++ .../browserstack-service/tests/util.test.ts | 18 ++++++- 4 files changed, 81 insertions(+), 6 deletions(-) diff --git a/packages/browserstack-service/src/service.ts b/packages/browserstack-service/src/service.ts index 6e6a4f86..1fa78f76 100644 --- a/packages/browserstack-service/src/service.ts +++ b/packages/browserstack-service/src/service.ts @@ -248,7 +248,9 @@ export default class BrowserstackService implements Services.ServiceInstance { // Ensure capabilities are not null in case of multiremote // Resolved here, after beforeSession's CLI bootstrap has applied the binary-supplied - // GRR/staging hosts (APIUtils.updateURLSForGRR); the defaults are the production hosts. + // GRR/staging hosts (APIUtils.updateURLSForGRR). Without the CLI, or when the config server + // returns the default grr_urls, these are the production hosts; for a GRR-enabled account + // they follow the regional api host the binary already uses to mark the session. this._sessionBaseUrl = `${APIUtils.BROWSERSTACK_AUTOMATE_API_URL}/automate/sessions` if (this._isAppAutomate()) { @@ -1345,7 +1347,14 @@ export default class BrowserstackService implements Services.ServiceInstance { headers }) const res = response.clone() - browserUrl = (await res.json()).automation_session.browser_url + const session = (await res.json())?.automation_session + if (!session) { + // e.g. a 401 when the classic (non-CLI) flow targets a named staging env, whose + // session REST host is only known to the CLI; don't throw out of before(). + BStackLogger.debug(`Could not fetch Browserstack session URL (${response.status}) at ${sessionUrl}`) + return + } + browserUrl = session.browser_url } if (!this._browser) { diff --git a/packages/browserstack-service/src/util.ts b/packages/browserstack-service/src/util.ts index e5406d33..9bef0ec0 100644 --- a/packages/browserstack-service/src/util.ts +++ b/packages/browserstack-service/src/util.ts @@ -1146,14 +1146,16 @@ export function getLtsSessionId(): string { * SDK-6948: a named internal staging env (`BROWSERSTACK_STAGING_ENV` set to a bare token such as * `k8s`) serves its hub/cdp from `*.bsstag.com` (e.g. `hub-k8s.bsstag.com`). Treat those hosts as * BrowserStack so session marking and Automate <-> TRA linking work there. A dotted value (full - * host) or an unset variable leaves the production-only host checks unchanged. + * host) or an unset/blank variable leaves the production-only host checks unchanged. The env value is + * trimmed and the hostname compared case-insensitively (shared rule across SDKs). */ export function isStagingEnvBsstagHost(hostname: string): boolean { - const stagingEnv = process.env.BROWSERSTACK_STAGING_ENV + const stagingEnv = process.env.BROWSERSTACK_STAGING_ENV?.trim() if (!stagingEnv || stagingEnv.includes('.')) { return false } - return hostname === 'bsstag.com' || hostname.endsWith('.bsstag.com') + const host = hostname.toLowerCase() + return host === 'bsstag.com' || host.endsWith('.bsstag.com') } export function getCloudProvider(browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser): string { diff --git a/packages/browserstack-service/tests/service.test.ts b/packages/browserstack-service/tests/service.test.ts index 5e015b41..84dee61a 100644 --- a/packages/browserstack-service/tests/service.test.ts +++ b/packages/browserstack-service/tests/service.test.ts @@ -458,6 +458,19 @@ describe('_printSessionURL', () => { expect(isBrowserstackSessionSpy).toHaveBeenCalled() }) + it('logs at debug instead of throwing when the session REST reply has no automation_session (SDK-6948)', async () => { + browser.isMultiremote = false + service['_browser'] = browser + vi.spyOn(utils, 'isBrowserstackSession').mockReturnValue(true) + const logInfoSpy = vi.spyOn(log, 'info').mockImplementation((string) => string) + const debugSpy = vi.spyOn(bstackLogger.BStackLogger, 'debug') + vi.mocked(fetch).mockReturnValueOnce(Promise.resolve(Response.json({ message: 'Unauthorized' }, { status: 401 }))) + + await expect(service._printSessionURL()).resolves.toBeUndefined() + expect(debugSpy).toHaveBeenCalledWith(expect.stringContaining('Could not fetch Browserstack session URL (401)')) + expect(logInfoSpy).not.toHaveBeenCalledWith(expect.stringContaining('session: ')) + }) + describe('if cant print', () => { describe('no browser object', () => { const logInfoSpy = vi.spyOn(log, 'info').mockImplementation((string) => string) @@ -740,6 +753,41 @@ describe('before', () => { } }) + it('uses the GRR app-automate host for the App Automate session base url (SDK-6948)', () => { + const original = APIUtils.BROWSERSTACK_AA_API_CLOUD_URL + APIUtils.BROWSERSTACK_AA_API_CLOUD_URL = 'https://api-cloud-k8s.bsstag.com' + try { + const service = new BrowserstackService({} as any, [{}] as any, { + user: 'foo', + key: 'bar', + capabilities: {} + }) + vi.spyOn(service as any, '_isAppAutomate').mockReturnValue(true) + service.before(service['_config'] as any, [], browser) + + expect(service['_sessionBaseUrl']).toEqual('https://api-cloud-k8s.bsstag.com/app-automate/sessions') + } finally { + APIUtils.BROWSERSTACK_AA_API_CLOUD_URL = original + } + }) + + it('uses the GRR automate host for the TurboScale session base url (SDK-6948)', () => { + const original = APIUtils.BROWSERSTACK_AUTOMATE_API_URL + APIUtils.BROWSERSTACK_AUTOMATE_API_URL = 'https://api-eu.browserstack.com' + try { + const service = new BrowserstackService({ turboScale: true } as any, {}, { + user: 'foo', + key: 'bar', + capabilities: {} + }) + service.before(service['_config'] as any, [], browser) + + expect(service['_sessionBaseUrl']).toEqual('https://api-eu.browserstack.com/automate-turboscale/v1/sessions') + } finally { + APIUtils.BROWSERSTACK_AUTOMATE_API_URL = original + } + }) + it('should overwrite execute command to route browserstack_executor via executeScript', async () => { (browser as any).isBidi = true const service = new BrowserstackService({} as any, [{}] as any, { user: 'foo', key: 'bar', capabilities: {} }) diff --git a/packages/browserstack-service/tests/util.test.ts b/packages/browserstack-service/tests/util.test.ts index 1f06fa63..a6728818 100644 --- a/packages/browserstack-service/tests/util.test.ts +++ b/packages/browserstack-service/tests/util.test.ts @@ -602,12 +602,28 @@ describe('SDK-6948 BROWSERSTACK_STAGING_ENV bsstag host recognition', () => { process.env.BROWSERSTACK_STAGING_ENV = 'sdk-k8s.bsstag.com' expect(utils.isStagingEnvBsstagHost('hub-k8s.bsstag.com')).toBe(false) expect(getCloudProvider({ options: { hostname: 'hub-k8s.bsstag.com' } } as any)).toEqual('unknown_grid') + expect(utils.isBrowserstackInfra(stagingConfig)).toBe(false) }) it('does not treat non-bsstag hosts as browserstack for a named staging env', () => { process.env.BROWSERSTACK_STAGING_ENV = 'k8s' expect(utils.isStagingEnvBsstagHost('hub.bsstag.com.evil.example')).toBe(false) - expect(getCloudProvider({ options: { hostname: 'anything-saucelabs.com' } } as any)).toEqual('unknown_grid') + expect(utils.isStagingEnvBsstagHost('bsstag.com.attacker.io')).toBe(false) + expect(utils.isStagingEnvBsstagHost('evilbsstag.com')).toBe(false) + expect(utils.isStagingEnvBsstagHost('bsstag.com')).toBe(true) + expect(getCloudProvider({ options: { hostname: 'evilbsstag.com' } } as any)).toEqual('unknown_grid') + }) + + it('trims BROWSERSTACK_STAGING_ENV and matches the host case-insensitively', () => { + process.env.BROWSERSTACK_STAGING_ENV = ' k8s ' + expect(utils.isStagingEnvBsstagHost('HUB-K8S.BSSTAG.COM')).toBe(true) + expect(getCloudProvider({ options: { hostname: 'Hub-K8s.BsStag.com' } } as any)).toEqual('browserstack') + }) + + it('treats a blank BROWSERSTACK_STAGING_ENV as unset', () => { + process.env.BROWSERSTACK_STAGING_ENV = ' ' + expect(utils.isStagingEnvBsstagHost('hub-k8s.bsstag.com')).toBe(false) + expect(getCloudProvider({ options: { hostname: 'hub-k8s.bsstag.com' } } as any)).toEqual('unknown_grid') }) }) From 5aa758947d661713f28b83a329acb4883e3e6f94 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 07:30:22 +0000 Subject: [PATCH 4/5] chore(changeset): auto-generate from PR template (patch) --- .changeset/pr-275.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.changeset/pr-275.md b/.changeset/pr-275.md index 8a399935..22fd7334 100644 --- a/.changeset/pr-275.md +++ b/.changeset/pr-275.md @@ -3,4 +3,5 @@ --- - SDK-6948: with `BROWSERSTACK_STAGING_ENV` set to a bare env name (e.g. `k8s`), `*.bsstag.com` hubs are now treated as BrowserStack, so session status marking and Automate <-> TRA session linking work on internal staging envs. Production behaviour is unchanged. -- SDK-6948: the session REST base URL in `service.ts` now uses the binary-supplied API host (`APIUtils`) instead of a hardcoded production host. +- SDK-6948: the session REST base URL in `service.ts` now uses the binary-supplied API host (`APIUtils`) instead of a hardcoded production host. For GRR-enabled accounts this is the regional api host the binary already uses to mark sessions. The classic (non-CLI) flow on a named staging env is not supported; `_printSessionURL` logs a non-session reply at debug instead of throwing. +- SDK-6948: `BROWSERSTACK_STAGING_ENV` is trimmed (blank means unset), and `bsstag` hosts are matched case-insensitively. From 1b89b023d55691944b3da3f5669af0b4f060561b Mon Sep 17 00:00:00 2001 From: Aakash Hotchandani Date: Thu, 8 Oct 2026 13:05:48 +0530 Subject: [PATCH 5/5] fix(util): also recognize bsstag hosts for BROWSERSTACK_ENV staging/preprod (SDK-6948) Cross-SDK consistency: a *.bsstag.com host is also treated as BrowserStack when BROWSERSTACK_ENV (trimmed, lowercased) is staging, stag, preprod or pre-prod, matching node-agent constants and the binary's getEnvironmentUrl. Production/unset values keep the prod-only checks; look-alike hosts are still rejected. Co-Authored-By: Claude Opus 5.5 --- packages/browserstack-service/src/util.ts | 8 ++++- .../browserstack-service/tests/util.test.ts | 32 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/packages/browserstack-service/src/util.ts b/packages/browserstack-service/src/util.ts index 9bef0ec0..32715d54 100644 --- a/packages/browserstack-service/src/util.ts +++ b/packages/browserstack-service/src/util.ts @@ -1148,10 +1148,16 @@ export function getLtsSessionId(): string { * BrowserStack so session marking and Automate <-> TRA linking work there. A dotted value (full * host) or an unset/blank variable leaves the production-only host checks unchanged. The env value is * trimmed and the hostname compared case-insensitively (shared rule across SDKs). + * `BROWSERSTACK_ENV` (trimmed, lowercased) of staging/stag/preprod/pre-prod also selects the bsstag + * hosts, matching node-agent constants and the binary's getEnvironmentUrl. */ +const BSTAG_BROWSERSTACK_ENVS = ['staging', 'stag', 'preprod', 'pre-prod'] + export function isStagingEnvBsstagHost(hostname: string): boolean { const stagingEnv = process.env.BROWSERSTACK_STAGING_ENV?.trim() - if (!stagingEnv || stagingEnv.includes('.')) { + const namedStagingEnv = Boolean(stagingEnv) && !stagingEnv!.includes('.') + const browserstackEnv = process.env.BROWSERSTACK_ENV?.trim().toLowerCase() || '' + if (!namedStagingEnv && !BSTAG_BROWSERSTACK_ENVS.includes(browserstackEnv)) { return false } const host = hostname.toLowerCase() diff --git a/packages/browserstack-service/tests/util.test.ts b/packages/browserstack-service/tests/util.test.ts index a6728818..d659a41b 100644 --- a/packages/browserstack-service/tests/util.test.ts +++ b/packages/browserstack-service/tests/util.test.ts @@ -568,6 +568,11 @@ describe('getCloudProvider', () => { describe('SDK-6948 BROWSERSTACK_STAGING_ENV bsstag host recognition', () => { const originalStagingEnv = process.env.BROWSERSTACK_STAGING_ENV + const originalBrowserstackEnv = process.env.BROWSERSTACK_ENV + + beforeEach(() => { + delete process.env.BROWSERSTACK_ENV + }) const stagingConfig = { user: 'foo', key: 'a'.repeat(20), hostname: 'hub-k8s.bsstag.com' } as any afterEach(() => { @@ -576,6 +581,11 @@ describe('SDK-6948 BROWSERSTACK_STAGING_ENV bsstag host recognition', () => { } else { process.env.BROWSERSTACK_STAGING_ENV = originalStagingEnv } + if (originalBrowserstackEnv === undefined) { + delete process.env.BROWSERSTACK_ENV + } else { + process.env.BROWSERSTACK_ENV = originalBrowserstackEnv + } }) it('treats *.bsstag.com hosts as browserstack for a named staging env', () => { @@ -620,6 +630,28 @@ describe('SDK-6948 BROWSERSTACK_STAGING_ENV bsstag host recognition', () => { expect(getCloudProvider({ options: { hostname: 'Hub-K8s.BsStag.com' } } as any)).toEqual('browserstack') }) + it.each(['staging', 'stag', 'preprod', 'pre-prod', ' PreProd '])('treats bsstag hosts as browserstack for BROWSERSTACK_ENV=%j', (env) => { + delete process.env.BROWSERSTACK_STAGING_ENV + process.env.BROWSERSTACK_ENV = env + expect(utils.isStagingEnvBsstagHost('hub-preprod.bsstag.com')).toBe(true) + expect(getCloudProvider({ options: { hostname: 'hub-preprod.bsstag.com' } } as any)).toEqual('browserstack') + expect(utils.isBrowserstackInfra({ ...stagingConfig, hostname: 'hub-preprod.bsstag.com' })).toBe(true) + }) + + it.each(['production', 'prod', ''])('keeps prod behaviour for BROWSERSTACK_ENV=%j', (env) => { + delete process.env.BROWSERSTACK_STAGING_ENV + process.env.BROWSERSTACK_ENV = env + expect(utils.isStagingEnvBsstagHost('hub-preprod.bsstag.com')).toBe(false) + expect(getCloudProvider({ options: { hostname: 'hub-preprod.bsstag.com' } } as any)).toEqual('unknown_grid') + }) + + it('rejects look-alike hosts for BROWSERSTACK_ENV=staging', () => { + delete process.env.BROWSERSTACK_STAGING_ENV + process.env.BROWSERSTACK_ENV = 'staging' + expect(utils.isStagingEnvBsstagHost('evilbsstag.com')).toBe(false) + expect(utils.isStagingEnvBsstagHost('hub.bsstag.com.attacker.io')).toBe(false) + }) + it('treats a blank BROWSERSTACK_STAGING_ENV as unset', () => { process.env.BROWSERSTACK_STAGING_ENV = ' ' expect(utils.isStagingEnvBsstagHost('hub-k8s.bsstag.com')).toBe(false)