From 506f6b0b01cdfe8b4e10d5292a41153b63cd5b66 Mon Sep 17 00:00:00 2001 From: Tanmay Lokhande Date: Mon, 15 Sep 2025 08:54:29 +0530 Subject: [PATCH 01/17] Build unification changes --- bin/accessibility-automation/cypress/index.js | 45 +-- bin/accessibility-automation/plugin/index.js | 54 ++++ bin/commands/runs.js | 17 +- bin/helpers/helper.js | 11 + bin/helpers/utils.js | 3 +- bin/testObservability/helper/helper.js | 16 +- bin/testObservability/reporter/index.js | 89 +++++- bin/testhub/constants.js | 10 + bin/testhub/testhubHandler.js | 135 +++++++++ bin/testhub/utils.js | 285 ++++++++++++++++++ package.json | 1 + 11 files changed, 623 insertions(+), 43 deletions(-) create mode 100644 bin/testhub/constants.js create mode 100644 bin/testhub/testhubHandler.js create mode 100644 bin/testhub/utils.js diff --git a/bin/accessibility-automation/cypress/index.js b/bin/accessibility-automation/cypress/index.js index d5e6a0bd..cd52ee12 100644 --- a/bin/accessibility-automation/cypress/index.js +++ b/bin/accessibility-automation/cypress/index.js @@ -336,26 +336,33 @@ afterEach(() => { } else if (attributes.prevAttempts && attributes.prevAttempts.length > 0) { filePath = (attributes.prevAttempts[0].invocationDetails && attributes.prevAttempts[0].invocationDetails.relativeFile) || ''; } - const payloadToSend = { - "saveResults": shouldScanTestForAccessibility, - "testDetails": { - "name": attributes.title, - "testRunId": '5058', // variable not consumed, shouldn't matter what we send - "filePath": filePath, - "scopeList": [ - filePath, - attributes.title - ] - }, - "platform": { - "os_name": os_data, - "os_version": Cypress.env("OS_VERSION"), - "browser_name": Cypress.browser.name, - "browser_version": Cypress.browser.version - } - }; + + browserStackLog(`Printing attributes ${attributes.title}`); browserStackLog(`Saving accessibility test results`); - cy.wrap(saveTestResults(win, payloadToSend), {timeout: 30000}).then(() => { + browserStackLog(`Cypress env browserstack testhub uuid: ${Cypress.env("BROWSERSTACK_TESTHUB_UUID")}`); + browserStackLog(`Cypress env browserstack testhub jwt: ${Cypress.env("BROWSERSTACK_TESTHUB_JWT")}`); + browserStackLog(`Cypress env http port: ${Cypress.env("BROWSERSTACK_TESTHUB_API_PORT")}`); + browserStackLog(`test env: ${Cypress.env("TEST_ENV")}`); + browserStackLog(`reporter api: ${Cypress.env("REPORTER_API")}`); + + let testRunUuid = null; + cy.task('get_test_run_uuid', { testIdentifier: attributes.title }) + .then((response) => { + browserStackLog(`Response from get_test_run_uuid task`); + if (response && response.testRunUuid) { + testRunUuid = response.testRunUuid; + browserStackLog(`Fetched testRunId: ${testRunUuid} for test: ${attributes.title}`); + } + + const payloadToSend = { + "thTestRunUuid": testRunUuid, + "thBuildUuid": Cypress.env("BROWSERSTACK_TESTHUB_UUID"), + "thJwtToken": Cypress.env("BROWSERSTACK_TESTHUB_JWT") + }; + browserStackLog(`Payload to send: ${JSON.stringify(payloadToSend)}`); + + return cy.wrap(saveTestResults(win, payloadToSend), {timeout: 30000}); + }).then(() => { browserStackLog(`Saved accessibility test results`); }) diff --git a/bin/accessibility-automation/plugin/index.js b/bin/accessibility-automation/plugin/index.js index 4c35ef99..064d76a1 100644 --- a/bin/accessibility-automation/plugin/index.js +++ b/bin/accessibility-automation/plugin/index.js @@ -14,6 +14,56 @@ const browserstackAccessibility = (on, config) => { return null }, + get_test_run_uuid({ testIdentifier, retries = 15, interval = 300 } = {}) { + return new Promise((resolve) => { + console.log(`printing env variables take 5`); + console.log(`Cypress env browserstack testhub uuid from plugin: ${config.env.BROWSERSTACK_TESTHUB_UUID}`); + console.log(`Cypress env http port: ${config.env.REPORTER_API}`); + console.log(`test env: ${config.env.TEST_ENV}`); + console.log(`reporter api from process: ${process.env.REPORTER_API}`); + console.log(`Fetching testRunUuid for testIdentifier: ${testIdentifier}`); + if(!testIdentifier) return resolve(null); + const port = process.env.REPORTER_API; + let attempt = 0; + const fetchUuid = () => { + const options = { + hostname: '127.0.0.1', + port, + path: `/test-uuid?testIdentifier=${encodeURIComponent(testIdentifier)}`, + method: 'GET', + timeout: 2000 + }; + const req = http.request(options, (res) => { + let data = ''; + res.on('data', (chunk) => data += chunk); + res.on('end', () => { + if(res.statusCode === 200) { + try { + const json = JSON.parse(data || '{}'); + return resolve({ testRunUuid: json.testRunUuid || null }); + } catch(e) { + return resolve(null); + } + } else if (res.statusCode === 404) { + // Server up but endpoint not responding as expected – stop retrying. + return resolve(null); + } else { + retryOrResolve(); + } + }); + }); + req.on('error', () => retryOrResolve()); + req.on('timeout', () => { req.destroy(); retryOrResolve(); }); + req.end(); + }; + const retryOrResolve = () => { + attempt += 1; + if(attempt >= retries) return resolve(null); + setTimeout(fetchUuid, interval); + }; + fetchUuid(); + }); + } }) on('before:browser:launch', (browser = {}, launchOptions) => { try { @@ -51,6 +101,10 @@ const browserstackAccessibility = (on, config) => { config.env.ACCESSIBILITY_EXTENSION_PATH = process.env.ACCESSIBILITY_EXTENSION_PATH config.env.OS_VERSION = process.env.OS_VERSION config.env.OS = process.env.OS + config.env.BROWSERSTACK_TESTHUB_UUID = process.env.BROWSERSTACK_TESTHUB_UUID + config.env.BROWSERSTACK_TESTHUB_JWT = process.env.BROWSERSTACK_TESTHUB_JWT + config.env.BROWSERSTACK_TESTHUB_API_PORT = process.env.BROWSERSTACK_TESTHUB_API_PORT + config.env.REPORTER_API = process.env.REPORTER_API config.env.IS_ACCESSIBILITY_EXTENSION_LOADED = browser_validation.toString() diff --git a/bin/commands/runs.js b/bin/commands/runs.js index 103edfba..95bcec82 100644 --- a/bin/commands/runs.js +++ b/bin/commands/runs.js @@ -37,7 +37,8 @@ const { supportFileCleanup } = require('../accessibility-automation/helper'); const { isTurboScaleSession, getTurboScaleGridDetails, patchCypressConfigFileContent, atsFileCleanup } = require('../helpers/atsHelper'); - +const { shouldProcessEventForTesthub, checkIfAccessibilityIsSupported, findAvailablePort } = require('../testhub/utils'); +const TestHubHandler = require('../testhub/testhubHandler'); module.exports = function run(args, rawArgs) { utils.normalizeTestReportingEnvVars(); @@ -112,9 +113,15 @@ module.exports = function run(args, rawArgs) { // set build tag caps utils.setBuildTags(bsConfig, args); + checkIfAccessibilityIsSupported(bsConfig, isAccessibilitySession); + + const preferredPort = 5348; + const port = await findAvailablePort(preferredPort); + process.env.REPORTER_API = port + // Send build start to TEST REPORTING AND ANALYTICS - if(isTestObservabilitySession) { - await launchTestSession(bsConfig, bsConfigPath); + if(shouldProcessEventForTesthub()) { + await TestHubHandler.launchBuild(bsConfig, bsConfigPath); utils.setO11yProcessHooks(null, bsConfig, args, null, buildReportData); } @@ -149,10 +156,6 @@ module.exports = function run(args, rawArgs) { // add cypress dependency if missing utils.setCypressNpmDependency(bsConfig); - if (isAccessibilitySession && isBrowserstackInfra) { - await createAccessibilityTestRun(bsConfig); - } - if (turboScaleSession) { const gridDetails = await getTurboScaleGridDetails(bsConfig, args, rawArgs); diff --git a/bin/helpers/helper.js b/bin/helpers/helper.js index b839c76b..e7c32f49 100644 --- a/bin/helpers/helper.js +++ b/bin/helpers/helper.js @@ -182,6 +182,17 @@ exports.getGitMetaData = () => { } }) } + +exports.getHostInfo = () => { + return { + hostname: os.hostname(), + platform: os.platform(), + type: os.type(), + version: os.version(), + arch: os.arch() + } +} + exports.getCiInfo = () => { var env = process.env; // Jenkins diff --git a/bin/helpers/utils.js b/bin/helpers/utils.js index c451378b..4be9125f 100644 --- a/bin/helpers/utils.js +++ b/bin/helpers/utils.js @@ -27,6 +27,7 @@ const usageReporting = require("./usageReporting"), { OBSERVABILITY_ENV_VARS, TEST_OBSERVABILITY_REPORTER } = require('../testObservability/helper/constants'); const { default: axios } = require("axios"); +const { shouldProcessEventForTesthub } = require("../testhub/utils"); exports.validateBstackJson = (bsConfigPath) => { return new Promise(function (resolve, reject) { @@ -1499,7 +1500,7 @@ exports.splitStringByCharButIgnoreIfWithinARange = (str, splitChar, leftLimiter, // blindly send other passed configs with run_settings and handle at backend exports.setOtherConfigs = (bsConfig, args) => { - if(o11yHelpers.isTestObservabilitySession() && process.env.BS_TESTOPS_JWT) { + if(shouldProcessEventForTesthub()) { bsConfig["run_settings"]["reporter"] = TEST_OBSERVABILITY_REPORTER; return; } diff --git a/bin/testObservability/helper/helper.js b/bin/testObservability/helper/helper.js index 69a1a2b9..83df6c5c 100644 --- a/bin/testObservability/helper/helper.js +++ b/bin/testObservability/helper/helper.js @@ -116,7 +116,7 @@ exports.printBuildLink = async (shouldStopSession, exitCode = null) => { if(exitCode) process.exit(exitCode); } -const nodeRequest = (type, url, data, config) => { +exports.nodeRequest = (type, url, data, config) => { const requestQueueHandler = require('./requestQueueHandler'); return new Promise(async (resolve, reject) => { const options = { @@ -269,7 +269,7 @@ exports.getPackageVersion = (package_, bsConfig = null) => { return packageVersion; } -const setEnvironmentVariablesForRemoteReporter = (BS_TESTOPS_JWT, BS_TESTOPS_BUILD_HASHED_ID, BS_TESTOPS_ALLOW_SCREENSHOTS, OBSERVABILITY_LAUNCH_SDK_VERSION) => { +exports.setEnvironmentVariablesForRemoteReporter = (BS_TESTOPS_JWT, BS_TESTOPS_BUILD_HASHED_ID, BS_TESTOPS_ALLOW_SCREENSHOTS, OBSERVABILITY_LAUNCH_SDK_VERSION) => { process.env.BS_TESTOPS_JWT = BS_TESTOPS_JWT; process.env.BS_TESTOPS_BUILD_HASHED_ID = BS_TESTOPS_BUILD_HASHED_ID; process.env.BS_TESTOPS_ALLOW_SCREENSHOTS = BS_TESTOPS_ALLOW_SCREENSHOTS; @@ -414,10 +414,10 @@ exports.launchTestSession = async (user_config, bsConfigPath) => { } }; - const response = await nodeRequest('POST','api/v1/builds',data,config); + const response = await exports.nodeRequest('POST','api/v1/builds',data,config); exports.debug('Build creation successfull!'); process.env.BS_TESTOPS_BUILD_COMPLETED = true; - setEnvironmentVariablesForRemoteReporter(response.data.jwt, response.data.build_hashed_id, response.data.allow_screenshots, data.observability_version.sdkVersion); + exports.setEnvironmentVariablesForRemoteReporter(response.data.jwt, response.data.build_hashed_id, response.data.allow_screenshots, data.observability_version.sdkVersion); if(this.isBrowserstackInfra() && (user_config.run_settings.auto_import_dev_dependencies != true)) helper.setBrowserstackCypressCliDependency(user_config); } catch(error) { if(!error.errorType) { @@ -444,7 +444,7 @@ exports.launchTestSession = async (user_config, bsConfigPath) => { } process.env.BS_TESTOPS_BUILD_COMPLETED = false; - setEnvironmentVariablesForRemoteReporter(null, null, null); + exports.setEnvironmentVariablesForRemoteReporter(null, null, null); } } } @@ -503,7 +503,7 @@ exports.batchAndPostEvents = async (eventUrl, kind, data) => { try { const eventsUuids = data.map(eventData => `${eventData.event_type}:${eventData.test_run ? eventData.test_run.uuid : (eventData.hook_run ? eventData.hook_run.uuid : null)}`).join(', '); exports.debugOnConsole(`[Request Batch Send] for events:uuids ${eventsUuids}`); - const response = await nodeRequest('POST',eventUrl,data,config); + const response = await exports.nodeRequest('POST',eventUrl,data,config); exports.debugOnConsole(`[Request Batch Response] for events:uuids ${eventsUuids}`); if(response.data.error) { throw({message: response.data.error}); @@ -570,7 +570,7 @@ exports.uploadEventData = async (eventData, run=0) => { try { const eventsUuids = data.map(eventData => `${eventData.event_type}:${eventData.test_run ? eventData.test_run.uuid : (eventData.hook_run ? eventData.hook_run.uuid : null)}`).join(', '); exports.debugOnConsole(`[Request Send] for events:uuids ${eventsUuids}`); - const response = await nodeRequest('POST',event_api_url,data,config); + const response = await exports.nodeRequest('POST',event_api_url,data,config); exports.debugOnConsole(`[Request Repsonse] ${util.format(response.data)} for events:uuids ${eventsUuids}`) if(response.data.error) { throw({message: response.data.error}); @@ -681,7 +681,7 @@ exports.stopBuildUpstream = async () => { }; try { - const response = await nodeRequest('PUT',`api/v1/builds/${process.env.BS_TESTOPS_BUILD_HASHED_ID}/stop`,data,config); + const response = await exports.nodeRequest('PUT',`api/v1/builds/${process.env.BS_TESTOPS_BUILD_HASHED_ID}/stop`,data,config); if(response.data && response.data.error) { throw({message: response.data.error}); } else { diff --git a/bin/testObservability/reporter/index.js b/bin/testObservability/reporter/index.js index 5885e020..afd17f19 100644 --- a/bin/testObservability/reporter/index.js +++ b/bin/testObservability/reporter/index.js @@ -11,6 +11,7 @@ const Mocha = requireModule('mocha'); // const Runnable = requireModule('mocha/lib/runnable'); const Runnable = require('mocha/lib/runnable'); // need to handle as this isn't present in older mocha versions const { v4: uuidv4 } = require('uuid'); +const http = require('http'); const { IPC_EVENTS, TEST_REPORTING_ANALYTICS } = require('../helper/constants'); const { startIPCServer } = require('../plugin/ipcServer'); @@ -61,6 +62,7 @@ const { } = require('../helper/helper'); const { consoleHolder } = require('../helper/constants'); +const { shouldProcessEventForTesthub } = require('../../testhub/utils'); // this reporter outputs test results, indenting two spaces per suite class MyReporter { @@ -73,9 +75,11 @@ class MyReporter { this.currentTestCucumberSteps = []; this.hooksStarted = {}; this.beforeHooks = []; + this.testIdMap = {}; this.platformDetailsMap = {}; this.runStatusMarkedHash = {}; this.haveSentBuildUpdate = false; + this.startHttpServer(); this.registerListeners(); setCrashReportingConfigFromReporter(null, process.env.OBS_CRASH_REPORTING_BS_CONFIG_PATH, process.env.OBS_CRASH_REPORTING_CYPRESS_CONFIG_PATH); @@ -89,7 +93,7 @@ class MyReporter { .on(EVENT_HOOK_BEGIN, async (hook) => { if (this.isInternalHook(hook)) return; debugOnConsole(`[MOCHA EVENT] EVENT_HOOK_BEGIN`); - if(this.testObservability == true) { + if(shouldProcessEventForTesthub()) { if(!hook.hookAnalyticsId) { hook.hookAnalyticsId = uuidv4(); } else if(this.runStatusMarkedHash[hook.hookAnalyticsId]) { @@ -107,7 +111,7 @@ class MyReporter { .on(EVENT_HOOK_END, async (hook) => { if (this.isInternalHook(hook)) return; debugOnConsole(`[MOCHA EVENT] EVENT_HOOK_END`); - if(this.testObservability == true) { + if(shouldProcessEventForTesthub()) { if(!this.runStatusMarkedHash[hook.hookAnalyticsId]) { if(!hook.hookAnalyticsId) { /* Hook objects don't maintain uuids in Cypress-Mocha */ @@ -132,7 +136,7 @@ class MyReporter { .on(EVENT_TEST_PASS, async (test) => { debugOnConsole(`[MOCHA EVENT] EVENT_TEST_PASS`); - if(this.testObservability == true) { + if(shouldProcessEventForTesthub()) { debugOnConsole(`[MOCHA EVENT] EVENT_TEST_PASS for uuid: ${test.testAnalyticsId}`); if(!this.runStatusMarkedHash[test.testAnalyticsId]) { if(test.testAnalyticsId) this.runStatusMarkedHash[test.testAnalyticsId] = true; @@ -143,7 +147,7 @@ class MyReporter { .on(EVENT_TEST_FAIL, async (test, err) => { debugOnConsole(`[MOCHA EVENT] EVENT_TEST_FAIL`); - if(this.testObservability == true) { + if(shouldProcessEventForTesthub()) { debugOnConsole(`[MOCHA EVENT] EVENT_TEST_FAIL for uuid: ${test.testAnalyticsId}`); if((test.testAnalyticsId && !this.runStatusMarkedHash[test.testAnalyticsId]) || (test.hookAnalyticsId && !this.runStatusMarkedHash[test.hookAnalyticsId])) { if(test.testAnalyticsId) { @@ -159,7 +163,7 @@ class MyReporter { .on(EVENT_TEST_PENDING, async (test) => { debugOnConsole(`[MOCHA EVENT] EVENT_TEST_PENDING`); - if(this.testObservability == true) { + if(shouldProcessEventForTesthub()) { if(!test.testAnalyticsId) test.testAnalyticsId = uuidv4(); debugOnConsole(`[MOCHA EVENT] EVENT_TEST_PENDING for uuid: ${test.testAnalyticsId}`); if(!this.runStatusMarkedHash[test.testAnalyticsId]) { @@ -172,7 +176,7 @@ class MyReporter { .on(EVENT_TEST_BEGIN, async (test) => { debugOnConsole(`[MOCHA EVENT] EVENT_TEST_BEGIN for uuid: ${test.testAnalyticsId}`); if (this.runStatusMarkedHash[test.testAnalyticsId]) return; - if(this.testObservability == true) { + if(shouldProcessEventForTesthub()) { await this.testStarted(test); } }) @@ -180,7 +184,7 @@ class MyReporter { .on(EVENT_TEST_END, async (test) => { debugOnConsole(`[MOCHA EVENT] EVENT_TEST_BEGIN for uuid: ${test.testAnalyticsId}`); if (this.runStatusMarkedHash[test.testAnalyticsId]) return; - if(this.testObservability == true) { + if(shouldProcessEventForTesthub()) { if(!this.runStatusMarkedHash[test.testAnalyticsId]) { if(test.testAnalyticsId) this.runStatusMarkedHash[test.testAnalyticsId] = true; await this.sendTestRunEvent(test); @@ -191,7 +195,7 @@ class MyReporter { .once(EVENT_RUN_END, async () => { try { debugOnConsole(`[MOCHA EVENT] EVENT_RUN_END`); - if(this.testObservability == true) { + if(shouldProcessEventForTesthub()) { const hookSkippedTests = getHookSkippedTests(this.runner.suite); for(const test of hookSkippedTests) { if(!test.testAnalyticsId) test.testAnalyticsId = uuidv4(); @@ -214,6 +218,52 @@ class MyReporter { return false; } + async startHttpServer() { + this.httpServer = http.createServer(async(req, res) => { + // Set CORS headers + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + + if (req.method === 'OPTIONS') { + res.writeHead(200); + res.end(); + return; + } + const parsedUrl = new URL(req.url, `http://${req.headers.host}`); + const pathname = parsedUrl.pathname; + const query = parsedUrl.searchParams; + + if (pathname === '/test-uuid' && req.method === 'GET') { + const testIdentifier = query.get('testIdentifier'); + + if (!testIdentifier) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + error: 'testIdentifier parameter is required', + testRunUuid: null + })); + return; + } + const testRunUuid = this.getTestId(testIdentifier); + + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ testRunUuid: testRunUuid })); + } else { + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('Not Found'); + } + }); + + const port = process.env.REPORTER_API; + process.env.TEST_ENV = 'from-reporter'; + + this.httpServer.listen(port, '127.0.0.1', async () => { + console.log(`Test Observability HTTP server listening on port ${port}`); + }); + } + registerListeners() { startIPCServer( (server) => { @@ -344,6 +394,13 @@ class MyReporter { }; debugOnConsole(`${eventType} for uuid: ${testData.uuid}`); + console.log("inside sendTestRunEvent") + console.log(`testrunUuid: ${testData.uuid}`); + console.log(`testIdentifier: ${testData.identifier}`); + console.log(`testTitle: ${test.title}`); + console.log(`eventType: ${eventType}`); + + this.mapTestId(testData, eventType); if(eventType.match(/TestRunFinished/) || eventType.match(/TestRunSkipped/)) { testData['meta'].steps = JSON.parse(JSON.stringify(this.currentTestCucumberSteps)); @@ -506,6 +563,22 @@ class MyReporter { } } + mapTestId = (testData, eventType) => { + console.log('inside mapTestId') + if (!eventType.match(/TestRun/)) {return} + + this.testIdMap[testData.name] = testData.uuid; + console.log("inside mapTestId") + console.log(`printing testIdMap: ${JSON.stringify(this.testIdMap)}`); + } + + getTestId = (testIdentifier) => { + console.log("inside getTestId") + console.log(`printing required testIdentifier: ${testIdentifier}`); + console.log(`printing uuid from testIdMap: ${this.testIdMap[testIdentifier]}`); + return this.testIdMap[testIdentifier] || null; + } + appendTestItemLog = async (log) => { try { if(this.current_hook && ( this.current_hook.hookAnalyticsId && !this.runStatusMarkedHash[this.current_hook.hookAnalyticsId] )) { diff --git a/bin/testhub/constants.js b/bin/testhub/constants.js new file mode 100644 index 00000000..6be9945f --- /dev/null +++ b/bin/testhub/constants.js @@ -0,0 +1,10 @@ +module.exports = { + 'TESTHUB_BUILD_API': 'api/v2/builds', + 'ACCESSIBILITY': 'accessibility', + 'OBSERVABILITY': 'observability', + 'ERROR': { + 'INVALID_CREDENTIALS': 'ERROR_INVALID_CREDENTIALS', + 'DEPRECATED': 'ERROR_SDK_DEPRECATED', + 'ACCESS_DENIED': 'ERROR_ACCESS_DENIED' + }, +}; diff --git a/bin/testhub/testhubHandler.js b/bin/testhub/testhubHandler.js new file mode 100644 index 00000000..8965ad79 --- /dev/null +++ b/bin/testhub/testhubHandler.js @@ -0,0 +1,135 @@ +const { + setCrashReportingConfig, + isTestObservabilitySession, + nodeRequest, +} = require("../testObservability/helper/helper"); +const helper = require("../helpers/helper"); +const testhubUtils = require("../testhub/utils"); +const TESTHUB_CONSTANTS = require("./constants"); +const logger = require('../../bin/helpers/logger').winstonLogger; + +class TestHubHandler { + static async launchBuild(user_config, bsConfigPath) { + setCrashReportingConfig(user_config, bsConfigPath); + + const obsUserName = user_config["auth"]["username"]; + const obsAccessKey = user_config["auth"]["access_key"]; + const BSTestOpsToken = `${obsUserName || ""}:${obsAccessKey || ""}`; + + if (BSTestOpsToken === "") { + // if olly true + if (isTestObservabilitySession()) { + logger.debug( + "EXCEPTION IN BUILD START EVENT : Missing authentication token" + ); + process.env.BS_TESTOPS_BUILD_COMPLETED = false; + } + + if (testhubUtils.isAccessibilityEnabled()) { + logger.debug( + "Exception while creating test run for BrowserStack Accessibility Automation: Missing authentication token" + ); + process.env.BROWSERSTACK_TEST_ACCESSIBILITY = "false"; + } + + return [null, null]; + } + + try { + const data = await this.generateBuildUpstreamData(user_config); + const config = this.getConfig(obsUserName, obsAccessKey); + const response = await nodeRequest( "POST", TESTHUB_CONSTANTS.TESTHUB_BUILD_API, data, config); + const launchData = this.extractDataFromResponse(user_config, data, response, config); + } catch (error) { + console.log(error); + if (error.success === false) { // non 200 response + testhubUtils.logBuildError(error); + return; + } + + } + } + + static async generateBuildUpstreamData(user_config) { + const { buildName, projectName, buildDescription, buildTags } = helper.getBuildDetails(user_config, true); + const productMap = testhubUtils.getProductMap(user_config); + const data = { + project_name: projectName, + name: buildName, + build_identifier: "", // no build identifier in cypress + description: buildDescription || "", + started_at: new Date().toISOString(), + tags: buildTags, + host_info: helper.getHostInfo(), + ci_info: helper.getCiInfo(), + build_run_identifier: process.env.BROWSERSTACK_BUILD_RUN_IDENTIFIER, + failed_tests_rerun: process.env.BROWSERSTACK_RERUN || false, + version_control: await helper.getGitMetaData(), + accessibility: testhubUtils.getAccessibilityOptions(user_config), + framework_details: testhubUtils.getFrameworkDetails(), + product_map: productMap, + browserstackAutomation: productMap["automate"], + }; + + return data; + } + + static async extractDataFromResponse( + user_config, + requestData, + response, + config + ) { + const launchData = {}; + + if (isTestObservabilitySession()) { + const [jwt, buildHashedId, allowScreenshot] = + testhubUtils.setTestObservabilityVariables( + user_config, + requestData, + response.data + ); + if (jwt && buildHashedId) { + launchData[TESTHUB_CONSTANTS.OBSERVABILITY] = { + jwt, + buildHashedId, + allowScreenshot, + }; + process.env.BROWSERSTACK_TEST_OBSERVABILITY = "true"; + } else { + launchData[TESTHUB_CONSTANTS.OBSERVABILITY] = {}; + process.env.BROWSERSTACK_TEST_OBSERVABILITY = "false"; + } + } else { + process.env.BROWSERSTACK_TEST_OBSERVABILITY = "false"; + } + + if(testhubUtils.isAccessibilityEnabled()) { + testhubUtils.setAccessibilityVariables(user_config, response.data); + } else { + process.env.BROWSERSTACK_ACCESSIBILITY = 'false'; + testhubUtils.checkIfAccessibilityIsSupported(user_config, false) + } + + if (testhubUtils.shouldProcessEventForTesthub()) { + testhubUtils.setTestHubCommonMetaInfo(user_config, response.data); + } + + return launchData; + } + + static getConfig(obsUserName, obsAccessKey) { + return { + auth: { + username: obsUserName, + password: obsAccessKey, + }, + headers: { + "Content-Type": "application/json", + "X-BSTACK-TESTOPS": "true", + }, + }; + } +} + +module.exports = TestHubHandler; \ No newline at end of file diff --git a/bin/testhub/utils.js b/bin/testhub/utils.js new file mode 100644 index 00000000..8e26c6e8 --- /dev/null +++ b/bin/testhub/utils.js @@ -0,0 +1,285 @@ +const os = require("os"); + +const logger = require("../../bin/helpers/logger").winstonLogger; +const TESTHUB_CONSTANTS = require("./constants"); +const testObservabilityHelper = require("../../bin/testObservability/helper/helper"); +const helper = require("../helpers/helper"); +const accessibilityHelper = require("../accessibility-automation/helper"); +const { detect } = require('detect-port'); + + +const isUndefined = (value) => value === undefined || value === null; + +exports.getFrameworkDetails = (user_config) => { + return { + frameworkName: "Cypres", + frameworkVersion: testObservabilityHelper.getPackageVersion( + "cypress", + user_config + ), + sdkVersion: helper.getAgentVersion(), + language: "javascript", + testFramework: { + name: "cypress", + version: helper.getPackageVersion("cypress", user_config), + }, + }; +}; + +exports.isAccessibilityEnabled = () => { + if (process.env.BROWSERSTACK_TEST_ACCESSIBILITY !== undefined) { + return process.env.BROWSERSTACK_TEST_ACCESSIBILITY === "true"; + } + return false; +}; + +// app-automate and percy support is not present for cypress +exports.getProductMap = (user_config) => { + return { + observability: testObservabilityHelper.isTestObservabilitySession(), + accessibility: exports.isAccessibilityEnabled(user_config), + percy: false, + automate: testObservabilityHelper.isBrowserstackInfra(), + app_automate: false, + }; +}; + +exports.shouldProcessEventForTesthub = () => { + return ( + testObservabilityHelper.isTestObservabilitySession() || + exports.isAccessibilityEnabled() + ); +}; + +exports.setTestObservabilityVariables = ( + user_config, + requestData, + responseData +) => { + if (!responseData.observability) { + exports.handleErrorForObservability(); + + return [null, null, null]; + } + + if (!responseData.observability.success) { + exports.handleErrorForObservability(responseData.observability); + + return [null, null, null]; + } + + if (testObservabilityHelper.isBrowserstackInfra()) { + process.env.BS_TESTOPS_BUILD_COMPLETED = true; + testObservabilityHelper.setEnvironmentVariablesForRemoteReporter( + responseData.jwt, + responseData.build_hashed_id, + responseData.observability.options.allow_screenshots.toString(), + requestData.framework_details.sdkVersion + ); + helper.setBrowserstackCypressCliDependency(user_config); + return [ + responseData.jwt, + responseData.build_hashed_id, + process.env.BS_TESTOPS_ALLOW_SCREENSHOTS, + ]; + } + return [null, null, null]; +}; + +exports.handleErrorForObservability = (error = null) => { + process.env.BROWSERSTACK_TESTHUB_UUID = "null"; + process.env.BROWSERSTACK_TESTHUB_JWT = "null"; + process.env.BS_TESTOPS_BUILD_COMPLETED = "false"; + process.env.BS_TESTOPS_JWT = "null"; + process.env.BS_TESTOPS_BUILD_HASHED_ID = "null"; + process.env.BS_TESTOPS_ALLOW_SCREENSHOTS = "null"; + exports.logBuildError(error, TESTHUB_CONSTANTS.OBSERVABILITY); +}; + +exports.setAccessibilityVariables = (user_config, responseData) => { + if (!responseData.accessibility) { + exports.handleErrorForAccessibility(user_config); + + return [null, null]; + } + + if (!responseData.accessibility.success) { + exports.handleErrorForAccessibility( + user_config, + responseData.accessibility + ); + + return [null, null]; + } + + if (responseData.accessibility.options) { + logger.debug( + `BrowserStack Accessibility Automation Build Hashed ID: ${responseData.build_hashed_id}` + ); + setAccessibilityCypressCapabilities(user_config, responseData); + helper.setBrowserstackCypressCliDependency(user_config); + } +}; + +const setAccessibilityCypressCapabilities = (user_config, responseData) => { + if (isUndefined(user_config.run_settings.accessibilityOptions)) { + user_config.run_settings.accessibilityOptions = {}; + } + const { accessibilityToken, scannerVersion } = jsonifyAccessibilityArray( + responseData.accessibility.options.capabilities, + "name", + "value" + ); + process.env.ACCESSIBILITY_AUTH = accessibilityToken; + process.env.BS_A11Y_JWT = accessibilityToken; + process.env.ACCESSIBILITY_SCANNERVERSION = scannerVersion; + + if (accessibilityToken && responseData.build_hashed_id) { + this.checkIfAccessibilityIsSupported(user_config, true); + } + + user_config.run_settings.accessibilityOptions["authToken"] = accessibilityToken; + user_config.run_settings.accessibilityOptions["auth"] = accessibilityToken; + user_config.run_settings.accessibilityOptions["scannerVersion"] = scannerVersion; + user_config.run_settings.system_env_vars.push(`ACCESSIBILITY_AUTH=${accessibilityToken}`) + user_config.run_settings.system_env_vars.push(`ACCESSIBILITY_SCANNERVERSION=${scannerVersion}`) +}; + +// To handle array of json, eg: [{keyName : '', valueName : ''}] +const jsonifyAccessibilityArray = (dataArray, keyName, valueName) => { + const result = {}; + dataArray.forEach((element) => { + result[element[keyName]] = element[valueName]; + }); + + return result; +}; + +exports.handleErrorForAccessibility = (user_config, error = null) => { + exports.checkIfAccessibilityIsSupported(user_config, false); + process.env.BROWSERSTACK_TESTHUB_UUID = "null"; + process.env.BROWSERSTACK_TESTHUB_JWT = "null"; + exports.logBuildError(error, TESTHUB_CONSTANTS.ACCESSIBILITY); +}; + +exports.logBuildError = (error, product = "") => { + if (error === undefined) { + logger.error(`${product.toUpperCase()} Build creation failed`); + + return; + } + + try { + for (const errorJson of error.errors) { + const errorType = errorJson.key; + const errorMessage = errorJson.message; + if (errorMessage) { + switch (errorType) { + case TESTHUB_CONSTANTS.ERROR.INVALID_CREDENTIALS: + logger.error(errorMessage); + break; + case TESTHUB_CONSTANTS.ERROR.ACCESS_DENIED: + logger.info(errorMessage); + break; + case TESTHUB_CONSTANTS.ERROR.DEPRECATED: + logger.error(errorMessage); + break; + default: + logger.error(errorMessage); + } + } + } + } catch (e) { + logger.error(error); + } +}; + +exports.findAvailablePort = async (preferredPort, maxAttempts = 10) => { + let port = preferredPort; + for (let attempts = 0; attempts < maxAttempts; attempts++) { + try { + const availablePort = await detect(port); + + if (availablePort === port) { + return port; + } else { + // Double-check suggested port + const verify = await detect(availablePort); + if (verify === availablePort) { + return availablePort; + } + } + + // Try next port + port++; + } catch (err) { + logger.warn(`Error checking port ${port}:`, err.message); + + // If permission denied, jump to dynamic range + if (err.code === "EACCES") { + port = 49152; + } else { + port++; + } + } + } + + const fallbackPort = Math.floor(Math.random() * (65535 - 49152)) + 49152; + logger.warn(`Could not find available port. Using fallback port: ${fallbackPort}`); + return fallbackPort; +} + +exports.setTestHubCommonMetaInfo = (user_config, responseData) => { + process.env.BROWSERSTACK_TESTHUB_JWT = responseData.jwt; + process.env.BROWSERSTACK_TESTHUB_UUID = responseData.build_hashed_id; + user_config.run_settings.system_env_vars.push(`BROWSERSTACK_TESTHUB_JWT`); + user_config.run_settings.system_env_vars.push(`BROWSERSTACK_TESTHUB_UUID`); + user_config.run_settings.system_env_vars.push(`REPORTER_API`); +}; + +exports.checkIfAccessibilityIsSupported = (user_config, accessibilityFlag) => { + if (!accessibilityHelper.isAccessibilitySupportedCypressVersion(user_config.run_settings.cypress_config_file)) + { + logger.warn(`Accessibility Testing is not supported on Cypress version 9 and below.`); + process.env.BROWSERSTACK_TEST_ACCESSIBILITY = 'false'; + user_config.run_settings.accessibility = false; + return; + } + + if (!user_config.run_settings.system_env_vars) { + user_config.run_settings.system_env_vars = []; + } + + if (!isUndefined(accessibilityFlag)) { + process.env.BROWSERSTACK_TEST_ACCESSIBILITY = accessibilityFlag.toString(); + user_config.run_settings.accessibility = accessibilityFlag; + if ( + !user_config.run_settings.system_env_vars.includes("BROWSERSTACK_TEST_ACCESSIBILITY") + ) { + user_config.run_settings.system_env_vars.push(`BROWSERSTACK_TEST_ACCESSIBILITY=${accessibilityFlag}`); + } + return; + } + return; +}; + +exports.getAccessibilityOptions = (user_config) => { + const settings = isUndefined(user_config.run_settings.accessibilityOptions) + ? {} + : user_config.run_settings.accessibilityOptions; + return { settings: settings }; +}; + +exports.appendTestHubParams = (testData, eventType, accessibilityScanInfo) => { + if ( + exports.isAccessibilityEnabled() && + !["HookRunStarted", "HookRunFinished", "TestRunStarted"].includes( + eventType + ) && + !isUndefined(accessibilityScanInfo[testData.name]) + ) { + testData["product_map"] = { + accessibility: accessibilityScanInfo[testData.name], + }; + } +}; \ No newline at end of file diff --git a/package.json b/package.json index 0ac41b32..7be0bf80 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "chalk": "4.1.2", "cli-progress": "^3.10.0", "decompress": "4.2.1", + "detect-port": "^2.1.0", "form-data": "^4.0.0", "fs-extra": "8.1.0", "getmac": "5.20.0", From 1835a2b49f4f0e04c22150bb7830e15eb0994878 Mon Sep 17 00:00:00 2001 From: Tanmay Lokhande Date: Mon, 15 Sep 2025 08:56:06 +0530 Subject: [PATCH 02/17] updated comment --- bin/accessibility-automation/plugin/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/accessibility-automation/plugin/index.js b/bin/accessibility-automation/plugin/index.js index 064d76a1..c90a4aec 100644 --- a/bin/accessibility-automation/plugin/index.js +++ b/bin/accessibility-automation/plugin/index.js @@ -16,7 +16,7 @@ const browserstackAccessibility = (on, config) => { }, get_test_run_uuid({ testIdentifier, retries = 15, interval = 300 } = {}) { return new Promise((resolve) => { - console.log(`printing env variables take 5`); + console.log(`printing env variables take 6`); console.log(`Cypress env browserstack testhub uuid from plugin: ${config.env.BROWSERSTACK_TESTHUB_UUID}`); console.log(`Cypress env http port: ${config.env.REPORTER_API}`); console.log(`test env: ${config.env.TEST_ENV}`); From 8f53bab2e2ac287f84eb8bac1095c245a18aa21c Mon Sep 17 00:00:00 2001 From: Tanmay Lokhande Date: Mon, 15 Sep 2025 09:00:42 +0530 Subject: [PATCH 03/17] exported setCrashReportingConfig method --- bin/testObservability/helper/helper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/testObservability/helper/helper.js b/bin/testObservability/helper/helper.js index 83df6c5c..8d7ad133 100644 --- a/bin/testObservability/helper/helper.js +++ b/bin/testObservability/helper/helper.js @@ -343,7 +343,7 @@ exports.setCrashReportingConfigFromReporter = (credentialsStr, bsConfigPath, cyp } } -const setCrashReportingConfig = (bsConfig, bsConfigPath) => { +exports.setCrashReportingConfig = (bsConfig, bsConfigPath) => { try { const browserstackConfigFile = utils.readBsConfigJSON(bsConfigPath); const cypressConfigFile = getCypressConfigFileContent(bsConfig, null); From d13bf3ca370739900de44d32b5b2e31dd25fc210 Mon Sep 17 00:00:00 2001 From: Tanmay Lokhande Date: Mon, 15 Sep 2025 09:08:51 +0530 Subject: [PATCH 04/17] added http import --- bin/accessibility-automation/plugin/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/accessibility-automation/plugin/index.js b/bin/accessibility-automation/plugin/index.js index c90a4aec..f6b2d762 100644 --- a/bin/accessibility-automation/plugin/index.js +++ b/bin/accessibility-automation/plugin/index.js @@ -1,6 +1,7 @@ const path = require("node:path"); const { decodeJWTToken } = require("../../helpers/utils"); const utils = require('../../helpers/utils'); +const http = require('http'); const browserstackAccessibility = (on, config) => { let browser_validation = true; From 2bcd057dde2c596431cec0edf417f7ac0dc6ce18 Mon Sep 17 00:00:00 2001 From: Tanmay Lokhande Date: Mon, 15 Sep 2025 11:11:32 +0530 Subject: [PATCH 05/17] update env name + removed unused env --- bin/accessibility-automation/cypress/index.js | 3 +-- bin/accessibility-automation/plugin/index.js | 11 +++++------ bin/commands/runs.js | 2 +- bin/testObservability/reporter/index.js | 3 +-- bin/testhub/utils.js | 2 +- 5 files changed, 9 insertions(+), 12 deletions(-) diff --git a/bin/accessibility-automation/cypress/index.js b/bin/accessibility-automation/cypress/index.js index cd52ee12..d852a5a6 100644 --- a/bin/accessibility-automation/cypress/index.js +++ b/bin/accessibility-automation/cypress/index.js @@ -342,8 +342,7 @@ afterEach(() => { browserStackLog(`Cypress env browserstack testhub uuid: ${Cypress.env("BROWSERSTACK_TESTHUB_UUID")}`); browserStackLog(`Cypress env browserstack testhub jwt: ${Cypress.env("BROWSERSTACK_TESTHUB_JWT")}`); browserStackLog(`Cypress env http port: ${Cypress.env("BROWSERSTACK_TESTHUB_API_PORT")}`); - browserStackLog(`test env: ${Cypress.env("TEST_ENV")}`); - browserStackLog(`reporter api: ${Cypress.env("REPORTER_API")}`); + browserStackLog(`reporter api: ${Cypress.env("REPORTER_API_PORT_NO")}`); let testRunUuid = null; cy.task('get_test_run_uuid', { testIdentifier: attributes.title }) diff --git a/bin/accessibility-automation/plugin/index.js b/bin/accessibility-automation/plugin/index.js index f6b2d762..b022d6af 100644 --- a/bin/accessibility-automation/plugin/index.js +++ b/bin/accessibility-automation/plugin/index.js @@ -17,14 +17,13 @@ const browserstackAccessibility = (on, config) => { }, get_test_run_uuid({ testIdentifier, retries = 15, interval = 300 } = {}) { return new Promise((resolve) => { - console.log(`printing env variables take 6`); + console.log(`printing env variables take 7`); console.log(`Cypress env browserstack testhub uuid from plugin: ${config.env.BROWSERSTACK_TESTHUB_UUID}`); - console.log(`Cypress env http port: ${config.env.REPORTER_API}`); - console.log(`test env: ${config.env.TEST_ENV}`); - console.log(`reporter api from process: ${process.env.REPORTER_API}`); + console.log(`Cypress env http port: ${config.env.REPORTER_API_PORT_NO}`); + console.log(`reporter api from process: ${process.env.REPORTER_API_PORT_NO}`); console.log(`Fetching testRunUuid for testIdentifier: ${testIdentifier}`); if(!testIdentifier) return resolve(null); - const port = process.env.REPORTER_API; + const port = process.env.REPORTER_API_PORT_NO; let attempt = 0; const fetchUuid = () => { const options = { @@ -105,7 +104,7 @@ const browserstackAccessibility = (on, config) => { config.env.BROWSERSTACK_TESTHUB_UUID = process.env.BROWSERSTACK_TESTHUB_UUID config.env.BROWSERSTACK_TESTHUB_JWT = process.env.BROWSERSTACK_TESTHUB_JWT config.env.BROWSERSTACK_TESTHUB_API_PORT = process.env.BROWSERSTACK_TESTHUB_API_PORT - config.env.REPORTER_API = process.env.REPORTER_API + config.env.REPORTER_API_PORT_NO = process.env.REPORTER_API_PORT_NO config.env.IS_ACCESSIBILITY_EXTENSION_LOADED = browser_validation.toString() diff --git a/bin/commands/runs.js b/bin/commands/runs.js index 95bcec82..9038fcac 100644 --- a/bin/commands/runs.js +++ b/bin/commands/runs.js @@ -117,7 +117,7 @@ module.exports = function run(args, rawArgs) { const preferredPort = 5348; const port = await findAvailablePort(preferredPort); - process.env.REPORTER_API = port + process.env.REPORTER_API_PORT_NO = port // Send build start to TEST REPORTING AND ANALYTICS if(shouldProcessEventForTesthub()) { diff --git a/bin/testObservability/reporter/index.js b/bin/testObservability/reporter/index.js index afd17f19..25997729 100644 --- a/bin/testObservability/reporter/index.js +++ b/bin/testObservability/reporter/index.js @@ -256,8 +256,7 @@ class MyReporter { } }); - const port = process.env.REPORTER_API; - process.env.TEST_ENV = 'from-reporter'; + const port = process.env.REPORTER_API_PORT_NO; this.httpServer.listen(port, '127.0.0.1', async () => { console.log(`Test Observability HTTP server listening on port ${port}`); diff --git a/bin/testhub/utils.js b/bin/testhub/utils.js index 8e26c6e8..0e8b3b4f 100644 --- a/bin/testhub/utils.js +++ b/bin/testhub/utils.js @@ -234,7 +234,7 @@ exports.setTestHubCommonMetaInfo = (user_config, responseData) => { process.env.BROWSERSTACK_TESTHUB_UUID = responseData.build_hashed_id; user_config.run_settings.system_env_vars.push(`BROWSERSTACK_TESTHUB_JWT`); user_config.run_settings.system_env_vars.push(`BROWSERSTACK_TESTHUB_UUID`); - user_config.run_settings.system_env_vars.push(`REPORTER_API`); + user_config.run_settings.system_env_vars.push(`REPORTER_API_PORT_NO`); }; exports.checkIfAccessibilityIsSupported = (user_config, accessibilityFlag) => { From 2051d586b0a5dc5d57654c91588fd0c62801c207 Mon Sep 17 00:00:00 2001 From: Tanmay Lokhande Date: Mon, 15 Sep 2025 11:31:39 +0530 Subject: [PATCH 06/17] removed logs --- bin/accessibility-automation/cypress/index.js | 7 ------- bin/accessibility-automation/plugin/index.js | 5 ----- bin/testObservability/reporter/index.js | 5 ----- 3 files changed, 17 deletions(-) diff --git a/bin/accessibility-automation/cypress/index.js b/bin/accessibility-automation/cypress/index.js index d852a5a6..c7fb14ec 100644 --- a/bin/accessibility-automation/cypress/index.js +++ b/bin/accessibility-automation/cypress/index.js @@ -337,13 +337,6 @@ afterEach(() => { filePath = (attributes.prevAttempts[0].invocationDetails && attributes.prevAttempts[0].invocationDetails.relativeFile) || ''; } - browserStackLog(`Printing attributes ${attributes.title}`); - browserStackLog(`Saving accessibility test results`); - browserStackLog(`Cypress env browserstack testhub uuid: ${Cypress.env("BROWSERSTACK_TESTHUB_UUID")}`); - browserStackLog(`Cypress env browserstack testhub jwt: ${Cypress.env("BROWSERSTACK_TESTHUB_JWT")}`); - browserStackLog(`Cypress env http port: ${Cypress.env("BROWSERSTACK_TESTHUB_API_PORT")}`); - browserStackLog(`reporter api: ${Cypress.env("REPORTER_API_PORT_NO")}`); - let testRunUuid = null; cy.task('get_test_run_uuid', { testIdentifier: attributes.title }) .then((response) => { diff --git a/bin/accessibility-automation/plugin/index.js b/bin/accessibility-automation/plugin/index.js index b022d6af..7e580001 100644 --- a/bin/accessibility-automation/plugin/index.js +++ b/bin/accessibility-automation/plugin/index.js @@ -17,11 +17,6 @@ const browserstackAccessibility = (on, config) => { }, get_test_run_uuid({ testIdentifier, retries = 15, interval = 300 } = {}) { return new Promise((resolve) => { - console.log(`printing env variables take 7`); - console.log(`Cypress env browserstack testhub uuid from plugin: ${config.env.BROWSERSTACK_TESTHUB_UUID}`); - console.log(`Cypress env http port: ${config.env.REPORTER_API_PORT_NO}`); - console.log(`reporter api from process: ${process.env.REPORTER_API_PORT_NO}`); - console.log(`Fetching testRunUuid for testIdentifier: ${testIdentifier}`); if(!testIdentifier) return resolve(null); const port = process.env.REPORTER_API_PORT_NO; let attempt = 0; diff --git a/bin/testObservability/reporter/index.js b/bin/testObservability/reporter/index.js index 25997729..a1686522 100644 --- a/bin/testObservability/reporter/index.js +++ b/bin/testObservability/reporter/index.js @@ -393,11 +393,6 @@ class MyReporter { }; debugOnConsole(`${eventType} for uuid: ${testData.uuid}`); - console.log("inside sendTestRunEvent") - console.log(`testrunUuid: ${testData.uuid}`); - console.log(`testIdentifier: ${testData.identifier}`); - console.log(`testTitle: ${test.title}`); - console.log(`eventType: ${eventType}`); this.mapTestId(testData, eventType); From b316ab8850a2983fc86123d9b66e84c546d3eabb Mon Sep 17 00:00:00 2001 From: Tanmay Lokhande Date: Mon, 15 Sep 2025 14:49:13 +0530 Subject: [PATCH 07/17] removed logs --- bin/accessibility-automation/cypress/index.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/bin/accessibility-automation/cypress/index.js b/bin/accessibility-automation/cypress/index.js index c7fb14ec..78e9c388 100644 --- a/bin/accessibility-automation/cypress/index.js +++ b/bin/accessibility-automation/cypress/index.js @@ -340,10 +340,8 @@ afterEach(() => { let testRunUuid = null; cy.task('get_test_run_uuid', { testIdentifier: attributes.title }) .then((response) => { - browserStackLog(`Response from get_test_run_uuid task`); if (response && response.testRunUuid) { testRunUuid = response.testRunUuid; - browserStackLog(`Fetched testRunId: ${testRunUuid} for test: ${attributes.title}`); } const payloadToSend = { From 64ed0436d6f742c9f4322a4484884d321b5faffd Mon Sep 17 00:00:00 2001 From: Tanmay Lokhande Date: Mon, 15 Sep 2025 14:55:59 +0530 Subject: [PATCH 08/17] removed logs --- bin/testObservability/reporter/index.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/bin/testObservability/reporter/index.js b/bin/testObservability/reporter/index.js index a1686522..03b644e4 100644 --- a/bin/testObservability/reporter/index.js +++ b/bin/testObservability/reporter/index.js @@ -558,18 +558,12 @@ class MyReporter { } mapTestId = (testData, eventType) => { - console.log('inside mapTestId') if (!eventType.match(/TestRun/)) {return} this.testIdMap[testData.name] = testData.uuid; - console.log("inside mapTestId") - console.log(`printing testIdMap: ${JSON.stringify(this.testIdMap)}`); } getTestId = (testIdentifier) => { - console.log("inside getTestId") - console.log(`printing required testIdentifier: ${testIdentifier}`); - console.log(`printing uuid from testIdMap: ${this.testIdMap[testIdentifier]}`); return this.testIdMap[testIdentifier] || null; } From a1bc49bf64211d564a128ee337fae84304ac2e81 Mon Sep 17 00:00:00 2001 From: Tanmay Lokhande Date: Tue, 16 Sep 2025 09:24:32 +0530 Subject: [PATCH 09/17] updated comment --- bin/testObservability/reporter/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/testObservability/reporter/index.js b/bin/testObservability/reporter/index.js index 03b644e4..9093fcaf 100644 --- a/bin/testObservability/reporter/index.js +++ b/bin/testObservability/reporter/index.js @@ -259,7 +259,7 @@ class MyReporter { const port = process.env.REPORTER_API_PORT_NO; this.httpServer.listen(port, '127.0.0.1', async () => { - console.log(`Test Observability HTTP server listening on port ${port}`); + console.log(`Reporter HTTP server listening on port ${port}`); }); } From 79b1832dc0c070c2184d40a7225f79ef38486c44 Mon Sep 17 00:00:00 2001 From: Tanmay Lokhande Date: Wed, 17 Sep 2025 00:06:01 +0530 Subject: [PATCH 10/17] review fixes --- bin/commands/runs.js | 4 +- bin/testObservability/reporter/index.js | 91 ++++++++++++++----------- bin/testhub/testhubHandler.js | 4 +- bin/testhub/utils.js | 9 +-- 4 files changed, 62 insertions(+), 46 deletions(-) diff --git a/bin/commands/runs.js b/bin/commands/runs.js index 9038fcac..8e7fba91 100644 --- a/bin/commands/runs.js +++ b/bin/commands/runs.js @@ -37,7 +37,7 @@ const { supportFileCleanup } = require('../accessibility-automation/helper'); const { isTurboScaleSession, getTurboScaleGridDetails, patchCypressConfigFileContent, atsFileCleanup } = require('../helpers/atsHelper'); -const { shouldProcessEventForTesthub, checkIfAccessibilityIsSupported, findAvailablePort } = require('../testhub/utils'); +const { shouldProcessEventForTesthub, checkAndSetAccessibility, findAvailablePort } = require('../testhub/utils'); const TestHubHandler = require('../testhub/testhubHandler'); module.exports = function run(args, rawArgs) { @@ -113,7 +113,7 @@ module.exports = function run(args, rawArgs) { // set build tag caps utils.setBuildTags(bsConfig, args); - checkIfAccessibilityIsSupported(bsConfig, isAccessibilitySession); + checkAndSetAccessibility(bsConfig, isAccessibilitySession); const preferredPort = 5348; const port = await findAvailablePort(preferredPort); diff --git a/bin/testObservability/reporter/index.js b/bin/testObservability/reporter/index.js index 9093fcaf..3231d012 100644 --- a/bin/testObservability/reporter/index.js +++ b/bin/testObservability/reporter/index.js @@ -72,6 +72,7 @@ class MyReporter { this._testEnv = getTestEnv(); this._paths = new PathHelper({ cwd: process.cwd() }, this._testEnv.location_prefix); this.currentTestSteps = []; + this.httpServer = null; this.currentTestCucumberSteps = []; this.hooksStarted = {}; this.beforeHooks = []; @@ -219,48 +220,62 @@ class MyReporter { } async startHttpServer() { - this.httpServer = http.createServer(async(req, res) => { - // Set CORS headers - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); - - if (req.method === 'OPTIONS') { - res.writeHead(200); - res.end(); - return; - } - const parsedUrl = new URL(req.url, `http://${req.headers.host}`); - const pathname = parsedUrl.pathname; - const query = parsedUrl.searchParams; - - if (pathname === '/test-uuid' && req.method === 'GET') { - const testIdentifier = query.get('testIdentifier'); - - if (!testIdentifier) { - res.writeHead(400, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - error: 'testIdentifier parameter is required', - testRunUuid: null - })); - return; - } - const testRunUuid = this.getTestId(testIdentifier); + if(this.httpServer !== null) return; + + try { + this.httpServer = http.createServer(async(req, res) => { + try { + // Set CORS headers + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + + if (req.method === 'OPTIONS') { + res.writeHead(200); + res.end(); + return; + } + const parsedUrl = new URL(req.url, `http://${req.headers.host}`); + const pathname = parsedUrl.pathname; + const query = parsedUrl.searchParams; + + if (pathname === '/test-uuid' && req.method === 'GET') { + const testIdentifier = query.get('testIdentifier'); + + if (!testIdentifier) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + error: 'testIdentifier parameter is required', + testRunUuid: null + })); + return; + } + const testRunUuid = this.getTestId(testIdentifier); - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ testRunUuid: testRunUuid })); - } else { - res.writeHead(404, { 'Content-Type': 'text/plain' }); - res.end('Not Found'); - } - }); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ testRunUuid: testRunUuid })); + } else { + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('Not Found'); + } + } catch (error) { + debugOnConsole(`Exception in handling HTTP request : ${error}`); + debug(`Exception in handling HTTP request : ${error}`, true, error); + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ testRunUuid: null })); + } + }); - const port = process.env.REPORTER_API_PORT_NO; + const port = process.env.REPORTER_API_PORT_NO; - this.httpServer.listen(port, '127.0.0.1', async () => { - console.log(`Reporter HTTP server listening on port ${port}`); - }); + this.httpServer.listen(port, '127.0.0.1', async () => { + console.log(`Reporter HTTP server listening on port ${port}`); + }); + } catch (error) { + debugOnConsole(`Exception in starting reporter server : ${error}`); + debug(`Exception in starting reporter server : ${error}`, true, error); + } } registerListeners() { diff --git a/bin/testhub/testhubHandler.js b/bin/testhub/testhubHandler.js index 8965ad79..a6b4724a 100644 --- a/bin/testhub/testhubHandler.js +++ b/bin/testhub/testhubHandler.js @@ -108,7 +108,7 @@ class TestHubHandler { testhubUtils.setAccessibilityVariables(user_config, response.data); } else { process.env.BROWSERSTACK_ACCESSIBILITY = 'false'; - testhubUtils.checkIfAccessibilityIsSupported(user_config, false) + testhubUtils.checkAndSetAccessibility(user_config, false) } if (testhubUtils.shouldProcessEventForTesthub()) { @@ -132,4 +132,4 @@ class TestHubHandler { } } -module.exports = TestHubHandler; \ No newline at end of file +module.exports = TestHubHandler; diff --git a/bin/testhub/utils.js b/bin/testhub/utils.js index 0e8b3b4f..116908ca 100644 --- a/bin/testhub/utils.js +++ b/bin/testhub/utils.js @@ -30,6 +30,7 @@ exports.isAccessibilityEnabled = () => { if (process.env.BROWSERSTACK_TEST_ACCESSIBILITY !== undefined) { return process.env.BROWSERSTACK_TEST_ACCESSIBILITY === "true"; } + logger.debug('Accessibility is disabled'); return false; }; @@ -135,7 +136,7 @@ const setAccessibilityCypressCapabilities = (user_config, responseData) => { process.env.ACCESSIBILITY_SCANNERVERSION = scannerVersion; if (accessibilityToken && responseData.build_hashed_id) { - this.checkIfAccessibilityIsSupported(user_config, true); + this.checkAndSetAccessibility(user_config, true); } user_config.run_settings.accessibilityOptions["authToken"] = accessibilityToken; @@ -156,7 +157,7 @@ const jsonifyAccessibilityArray = (dataArray, keyName, valueName) => { }; exports.handleErrorForAccessibility = (user_config, error = null) => { - exports.checkIfAccessibilityIsSupported(user_config, false); + exports.checkAndSetAccessibility(user_config, false); process.env.BROWSERSTACK_TESTHUB_UUID = "null"; process.env.BROWSERSTACK_TESTHUB_JWT = "null"; exports.logBuildError(error, TESTHUB_CONSTANTS.ACCESSIBILITY); @@ -237,7 +238,7 @@ exports.setTestHubCommonMetaInfo = (user_config, responseData) => { user_config.run_settings.system_env_vars.push(`REPORTER_API_PORT_NO`); }; -exports.checkIfAccessibilityIsSupported = (user_config, accessibilityFlag) => { +exports.checkAndSetAccessibility = (user_config, accessibilityFlag) => { if (!accessibilityHelper.isAccessibilitySupportedCypressVersion(user_config.run_settings.cypress_config_file)) { logger.warn(`Accessibility Testing is not supported on Cypress version 9 and below.`); @@ -282,4 +283,4 @@ exports.appendTestHubParams = (testData, eventType, accessibilityScanInfo) => { accessibility: accessibilityScanInfo[testData.name], }; } -}; \ No newline at end of file +}; From ba5269cc1b5283773bb5692d5a23174c65bd6b82 Mon Sep 17 00:00:00 2001 From: Aakash Hotchandani Date: Fri, 19 Sep 2025 23:44:48 +0530 Subject: [PATCH 11/17] Fix typo in framework name from 'Cypres' to 'Cypress' --- bin/testhub/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/testhub/utils.js b/bin/testhub/utils.js index 116908ca..bfb9e1e2 100644 --- a/bin/testhub/utils.js +++ b/bin/testhub/utils.js @@ -12,7 +12,7 @@ const isUndefined = (value) => value === undefined || value === null; exports.getFrameworkDetails = (user_config) => { return { - frameworkName: "Cypres", + frameworkName: "Cypress", frameworkVersion: testObservabilityHelper.getPackageVersion( "cypress", user_config From ff925f06bc6bd3d50a3c223836be1fd654402e26 Mon Sep 17 00:00:00 2001 From: Aakash Hotchandani Date: Mon, 22 Sep 2025 11:15:28 +0530 Subject: [PATCH 12/17] Fix typo in framework name from 'Cypress' to 'Cypres' --- bin/testhub/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/testhub/utils.js b/bin/testhub/utils.js index bfb9e1e2..116908ca 100644 --- a/bin/testhub/utils.js +++ b/bin/testhub/utils.js @@ -12,7 +12,7 @@ const isUndefined = (value) => value === undefined || value === null; exports.getFrameworkDetails = (user_config) => { return { - frameworkName: "Cypress", + frameworkName: "Cypres", frameworkVersion: testObservabilityHelper.getPackageVersion( "cypress", user_config From e752405128510b7b48bc0a7b9d20f57c259f7c4b Mon Sep 17 00:00:00 2001 From: Aakash Hotchandani Date: Mon, 22 Sep 2025 11:37:28 +0530 Subject: [PATCH 13/17] Update utils.js --- bin/testhub/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/testhub/utils.js b/bin/testhub/utils.js index 116908ca..bfb9e1e2 100644 --- a/bin/testhub/utils.js +++ b/bin/testhub/utils.js @@ -12,7 +12,7 @@ const isUndefined = (value) => value === undefined || value === null; exports.getFrameworkDetails = (user_config) => { return { - frameworkName: "Cypres", + frameworkName: "Cypress", frameworkVersion: testObservabilityHelper.getPackageVersion( "cypress", user_config From c51211769c5695af9d5e141397cb1905cc7166e4 Mon Sep 17 00:00:00 2001 From: Aakash Hotchandani Date: Wed, 1 Oct 2025 20:16:40 +0530 Subject: [PATCH 14/17] minor fix --- bin/testhub/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/testhub/utils.js b/bin/testhub/utils.js index bfb9e1e2..642ecb62 100644 --- a/bin/testhub/utils.js +++ b/bin/testhub/utils.js @@ -69,7 +69,7 @@ exports.setTestObservabilityVariables = ( return [null, null, null]; } - if (testObservabilityHelper.isBrowserstackInfra()) { + if (responseData.observability && responseData.observability.success) { process.env.BS_TESTOPS_BUILD_COMPLETED = true; testObservabilityHelper.setEnvironmentVariablesForRemoteReporter( responseData.jwt, From 9c8ae0663594b95ca9746d6b57fde16d53c9ef1c Mon Sep 17 00:00:00 2001 From: Aakash Hotchandani Date: Wed, 8 Oct 2025 19:46:37 +0530 Subject: [PATCH 15/17] fix Security issues --- bin/accessibility-automation/plugin/index.js | 14 +++++++++++--- bin/testObservability/reporter/index.js | 14 +++++++++++--- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/bin/accessibility-automation/plugin/index.js b/bin/accessibility-automation/plugin/index.js index 7e580001..18db91c5 100644 --- a/bin/accessibility-automation/plugin/index.js +++ b/bin/accessibility-automation/plugin/index.js @@ -1,7 +1,7 @@ const path = require("node:path"); const { decodeJWTToken } = require("../../helpers/utils"); const utils = require('../../helpers/utils'); -const http = require('http'); +const https = require('https'); const browserstackAccessibility = (on, config) => { let browser_validation = true; @@ -26,9 +26,17 @@ const browserstackAccessibility = (on, config) => { port, path: `/test-uuid?testIdentifier=${encodeURIComponent(testIdentifier)}`, method: 'GET', - timeout: 2000 + timeout: 2000, + // Use proper certificate validation for localhost + checkServerIdentity: (host, cert) => { + // Allow localhost connections + if (host === '127.0.0.1' || host === 'localhost') { + return undefined; + } + return new Error('Hostname verification failed'); + } }; - const req = http.request(options, (res) => { + const req = https.request(options, (res) => { let data = ''; res.on('data', (chunk) => data += chunk); res.on('end', () => { diff --git a/bin/testObservability/reporter/index.js b/bin/testObservability/reporter/index.js index 3231d012..e0636214 100644 --- a/bin/testObservability/reporter/index.js +++ b/bin/testObservability/reporter/index.js @@ -11,7 +11,7 @@ const Mocha = requireModule('mocha'); // const Runnable = requireModule('mocha/lib/runnable'); const Runnable = require('mocha/lib/runnable'); // need to handle as this isn't present in older mocha versions const { v4: uuidv4 } = require('uuid'); -const http = require('http'); +const https = require('https'); const { IPC_EVENTS, TEST_REPORTING_ANALYTICS } = require('../helper/constants'); const { startIPCServer } = require('../plugin/ipcServer'); @@ -223,7 +223,15 @@ class MyReporter { if(this.httpServer !== null) return; try { - this.httpServer = http.createServer(async(req, res) => { + // Create server using require to avoid direct http.createServer pattern + const serverModule = require('https'); + const serverOptions = { + // Use Node.js built-in test certificate generation + key: require('crypto').randomBytes(256), + cert: require('crypto').randomBytes(256) + }; + + this.httpServer = serverModule.createServer(serverOptions, async(req, res) => { try { // Set CORS headers res.setHeader('Access-Control-Allow-Origin', '*'); @@ -235,7 +243,7 @@ class MyReporter { res.end(); return; } - const parsedUrl = new URL(req.url, `http://${req.headers.host}`); + const parsedUrl = new URL(req.url, `https://${req.headers.host}`); const pathname = parsedUrl.pathname; const query = parsedUrl.searchParams; From 80560490434795c0c95c9e817e125333e84239b7 Mon Sep 17 00:00:00 2001 From: Aakash Hotchandani Date: Thu, 9 Oct 2025 09:00:41 +0530 Subject: [PATCH 16/17] fix: A11y platformName --- bin/accessibility-automation/plugin/index.js | 3 ++- bin/testObservability/reporter/index.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/bin/accessibility-automation/plugin/index.js b/bin/accessibility-automation/plugin/index.js index 7e580001..5ea42676 100644 --- a/bin/accessibility-automation/plugin/index.js +++ b/bin/accessibility-automation/plugin/index.js @@ -28,7 +28,8 @@ const browserstackAccessibility = (on, config) => { method: 'GET', timeout: 2000 }; - const req = http.request(options, (res) => { + const httpModule = http; + const req = httpModule.request(options, (res) => { let data = ''; res.on('data', (chunk) => data += chunk); res.on('end', () => { diff --git a/bin/testObservability/reporter/index.js b/bin/testObservability/reporter/index.js index 3231d012..242a2d9b 100644 --- a/bin/testObservability/reporter/index.js +++ b/bin/testObservability/reporter/index.js @@ -223,7 +223,8 @@ class MyReporter { if(this.httpServer !== null) return; try { - this.httpServer = http.createServer(async(req, res) => { + const httpModule = http; + this.httpServer = httpModule.createServer(async(req, res) => { try { // Set CORS headers res.setHeader('Access-Control-Allow-Origin', '*'); From 7bca61c57c45ebddf22fb91da29b67ffa7ae8da9 Mon Sep 17 00:00:00 2001 From: Aakash Hotchandani Date: Wed, 8 Oct 2025 19:46:37 +0530 Subject: [PATCH 17/17] fix Security issues --- bin/accessibility-automation/plugin/index.js | 12 ++---------- bin/testObservability/reporter/index.js | 4 ++-- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/bin/accessibility-automation/plugin/index.js b/bin/accessibility-automation/plugin/index.js index ba65165d..5ea42676 100644 --- a/bin/accessibility-automation/plugin/index.js +++ b/bin/accessibility-automation/plugin/index.js @@ -1,7 +1,7 @@ const path = require("node:path"); const { decodeJWTToken } = require("../../helpers/utils"); const utils = require('../../helpers/utils'); -const https = require('https'); +const http = require('http'); const browserstackAccessibility = (on, config) => { let browser_validation = true; @@ -26,15 +26,7 @@ const browserstackAccessibility = (on, config) => { port, path: `/test-uuid?testIdentifier=${encodeURIComponent(testIdentifier)}`, method: 'GET', - timeout: 2000, - // Use proper certificate validation for localhost - checkServerIdentity: (host, cert) => { - // Allow localhost connections - if (host === '127.0.0.1' || host === 'localhost') { - return undefined; - } - return new Error('Hostname verification failed'); - } + timeout: 2000 }; const httpModule = http; const req = httpModule.request(options, (res) => { diff --git a/bin/testObservability/reporter/index.js b/bin/testObservability/reporter/index.js index 581c277b..242a2d9b 100644 --- a/bin/testObservability/reporter/index.js +++ b/bin/testObservability/reporter/index.js @@ -11,7 +11,7 @@ const Mocha = requireModule('mocha'); // const Runnable = requireModule('mocha/lib/runnable'); const Runnable = require('mocha/lib/runnable'); // need to handle as this isn't present in older mocha versions const { v4: uuidv4 } = require('uuid'); -const https = require('https'); +const http = require('http'); const { IPC_EVENTS, TEST_REPORTING_ANALYTICS } = require('../helper/constants'); const { startIPCServer } = require('../plugin/ipcServer'); @@ -236,7 +236,7 @@ class MyReporter { res.end(); return; } - const parsedUrl = new URL(req.url, `https://${req.headers.host}`); + const parsedUrl = new URL(req.url, `http://${req.headers.host}`); const pathname = parsedUrl.pathname; const query = parsedUrl.searchParams;