@testim/testim-cli 3.235.0 → 3.236.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/agent/server.js CHANGED
@@ -54,7 +54,7 @@ function initServer({
54
54
  /**
55
55
  * Init testim auth for making services request.
56
56
  */
57
- let initFn = () => {};
57
+ let initFn = () => { };
58
58
  if (project) {
59
59
  testimCustomToken.init(project, token);
60
60
  initFn = (app) => {
@@ -99,7 +99,7 @@ function initServer({
99
99
  case 'EPERM':
100
100
  return reject(new ArgError(`Port ${agentPort} requires elevated privileges`));
101
101
  case 'EADDRINUSE':
102
- return reject(new ArgError(`Port ${agentPort} is already in use`));
102
+ return reject(new ArgError(`Port ${agentPort} is already in use, is another Testim instance running?`));
103
103
  default:
104
104
  return reject(error);
105
105
  }
package/cli.js CHANGED
@@ -72,11 +72,9 @@ function main() {
72
72
  const codimCli = require('./codim/codim-cli');
73
73
  return codimCli.init(options.initTestProject);
74
74
  }
75
-
76
75
  if (options.loginMode) {
77
76
  return undefined;
78
77
  }
79
-
80
78
  if (options.createPrefechedData) {
81
79
  const runnerFileCache = require('./commons/runnerFileCache');
82
80
  await runnerFileCache.clear();
package/cliAgentMode.js CHANGED
@@ -1,3 +1,4 @@
1
+ /* eslint-disable camelcase */
1
2
  // @ts-check
2
3
 
3
4
  'use strict';
@@ -8,12 +9,15 @@ const fs = require('fs-extra');
8
9
  const ms = require('ms');
9
10
  const WebSocket = require('ws');
10
11
  const Bluebird = require('bluebird');
12
+ const ChromeLauncher = require('chrome-launcher');
11
13
  const httpRequest = require('./commons/httpRequest');
12
14
  const config = require('./commons/config');
13
15
  const { ArgError } = require('./errors');
14
16
  const lazyRequire = require('./commons/lazyRequire');
15
17
  const prepareUtils = require('./commons/prepareRunnerAndTestimStartUtils');
16
18
  const { unzipFile } = require('./utils');
19
+ const { downloadAndSave } = require('./utils');
20
+ const ora = require('ora');
17
21
 
18
22
  const LOG_LEVEL = config.WEBDRIVER_DEBUG ? 'verbose' : 'silent';
19
23
  const EXTENSION_CACHE_TIME = ms('1h');
@@ -49,7 +53,6 @@ async function runAgentMode(options) {
49
53
 
50
54
  if (options.startTestimBrowser) {
51
55
  await getRidOfPossiblyRunningChromeWithOurDataDir();
52
-
53
56
  try {
54
57
  // Consider moving that into the agent server and add endpoint to start browser?
55
58
  testimStandaloneBrowser = await startTestimStandaloneBrowser(options);
@@ -81,7 +84,7 @@ async function runAgentMode(options) {
81
84
  ];
82
85
 
83
86
  for (const packageToInstall of packages) {
84
- await lazyRequire(packageToInstall, { silent: true }).catch(() => {});
87
+ await lazyRequire(packageToInstall, { silent: true }).catch(() => { });
85
88
  }
86
89
  }, LOAD_PLAYER_DELAY);
87
90
  }
@@ -108,10 +111,115 @@ function getStartedWithStart() {
108
111
  return startedWithStart;
109
112
  }
110
113
 
111
- async function startTestimStandaloneBrowser(options) {
112
- const ora = require('ora');
113
- const { downloadAndSave } = require('./utils');
114
+ function getCurrentPlatform() {
115
+ const osType = os.type().toLowerCase();
116
+ if (osType === 'darwin') {
117
+ return os.arch() === 'arm' ? 'mac_arm' : 'mac';
118
+ }
119
+ if (osType === 'windows_nt') {
120
+ return os.arch() === 'x64' ? 'win64' : 'win32';
121
+ }
122
+ return 'linux';
123
+ }
124
+ async function downloadAndInstallChrome(platform, revision, saveLocation) {
125
+ // Inspired by puppeteer's implementation https://github.com/puppeteer/puppeteer/blob/main/src/node/BrowserFetcher.ts#L45
126
+ // example download url: https://storage.googleapis.com/chromium-browser-snapshots/Mac/1000968/chrome-mac.zip
127
+ const storageBaseUrl = 'https://storage.googleapis.com/chromium-browser-snapshots';
128
+ const platformFolder = {
129
+ linux: 'Linux_x64',
130
+ mac: 'Mac',
131
+ mac_arm: 'Mac_Arm',
132
+ win32: 'Win',
133
+ win64: 'Win_x64',
134
+ };
135
+ if (!(platform in platformFolder)) {
136
+ throw new ArgError(`Unsupported platform: ${platform}`);
137
+ }
138
+ // Windows archive name changed at r591479.
139
+ const winArchiveName = parseInt(revision, 10) > 591479 ? 'chrome-win' : 'chrome-win32';
140
+ const platformArchiveName = {
141
+ linux: 'chrome-linux',
142
+ mac: 'chrome-mac',
143
+ mac_arm: 'chrome-mac',
144
+ win32: winArchiveName,
145
+ win64: winArchiveName,
146
+ };
147
+ const binaryPaths = {
148
+ linux: 'chrome',
149
+ mac: 'Chromium.app/Contents/MacOS/Chromium',
150
+ mac_arm: 'Chromium.app/Contents/MacOS/Chromium',
151
+ win32: 'chrome.exe',
152
+ win64: 'chrome.exe',
153
+ };
154
+ const downloadUrl = `${storageBaseUrl}/${platformFolder[platform]}/${revision}/${platformArchiveName[platform]}.zip`;
155
+ const downloadArchivePath = path.join(saveLocation, platformArchiveName[platform]);
156
+ const downloadedZipFile = `${downloadArchivePath}.zip`;
157
+ const binaryPath = path.join(downloadArchivePath, binaryPaths[platform]);
114
158
 
159
+ if (await fs.pathExists(binaryPath)) {
160
+ return binaryPath;
161
+ }
162
+ if (!(await fs.pathExists(downloadedZipFile))) {
163
+ const downloadSpinner = ora('Downloading Chromium').start();
164
+ try {
165
+ await fs.mkdirp(saveLocation);
166
+ await downloadAndSave(downloadUrl, downloadedZipFile);
167
+ // todo - We can add a failover here if the download fails if we host the file too
168
+ } catch (e) {
169
+ const errorMessage = `Failed to download Chromium: ${e.message}`;
170
+ downloadSpinner.fail(errorMessage);
171
+ throw new Error(errorMessage);
172
+ }
173
+ downloadSpinner.succeed();
174
+ }
175
+ const extractSpinner = ora('Extracting Chromium').start();
176
+ try {
177
+ await unzipFile(downloadedZipFile, saveLocation);
178
+ } catch (e) {
179
+ const errorMessage = `Failed to extract Chromium: ${e.message}`;
180
+ extractSpinner.fail(errorMessage);
181
+ throw new Error(errorMessage);
182
+ }
183
+ extractSpinner.succeed();
184
+ return binaryPath;
185
+ }
186
+
187
+ async function startFixedVersionBrowser(options, extensionBase64, downloadedExtensionPathUnzipped) {
188
+ const CHROME_VERSION = '1000968';
189
+ const DOWNLOAD_CHROME_FOLDER = path.join(TESTIM_BROWSER_PROFILES_CONTAINER, `chrome-${CHROME_VERSION}`);
190
+
191
+ const chromeBinary = await downloadAndInstallChrome(getCurrentPlatform(), CHROME_VERSION, DOWNLOAD_CHROME_FOLDER);
192
+
193
+ if (!(await fs.pathExists(USER_DATA_DIR))) {
194
+ await fs.mkdirp(USER_DATA_DIR);
195
+ }
196
+ const capabilities = buildSeleniumOptions(USER_DATA_DIR, extensionBase64, downloadedExtensionPathUnzipped, chromeBinary);
197
+ const chromeFlags = [
198
+ ...capabilities.desiredCapabilities.chromeOptions.args,
199
+ ...ChromeLauncher.Launcher.defaultFlags().filter(flag => ![
200
+ '--disable-extensions',
201
+ '--disable-component-extensions-with-background-pages', // causes google connect to disallow some accounts (eg gmail accounts get a "This browser or app may not be secure" error)
202
+ ].includes(flag)),
203
+ ];
204
+ // Chromium needs API keys to communicate with google APIs (https://www.chromium.org/developers/how-tos/api-keys/)
205
+ // These are keys are keys that were included in some chrome builds
206
+ const envVars = {
207
+ GOOGLE_API_KEY: 'AIzaSyCkfPOPZXDKNn8hhgu3JrA62wIgC93d44k',
208
+ GOOGLE_DEFAULT_CLIENT_ID: '811574891467.apps.googleusercontent.com',
209
+ GOOGLE_DEFAULT_CLIENT_SECRET: 'kdloedMFGdGla2P1zacGjAQh',
210
+ };
211
+ const appUrl = `${options.extensionPath ? 'http://localhost:3000/app/' : 'https://app.testim.io'}?startMode=true`;
212
+ const chrome = await ChromeLauncher.launch({ chromeFlags, startingUrl: appUrl, ignoreDefaultFlags: true, userDataDir: USER_DATA_DIR, chromePath: chromeBinary, envVars });
213
+ const onBrowserClosed = () => process.exit(0);
214
+
215
+ chrome.process.once('exit', onBrowserClosed);
216
+ chrome.process.once('close', onBrowserClosed);
217
+ return {
218
+ webdriverApi: chrome,
219
+ };
220
+ }
221
+
222
+ async function startTestimStandaloneBrowser(options) {
115
223
  // After next clickim release we will have also testim-full.zip
116
224
  // const fullExtensionUrl = "https://testimstatic.blob.core.windows.net/extension/testim-full-master.zip";
117
225
  // CDN url
@@ -128,7 +236,6 @@ async function startTestimStandaloneBrowser(options) {
128
236
  const stat = await fs.stat(downloadedExtensionPath);
129
237
  shouldDownloadExtension = (Date.now() - EXTENSION_CACHE_TIME > stat.mtimeMs);
130
238
  }
131
-
132
239
  await fs.mkdirp(TESTIM_BROWSER_PROFILES_CONTAINER);
133
240
 
134
241
  if (shouldDownloadExtension) {
@@ -144,27 +251,31 @@ async function startTestimStandaloneBrowser(options) {
144
251
  await downloadAndSave(fullExtensionUrl, downloadedExtensionPath);
145
252
  try {
146
253
  await unzipFile(downloadedExtensionPath, downloadedExtensionPathUnzipped);
147
- } catch (e) {
254
+ } catch (err) {
148
255
  // zip is bad again.
149
256
  await fs.remove(downloadedExtensionPath);
150
257
  spinner.fail('Failed to download Testim Editor');
151
258
  throw new Error('Failed to download Testim Editor');
152
259
  }
153
260
  } finally {
154
- await fs.remove(downloadedExtensionPathUnzipped);
261
+ if (!options.downloadBrowser) {
262
+ await fs.remove(downloadedExtensionPathUnzipped);
263
+ }
155
264
  }
156
265
 
157
266
  spinner.succeed();
158
267
  }
159
268
 
160
269
  const extensionBase64 = options.extensionPath ? null : (await fs.readFile(options.ext || downloadedExtensionPath)).toString('base64');
161
-
270
+ if (options.downloadBrowser) {
271
+ return await startFixedVersionBrowser(options, extensionBase64, downloadedExtensionPathUnzipped);
272
+ }
162
273
  await prepareUtils.prepareChromeDriver(
163
274
  { projectId: options.project },
164
- { chromeBinaryLocation: options.chromeBinaryLocation },
275
+ { chromeBinaryLocation: options.chromeBinaryLocations },
165
276
  );
166
277
 
167
- const seleniumOptions = buildSeleniumOptions(USER_DATA_DIR, extensionBase64, options.extensionPath);
278
+ const seleniumOptions = buildSeleniumOptions(USER_DATA_DIR, extensionBase64, options.extensionPath, options.chromeBinaryLocations);
168
279
 
169
280
  const WebDriver = require('./player/webdriver');
170
281
  const { SeleniumPerfStats } = require('./commons/SeleniumPerfStats');
@@ -184,8 +295,7 @@ async function startTestimStandaloneBrowser(options) {
184
295
  // const { getEditorUrl } = require('./commons/testimServicesApi');
185
296
 
186
297
  startedWithStart = true;
187
- const appUrl = `${options.extensionPath ? 'http://localhost:3000/app/' : 'https://app.testim.io'
188
- }?startMode=true`;
298
+ const appUrl = `${options.extensionPath ? 'http://localhost:3000/app/' : 'https://app.testim.io'}?startMode=true`;
189
299
 
190
300
  await webdriverApi.url(appUrl);
191
301
  // save the initial URL we navigated to so we don't consider it the AuT
@@ -197,6 +307,7 @@ async function startTestimStandaloneBrowser(options) {
197
307
  } catch (e) {
198
308
  // ignore error
199
309
  }
310
+
200
311
  return {
201
312
  webdriverApi,
202
313
  };
@@ -206,11 +317,13 @@ async function startTestimStandaloneBrowser(options) {
206
317
  * @param {string} userDataDir
207
318
  * @param {string} fullExtensionPath
208
319
  */
209
- function buildSeleniumOptions(userDataDir, fullExtensionPath, unpackedExtensionPath) {
320
+ function buildSeleniumOptions(userDataDir, fullExtensionPath, unpackedExtensionPath, chromeBinaryPath) {
210
321
  const extensions = fullExtensionPath ? [fullExtensionPath] : [];
211
322
  const args = [
212
- `--user-data-dir=${userDataDir}`,
323
+ `--user-data-dir=${userDataDir}`, // crashes chromium, re-enable if using chrome
324
+ '--log-level=OFF',
213
325
  '--silent-debugger-extension-api',
326
+ '--no-first-run',
214
327
  ];
215
328
  if (unpackedExtensionPath) {
216
329
  args.push(`--load-extension=${unpackedExtensionPath}`);
@@ -222,6 +335,7 @@ function buildSeleniumOptions(userDataDir, fullExtensionPath, unpackedExtensionP
222
335
  chromeOptions: {
223
336
  args,
224
337
  extensions,
338
+ binary: chromeBinaryPath,
225
339
  },
226
340
  browserName: 'chrome',
227
341
  },
@@ -17,6 +17,7 @@ const DEFAULT_DRIVER_ARGS = [
17
17
  '--disable-build-check',
18
18
  // allow any ip to connect chrome driver
19
19
  '--whitelisted-ips=0.0.0.0',
20
+ '--log-level=OFF', // instead we could try to log it somehow or at least have a flag to enable this
20
21
  ];
21
22
 
22
23
  // [NOTE] This is a "smart installation":
@@ -57,6 +57,7 @@ class FeatureFlagsService {
57
57
  addCustomCapabilities: new Rox.Variant('{}'),
58
58
  enableWorkerThreadsCliCodeExecution: new Rox.Flag(true),
59
59
  LTNetworkCapabilities: new Rox.Flag(),
60
+ downloadToBase64: new Rox.Flag(),
60
61
  };
61
62
  Rox.register('default', this.flags);
62
63
  }
@@ -89,11 +89,13 @@ function prepareExtension(locations) {
89
89
  const fullLocations = locations.map(location => ({ location, path: getSourcePath(location) }));
90
90
  return localRunnerCache.memoize(
91
91
  () => Promise.map(fullLocations, ({ location, path }) => getSource(location, path)),
92
- 'prepareExtension', MSEC_IN_HALF_DAY, fullLocations
92
+ 'prepareExtension',
93
+ MSEC_IN_HALF_DAY,
94
+ fullLocations
93
95
  )();
94
96
  }
95
97
 
96
- async function prepareChromeDriver(userDetails = {}, driverOptions = {}, skipIsReadyCheck) {
98
+ async function prepareChromeDriver(userDetails = {}, driverOptions = {}, skipIsReadyCheck = false) {
97
99
  const ora = require('ora');
98
100
  const spinner = ora('Starting Driver').start();
99
101
  const chromedriverWrapper = require('./chromedriverWrapper');
@@ -184,6 +186,8 @@ function preparePlayer(location, canary) {
184
186
  () => getPlayerLocation(location, canary)
185
187
  .then(loc => downloadAndUnzip(loc, playerFileName))
186
188
  .then(() => ({})),
187
- 'preparePlayer', MSEC_IN_HALF_DAY, [location, canary, playerFileName]
189
+ 'preparePlayer',
190
+ MSEC_IN_HALF_DAY,
191
+ [location, canary, playerFileName]
188
192
  )();
189
193
  }
@@ -1,10 +1,9 @@
1
1
  'use strict';
2
2
 
3
3
  const os = require('os');
4
- const path = require('path');
5
- const { spawn } = require('child_process');
4
+ const childProcess = require('child_process');
6
5
  const fse = require('fs-extra');
7
- const { downloadAndSave, unzipFile } = require('../utils');
6
+ const utils = require('../utils');
8
7
  const servicesApi = require('./testimServicesApi.js');
9
8
 
10
9
  const TUNNEL_BINARY_ORIGIN = 'https://github.com/cloudflare/cloudflared/releases/download/2022.4.1/';
@@ -33,9 +32,9 @@ async function prepareTunnel() {
33
32
  throw new Error(`tunnel on ${os.platform() + os.arch()} platform is not supported.`);
34
33
  }
35
34
  const destination = downloadUrl.extract ? TUNNEL_BINARY_DIRECTORY + downloadUrl.path : TUNNEL_BINARY_LOCATION;
36
- await downloadAndSave(`${TUNNEL_BINARY_ORIGIN}/${downloadUrl.path}`, destination);
35
+ await utils.downloadAndSave(`${TUNNEL_BINARY_ORIGIN}/${downloadUrl.path}`, destination);
37
36
  if (downloadUrl.extract) {
38
- await unzipFile(destination, TUNNEL_BINARY_DIRECTORY);
37
+ await utils.unzipFile(destination, TUNNEL_BINARY_DIRECTORY);
39
38
  }
40
39
  await fse.chmodSync(TUNNEL_BINARY_LOCATION, '755');
41
40
  }
@@ -43,10 +42,10 @@ async function prepareTunnel() {
43
42
  const connectTunnel = async (options) => {
44
43
  const [result] = await Promise.all([
45
44
  servicesApi.getCloudflareTunnel(options.company.companyId, options.tunnelRoutes),
46
- prepareTunnel(),
45
+ module.exports.prepareTunnel(),
47
46
  ]);
48
47
  tunnelId = result._id;
49
- tunnelProcess = spawn(TUNNEL_BINARY_LOCATION, ['tunnel', '--no-autoupdate', 'run', '--force', '--token', result.token], { stdio: 'inherit' });
48
+ tunnelProcess = childProcess.spawn(TUNNEL_BINARY_LOCATION, ['tunnel', '--no-autoupdate', 'run', '--force', '--token', result.token], { stdio: 'inherit' });
50
49
  await servicesApi.forceUpdateCloudflareTunnelRoutes(options.company.companyId, tunnelId);
51
50
  await fse.writeFileSync(options.tunnelRoutesOutput, JSON.stringify(result.routesMapping, null, 2));
52
51
  };
@@ -60,7 +59,7 @@ const disconnectTunnel = async (options) => {
60
59
  promises.push(new Promise((resolve, reject) => {
61
60
  tunnelProcess.on('close', (code) => {
62
61
  if (code) {
63
- reject();
62
+ reject(new Error(`tunnel process exited with code ${code}`));
64
63
  }
65
64
  resolve();
66
65
  });
@@ -73,4 +72,5 @@ const disconnectTunnel = async (options) => {
73
72
  module.exports = {
74
73
  connectTunnel,
75
74
  disconnectTunnel,
75
+ prepareTunnel,
76
76
  };
@@ -0,0 +1,162 @@
1
+ /* eslint-disable no-console */
2
+
3
+ 'use strict';
4
+
5
+ const { sinon, expect } = require('../../test/utils/testUtils');
6
+ const testimCloudflare = require('./testimCloudflare');
7
+ const servicesApi = require('./testimServicesApi.js');
8
+ const utils = require('../utils');
9
+ const fse = require('fs-extra');
10
+ const os = require('os');
11
+ const childProcess = require('child_process');
12
+ const EventEmitter = require('events');
13
+
14
+ class Process extends EventEmitter {
15
+ constructor() {
16
+ super();
17
+ }
18
+ setCode(code) {
19
+ this.code = code;
20
+ }
21
+ kill() {
22
+ this.emit('close', this.code);
23
+ }
24
+ }
25
+
26
+ describe('testimCloudflare', () => {
27
+ let sandbox;
28
+ beforeEach(() => {
29
+ sandbox = sinon.createSandbox();
30
+ });
31
+ afterEach(() => {
32
+ sandbox.restore();
33
+ });
34
+
35
+ describe('disconnectTunnel', () => {
36
+ let deleteCloudflareTunnelStub;
37
+ let processMock;
38
+ let killStub;
39
+
40
+ beforeEach(() => {
41
+ deleteCloudflareTunnelStub = sandbox.stub(servicesApi, 'deleteCloudflareTunnel');
42
+ processMock = new Process();
43
+ killStub = sandbox.stub(processMock, 'kill').callThrough();
44
+ sandbox.stub(childProcess, 'spawn').returns(processMock);
45
+ sandbox.stub(testimCloudflare, 'prepareTunnel');
46
+ sandbox.stub(servicesApi, 'getCloudflareTunnel').resolves({ _id: utils.guid() });
47
+ sandbox.stub(servicesApi, 'forceUpdateCloudflareTunnelRoutes');
48
+ sandbox.stub(fse, 'writeFileSync');
49
+ });
50
+
51
+ it('should do nothing when no tunnel', async () => {
52
+ await testimCloudflare.disconnectTunnel({ company: {} });
53
+ sinon.assert.notCalled(deleteCloudflareTunnelStub);
54
+ });
55
+
56
+ it('should delete the tunnel', async () => {
57
+ await testimCloudflare.connectTunnel({ company: {} });
58
+ await testimCloudflare.disconnectTunnel({ company: {} });
59
+ sinon.assert.calledOnce(deleteCloudflareTunnelStub);
60
+ });
61
+
62
+ it('should kill the tunnel', async () => {
63
+ await testimCloudflare.connectTunnel({ company: {} });
64
+ await testimCloudflare.disconnectTunnel({ company: {} });
65
+ sinon.assert.calledOnce(killStub);
66
+ });
67
+
68
+ it('should reject when killing the tunnel fails', async () => {
69
+ processMock.setCode(1);
70
+ await testimCloudflare.connectTunnel({ company: {} });
71
+ await expect(testimCloudflare.disconnectTunnel({ company: {} })).to.be.rejectedWith(Error);
72
+ sinon.assert.calledOnce(killStub);
73
+ });
74
+ });
75
+
76
+ describe('prepareTunnel', () => {
77
+ it('should do nothing when cloudflared binary already exists', async () => {
78
+ sandbox.stub(fse, 'pathExists').resolves(true);
79
+ const chmod = sandbox.stub(fse, 'chmodSync');
80
+ await testimCloudflare.prepareTunnel();
81
+ expect(chmod).not.to.have.been.called;
82
+ });
83
+
84
+ it('should throw when unsupported os', async () => {
85
+ sandbox.stub(fse, 'pathExists').resolves(false);
86
+ sandbox.stub(os, 'platform').returns('wtf');
87
+ sandbox.stub(os, 'arch').returns('wtf');
88
+
89
+ await expect(testimCloudflare.prepareTunnel()).to.be.rejectedWith(Error, 'tunnel on wtfwtf platform is not supported.');
90
+ });
91
+
92
+ it('should download cloudflared binary', async () => {
93
+ sandbox.stub(fse, 'pathExists').resolves(false);
94
+ sandbox.stub(os, 'platform').returns('win32');
95
+ sandbox.stub(os, 'arch').returns('x64');
96
+
97
+ const chmod = sandbox.stub(fse, 'chmodSync');
98
+ const download = sandbox.stub(utils, 'downloadAndSave');
99
+
100
+ await testimCloudflare.prepareTunnel();
101
+ sinon.assert.calledOnce(chmod);
102
+ sinon.assert.calledOnce(download);
103
+ expect(download.args[0][0]).to.startWith('https://github.com/cloudflare/cloudflared/releases/download');
104
+ });
105
+
106
+ it('should extract tgz file', async () => {
107
+ sandbox.stub(fse, 'pathExists').resolves(false);
108
+ sandbox.stub(os, 'platform').returns('darwin');
109
+ sandbox.stub(os, 'arch').returns('x64');
110
+ sandbox.stub(fse, 'chmodSync');
111
+
112
+ sandbox.stub(utils, 'downloadAndSave').resolves();
113
+ const unzip = sandbox.stub(utils, 'unzipFile').resolves();
114
+ await testimCloudflare.prepareTunnel();
115
+
116
+ sinon.assert.calledOnce(unzip);
117
+ });
118
+ });
119
+
120
+ describe('connectTunnel', () => {
121
+ let prepareTunnelStub;
122
+ let getCloudflareTunnelStub;
123
+ let forceUpdateCloudflareTunnelRoutesStub;
124
+ let writeFileSyncStub;
125
+ let spawnStub;
126
+
127
+ let tunnelData;
128
+
129
+ beforeEach(() => {
130
+ tunnelData = { _id: utils.guid(), token: utils.guid() };
131
+ prepareTunnelStub = sandbox.stub(testimCloudflare, 'prepareTunnel');
132
+ getCloudflareTunnelStub = sandbox.stub(servicesApi, 'getCloudflareTunnel').resolves(tunnelData);
133
+ forceUpdateCloudflareTunnelRoutesStub = sandbox.stub(servicesApi, 'forceUpdateCloudflareTunnelRoutes');
134
+ writeFileSyncStub = sandbox.stub(fse, 'writeFileSync');
135
+ spawnStub = sandbox.stub(childProcess, 'spawn');
136
+ });
137
+
138
+ it('should prepare the tunnel', async () => {
139
+ await testimCloudflare.connectTunnel({ company: {} });
140
+ sinon.assert.calledOnce(prepareTunnelStub);
141
+ });
142
+
143
+ it('should get and the tunnel routes', async () => {
144
+ await testimCloudflare.connectTunnel({ company: {} });
145
+ sinon.assert.calledOnce(getCloudflareTunnelStub);
146
+ sinon.assert.calledWith(forceUpdateCloudflareTunnelRoutesStub);
147
+ });
148
+
149
+ it('should spawn the cloudflard process', async () => {
150
+ await testimCloudflare.connectTunnel({ company: {} });
151
+ sinon.assert.calledOnce(spawnStub);
152
+ expect(spawnStub.args[0][1]).to.eql(['tunnel', '--no-autoupdate', 'run', '--force', '--token', tunnelData.token]);
153
+ });
154
+
155
+ it('should write the tunnel data to a file', async () => {
156
+ tunnelData.routesMapping = utils.guid();
157
+ await testimCloudflare.connectTunnel({ company: {} });
158
+ sinon.assert.calledOnce(writeFileSyncStub);
159
+ expect(writeFileSyncStub.args[0][1]).to.eql(JSON.stringify(tunnelData.routesMapping));
160
+ });
161
+ });
162
+ });
@@ -10,7 +10,7 @@ const WHITELISTED_TUNNEL_DOMAIN_SUFFIX = '.whitelisted-ngrok.testim.io';
10
10
  let ngrokTunnelUrl = '';
11
11
  let statsTimeout;
12
12
 
13
- const connectTunnel = async (options, authData) => {
13
+ const connectTunnel = async (options, authData = {}) => {
14
14
  if (!authData.ngrokToken) {
15
15
  throw new ArgError('tunnel feature is not enabled, please contact support - info@testim.io.');
16
16
  }
@@ -27,6 +27,7 @@ const connectTunnel = async (options, authData) => {
27
27
  hostname,
28
28
  };
29
29
  if (options.tunnelHostHeader) {
30
+ // eslint-disable-next-line camelcase
30
31
  connectOpt.host_header = options.tunnelHostHeader;
31
32
  }
32
33
  if (options.tunnelRegion) {
@@ -46,7 +47,7 @@ const connectTunnel = async (options, authData) => {
46
47
  }
47
48
 
48
49
  if (options.tunnelDiagnostics) {
49
- collectNgrokStats();
50
+ module.exports.collectNgrokStats();
50
51
  }
51
52
  options.baseUrl = ngrokTunnelUrl;
52
53
  };
@@ -65,7 +66,7 @@ const collectNgrokStats = async (rerun = true) => {
65
66
  logger.error('error collecting ngrok stats', { err });
66
67
  }
67
68
  if (rerun) {
68
- statsTimeout = setTimeout(() => collectNgrokStats(), 10000);
69
+ statsTimeout = setTimeout(() => module.exports.collectNgrokStats(), 10000);
69
70
  }
70
71
  };
71
72
 
@@ -76,7 +77,7 @@ const disconnectTunnel = async (options) => {
76
77
 
77
78
  clearTimeout(statsTimeout);
78
79
  if (options.tunnelDiagnostics) {
79
- await collectNgrokStats(false);
80
+ await module.exports.collectNgrokStats(false);
80
81
  }
81
82
  const ngrok = await lazyRequire('ngrok');
82
83
  await ngrok.disconnect(ngrokTunnelUrl);
@@ -85,4 +86,5 @@ const disconnectTunnel = async (options) => {
85
86
  module.exports = {
86
87
  connectTunnel,
87
88
  disconnectTunnel,
89
+ collectNgrokStats,
88
90
  };