diff --git a/bin/testObservability/helper/helper.js b/bin/testObservability/helper/helper.js index 434d4a42..ad9c56ba 100644 --- a/bin/testObservability/helper/helper.js +++ b/bin/testObservability/helper/helper.js @@ -92,8 +92,16 @@ const supportFileCleanup = () => { exports.buildStopped = false; +const isValidTestHubValue = (value) => !!value && value !== "null" && value !== "undefined"; + +// A build started at TestHub for any product (e.g. accessibility with observability off) +// must also be stopped at TestHub, otherwise TestHub never finalises it downstream. +exports.isTestHubBuildLaunched = () => { + return isValidTestHubValue(process.env.BROWSERSTACK_TESTHUB_UUID) && isValidTestHubValue(process.env.BROWSERSTACK_TESTHUB_JWT); +} + exports.printBuildLink = async (shouldStopSession, exitCode = null) => { - if(!this.isTestObservabilitySession()) return; + if(!this.isTestObservabilitySession() && !this.isTestHubBuildLaunched()) return; // SDK-6211: the build-stop may be sent early (runs.js fires it at poll-resolution, before the // post-test 5s wait + artifact download + report generation, so builds_th.finished_at — which // the collector stamps at stop-event receipt — reflects the test window rather than the full CLI @@ -677,8 +685,11 @@ exports.shouldReRunObservabilityTests = () => { } exports.stopBuildUpstream = async () => { - if (process.env.BS_TESTOPS_BUILD_COMPLETED === "true") { - if(process.env.BS_TESTOPS_JWT == "null" || process.env.BS_TESTOPS_BUILD_HASHED_ID == "null") { + const observabilityBuildLaunched = process.env.BS_TESTOPS_BUILD_COMPLETED === "true"; + if (observabilityBuildLaunched || exports.isTestHubBuildLaunched()) { + const jwt = observabilityBuildLaunched ? process.env.BS_TESTOPS_JWT : process.env.BROWSERSTACK_TESTHUB_JWT; + const buildHashedId = observabilityBuildLaunched ? process.env.BS_TESTOPS_BUILD_HASHED_ID : process.env.BROWSERSTACK_TESTHUB_UUID; + if(!isValidTestHubValue(jwt) || !isValidTestHubValue(buildHashedId)) { exports.debug(`EXCEPTION IN stopBuildUpstream REQUEST TO ${TEST_REPORTING_ANALYTICS} : Missing authentication token`); return { status: 'error', @@ -692,14 +703,14 @@ exports.stopBuildUpstream = async () => { }; const config = { headers: { - 'Authorization': `Bearer ${process.env.BS_TESTOPS_JWT}`, + 'Authorization': `Bearer ${jwt}`, 'Content-Type': 'application/json', 'X-BSTACK-TESTOPS': 'true' } }; try { - const response = await exports.nodeRequest('PUT',`api/v1/builds/${process.env.BS_TESTOPS_BUILD_HASHED_ID}/stop`,data,config); + const response = await exports.nodeRequest('PUT',`api/v1/builds/${buildHashedId}/stop`,data,config); if(response.data && response.data.error) { throw({message: response.data.error}); } else { diff --git a/test/unit/bin/testObservability/buildStop.js b/test/unit/bin/testObservability/buildStop.js new file mode 100644 index 00000000..b8089a94 --- /dev/null +++ b/test/unit/bin/testObservability/buildStop.js @@ -0,0 +1,88 @@ +'use strict'; +const chai = require('chai'); +const expect = chai.expect; +const sinon = require('sinon'); + +const helper = require('../../../../bin/testObservability/helper/helper'); + +const ENV_KEYS = [ + 'BROWSERSTACK_TEST_OBSERVABILITY', + 'BS_TESTOPS_BUILD_COMPLETED', + 'BS_TESTOPS_JWT', + 'BS_TESTOPS_BUILD_HASHED_ID', + 'BROWSERSTACK_TESTHUB_UUID', + 'BROWSERSTACK_TESTHUB_JWT', +]; + +describe('TestHub build stop', () => { + let savedEnv, nodeRequest; + + beforeEach(() => { + savedEnv = {}; + ENV_KEYS.forEach((k) => { savedEnv[k] = process.env[k]; delete process.env[k]; }); + helper.buildStopped = false; + nodeRequest = sinon.stub(helper, 'nodeRequest').resolves({ data: {} }); + }); + + afterEach(() => { + sinon.restore(); + ENV_KEYS.forEach((k) => { + if (savedEnv[k] === undefined) delete process.env[k]; else process.env[k] = savedEnv[k]; + }); + helper.buildStopped = false; + }); + + const stopCall = () => nodeRequest.getCalls().find((c) => c.args[0] === 'PUT'); + + it('stops an accessibility-only TestHub build when observability is off', async () => { + process.env.BROWSERSTACK_TEST_OBSERVABILITY = 'false'; + process.env.BS_TESTOPS_BUILD_COMPLETED = 'false'; + process.env.BS_TESTOPS_JWT = 'null'; + process.env.BS_TESTOPS_BUILD_HASHED_ID = 'null'; + process.env.BROWSERSTACK_TESTHUB_UUID = 'th-build-uuid'; + process.env.BROWSERSTACK_TESTHUB_JWT = 'th-jwt'; + + await helper.printBuildLink(true); + + const call = stopCall(); + expect(call, 'PUT stop request').to.exist; + expect(call.args[1]).to.equal('api/v1/builds/th-build-uuid/stop'); + expect(call.args[3].headers.Authorization).to.equal('Bearer th-jwt'); + }); + + it('keeps using the observability token and build id when observability launched the build', async () => { + process.env.BROWSERSTACK_TEST_OBSERVABILITY = 'true'; + process.env.BS_TESTOPS_BUILD_COMPLETED = 'true'; + process.env.BS_TESTOPS_JWT = 'o11y-jwt'; + process.env.BS_TESTOPS_BUILD_HASHED_ID = 'o11y-build'; + process.env.BROWSERSTACK_TESTHUB_UUID = 'o11y-build'; + process.env.BROWSERSTACK_TESTHUB_JWT = 'o11y-jwt'; + + await helper.printBuildLink(true); + + const call = stopCall(); + expect(call.args[1]).to.equal('api/v1/builds/o11y-build/stop'); + expect(call.args[3].headers.Authorization).to.equal('Bearer o11y-jwt'); + }); + + it('sends no stop when no TestHub build was launched', async () => { + process.env.BROWSERSTACK_TEST_OBSERVABILITY = 'false'; + process.env.BROWSERSTACK_TESTHUB_UUID = 'null'; + process.env.BROWSERSTACK_TESTHUB_JWT = 'null'; + + await helper.printBuildLink(true); + + expect(stopCall()).to.be.undefined; + }); + + it('sends the stop only once across repeated calls', async () => { + process.env.BROWSERSTACK_TEST_OBSERVABILITY = 'false'; + process.env.BROWSERSTACK_TESTHUB_UUID = 'th-build-uuid'; + process.env.BROWSERSTACK_TESTHUB_JWT = 'th-jwt'; + + await helper.printBuildLink(true); + await helper.printBuildLink(true); + + expect(nodeRequest.getCalls().filter((c) => c.args[0] === 'PUT')).to.have.length(1); + }); +});