diff --git a/lib/config.js b/lib/config.js index 27fa84d2a..35adadead 100644 --- a/lib/config.js +++ b/lib/config.js @@ -23,7 +23,8 @@ var parseConfig = function(configFilePath, cliOptions) { reporter: 'progress', singleRun: false, browsers: [], - proxies: {} + proxies: {}, + urlRoot: '/' }; var ADAPTER_DIR = __dirname + '/../adapter'; diff --git a/lib/launcher.js b/lib/launcher.js index ec1ddc84e..bf066eca3 100644 --- a/lib/launcher.js +++ b/lib/launcher.js @@ -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) { @@ -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); }); }; diff --git a/lib/proxy.js b/lib/proxy.js new file mode 100644 index 000000000..388ff0a78 --- /dev/null +++ b/lib/proxy.js @@ -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; diff --git a/lib/reporter.js b/lib/reporter.js index 205223c4e..928cc6c29 100644 --- a/lib/reporter.js +++ b/lib/reporter.js @@ -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 @@ -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)); }; diff --git a/lib/server.js b/lib/server.js index 0dcef1be4..912ee3af1 100644 --- a/lib/server.js +++ b/lib/server.js @@ -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'] }); @@ -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); @@ -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'); @@ -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; } diff --git a/lib/web-server.js b/lib/web-server.js index 650abbb58..45db9dd84 100644 --- a/lib/web-server.js +++ b/lib/web-server.js @@ -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 = ''; var MIME_TYPE = { @@ -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) { @@ -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(); } } @@ -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)); + } }; diff --git a/static/client.html b/static/client.html index 50503cb58..1a9fa9e1a 100644 --- a/static/client.html +++ b/static/client.html @@ -14,7 +14,7 @@

Testacular - starting

- - + + diff --git a/static/testacular.src.js b/static/testacular.src.js index 318426cc9..cbd4ae696 100644 --- a/static/testacular.src.js +++ b/static/testacular.src.js @@ -1,10 +1,12 @@ -var CONTEXT_URL = '/context.html'; +var CONTEXT_URL = 'context.html'; // connect socket.io // https://github.com/LearnBoost/Socket.IO/wiki/Configuring-Socket.IO -var socket = io.connect(location, { +var testacularSrcPrefix = '%TESTACULAR_SRC_PREFIX%'; +var socket = io.connect(location.href.substring(0, location.href.length - testacularSrcPrefix.length), { 'reconnection delay': 500, 'reconnection limit': 2000, + 'resource': testacularSrcPrefix + 'socket.io', 'max reconnection attempts': Infinity }); diff --git a/test/unit/launcher.spec.coffee b/test/unit/launcher.spec.coffee index b090801fe..8d4a1b2ea 100644 --- a/test/unit/launcher.spec.coffee +++ b/test/unit/launcher.spec.coffee @@ -59,8 +59,8 @@ describe 'launcher', -> @kill = jasmine.createSpy 'kill' instance = @ - l.launch [customLauncher], 1234 - expect(instance.start).toHaveBeenCalledWith 'http://localhost:1234/?id=1' + l.launch [customLauncher], 1234, '/_testacular_/' + expect(instance.start).toHaveBeenCalledWith 'http://localhost:1234/_testacular_/?id=1' l.kill() expect(instance.kill).toHaveBeenCalled() diff --git a/test/unit/proxy.spec.coffee b/test/unit/proxy.spec.coffee new file mode 100644 index 000000000..4ab0866c5 --- /dev/null +++ b/test/unit/proxy.spec.coffee @@ -0,0 +1,75 @@ + +describe 'proxy unit tests', -> + util = require '../test-util' + fsMock = require('mocks').fs + httpMock = require('mocks').http + loadFile = require('mocks').loadFile + actualOptions = requestedUrl = null + # async helper + waitForFinishingResponse = -> + waitsFor (-> response._isFinished()), 'Finish response', 100 + mocks = {} + + mocks['http-proxy'] = {} + globals = process: {} + + m = loadFile __dirname + '/../../lib/proxy.js', mocks, globals + + mockProxy = {} + mockProxy.proxyRequest = (req, res, opt) -> + actualOptions = opt + requestedUrl = req.url + res.writeHead 200 + res.end 'DONE' + response = null + + beforeEach -> + actualOptions = {} + requestedUrl = '' + response = new httpMock.ServerResponse + + it 'should proxy requests', -> + proxy = m.createProxyHandler mockProxy, {'/proxy': 'http://localhost:9000'} + + expect(proxy new httpMock.ServerRequest('/proxy/test.html'), response).toBeTruthy() + waitForFinishingResponse() + + runs -> + expect(requestedUrl).toEqual '/test.html' + expect(actualOptions).toEqual {host: 'localhost', port: '9000'} + + it 'should support multiple proxies', -> + proxy = m.createProxyHandler mockProxy, {'/proxy': 'http://localhost:9000', '/static': 'http://gstatic.com'} + expect(proxy new httpMock.ServerRequest('/static/test.html'), response).toBeTruthy() + waitForFinishingResponse() + + runs -> + expect(requestedUrl).toEqual '/test.html' + expect(actualOptions).toEqual {host: 'gstatic.com', port: '80'} + + it 'should handle nested proxies', -> + proxy = m.createProxyHandler mockProxy, {'/sub': 'http://localhost:9000', '/sub/some': 'http://gstatic.com/something'} + expect(proxy new httpMock.ServerRequest('/sub/some/Test.html'), response).toBeTruthy() + waitForFinishingResponse() + + runs -> + expect(requestedUrl).toEqual '/something/Test.html' + expect(actualOptions).toEqual {host: 'gstatic.com', port: '80'} + + it 'should parse a simple proxy config', -> + proxy = {'/base/': 'http://localhost:8000'} + parsedProxyConfig = m.parseProxyConfig proxy + expect(parsedProxyConfig).toEqual({'/base/': {host: 'localhost', port: '8000', baseProxyUrl: '/'}}) + + it 'should handle proxy configs without trailing slash', -> + proxy = {'/base': 'http://internationalhost:8000'} + parsedProxyConfig = m.parseProxyConfig proxy + expect(parsedProxyConfig).toEqual({'/base/': {host: 'internationalhost', port: '8000', baseProxyUrl: '/'}}) + + it 'should handle proxy configs with paths', -> + proxy = {'/base': 'http://localhost:8000/proxy'} + parsedProxyConfig = m.parseProxyConfig proxy + expect(parsedProxyConfig).toEqual({'/base/': {host: 'localhost', port: '8000', baseProxyUrl: '/proxy/'}}) + + it 'should handle empty proxy config', -> + expect(m.parseProxyConfig {}).toEqual({}) diff --git a/test/unit/reporter.spec.coffee b/test/unit/reporter.spec.coffee index 888935d95..c849aaa07 100644 --- a/test/unit/reporter.spec.coffee +++ b/test/unit/reporter.spec.coffee @@ -41,7 +41,7 @@ describe 'reporter', -> formatError = null beforeEach -> - formatError = m.createErrorFormatter '' + formatError = m.createErrorFormatter '', '/' it 'should indent', -> @@ -53,6 +53,11 @@ describe 'reporter', -> 'http://127.0.0.1:8080/base/home/b.js'). toBe 'file /usr/a.js and /home/b.js\n' + it 'should handle non default testacular service folders', -> + formatError = m.createErrorFormatter '', '/_testacular_/' + expect(formatError 'file http://localhost:8080/_testacular_/base/usr/a.js and ' + + 'http://127.0.0.1:8080/_testacular_/base/home/b.js'). + toBe 'file /usr/a.js and /home/b.js\n' it 'should remove timestamps', -> expect(formatError 'file http://localhost:8080/base/usr/file.js?1325400290544 and ' + @@ -65,7 +70,7 @@ describe 'reporter', -> it 'should restore base paths', -> - formatError = m.createErrorFormatter '/some/base' + formatError = m.createErrorFormatter '/some/base', '/' expect(formatError 'at http://localhost:123/base/a.js?123').toBe 'at /some/base/a.js\n' diff --git a/test/unit/web-server.spec.coffee b/test/unit/web-server.spec.coffee index 8a9e09590..dd44acc15 100644 --- a/test/unit/web-server.spec.coffee +++ b/test/unit/web-server.spec.coffee @@ -7,8 +7,15 @@ describe 'web-server', -> httpMock = require('mocks').http loadFile = require('mocks').loadFile + staticFolderPath = '/tcular/static' + adapterFolderPath = '/tcular/adapter' + baseFolder = '/base/path' + beforeEach util.disableLogger + mocks = {} + + mocks['http-proxy'] = {} files = handler = response = null ZERO_DATE = (new Date 0).toString() @@ -20,7 +27,6 @@ describe 'web-server', -> waitForFinishingResponse = -> waitsFor (-> response._isFinished()), 'Finish response', 100 - mocks = {} mocks.fs = fsMock.create tcular: adapter: @@ -38,225 +44,253 @@ describe 'web-server', -> other: 'file.js': fsMock.file(0, 'js-source') - mocks['http-proxy'] = {} - globals = process: {} # load file under test m = loadFile __dirname + '/../../lib/web-server.js', mocks, globals - beforeEach -> - handler = m.createHandler fileList, '/tcular/static', '/tcular/adapter', '/base/path', {}, {} - response = new httpMock.ServerResponse - files = [] - globals.process.platform = 'darwin' + describe 'integration tests', -> + handler = null + mockProxy = {}; + actualOptions = null + mockProxy.proxyRequest = (req, res, opt) -> + actualOptions = opt + res.writeHead 200 + res.end 'DONE' - it 'should serve client.html', -> - handler new httpMock.ServerRequest('/'), response - waitForFinishingResponse() + beforeEach -> + files = [{path: '/first.js', mtime: new Date 12345}, + {path: '/second.js', mtime: new Date 67890}, + {path: '/base/path/a.js', mtime: new Date 12345}] - runs -> - expect(response._body).toEqual 'CLIENT HTML' - expect(response._status).toBe 200 + handler = m.createHandler fileList, staticFolderPath, adapterFolderPath, baseFolder, mockProxy, {'/_testacular_/': 'http://localhost:9000'}, '/_testacular_/' + actualOptions = {} + response = new httpMock.ServerResponse() + it 'should first look for testacular files', -> + handler new httpMock.ServerRequest('/_testacular_/'), response + waitForFinishingResponse() - it 'should allow /?id=xxx', -> - handler new httpMock.ServerRequest('/?id=123'), response - waitForFinishingResponse() + runs -> + expect(response._body).toEqual 'CLIENT HTML' + expect(response._status).toBe 200 + expect(actualOptions).toEqual {} - runs -> - expect(response._body).toEqual 'CLIENT HTML' - expect(response._status).toBe 200 + it 'should delegate to proxy after testacular files', -> + handler new httpMock.ServerRequest('/_testacular_/not_client.html'), response + waitForFinishingResponse() - it 'should proxy requests', -> - actualOptions = {} - mocks.proxy = {} - mocks.proxy.proxyRequest = (req, res, opt) -> - actualOptions = opt - res.writeHead 200 - res.end 'DONE' + runs -> + expect(actualOptions).toEqual {host: 'localhost', port: '9000'} - handler = m.createHandler fileList, '/tcular/static', '/tcular/adapter', '/base/path', - mocks.proxy, {'/proxy': 'http://localhost:9000'} - handler new httpMock.ServerRequest('/proxy/test.html'), response - waitForFinishingResponse() + it 'should check for forbidden files before serving', -> + handler new httpMock.ServerRequest('/base/other.js'), response + waitForFinishingResponse() - runs -> - expect(actualOptions).toEqual {host: 'localhost', port: '9000'} + runs -> + expect(response._status).toBe 404 + expect(response._body).toBe 'NOT FOUND' - it 'should support multiple proxies', -> - actualOptions = {} - mocks.proxy = {} - mocks.proxy.proxyRequest = (req, res, opt) -> - actualOptions = opt - res.writeHead 200 - res.end 'DONE' + it 'should serve static files last', -> + handler new httpMock.ServerRequest('/base/a.js'), response + waitForFinishingResponse() - handler = m.createHandler fileList, '/tcular/static', '/tcular/adapter', '/base/path', - mocks.proxy, {'/proxy': 'http://localhost:9000', '/static': 'http://gstatic.com'} - handler new httpMock.ServerRequest('/static/test.html'), response - waitForFinishingResponse() + runs -> + expect(response._body).toBe 'js-src-a' + expect(response._status).toBe 200 - runs -> - expect(actualOptions).toEqual {host: 'gstatic.com', port: '80'} + it 'should give 404 for missing files', -> + handler new httpMock.ServerRequest('/base/non-existent.html'), response + waitForFinishingResponse() - it 'should handle nested proxies', -> - actualOptions = {} - mocks.proxy = {} - mocks.proxy.proxyRequest = (req, res, opt) -> - actualOptions = opt - res.writeHead 200 - res.end 'DONE' + runs -> + expect(response._status).toBe 404 + expect(response._body).toBe 'NOT FOUND' - handler = m.createHandler fileList, '/tcular/static', '/tcular/adapter', '/base/path', - mocks.proxy, {'/sub': 'http://localhost:9000', '/sub/some': 'http://gstatic.com'} - handler new httpMock.ServerRequest('/sub/some/Test.html'), response - waitForFinishingResponse() + describe 'testacular source handler', -> - runs -> - expect(actualOptions).toEqual {host: 'gstatic.com', port: '80'} + tcularSrcHandler = null + beforeEach -> + response = new httpMock.ServerResponse + files = [] + globals.process.platform = 'darwin' + tcularSrcHandler = m.createTestacularSourceHandler fileList, staticFolderPath, adapterFolderPath, baseFolder, '/_testacular_/' - it 'should server context.html with replaced script tags', -> - files = [{path: '/first.js', mtime: new Date 12345}, - {path: '/second.js', mtime: new Date 67890}] + it 'should serve client.html', -> + retVal = tcularSrcHandler new httpMock.ServerRequest('/_testacular_/'), response + expect(retVal).toBeTruthy() + waitForFinishingResponse() + + runs -> + expect(response._body).toEqual 'CLIENT HTML' + expect(response._status).toBe 200 + + + it 'should allow /?id=xxx', -> + retVal = tcularSrcHandler new httpMock.ServerRequest('/_testacular_/?id=123'), response + expect(retVal).toBeTruthy() + waitForFinishingResponse() + + runs -> + expect(response._body).toEqual 'CLIENT HTML' + expect(response._status).toBe 200 - handler new httpMock.ServerRequest('/context.html'), response - waitForFinishingResponse() + it 'should serve context.html with replaced script tags', -> + files = [{path: '/first.js', mtime: new Date 12345}, + {path: '/second.js', mtime: new Date 67890}] - runs -> - expect(response._body).toEqual 'CONTEXT\n' + + retVal = tcularSrcHandler new httpMock.ServerRequest('/_testacular_/context.html'), response + expect(retVal).toBeTruthy() + waitForFinishingResponse() + + runs -> + expect(response._body).toEqual 'CONTEXT\n' + '\n' + '' - expect(response._status).toBe 200 + expect(response._status).toBe 200 - it 'should server debug.html with replaced script tags without timestamps', -> - files = [{path: '/first.js', mtime: new Date 12345}, - {path: '/second.js', mtime: new Date 67890}] + it 'should serve debug.html with replaced script tags without timestamps', -> + files = [{path: '/first.js', mtime: new Date 12345}, + {path: '/second.js', mtime: new Date 67890}] - handler new httpMock.ServerRequest('/debug.html'), response - waitForFinishingResponse() + retVal = tcularSrcHandler new httpMock.ServerRequest('/_testacular_/debug.html'), response + expect(retVal).toBeTruthy() + waitForFinishingResponse() - runs -> - expect(response._body).toEqual 'RUNNER\n' + + runs -> + expect(response._body).toEqual 'RUNNER\n' + '\n' + '' - expect(response._status).toBe 200 + expect(response._status).toBe 200 - it 'should serve context.html with /basepath/*, /adapter/*, /absolute/* ', -> - files = [{path: '/some/abs/a.js', mtime: new Date 12345}, - {path: '/base/path/b.js', mtime: new Date 67890}, - {path: '/tcular/adapter/c.js', mtime: new Date 321}] + it 'should serve context.html with /basepath/*, /adapter/*, /absolute/* ', -> + files = [{path: '/some/abs/a.js', mtime: new Date 12345}, + {path: '/base/path/b.js', mtime: new Date 67890}, + {path: '/tcular/adapter/c.js', mtime: new Date 321}] - handler new httpMock.ServerRequest('/context.html'), response - waitForFinishingResponse() + retVal = tcularSrcHandler new httpMock.ServerRequest('/_testacular_/context.html'), response + expect(retVal).toBeTruthy() + waitForFinishingResponse() - runs -> - expect(response._body).toEqual 'CONTEXT\n' + + runs -> + expect(response._body).toEqual 'CONTEXT\n' + '\n' + '\n' + '' - expect(response._status).toBe 200 + expect(response._status).toBe 200 - it 'should not change urls', -> - files = [{path: 'http://some.url.com/whatever', isUrl: true}] + it 'should not change urls', -> + files = [{path: 'http://some.url.com/whatever', isUrl: true}] - handler new httpMock.ServerRequest('/context.html'), response - waitForFinishingResponse() + retVal = tcularSrcHandler new httpMock.ServerRequest('/_testacular_/context.html'), response + expect(retVal).toBeTruthy() + waitForFinishingResponse() - runs -> - expect(response._body).toEqual 'CONTEXT\n' + + runs -> + expect(response._body).toEqual 'CONTEXT\n' + '' - expect(response._status).toBe 200 - + expect(response._status).toBe 200 - it 'should send non-caching headers for context.html', -> - handler new httpMock.ServerRequest('/context.html'), response - waitForFinishingResponse() - runs -> - expect(response._headers['Cache-Control']).toBe 'no-cache' - # idiotic IE8 needs more - expect(response._headers['Pragma']).toBe 'no-cache' - expect(response._headers['Expires']).toBe ZERO_DATE + it 'should send non-caching headers for context.html', -> + retVal = tcularSrcHandler new httpMock.ServerRequest('/_testacular_/context.html'), response + expect(retVal).toBeTruthy() + waitForFinishingResponse() + runs -> + expect(response._headers['Cache-Control']).toBe 'no-cache' + # idiotic IE8 needs more + expect(response._headers['Pragma']).toBe 'no-cache' + expect(response._headers['Expires']).toBe ZERO_DATE - it 'should serve absolute js source files ignoring timestamp', -> - files = [{path: '/src/some.js', mtime: new Date 12345}] - handler new httpMock.ServerRequest('/absolute/src/some.js?123345'), response - waitForFinishingResponse() - runs -> - expect(response._body).toBe 'js-source' - expect(response._status).toBe 200 + describe 'source files handler', -> + srcFileHandler = null + beforeEach -> + response = new httpMock.ServerResponse + srcFileHandler = m.createSourceFileHandler fileList, '/tcular/adapter', '/base/path' + it 'should serve absolute js source files ignoring timestamp', -> + files = [{path: '/src/some.js', mtime: new Date 12345}] - it 'should serve js source files from base folder ignoring timestamp', -> - files = [{path: '/base/path/a.js', mtime: new Date 12345}] + srcFileHandler new httpMock.ServerRequest('/absolute/src/some.js?123345'), response + waitForFinishingResponse() - handler new httpMock.ServerRequest('/base/a.js?123345'), response - waitForFinishingResponse() + runs -> + expect(response._body).toBe 'js-source' + expect(response._status).toBe 200 - runs -> - expect(response._body).toBe 'js-src-a' - expect(response._status).toBe 200 + it 'should serve js source files from base folder ignoring timestamp', -> + files = [{path: '/base/path/a.js', mtime: new Date 12345}] - it 'should serve js source files from adapter folder ignoring timestamp', -> - files = [{path: '/tcular/adapter/jasmine.js', mtime: new Date 12345}] + srcFileHandler new httpMock.ServerRequest('/base/a.js?123345'), response - handler new httpMock.ServerRequest('/adapter/jasmine.js?123345'), response - waitForFinishingResponse() + waitForFinishingResponse() - runs -> - expect(response._body).toBe 'js-src-jasmine' - expect(response._status).toBe 200 + runs -> + expect(response._body).toBe 'js-src-a' + expect(response._status).toBe 200 - it 'should send strict caching headers for js source files with timestamps', -> - files = [{path: '/src/some.js', mtime: new Date 12345}] + it 'should serve js source files from adapter folder ignoring timestamp', -> + files = [{path: '/tcular/adapter/jasmine.js', mtime: new Date 12345}] - handler new httpMock.ServerRequest('/absolute/src/some.js?12323'), response - waitForFinishingResponse() + srcFileHandler new httpMock.ServerRequest('/adapter/jasmine.js?123345'), response + waitForFinishingResponse() - runs -> - expect(response._headers['Cache-Control']).toEqual ['public', 'max-age=31536000'] + runs -> + expect(response._body).toBe 'js-src-jasmine' + expect(response._status).toBe 200 - it 'should send no-caching headers for js source files without timestamps', -> - files = [{path: '/src/some.js', mtime: new Date 12345}] + it 'should send strict caching headers for js source files with timestamps', -> + files = [{path: '/src/some.js', mtime: new Date 12345}] - handler new httpMock.ServerRequest('/absolute/src/some.js'), response - waitForFinishingResponse() + srcFileHandler new httpMock.ServerRequest('/absolute/src/some.js?12323'), response + waitForFinishingResponse() - runs -> - expect(response._headers['Cache-Control']).toBe 'no-cache' - # idiotic IE8 needs more - expect(response._headers['Pragma']).toBe 'no-cache' - expect(response._headers['Expires']).toBe ZERO_DATE + runs -> + expect(response._headers['Cache-Control']).toEqual ['public', 'max-age=31536000'] - it 'should serve 404 page for non-existing files', -> - handler new httpMock.ServerRequest('/base/non-existing.html'), response - waitForFinishingResponse() + it 'should send no-caching headers for js source files without timestamps', -> + files = [{path: '/src/some.js', mtime: new Date 12345}] - runs -> - expect(response._body).toBe 'NOT FOUND' - expect(response._status).toBe 404 + srcFileHandler new httpMock.ServerRequest('/absolute/src/some.js'), response + waitForFinishingResponse() + runs -> + expect(response._headers['Cache-Control']).toBe 'no-cache' + # idiotic IE8 needs more + expect(response._headers['Pragma']).toBe 'no-cache' + expect(response._headers['Expires']).toBe ZERO_DATE - it 'should not allow resources that are not in the file list', -> - files = [{path: '/first.js', mtime: new Date 12345}, - {path: '/second.js', mtime: new Date 67890}] - runs -> - handler new httpMock.ServerRequest('/base/other.js'), response + it 'should serve 404 page for non-existing files', -> + srcFileHandler new httpMock.ServerRequest('/base/non-existing.html'), response waitForFinishingResponse() - runs -> - expect(response._status).toBe 404 - expect(response._body).toBe 'NOT FOUND' + runs -> + expect(response._body).toBe 'NOT FOUND' + expect(response._status).toBe 404 + + + it 'should not allow resources that are not in the file list', -> + response = new httpMock.ServerResponse + files = [{path: '/first.js', mtime: new Date 12345}, + {path: '/second.js', mtime: new Date 67890}] + + runs -> + expect(srcFileHandler new httpMock.ServerRequest('/base/other.js'), response).toBeFalsy() + waitForFinishingResponse() + + runs -> + expect(response._status).toBe 404 + expect(response._body).toBe 'NOT FOUND' +