Skip to content

Commit

Permalink
update endpoint to retrive SC download
Browse files Browse the repository at this point in the history
Signed-off-by: Alana McKenzie <[email protected]>
  • Loading branch information
amckenzie132 committed Jan 23, 2025
1 parent 05961a9 commit fe62ec0
Show file tree
Hide file tree
Showing 19 changed files with 376 additions and 531 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,12 @@ or start Sauce Connect Proxy in EU datacenter:
# start Sauce Connect tunnel for eu-central-1 region
$ sl sc --region eu --tunnel-name "my-tunnel"
# run a specific Sauce Connect version
$ sl sc --scVersion 4.9.1
$ sl sc --scVersion 5.2.2
# see all available Sauce Connect parameters via:
$ sl sc --help
```
You can see all available Sauce Connect parameters on the [Sauce Labs Docs](https://docs.saucelabs.com/dev/cli/sauce-connect-proxy/).
You can see all available Sauce Connect parameters on the [Sauce Labs Docs](https://docs.saucelabs.com/dev/cli/sauce-connect-5/run/).
### As NPM Package
Expand Down Expand Up @@ -179,7 +179,7 @@ import SauceLabs from 'saucelabs';
*/
logger: (stdout) => console.log(stdout),
/**
* see all available parameters here: https://docs.saucelabs.com/dev/cli/sauce-connect-proxy/
* see all available parameters here: https://docs.saucelabs.com/dev/cli/sauce-connect-5/run/
* all parameters have to be applied camel cased instead of with hyphens, e.g.
* to apply the `--tunnel-name` parameter, set:
*/
Expand Down Expand Up @@ -219,6 +219,7 @@ const myAccount = new SauceLabs({
user: 'YOUR-USER',
key: 'YOUR-ACCESS-KEY',
region: 'eu', // run in EU datacenter
tunnelName: 'my-tunnel',
});
// get full webdriver url from the client depending on `region` option:
Expand Down
61 changes: 52 additions & 9 deletions apis/sauce.json
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,36 @@
},
"type": "object"
},
"SauceConnectDownload": {
"type": "object",
"properties": {
"download": {
"type": "object",
"properties": {
"url": {
"type": "string"
},
"version": {
"type": "string"
},
"checksums": {
"type": "array",
"items": {
"type": "object",
"properties": {
"value": {
"type": "string"
},
"algorithm": {
"type": "string"
}
}
}
}
}
}
}
},
"SauceConnectDownloadInfo": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -827,10 +857,24 @@
"required": false,
"type": "string"
},
"clientArch": {
"description": "SC client host CPU arch",
"in": "query",
"name": "arch",
"required": true,
"type": "string"
},
"clientOS": {
"description": "SC client host OS",
"in": "query",
"name": "os",
"required": true,
"type": "string"
},
"clientVersion": {
"description": "SC client version",
"in": "query",
"name": "client_version",
"name": "version",
"required": false,
"type": "string"
},
Expand Down Expand Up @@ -1768,25 +1812,25 @@
"tags": ["Tunnel"]
}
},
"/v1/public/tunnels/info/versions": {
"/v1/public/tunnels/sauce-connect/download": {
"get": {
"operationId": "sc_versions",
"operationId": "sc_download",
"parameters": [
{
"$ref": "#/parameters/clientVersion"
"$ref": "#/parameters/clientArch"
},
{
"$ref": "#/parameters/clientHost"
"$ref": "#/parameters/clientOS"
},
{
"$ref": "#/parameters/all"
"$ref": "#/parameters/clientVersion"
}
],
"responses": {
"200": {
"description": "Tunnels",
"description": "download",
"schema": {
"$ref": "#/definitions/SauceConnectVersions"
"$ref": "#/definitions/SauceConnectDownload"
}
},
"default": {
Expand All @@ -1796,7 +1840,6 @@
}
}
},
"summary": "Get tunnels for the user or all the users in the team",
"tags": ["Tunnel"]
}
},
Expand Down
8 changes: 4 additions & 4 deletions docs/interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,12 +185,12 @@ The following commands are available via package or cli tool:
</tr>
<tr>
<td>
<b>GET</b> <code>/v1/public/tunnels/info/versions</code><br>
Get tunnels for the user or all the users in the team
<b>GET</b> <code>/v1/public/tunnels/sauce-connect/download</code><br>
Get Sauce Connect download information for the latest version
<h3>Example:</h3>
<code>api.scVersions({ ...options })</code>
<code>api.scDownload({ ...options })</code>
<br><h4>Options</h4>
<ul> <li><b>client_version</b>: SC client version</li> <li><b>client_host</b>: SC client host OS and CPU arch</li> <li><b>all</b>: Should the response contain the same team user data</li> </ul> </td>
<ul> <li><b>arch</b>: SC client host CPU architecture</li> <li><b>os</b>: SC client host OS</li> <li><b>version</b>: Optional. Return the newest version matching this minimal version</li> </ul> </td>
</tr>
<tr>
<td>
Expand Down
31 changes: 23 additions & 8 deletions e2e/sc.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,42 @@ test('should be able to get Sauce Connect versions', async () => {
return;
}
const api = new SauceLabs();
const scVersion = await api.scVersions({
clientVersion: '5.1.0',
clientHost: 'darwin-arm64',
const response = await api.scDownload({
version: '5.2.2',
arch: 'arm64',
os: 'macos',
});
expect(scVersion.status).toEqual('UPGRADE');
expect(scVersion.latest_version).toMatch(/5\./);
console.log(scVersion.download_url);

expect(response.download.version).toEqual('5.2.2');
expect(response.download.url).toMatch(/5\.2\.2/);
console.log(response.download.url);
});

test('should not be able to run Sauce Connect due to invalid credentials', async () => {
if (SKIP_TEST) {
return;
}
const api = new SauceLabs({key: 'foobar'});
const api = new SauceLabs({key: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'});
const err = await api
.startSauceConnect({
logger: console.log.bind(console),
tunnelName: `node-saucelabs E2E test - ${ID}`,
})
.catch((err) => err);
expect(err.message).toContain('Unauthorized');
expect(err.message).toContain('Not authorized');
});

test('should not be able to run Sauce Connect due to missing tunnel-name', async () => {
if (SKIP_TEST) {
return;
}
const api = new SauceLabs();
const err = await api
.startSauceConnect({
logger: console.log.bind(console),
})
.catch((err) => err);
expect(err.message).toContain('Missing tunnel-name');
});

test('should be able to run Sauce Connect', async () => {
Expand Down
1 change: 0 additions & 1 deletion src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export const run = () => {
.epilog(EPILOG)
.demandCommand()
.commandDir('commands')
.wrap(yargs.terminalWidth())
.help()
.version(SAUCE_VERSION_NOTE);

Expand Down
10 changes: 3 additions & 7 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import os from 'os';

import {version} from '../package.json';

export const DEFAULT_SAUCE_CONNECT_VERSION = '5.2.0';
export const DEFAULT_SAUCE_CONNECT_VERSION = '5.2.2';
export const SAUCE_VERSION_NOTE = `node-saucelabs v${version}\nSauce Connect v${DEFAULT_SAUCE_CONNECT_VERSION}`;

const protocols = [
Expand Down Expand Up @@ -313,10 +313,6 @@ export const SC_PARAMS_TO_STRIP = [
];

export const SC_READY_MESSAGE = 'Sauce Connect is up, you may start your tests';
export const SC_FAILURE_MESSAGES = [
'Sauce Connect could not establish a connection',
'Sauce Connect failed to start',
];
export const SC_WAIT_FOR_MESSAGES = ['\u001b[K', 'Please wait for']; // "\u001b" = Escape character
export const SC_CLOSE_MESSAGE = 'Goodbye';
export const SC_FAILURE_MESSAGES = ['fatal error exiting'];
export const SC_CLOSE_MESSAGE = 'tunnel was shutdown';
export const SC_CLOSE_TIMEOUT = 5000;
92 changes: 64 additions & 28 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ import {
SC_CLOSE_TIMEOUT,
DEFAULT_SAUCE_CONNECT_VERSION,
SC_FAILURE_MESSAGES,
SC_WAIT_FOR_MESSAGES,
SC_BOOLEAN_CLI_PARAMS,
} from './constants';
import SauceConnectLoader from './sauceConnectLoader';
Expand Down Expand Up @@ -243,6 +242,12 @@ export default class SauceLabs {
}

const sauceConnectVersion = argv.scVersion || DEFAULT_SAUCE_CONNECT_VERSION;
if (sauceConnectVersion.startsWith('4')) {
throw new Error(
`This Sauce Connect version (${sauceConnectVersion}) is no longer supported. Please use Sauce Connect 5.`
);
}

const scUpstreamProxy = argv.scUpstreamProxy;
const args = Object.entries(argv)
/**
Expand All @@ -255,6 +260,7 @@ export default class SauceLabs {
'$0',
'sc-version',
'sc-upstream-proxy',
'tunnel-name',
'logger',
...SC_PARAMS_TO_STRIP,
].includes(k)
Expand Down Expand Up @@ -292,17 +298,34 @@ export default class SauceLabs {
// --region is required for Sauce Connect 5.
throw new Error('Missing region');
}
const scLoader = new SauceConnectLoader({sauceConnectVersion});

const tunnelName = argv.tunnelName;
if (tunnelName) {
args.push(`--tunnel-name=${tunnelName}`);
} else {
// --tunnel-name is required for Sauce Connect 5.
throw new Error('Missing tunnel-name');
}

// download and verify the Sauce Connect client
let scLoader = new SauceConnectLoader({version: sauceConnectVersion});
const isDownloaded = await scLoader.verifyAlreadyDownloaded();

if (!isDownloaded) {
let sauceConnectURL = await this._getSauceConnectDownloadURL(
sauceConnectVersion
);
await scLoader.verifyAlreadyDownloaded({url: sauceConnectURL});
let download = await this._getSauceConnectDownload(sauceConnectVersion);

// downloaded version may differ from the input version, eg. if a partial version is given as input
// update scLoader if necessary
if (download.version != sauceConnectVersion) {
scLoader = new SauceConnectLoader({version: download.version});
}
await scLoader.verifyAlreadyDownloaded({url: download.url});
}

if (args.length == 0 || args[0] != 'run') {
args.unshift('run');
}

const cp = spawn(scLoader.path, args);
return new Promise((resolve, reject) => {
const close = () =>
Expand All @@ -321,19 +344,6 @@ export default class SauceLabs {

cp.stderr.on('data', (data) => {
const output = data.toString();

/**
* check if error output is just an escape sequence or
* other expected data
*/
if (
SC_WAIT_FOR_MESSAGES.find((msg) =>
escape(output).includes(escape(msg))
)
) {
return;
}

return reject(new Error(output));
});
cp.stdout.on('data', (data) => {
Expand Down Expand Up @@ -372,17 +382,43 @@ export default class SauceLabs {
});
}

async _getSauceConnectDownloadURL(sauceConnectVersion) {
/**
* Retrieve the download URL for the Sauce Connect client specific to this device's OS and architecture.
* Throws an exception on any error response
* @param {string} version Full or partial version for the download to match
* @returns {Object} download
* @returns {string} download.url
* @returns {string} download.version
* @returns {Object[]} download.checksums
* @returns {string} download.checksums[].value
* @returns {string} download.checksums[].algorithm
*/
async _getSauceConnectDownload(version) {
const platform = getPlatform();
const cpuARCH = getCPUArch();
const scVersionInfo = await this._callAPI('scVersions', {
client_host: `${platform}-${cpuARCH}`,
client_version: sauceConnectVersion,
});
if (!scVersionInfo.download_url) {
throw new Error('Failed to retrieve Sauce Connect download URL');
const cpuArch = getCPUArch();
var response = {};
try {
response = await this._callAPI('scDownload', {
os: platform,
arch: cpuArch,
version: version,
});
} catch (err) {
// if this endpoint is down, the start tunnels endpoint is likely down as well.
throw new Error(`Failed to retrieve Sauce Connect download. ${err}`);
}

if (response.error) {
// likely an input value error. some platform/arch combinations may not be supported.
throw new Error(
`Failed to retrieve Sauce Connect download. code: ${response.error.code} message: ${response.error.message}`
);
}
if (!response.download) {
// unexpected, inconsistent with API definition
throw new Error(`Failed to retrieve Sauce Connect download.`);
}
return scVersionInfo.download_url;
return response.download;
}

async _downloadJobAsset(jobId, assetName, {filepath} = {}) {
Expand Down
2 changes: 1 addition & 1 deletion src/sauceConnectLoader.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export default class SauceConnectLoader {
strip: 1,
})
.then(() => {
if (getPlatform() !== 'win32') {
if (getPlatform() !== 'windows') {
// ensure the sc executable is actually executable
return fs.chmod(this.path, 0o755);
}
Expand Down
Loading

0 comments on commit fe62ec0

Please sign in to comment.