Repository: saucelabs/node-saucelabs (js)

Issue:
Pull always latest Sauce Connect version if not specified otherwise
We should update the `sc` command to always pull the latest Sauce Connect version from https://saucelabs.com/versions.json.

Pull request description:
Always pull latest Sauce Connect version if not specified otherwise
Fallback to hardcoded version in case the API call fails.

Closes #93 

Patch:
diff --git a/src/constants.js b/src/constants.js
index ceff242..06a06cf 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -5,6 +5,7 @@ import { version } from '../package.json'
 
 export const DEFAULT_SAUCE_CONNECT_VERSION = '4.6.2'
 export const SAUCE_CONNECT_BASE = 'https://saucelabs.com/downloads'
+export const SAUCE_CONNECT_VERSIONS_ENDPOINT = 'https://saucelabs.com/versions.json'
 export const SAUCE_CONNECT_DISTS = [
     [`${SAUCE_CONNECT_BASE}/sc-%s-osx.zip`, 'darwin'],
     [`${SAUCE_CONNECT_BASE}/sc-%s-win32.zip`, 'win32'],
diff --git a/src/index.js b/src/index.js
index b2979a9..30642bf 100644
--- a/src/index.js
+++ b/src/index.js
@@ -16,7 +16,7 @@ import {
     PROTOCOL_MAP, DEFAULT_OPTIONS, SYMBOL_INSPECT, SYMBOL_TOSTRING,
     SYMBOL_ITERATOR, TO_STRING_TAG, SAUCE_CONNECT_DISTS,
     SC_PARAMS_TO_STRIP, SC_READY_MESSAGE, SC_CLOSE_MESSAGE,
-    SC_CLOSE_TIMEOUT, DEFAULT_SAUCE_CONNECT_VERSION, SC_FAILURE_MESSAGE
+    SC_CLOSE_TIMEOUT, DEFAULT_SAUCE_CONNECT_VERSION, SC_FAILURE_MESSAGE, SAUCE_CONNECT_VERSIONS_ENDPOINT
 } from './constants'
 
 export default class SauceLabs {
@@ -214,7 +214,10 @@ export default class SauceLabs {
             }
         }
 
-        const sauceConnectVersion = argv.scVersion || DEFAULT_SAUCE_CONNECT_VERSION
+        let sauceConnectVersion = argv.scVersion
+        if (!sauceConnectVersion) {
+            sauceConnectVersion = await this._getLatestSauceConnectVersion()
+        }
         const { servers, basePath } = PROTOCOL_MAP.get('listJobs')
         const restUrl = getAPIHost(servers, basePath, this._options) + '/v1'
         const args = Object.entries(argv)
@@ -291,6 +294,17 @@ export default class SauceLabs {
         })
     }
 
+    async _getLatestSauceConnectVersion () {
+        try {
+            const {body} = await this._api.get(SAUCE_CONNECT_VERSIONS_ENDPOINT, {responseType: 'json'})
+            const responseJson = body.data
+            return responseJson['Sauce Connect']['version']
+        } catch (err) {
+            // fallback
+            return DEFAULT_SAUCE_CONNECT_VERSION
+        }
+    }
+
     async _downloadJobAsset (jobId, assetName, { filepath } = {}) {
         /**
          * check job id


Test patch:
diff --git a/tests/__responses__/versions.json b/tests/__responses__/versions.json
new file mode 100644
index 0000000..3a9c6b7
--- /dev/null
+++ b/tests/__responses__/versions.json
@@ -0,0 +1,30 @@
+{
+    "Sauce Connect": {
+        "download_url": "https://wiki.saucelabs.com/display/DOCS/Setting+Up+Sauce+Connect",
+        "linux": {
+            "build": 5183,
+            "download_url": "https://saucelabs.com/downloads/sc-4.6.2-linux.tar.gz",
+            "sha1": "7b7f35433af9c3380758e048894d7b9aecf3754e"
+        },
+        "linux32": {
+            "build": 5183,
+            "download_url": "https://saucelabs.com/downloads/sc-4.6.2-linux32.tar.gz",
+            "sha1": "ce96d8543f013e0112a678aa9980e95331421cb1"
+        },
+        "osx": {
+            "build": 5183,
+            "download_url": "https://saucelabs.com/downloads/sc-4.6.2-osx.zip",
+            "sha1": "51c6c152eb54c20d8969a9d5917ebecc3ac868e1"
+        },
+        "version": "1.2.4",
+        "win32": {
+            "build": 5183,
+            "download_url": "https://saucelabs.com/downloads/sc-4.6.2-win32.zip",
+            "sha1": "4b8955c806998226ed41592cbe45c929c0f88a87"
+        }
+    },
+    "Sauce Connect 2": {
+        "download_url": "https://docs.saucelabs.com/reference/sauce-connect/",
+        "version": "4.3.13-r999"
+    }
+}
diff --git a/tests/index.test.js b/tests/index.test.js
index 7f7903c..4b9933b 100644
--- a/tests/index.test.js
+++ b/tests/index.test.js
@@ -5,6 +5,8 @@ import FormData from 'form-data'
 
 import SauceLabs from '../src'
 import { instances } from 'bin-wrapper'
+import versions from './__responses__/versions.json'
+import {DEFAULT_SAUCE_CONNECT_VERSION} from '../src/constants'
 
 jest.mock('fs')
 const fs = require('fs')
@@ -29,6 +31,8 @@ const origKill = ::process.kill
 beforeEach(() => {
     spawn.mockClear()
     process.kill = jest.fn()
+    // clean instances array
+    instances.splice(0,instances.length)
 })
 test('should be inspectable', () => {
     const api = new SauceLabs({ user: 'foo', key: 'bar' })
@@ -357,6 +361,40 @@ describe('startSauceConnect', () => {
             .toBe(true)
     })
 
+    it('should start sauce connect with latest version if no version is specified in the args', async () => {
+        const logs = []
+        const api = new SauceLabs({ user: 'foo', key: 'bar', headless: true })
+        got.mockReturnValue(Promise.resolve({
+            body: {
+                data: {
+                    ...versions
+                }
+            }
+        }))
+        setTimeout(() => stdoutEmitter.emit('data', 'Sauce Connect is up, you may start your tests'), 50)
+        await api.startSauceConnect({
+            tunnelIdentifier: 'my-tunnel',
+            'proxy-tunnel': 'abc',
+            logger: (log) => logs.push(log)
+        })
+        expect(instances[0].dest.mock.calls[0][0].endsWith('.sc-v1.2.4'))
+            .toBe(true)
+    })
+
+    it('should start sauce connect with fallback default version in case the call to the API failed', async () => {
+        const logs = []
+        const api = new SauceLabs({ user: 'foo', key: 'bar', headless: true })
+        got.mockImplementation(() => { throw new Error('Endpoint not available!') })
+        setTimeout(() => stdoutEmitter.emit('data', 'Sauce Connect is up, you may start your tests'), 50)
+        await api.startSauceConnect({
+            tunnelIdentifier: 'my-tunnel',
+            'proxy-tunnel': 'abc',
+            logger: (log) => logs.push(log)
+        })
+        expect(instances[0].dest.mock.calls[0][0].endsWith(`.sc-v${DEFAULT_SAUCE_CONNECT_VERSION}`))
+            .toBe(true)
+    })
+
     it('should properly fail if connection could not be established', async () => {
         const errMessage = 'Sauce Connect could not establish a connection'
         const api = new SauceLabs({ user: 'foo', key: 'bar', headless: true })