Skip to content

Commit

Permalink
Implement a possibility to run TestCafe over https (close #1985) (#2540)
Browse files Browse the repository at this point in the history
* implement a possibility to run TestCafe over https

* update text

* update option description

* fix review issues
  • Loading branch information
miherlosev committed Jun 25, 2018
1 parent ba81421 commit 4be7652
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 22 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@
"minimist": "^1.2.0",
"multer": "^1.1.0",
"npm-auditor": "^1.1.1",
"openssl-self-signed-certificate": "^1.1.6",
"opn": "^4.0.2",
"publish-please": "^3.0.2",
"recursive-copy": "^2.0.5",
Expand Down
10 changes: 8 additions & 2 deletions src/cli/argument-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { assertType, is } from '../errors/runtime/type-assertions';
import getViewPortWidth from '../utils/get-viewport-width';
import { wordWrap, splitQuotedText } from '../utils/string';
import { stat, ensureDir } from '../utils/promisified-functions';

import parseSslOptions from './parse-ssl-options';

const REMOTE_ALIAS_RE = /^remote(?::(\d*))?$/;
const DEFAULT_TEST_LOOKUP_DIRS = ['test/', 'tests/'];
Expand Down Expand Up @@ -107,6 +107,7 @@ export default class CLIArgumentParser {
.option('--ports <port1,port2>', 'specify custom port numbers')
.option('--hostname <name>', 'specify the hostname')
.option('--proxy <host>', 'specify the host of the proxy server')
.option('--ssl', 'specify SSL options to run TestCafe proxy server over the HTTPS protocol')
.option('--proxy-bypass <rules>', 'specify a comma-separated list of rules that define URLs accessed bypassing the proxy server')
.option('--disable-page-reloads', 'disable page reloads between tests')
.option('--qr-code', 'outputs QR-code that repeats URLs used to connect the remote browsers')
Expand Down Expand Up @@ -157,7 +158,6 @@ export default class CLIArgumentParser {
}
}


_parseSelectorTimeout () {
if (this.opts.selectorTimeout) {
assertType(is.nonNegativeNumberString, null, 'Selector timeout', this.opts.selectorTimeout);
Expand Down Expand Up @@ -210,6 +210,11 @@ export default class CLIArgumentParser {
.filter(browser => browser && this._filterAndCountRemotes(browser));
}

_parseSslOptions () {
if (this.opts.ssl)
this.opts.ssl = parseSslOptions(this.program.args[0]);
}

async _parseReporters () {
if (!this.opts.reporter) {
this.opts.reporters = [];
Expand Down Expand Up @@ -320,6 +325,7 @@ export default class CLIArgumentParser {
this._parsePorts();
this._parseBrowserList();
this._parseConcurrency();
this._parseSslOptions();

await Promise.all([
this._parseScreenshotsPath(),
Expand Down
2 changes: 1 addition & 1 deletion src/cli/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ async function runTests (argParser) {

log.showSpinner();

var testCafe = await createTestCafe(opts.hostname, port1, port2);
var testCafe = await createTestCafe(opts.hostname, port1, port2, opts.ssl);
var concurrency = argParser.concurrency || 1;
var remoteBrowsers = await remotesWizard(testCafe, argParser.remoteCount, opts.qrCode);
var browsers = argParser.browsers.concat(remoteBrowsers);
Expand Down
60 changes: 60 additions & 0 deletions src/cli/parse-ssl-options.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import fs from 'fs';
import os from 'os';

const MAX_PATH_LENGTH = {
'Linux': 4096,
'Windows_NT': 260,
'Darwin': 1024
};

const OS_MAX_PATH_LENGTH = MAX_PATH_LENGTH[os.type()];

const OPTIONS_SEPARATOR = ';';
const OPTION_KEY_VALUE_SEPARATOR = '=';
const FILE_OPTION_NAMES = ['cert', 'key', 'pfx'];
const NUMBER_REG_EX = /^[0-9-.,]+$/;
const BOOLEAN_STRING_VALUES = ['true', 'false'];

export default function (optionsStr = '') {
const splittedOptions = optionsStr.split(OPTIONS_SEPARATOR);

if (!splittedOptions.length)
return null;

const parsedOptions = {};

splittedOptions.forEach(item => {
const keyValuePair = item.split(OPTION_KEY_VALUE_SEPARATOR);
const key = keyValuePair[0];
let value = keyValuePair[1];

if (!key || !value)
return;

value = convertToBestFitType(value);

if (FILE_OPTION_NAMES.includes(key) && value.length < OS_MAX_PATH_LENGTH && fs.existsSync(value))
value = fs.readFileSync(value);

parsedOptions[key] = value;
});

return parsedOptions;
}

function convertToBestFitType (valueStr) {
if (typeof valueStr !== 'string')
return void 0;

else if (NUMBER_REG_EX.test(valueStr))
return parseFloat(valueStr);

else if (BOOLEAN_STRING_VALUES.includes(valueStr))
return valueStr === 'true';

else if (!valueStr.length)
return void 0;

return valueStr;
}

4 changes: 2 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ async function getValidPort (port) {
}

// API
async function createTestCafe (hostname, port1, port2) {
async function createTestCafe (hostname, port1, port2, sslOptions) {
[hostname, port1, port2] = await Promise.all([
getValidHostname(hostname),
getValidPort(port1),
getValidPort(port2)
]);

var testcafe = new TestCafe(hostname, port1, port2);
var testcafe = new TestCafe(hostname, port1, port2, sslOptions);

setupExitHook(cb => testcafe.close().then(cb));

Expand Down
4 changes: 2 additions & 2 deletions src/testcafe.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ const FAVICON = read('./client/ui/favicon.ico', true);


export default class TestCafe {
constructor (hostname, port1, port2) {
constructor (hostname, port1, port2, sslOptions) {
this.closed = false;
this.proxy = new Proxy(hostname, port1, port2);
this.proxy = new Proxy(hostname, port1, port2, sslOptions);
this.browserConnectionGateway = new BrowserConnectionGateway(this.proxy);
this.runners = [];

Expand Down
66 changes: 60 additions & 6 deletions test/server/cli-argument-parser-test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
var expect = require('chai').expect;
var path = require('path');
var fs = require('fs');
var tmp = require('tmp');
var find = require('lodash').find;
var CliArgumentParser = require('../../lib/cli/argument-parser');
const expect = require('chai').expect;
const path = require('path');
const fs = require('fs');
const tmp = require('tmp');
const find = require('lodash').find;
const CliArgumentParser = require('../../lib/cli/argument-parser');
const nanoid = require('nanoid');

describe('CLI argument parser', function () {
this.timeout(10000);
Expand Down Expand Up @@ -260,6 +261,58 @@ describe('CLI argument parser', function () {
});
});

describe('Ssl options', () => {
it('Should parse ssl options', () => {
return parse('--ssl passphrase=sample;sessionTimeout=1000;rejectUnauthorized=false;=onlyValue;onlyKey=')
.then(parser => {
expect(parser.opts.ssl.passphrase).eql('sample');
expect(parser.opts.ssl.sessionTimeout).eql(1000);
expect(parser.opts.ssl.rejectUnauthorized).eql(false);
expect(parser.opts.ssl.onlyKey).to.be.undefined;
});
});

describe('`key`, `cert` and `pfx` keys', () => {
it('Should parse keys as file paths and read its content', () => {
const keyFile = tmp.fileSync();
const certFile = tmp.fileSync();
const pfxFile = tmp.fileSync();
const keyFileContent = Buffer.from(nanoid());
const certFileContent = Buffer.from(nanoid());
const pfxFileContent = Buffer.from(nanoid());

fs.writeFileSync(keyFile.name, keyFileContent);
fs.writeFileSync(certFile.name, certFileContent);
fs.writeFileSync(pfxFile.name, pfxFileContent);

return parse(`--ssl key=${keyFile.name};cert=${certFile.name};pfx=${pfxFile.name}`)
.then(parser => {
expect(parser.opts.ssl.key).deep.eql(keyFileContent);
expect(parser.opts.ssl.cert).deep.eql(certFileContent);
expect(parser.opts.ssl.pfx).deep.eql(pfxFileContent);
});
});

it('Should not read file content if file does not exists', () => {
const dummyFilePath = '/dummy-file-path';

return parse(`--ssl key=${dummyFilePath}`)
.then(parser => {
expect(parser.opts.ssl.key).eql(dummyFilePath);
});
});

it('Should interpret a long path as a certificate content', () => {
const keyFileContent = nanoid(5000);

return parse(`--ssl key=${keyFileContent}`)
.then(parser => {
expect(parser.opts.ssl.key).eql(keyFileContent);
});
});
});
});

it('Should accept globs and paths as source files', function () {
var cwd = process.cwd();

Expand Down Expand Up @@ -381,6 +434,7 @@ describe('CLI argument parser', function () {
{ long: '--proxy' },
{ long: '--proxy-bypass' },
{ long: '--disable-page-reloads' },
{ long: '--ssl' },
{ long: '--qr-code' },
{ long: '--color' },
{ long: '--no-color' }
Expand Down
33 changes: 24 additions & 9 deletions test/server/create-testcafe-test.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
var expect = require('chai').expect;
var url = require('url');
var net = require('net');
var createTestCafe = require('../../lib/');
var exportableLib = require('../../lib/api/exportable-lib');
var Promise = require('pinkie');

const expect = require('chai').expect;
const url = require('url');
const net = require('net');
const createTestCafe = require('../../lib/');
const exportableLib = require('../../lib/api/exportable-lib');
const Promise = require('pinkie');
const selfSignedCertificate = require('openssl-self-signed-certificate');

describe('TestCafe factory function', function () {
var testCafe = null;
var server = null;

function getTestCafe (hostname, port1, port2) {
return createTestCafe(hostname, port1, port2)
function getTestCafe (hostname, port1, port2, sslOptions) {
return createTestCafe(hostname, port1, port2, sslOptions)
.then(function (tc) {
testCafe = tc;
});
Expand Down Expand Up @@ -93,4 +93,19 @@ describe('TestCafe factory function', function () {
expect(createTestCafe.Role).eql(exportableLib.Role);
expect(createTestCafe.ClientFunction).eql(exportableLib.ClientFunction);
});

it('Should pass sslOptions to proxy', () => {
const sslOptions = {
key: selfSignedCertificate.key,
cert: selfSignedCertificate.cert
};

return getTestCafe('localhost', 1338, 1339, sslOptions)
.then(() => {
expect(testCafe.proxy.server1.key).eql(sslOptions.key);
expect(testCafe.proxy.server1.cert).eql(sslOptions.cert);
expect(testCafe.proxy.server2.key).eql(sslOptions.key);
expect(testCafe.proxy.server2.cert).eql(sslOptions.cert);
});
});
});

0 comments on commit 4be7652

Please sign in to comment.