Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support subpaths in proxies for Testacular #29

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ var parseConfig = function(configFilePath, cliOptions) {
reporter: 'progress',
singleRun: false,
browsers: [],
proxies: {}
proxies: {},
urlRoot: '/'
};

var ADAPTER_DIR = __dirname + '/../adapter';
Expand Down
6 changes: 3 additions & 3 deletions lib/launcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,8 @@ PhantomJSBrowser.prototype = {
var Launcher = function() {
var browsers = [];

this.launch = function(names, port) {
var url = 'http://localhost:' + port;
this.launch = function(names, port, urlRoot) {
var url = 'http://localhost:' + port + urlRoot;
var Cls, browser, id;

names.forEach(function(nameOrCls) {
Expand All @@ -249,7 +249,7 @@ var Launcher = function() {
browser.isCaptured = false;

log.info('Starting browser "%s"', browser.name || 'Custom');
browser.start(url + '/?id=' + browser.id);
browser.start(url + '?id=' + browser.id);
browsers.push(browser);
});
};
Expand Down
61 changes: 61 additions & 0 deletions lib/proxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
var url = require('url');

var parseProxyConfig = function(proxies) {
var proxyConfig = {};
var endsWith = function(str, suffix) {
return str.substr(-suffix.length) === suffix;
}
if (!proxies) { return; }
Object.keys(proxies).forEach(function(proxyPath) {
var proxyUrl = proxies[proxyPath];
if (!endsWith(proxyPath, '/')) {
proxyPath = proxyPath + '/';
}
var proxyDetails = url.parse(proxyUrl);
if (!endsWith(proxyDetails.path, '/')) {
proxyDetails.path = proxyDetails.path + '/';
}
proxyConfig[proxyPath] = {
host: proxyDetails.hostname,
port: proxyDetails.port || '80',
baseProxyUrl: proxyDetails.path
};
});
return proxyConfig;
};

exports.parseProxyConfig = parseProxyConfig;

/**
* Returns a handler which understands the proxies and its redirects, along with the proxy to use
* @param proxy A http-proxy.RoutingProxy object with the proxyRequest method
* @param proxies a map of routes to proxy url
* @return {Function} handler function
*/
var createProxyHandler = function(proxy, proxyConfig) {
var proxies = parseProxyConfig(proxyConfig);
var proxiesList = [];
if (proxies) {
proxiesList = Object.keys(proxies);
proxiesList.sort();
proxiesList.reverse();
}
return function(request, response) {
var proxiedUrl;
if (proxies) {
for (var i = 0; i < proxiesList.length; i++) {
if (request.url.indexOf(proxiesList[i]) === 0) {
proxiedUrl = proxies[proxiesList[i]];
request.url = request.url.replace(proxiesList[i], proxiedUrl.baseProxyUrl);
break;
}
}
}
if (proxiedUrl) {
proxy.proxyRequest(request, response, {host: proxiedUrl.host, port: proxiedUrl.port});
return true;
}
return false;
};
};
exports.createProxyHandler = createProxyHandler;
8 changes: 4 additions & 4 deletions lib/reporter.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
var util = require('util');
var u = require('./util');

var createErrorFormatter = function(basePath) {
var URL_REGEXP = /http:\/\/[^\/]*\/(base|absolute)([^\?\s]*)(\?[0-9]*)?/g;
var createErrorFormatter = function(basePath, urlRoot) {
var URL_REGEXP = new RegExp('http:\\/\\/[^\\/]*' + urlRoot.replace(/\//g, '\\/')+ '(base|absolute)([^\\?\\s]*)(\\?[0-9]*)?', 'g');

return function(msg, indentation) {
// remove domain and timestamp from source files
Expand Down Expand Up @@ -260,6 +260,6 @@ exports.DotsColor = DotsColorReporter;
exports.ProgressColor = ProgressColorReporter;


exports.createReporter = function(name, useColors, basePath) {
return new exports[u.ucFirst(name) + (useColors ? 'Color' : '')](createErrorFormatter(basePath));
exports.createReporter = function(name, useColors, basePath, urlRoot) {
return new exports[u.ucFirst(name) + (useColors ? 'Color' : '')](createErrorFormatter(basePath, urlRoot));
};
15 changes: 8 additions & 7 deletions lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ exports.start = function(cliOptions) {
}
});

var webServer = ws.createWebServer(fileList, config.basePath, config.proxies);
var webServer = ws.createWebServer(fileList, config.basePath, config.proxies, config.urlRoot);
var socketServer = io.listen(webServer, {
logger: logger.create('socket.io', 0),
resource: config.urlRoot + 'socket.io',
transports: ['websocket', 'xhr-polling', 'jsonp-polling']
});

Expand All @@ -50,14 +51,14 @@ exports.start = function(cliOptions) {
});

webServer.listen(config.port, function() {
log.info('Web server started at http://localhost:' + config.port);
log.info('Web server started at http://localhost:' + config.port + config.urlRoot);

if (config.browsers && config.browsers.length) {
launcher.launch(config.browsers, config.port);
launcher.launch(config.browsers, config.port, config.urlRoot);
}
});

var resultReporter = reporter.createReporter(config.reporter, config.colors, config.basePath);
var resultReporter = reporter.createReporter(config.reporter, config.colors, config.basePath, config.urlRoot);
globalEmitter.bind(resultReporter);

var capturedBrowsers = new browser.Collection(globalEmitter);
Expand All @@ -84,7 +85,7 @@ exports.start = function(cliOptions) {
var nonReady = [];

if (!capturedBrowsers.length) {
log.warn('No captured browser, open http://localhost:' + config.port);
log.warn('No captured browser, open http://localhost:' + config.port + config.urlRoot);
return false;
} else if (capturedBrowsers.areAllReady(nonReady)) {
log.debug('All browsers are ready, executing');
Expand Down Expand Up @@ -145,8 +146,8 @@ exports.start = function(cliOptions) {
log.debug('Execution (fired by runner)');

if (!capturedBrowsers.length) {
log.warn('No captured browser, open http://localhost:' + config.port);
socket.end('No captured browser, open http://localhost:' + config.port + '\n');
log.warn('No captured browser, open http://localhost:' + config.port + config.urlRoot);
socket.end('No captured browser, open http://localhost:' + config.port + config.urlRoot + '\n');
return;
}

Expand Down
186 changes: 87 additions & 99 deletions lib/web-server.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
var fs = require('fs'),
http = require('http'),
util = require('util'),
var http = require('http'),
u = require('./util'),
path = require('path'),
httpProxy = require('http-proxy'),
proxy = require('./proxy'),
fs = require('fs'),
log = require('./logger').create('web server'),
url = require('url'),
httpProxy = require('http-proxy');
util = require('util');

var SCRIPT_TAG = '<script type="text/javascript" src="%s"></script>';
var MIME_TYPE = {
Expand All @@ -21,84 +21,79 @@ var setNoCacheHeaders = function(response) {
};


/**
* Web Server handler
*
* URL schema structure:
* /base/... (files in basePath, commonly project root, relative paths)
* /absolute/... (files outside of basePath, absolute paths)
* /adapter/... (testacular adapters)
*/
var createHandler = function(fileList, staticFolder, adapterFolder, baseFolder, proxy, proxies) {

return function(request, response) {

var files = fileList.getFiles();

var getProxiedPath = function(requestUrl) {
var proxiedUrl;
if (proxies) {
var proxiesList = Object.keys(proxies);
proxiesList.sort();
proxiesList.reverse();
for (var i = 0; i < proxiesList.length; i++) {
if (requestUrl.indexOf(proxiesList[i]) === 0) {
proxiedUrl = url.parse(proxies[proxiesList[i]]);
break;
}
}
}
return proxiedUrl;
};
exports.createWebServer = function (fileList, baseFolder, proxies, urlRoot) {
var staticFolder = path.normalize(__dirname + '/../static');
var adapterFolder = path.normalize(__dirname + '/../adapter');

// helper for serving static file
var serveStaticFile = function(file, process) {
fs.readFile(file, function(error, data) {
return http.createServer(createHandler(fileList, u.normalizeWinPath(staticFolder),
u.normalizeWinPath(adapterFolder), baseFolder,
new httpProxy.RoutingProxy(), proxies, urlRoot));
};

if (error) {
log.warn('404: ' + file);
response.writeHead(404);
return response.end('NOT FOUND');
}
var createHandler = function(fileList, staticFolder, adapterFolder, baseFolder, proxyFn, proxies, urlRoot) {
var testacularSrcHandler = createTestacularSourceHandler(fileList, staticFolder, adapterFolder, baseFolder, urlRoot);
var proxiedPathsHandler = proxy.createProxyHandler(proxyFn, proxies);
var sourceFileHandler = createSourceFileHandler(fileList, adapterFolder, baseFolder);
return function(request, response) {
if (testacularSrcHandler(request, response)) {
return;
}
if (proxiedPathsHandler(request, response)) {
return;
}
return sourceFileHandler(request, response);
};
};

// set content type
response.setHeader('Content-Type', MIME_TYPE[file.split('.').pop()] || MIME_TYPE.txt);
var serveStaticFile = function(file, response, process) {
fs.readFile(file, function(error, data) {
if (error) {
log.warn('404: ' + file);
response.writeHead(404);
return response.end('NOT FOUND');
}

// call custom process fn to transform the data
var responseData = process && process(data.toString(), response) || data;
response.writeHead(200);
// set content type
response.setHeader('Content-Type', MIME_TYPE[file.split('.').pop()] || MIME_TYPE.txt);

log.debug('serving: ' + file);
return response.end(responseData);
});
};
// call custom process fn to transform the data
var responseData = process && process(data.toString(), response) || data;
response.writeHead(200);

// TODO(vojta): clean the url namespace (put everything to /__testacular__/ or so)
// TODO(vojta): no cache for testacular.js and client.html ? (updating testacular)
log.debug('serving: ' + file);
return response.end(responseData);
});
};

var requestedFilePath = request.url.replace(/\?.*/, '')
.replace(/^\/adapter/, adapterFolder)
.replace(/^\/absolute/, '')
.replace(/^\/base/, baseFolder);

// SERVE client.html - main entry point
if (requestedFilePath === '/') {
return serveStaticFile(staticFolder + '/client.html');
var createTestacularSourceHandler = function(fileList, staticFolder, adapterFolder, baseFolder, urlRoot) {
return function(request, response) {
var requestUrl = request.url.replace(/\?.*/, '');
if (requestUrl.indexOf(urlRoot) !== 0) {
return false;
}
requestUrl = requestUrl.substring(urlRoot.length - 1);
if (requestUrl === '/') {
serveStaticFile(staticFolder + '/client.html', response);
return true;
}

// SERVE testacular.js
if (request.url === '/testacular.js') {
return serveStaticFile(staticFolder + '/testacular.js');
if (requestUrl === '/testacular.js') {
serveStaticFile(staticFolder + '/testacular.js', response, function(data, response) {
return data.replace('%TESTACULAR_SRC_PREFIX%', urlRoot.substring(1));
});
return true;
}

// SERVE context.html - execution context within the iframe
// or runner.html - execution context without channel to the server
if (request.url === '/context.html' || request.url === '/debug.html') {
return serveStaticFile(staticFolder + request.url, function(data, response) {
if (requestUrl === '/context.html' || requestUrl === '/debug.html') {
serveStaticFile(staticFolder + requestUrl, response, function(data, response) {
// never cache
setNoCacheHeaders(response);

var scriptTags = files.map(function(file) {
var scriptTags = fileList.getFiles().map(function(file) {
var filePath = file.path;

if (!file.isUrl) {
Expand All @@ -110,7 +105,7 @@ var createHandler = function(fileList, staticFolder, adapterFolder, baseFolder,
filePath = '/absolute' + filePath;
}

if (request.url === '/context.html') {
if (requestUrl === '/context.html') {
filePath += '?' + file.mtime.getTime();
}
}
Expand All @@ -120,45 +115,38 @@ var createHandler = function(fileList, staticFolder, adapterFolder, baseFolder,

return data.replace('%SCRIPTS%', scriptTags.join('\n'));
});
return true;
}
return false;
};
};

var createSourceFileHandler = function(fileList, adapterFolder, baseFolder) {
return function(request, response) {
var requestedFilePath = request.url.replace(/\?.*/, '')
.replace(/^\/adapter/, adapterFolder)
.replace(/^\/absolute/, '')
.replace(/^\/base/, baseFolder);

var equalsPath = function(file) {
return file.path === requestedFilePath;
};

// Check if proxied path, and if so, route it through proxy
var proxiedPath = getProxiedPath(request.url);
if (proxiedPath) {
proxiedPath.port = proxiedPath.port || '80';
return proxy.proxyRequest(request, response, {host: proxiedPath.hostname, port: proxiedPath.port});
}


// not in the file list - forbidden
if (!files.some(equalsPath)) {
if (fileList.getFiles().some(equalsPath)) {
serveStaticFile(requestedFilePath, response, function(data, response) {
if (/\?\d+/.test(request.url)) {
// files with timestamps - cache one year, rely on timestamps
response.setHeader('Cache-Control', ['public', 'max-age=31536000']);
} else {
// without timestamps - no cache (debug)
setNoCacheHeaders(response);
}
});
return true;
} else {
response.writeHead(404);
return response.end('NOT FOUND');
response.end('NOT FOUND');
return false;
}

// OTHERWISE - js files
return serveStaticFile(requestedFilePath, function(data, response) {
if (/\?\d+/.test(request.url)) {
// files with timestamps - cache one year, rely on timestamps
response.setHeader('Cache-Control', ['public', 'max-age=31536000']);
} else {
// without timestamps - no cache (debug)
setNoCacheHeaders(response);
}
});
};
};

exports.createWebServer = function (fileList, baseFolder, proxies) {
var staticFolder = path.normalize(__dirname + '/../static');
var adapterFolder = path.normalize(__dirname + '/../adapter');
var proxy = new httpProxy.RoutingProxy();

return http.createServer(createHandler(fileList, u.normalizeWinPath(staticFolder),
u.normalizeWinPath(adapterFolder), baseFolder,
proxy, proxies));
}
};
Loading