Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/pr-275.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@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. 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.
20 changes: 17 additions & 3 deletions packages/browserstack-service/src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -246,12 +247,18 @@ 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). 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`

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Warning — [CORRECTNESS] Classic (non-CLI) flow now sends staging creds to the prod session REST API

Problem

APIUtils is localized in only one place: BrowserstackCLI.loadModules() → APIUtils.updateURLSForGRR(config.apis) (src/cli/index.ts:156). On the classic flow, that call never runs. The classic flow covers multiremote (BROWSERSTACK_IS_MULTIREMOTE=true), frameworks outside CLISupportedFrameworks, and a CLI bootstrap that errors and is logged at cli/index.ts:104. On that flow APIUtils.BROWSERSTACK_AUTOMATE_API_URL stays https://api.browserstack.com, so line 252 still produces the production host.

Before this PR that did not matter on staging. getCloudProvider returned unknown_grid for hub-k8s.bsstag.com, so isBrowserstackSession() was false and _printSessionURL, _update and _updateJob all returned early. Fix (B) now makes isBrowserstackSession() true on staging, including multiremote. Because the multiremote branch of getCloudProvider was also changed, the classic flow now actively calls these:

  • GET https://api.browserstack.com/automate/sessions/<id>.json in before() (_printSessionURL)
  • PUT …/<id>.json in after() (_updateJob, gated !isRunning()) and in onReload (_update)

All three carry the staging user:key. That is exactly what the PR body says (C) fixes ("On staging it sent staging creds to the production API"). Two things follow. Session status is never marked on staging for these runs. _printSessionURL also dereferences (await res.json()).automation_session.browser_url without a guard, so a 401 JSON reply throws a TypeError out of before(). WDIO logs it rather than failing the test, but it is new noise. This is anti-pattern #23 (one path fixed, the parallel path not). Production is unaffected, because with the variable unset isStagingEnvBsstagHost returns false.

Suggested Fix

When the CLI is not running and BROWSERSTACK_STAGING_ENV is a bare token, derive the staging API host locally, mirroring binary #1889's stagingApiHost. Do it in one helper next to isStagingEnvBsstagHost so both checks share the "named env" definition:

export function stagingEnvApiHost(): string | undefined {
    const stg = process.env.BROWSERSTACK_STAGING_ENV
    if (!stg || stg.includes('.')) { return undefined }
    return stg === 'preprod' ? 'https://api-preprod.bsstag.com' : `https://api${stg}.bsstag.com`
}
// before():
const automateApi = BrowserstackCLI.getInstance().isRunning()
    ? APIUtils.BROWSERSTACK_AUTOMATE_API_URL
    : (stagingEnvApiHost() ?? APIUtils.BROWSERSTACK_AUTOMATE_API_URL)

If that is out of scope, document the classic flow as unsupported on staging in the PR body and the changeset. Add a unit test that pins the classic + staging host either way.

Confidence: 🟢 Objectively traceable. updateURLSForGRR has a single caller, in CLI loadModules, and the gates on isBrowserstackSession / !isRunning() are in the file. Matches shared anti-pattern #23 (parallel-path drift).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid, thanks. Following the cross-SDK decision for this ticket, I did not add staging host-name derivation to the SDK. Instead, b01c4dc: (1) The PR body now documents that the classic (non-CLI) flow is not supported on named staging envs. It covers multiremote, frameworks outside CLISupportedFrameworks, and a failed CLI bootstrap; in all of them APIUtils keeps the prod hosts, so marking there gets a 401. (2) _printSessionURL no longer dereferences .automation_session.browser_url unguarded. A reply without automation_session (e.g. a 401) logs Could not fetch Browserstack session URL (<status>) at debug and returns, instead of throwing a TypeError out of before(). Test: logs at debug instead of throwing when the session REST reply has no automation_session, which fails without the guard. Production is unaffected, because isStagingEnvBsstagHost is false with the variable unset.


if (this._isAppAutomate()) {
this._sessionBaseUrl = 'https://api-cloud.browserstack.com/app-automate/sessions'
this._sessionBaseUrl = `${APIUtils.BROWSERSTACK_AA_API_CLOUD_URL}/app-automate/sessions`

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Warning — [CORRECTNESS] App Automate session REST on a named env resolves to api-cloud-<env>.bsstag.com, not the API_SUBDOMAIN host

Problem

To keep production byte-identical, the App Automate base URL is mapped to BROWSERSTACK_AA_API_CLOUD_URL. updateURLSForGRR fills that from apis.appAutomate.upload, not apis.appAutomate.api. In production the binary's appAutomate.upload is https://api-cloud.browserstack.com, so the value matches the old hardcoded host. ✅

On a named env, binary core/config/apis.js (main, and unchanged by #1889) sets grp.upload = https://api-cloud-${stg}.bsstag.com. The resolved URL is then https://api-cloud-k8s.bsstag.com/app-automate/sessions/<id>.json. Binary #1889's own rationale says railsApp serves /automate/* and /app-automate/* only when the subdomain equals API_SUBDOMAIN (api<env>, or api-preprod). By that rule, /app-automate/sessions on api-cloud-<env> reaches Rails and 404s. So (C) does not yet deliver for App Automate on staging. It is not a regression (before, the request went to production with staging creds), but the PR body and changeset say the session REST host "follows the binary-supplied hosts" without that caveat. The E2E only covered Automate (Chrome / Windows 11).

Suggested Fix

Either verify api-cloud-k8s.bsstag.com/app-automate/sessions/<id>.json on k8s, or limit the claim. The PR body and changeset would then say App Automate session REST on named envs is pending. The binary appAutomate.upload staging host (or an apis.appAutomate.api-based path for staging only) needs a follow-up there. Also add a service.test.ts case that sets APIUtils.BROWSERSTACK_AA_API_CLOUD_URL and asserts the /app-automate/sessions base, so the App Automate mapping is pinned.

Confidence: 🟡 It depends on how staging infra routes api-cloud-<env>.bsstag.com, which was not verified E2E. Inferred from binary #1889's documented API_SUBDOMAIN rule.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partly valid. I checked it on k8s with the same credentials. api-cloud-k8s.bsstag.com/automate/sessions/<automate id>.json returns 200 with the same JSON as apik8s, and an unknown /app-automate/sessions/<id>.json returns the same JSON {"status":404,"message":"Not Found"} on both api-cloud-k8s and apik8s. So api-cloud-<env> does reach the Rails API, which goes against the "404s at the edge" inference. I did not have an App Automate session to prove the real AA path, so b01c4dc scopes the claim: the PR body says the App Automate path is not E2E-validated (Automate only). It also adds a service.test.ts case that pins BROWSERSTACK_AA_API_CLOUD_URL to /app-automate/sessions. No code change: production stays identical (api-cloud.browserstack.com).

}

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 = []
Expand Down Expand Up @@ -1340,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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Suggestion: [ROBUSTNESS] The guard only covers JSON replies

res.json() throws a SyntaxError on a non-JSON body before ?.automation_session is ever checked. The PR body's "Known gap" saw exactly this ("fails with an HTML reply"), and a plain-text Basic-auth 401 would do the same. In those cases before() still throws (anti-pattern #5). Checking response.ok first covers both cases:

if (!response.ok) {
    BStackLogger.debug(`Could not fetch Browserstack session URL (${response.status}) at ${sessionUrl}`)
    return
}

A test with new Response('<html>…</html>', { status: 401 }) would pin it.

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) {
Expand Down
28 changes: 25 additions & 3 deletions packages/browserstack-service/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1142,6 +1142,28 @@ 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/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()
const namedStagingEnv = Boolean(stagingEnv) && !stagingEnv!.includes('.')
const browserstackEnv = process.env.BROWSERSTACK_ENV?.trim().toLowerCase() || ''

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Warning: [CORRECTNESS/DOCS] BROWSERSTACK_ENV staging/preprod widens bsstag recognition with no E2E

With BROWSERSTACK_ENV set to staging, stag, preprod or pre-prod, isBrowserstackSession() is now true for *.bsstag.com hubs. On this path, though, APIUtils is not localized even on the CLI flow. The binary's API.initialize(grrDetails) re-asserts bsstag hosts only for BROWSERSTACK_STAGING_ENV. Otherwise automate.api / appAutomate.upload come from the config server's grr_urls, which the binary's own comment says are prod values. As a result, _printSessionURL and onReload → _update (plus _updateJob on the classic flow) can now send preprod or staging credentials to api.browserstack.com. Before this commit they returned early on unknown_grid. This is the W1 / anti-pattern #23 surface again, on a second env selector. It affects internal envs only.

Two smaller gaps:

  • The binary's getEnvironmentUrl lowercases the value but does not trim it, so the ' PreProd ' case is SDK-only parity.
  • The E2E covers only BROWSERSTACK_STAGING_ENV=k8s, and .changeset/pr-275.md does not mention BROWSERSTACK_ENV.

Suggested fix: do one of these, and add BROWSERSTACK_ENV to the changeset:

  • Validate a BROWSERSTACK_ENV=preprod run and confirm _sessionBaseUrl is api-preprod.bsstag.com.
  • Extend the PR body's "Not supported" note to say that SDK-side session REST calls may target prod hosts under BROWSERSTACK_ENV.

Confidence: 🟡. The grr_urls contents on preprod or staging were not verified. This is inferred from binary core/config/apis.js initialize().

if (!namedStagingEnv && !BSTAG_BROWSERSTACK_ENVS.includes(browserstackEnv)) {
return false
}
const host = hostname.toLowerCase()
return host === 'bsstag.com' || host.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,
Expand All @@ -1157,11 +1179,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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Suggestion — [PROCESS] #56 / #65 are still OPEN (not on main), edit the same three lines, and would reintroduce unconditional bsstag matching

The PR body says #56/#65 are "on main". They are both OPEN and unmerged. Each rewrites the same three lines (both getCloudProvider branches with .includes('bsstag'), and isBrowserstackInfra with endsWith('.bsstag.com')), ungated. Whichever merges second conflicts. A careless resolution that keeps both would bring back production-wide bsstag matching (substring includes('bsstag') also matches evilbsstag.com). Close #56/#65 as superseded by this PR, and correct the body.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, thanks. The PR body wording was wrong: #56 and #65 are open, unmerged PRs against main, not on main. I have corrected the Overlaps paragraph. It now says they rewrite the same three lines with ungated includes('bsstag') / endsWith('.bsstag.com'), that the second to merge will conflict, and that keeping both would bring back production-wide bsstag matching. This PR supersedes their util.ts change. Closing #56/#65 is left to their author (no change in this PR).

return 'browserstack'
}
return 'unknown_grid'
Expand Down Expand Up @@ -1270,7 +1292,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)) {
Expand Down
66 changes: 66 additions & 0 deletions packages/browserstack-service/tests/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -457,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)
Expand Down Expand Up @@ -722,6 +736,58 @@ 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('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: {} })
Expand Down
93 changes: 93 additions & 0 deletions packages/browserstack-service/tests/util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,99 @@ 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(() => {
if (originalStagingEnv === undefined) {
delete process.env.BROWSERSTACK_STAGING_ENV
} 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', () => {
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')
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(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.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)
expect(getCloudProvider({ options: { hostname: 'hub-k8s.bsstag.com' } } as any)).toEqual('unknown_grid')
})
})

describe('isBrowserstackSession', () => {
it('return false if run locally', () => {
expect(isBrowserstackSession({})).toEqual(false)
Expand Down
Loading