@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 +2 -2
- package/cli.js +0 -2
- package/cliAgentMode.js +129 -15
- package/commons/chromedriverWrapper.js +1 -0
- package/commons/featureFlags.js +1 -0
- package/commons/prepareRunnerAndTestimStartUtils.js +7 -3
- package/commons/testimCloudflare.js +8 -8
- package/commons/testimCloudflare.test.js +162 -0
- package/commons/testimNgrok.js +6 -4
- package/commons/testimNgrok.test.js +140 -0
- package/commons/testimTunnel.js +5 -5
- package/commons/testimTunnel.test.js +69 -0
- package/npm-shrinkwrap.json +261 -234
- package/package.json +1 -1
- package/player/stepActions/inputFileStepAction.js +23 -14
- package/player/stepActions/textValidationStepAction.js +3 -0
- package/player/webdriver.js +10 -1
- package/runOptions.js +1 -0
- package/runOptionsAgentFlow.js +5 -0
- package/runner.js +1 -2
- package/runners/ParallelWorkerManager.js +0 -1
- package/runners/TestPlanRunner.js +8 -9
- package/services/lambdatestService.js +8 -9
- package/services/lambdatestService.test.js +259 -0
- package/testRunStatus.js +34 -5
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
|
-
|
|
112
|
-
const
|
|
113
|
-
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
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":
|
package/commons/featureFlags.js
CHANGED
|
@@ -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',
|
|
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',
|
|
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
|
|
5
|
-
const { spawn } = require('child_process');
|
|
4
|
+
const childProcess = require('child_process');
|
|
6
5
|
const fse = require('fs-extra');
|
|
7
|
-
const
|
|
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
|
+
});
|
package/commons/testimNgrok.js
CHANGED
|
@@ -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
|
};
|