diff --git a/.changeset/pr-238.md b/.changeset/pr-238.md new file mode 100644 index 00000000..a03f9a8b --- /dev/null +++ b/.changeset/pr-238.md @@ -0,0 +1,5 @@ +--- +"@wdio/browserstack-service": patch +--- + +- Added a warning when `BROWSERSTACK_USERNAME`/`BROWSERSTACK_ACCESS_KEY` or `testObservabilityOptions.user` point to a different BrowserStack account than the WebdriverIO `user`/`key`. Such runs send test results to a different account than their sessions. diff --git a/packages/browserstack-service/src/launcher.ts b/packages/browserstack-service/src/launcher.ts index 390e4e80..07538037 100644 --- a/packages/browserstack-service/src/launcher.ts +++ b/packages/browserstack-service/src/launcher.ts @@ -39,6 +39,8 @@ import { isTrue, getBrowserStackUser, getBrowserStackKey, + getCredentialMismatchWarning, + isBrowserstackInfra, uploadLogs, ObjectsAreEqual, getBasicAuthHeader, isValidCapsForHealing, @@ -274,6 +276,13 @@ export default class BrowserstackLauncherService implements Services.ServiceInst // edge-1 conflict, but browserStackConfig.app was copied earlier in the constructor. this.browserStackConfig.app = this._options.app + if (isBrowserstackInfra(config as BrowserstackConfig & Options.Testrunner, capabilities as Capabilities.BrowserStackCapabilities)) { + const credentialMismatchWarning = getCredentialMismatchWarning(this._options, config) + if (credentialMismatchWarning) { + BStackLogger.warn(credentialMismatchWarning) + } + } + // Send Funnel start request await sendStart(this.browserStackConfig) diff --git a/packages/browserstack-service/src/util.ts b/packages/browserstack-service/src/util.ts index 5f5e0282..5aa1e9bb 100644 --- a/packages/browserstack-service/src/util.ts +++ b/packages/browserstack-service/src/util.ts @@ -1450,6 +1450,38 @@ export function getBrowserStackKey(config: Options.Testrunner) { return config.key } +// Sessions authenticate with config.user, but the CLI / Test Reporting build prefers the +// env credentials, then testObservabilityOptions.user — a mismatch splits one run across two accounts. +export function getCredentialMismatchWarning(options: BrowserstackConfig & Options.Testrunner, config: Options.Testrunner): string | undefined { + const hubUser = config.user + if (typeof hubUser !== 'string' || hubUser.length === 0) { + return undefined + } + + let source: string | undefined + let reportingUser: string | undefined + for (const envVar of ['BROWSERSTACK_USERNAME', 'BROWSERSTACK_USER_NAME']) { + if (process.env[envVar]) { + source = `the ${envVar} environment variable` + reportingUser = process.env[envVar] + break + } + } + if (!source && options.testObservabilityOptions?.user) { + source = 'testObservabilityOptions.user' + reportingUser = options.testObservabilityOptions.user + } + + if (!source || reportingUser === hubUser) { + return undefined + } + + return `BrowserStack credential mismatch: the \`user\` in your WebdriverIO config and ${source} point to different BrowserStack accounts. ` + + `Test sessions are created with the config \`user\`, but Test Reporting & Analytics builds are created with ${source}, ` + + 'so this run\'s test results will not appear under the same account as its sessions. ' + + `Use the same BrowserStack credentials in both places (or remove ${source}) to see them together.` +} + export function isUndefined(value: unknown) { let res = (value === undefined || value === null) if (typeof value === 'string') { diff --git a/packages/browserstack-service/tests/launcher.test.ts b/packages/browserstack-service/tests/launcher.test.ts index 292cae42..c1cfa7a7 100644 --- a/packages/browserstack-service/tests/launcher.test.ts +++ b/packages/browserstack-service/tests/launcher.test.ts @@ -91,6 +91,33 @@ describe('onPrepare', () => { vi.spyOn(thUtils, 'getProductMap').mockImplementation(() => productMap) }) + it('warns when BROWSERSTACK_USERNAME points to a different account than config.user', async () => { + const warnSpy = vi.spyOn(bstackLogger.BStackLogger, 'warn') + process.env.BROWSERSTACK_USERNAME = 'another-account' + try { + const service = new BrowserstackLauncher({ testObservability: false } as any, caps, config) + await service.onPrepare(config, caps) + } finally { + delete process.env.BROWSERSTACK_USERNAME + } + + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('BrowserStack credential mismatch')) + }) + + it('does not warn about a credential mismatch when not running on BrowserStack', async () => { + const warnSpy = vi.spyOn(bstackLogger.BStackLogger, 'warn') + process.env.BROWSERSTACK_USERNAME = 'another-account' + const nonBstackConfig = { ...config, hostname: 'localhost' } + try { + const service = new BrowserstackLauncher({ testObservability: false } as any, caps, nonBstackConfig) + await service.onPrepare(nonBstackConfig, caps) + } finally { + delete process.env.BROWSERSTACK_USERNAME + } + + expect(warnSpy).not.toHaveBeenCalledWith(expect.stringContaining('BrowserStack credential mismatch')) + }) + it('should not try to upload app is app is undefined', async () => { const service = new BrowserstackLauncher({ testObservability: false } as any, caps, config) await service.onPrepare(config, caps) diff --git a/packages/browserstack-service/tests/util.test.ts b/packages/browserstack-service/tests/util.test.ts index 1acc7b4d..a6e2e6bc 100644 --- a/packages/browserstack-service/tests/util.test.ts +++ b/packages/browserstack-service/tests/util.test.ts @@ -43,6 +43,7 @@ import { isTrue, uploadLogs, getObservabilityProduct, + getCredentialMismatchWarning, isUndefined, processTestObservabilityResponse, processAccessibilityResponse, @@ -1147,6 +1148,50 @@ describe('getObservabilityUser', () => { }) }) +describe('getCredentialMismatchWarning', () => { + const envVars = ['BROWSERSTACK_USERNAME', 'BROWSERSTACK_USER_NAME'] + beforeEach(() => envVars.forEach((v) => delete process.env[v])) + afterEach(() => envVars.forEach((v) => delete process.env[v])) + + it('warns when BROWSERSTACK_USERNAME differs from config.user', () => { + process.env.BROWSERSTACK_USERNAME = 'other-account' + const warning = getCredentialMismatchWarning({} as any, { user: 'hub-account' }) + expect(warning).toContain('BROWSERSTACK_USERNAME environment variable') + expect(warning).not.toContain('hub-account') + expect(warning).not.toContain('other-account') + }) + + it('warns when BROWSERSTACK_USER_NAME differs from config.user', () => { + process.env.BROWSERSTACK_USER_NAME = 'other-account' + expect(getCredentialMismatchWarning({} as any, { user: 'hub-account' })).toContain('BROWSERSTACK_USER_NAME environment variable') + }) + + it('warns when testObservabilityOptions.user differs from config.user', () => { + const warning = getCredentialMismatchWarning({ testObservabilityOptions: { user: 'other-account' } } as any, { user: 'hub-account' }) + expect(warning).toContain('testObservabilityOptions.user') + }) + + it('names the env var when it takes precedence over testObservabilityOptions.user', () => { + process.env.BROWSERSTACK_USERNAME = 'other-account' + const warning = getCredentialMismatchWarning({ testObservabilityOptions: { user: 'hub-account' } } as any, { user: 'hub-account' }) + expect(warning).toContain('BROWSERSTACK_USERNAME environment variable') + }) + + it('does not warn when the env var matches config.user', () => { + process.env.BROWSERSTACK_USERNAME = 'hub-account' + expect(getCredentialMismatchWarning({ testObservabilityOptions: { user: 'other-account' } } as any, { user: 'hub-account' })).toBeUndefined() + }) + + it('does not warn when no alternative credential source is set', () => { + expect(getCredentialMismatchWarning({} as any, { user: 'hub-account' })).toBeUndefined() + }) + + it('does not warn when config.user is not set', () => { + process.env.BROWSERSTACK_USERNAME = 'other-account' + expect(getCredentialMismatchWarning({} as any, {})).toBeUndefined() + }) +}) + describe('getObservabilityKey', () => { it('get env var', () => { process.env.BROWSERSTACK_ACCESS_KEY = 'try'