diff --git a/package.json b/package.json index 12d71fc..5b80e47 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,9 @@ "dependencies": { "http-proxy": "~0.10" }, + "devDependencies": { + "node-webkit-agent": "~0.1.2" + }, "engines": { "node": ">=0.6.6", "npm": ">=1.1.0" diff --git a/test/runner.js b/test/runner.js new file mode 100644 index 0000000..1d634d0 --- /dev/null +++ b/test/runner.js @@ -0,0 +1,126 @@ +#!/usr/bin/env node +// Go to the end of the file to see the declarations of the tests + +// jshint node:true, sub:true +'use strict'; +var http = require('http'); +var assert = require('assert'); +var fork = require('child_process').fork; +var exec = require('child_process').exec; + +var host = '127.0.0.1'; +// CORS Anywhere API endpoint +var port = 11400; +var cors_api_url = 'http://' + host + ':' + port + '/'; +// Server with redirects +var portServer = 11302; +var no_cors_url = 'http://' + host + ':' + portServer + '/'; +// Memleak debugging server +var portLeak = process.env.DEBUG_PORT = 9998; + + +setupRedirectServer(function() { + setupCORSServer(function(activateDevtoolsAgent) { + runTest(function() { + if (activateDevtoolsAgent) { + activateDevtoolsAgent(); + suggestLeakTest(); + } else { + console.log('Leak tests not run.'); + process.exit(); + } + }); + }); +}); + +/** + * @param callback {function(activateDevtoolsAgent)} activateDevtoolsAgent is a function + * if the devtools agent is available, omitted otherwise. + */ +function setupCORSServer(callback) { + var child = fork('./sub-server', [host, port]); + child.on('message', function(message) { + if (message.hasDevtoolsAgent) { + callback(function() { + child.kill('SIGUSR2'); + child = null; + }); + } else if (message.hasDevtoolsAgent === false) { + callback(); + } else if (message.action === 'leakTest') { + runLeakTest(message.parallel, message.requestCount); + } else { + console.error('Unexpected message: ', message); + } + }); +} + +function setupRedirectServer(callback) { + var child = fork('./sub-no-cors-server', [host, portServer]); + child.once('message', function() { + callback(); + }); +} + +// +// Actual tests +// + +function runTest(callback) { + var url = cors_api_url + no_cors_url + '2'; + console.log('Test, GET: ' + url); + http.get(url, function(res) { + console.log(''); + assert.equal(res.statusCode, 200, 'HTTP status must be 200'); + console.log('Response headers:', res.headers); + assert.equal(res.headers['access-control-allow-origin'], '*'); + assert.equal(res.headers['whatever'], 'header', 'Custom header must be passed through.'); + assert.equal(res.headers['x-request-url'], no_cors_url + '2', 'x-request-url should match original URL'); + assert.equal(res.headers['x-final-url'], no_cors_url + '0', 'x-final-url should match the last URL'); + assert(!res.headers['set-cookie'], 'Cookies must be absent'); + assert.equal(res.headers['x-cors-redirect-1'], '302 ' + no_cors_url + '1', 'x-cors-redirect-1 must provide info about redirect'); + assert.equal(res.headers['x-cors-redirect-2'], '302 ' + no_cors_url + '0', 'x-cors-redirect-2 must provide info about redirect'); + + callback(); + }); +} + +function suggestLeakTest() { + console.log('1. Visit http://c4milo.github.io/node-webkit-agent/26.0.1410.65/inspector.html?host=localhost:' + portLeak + '&page=0'); + console.log('2. Go to the profiles tab, select "Take Heap Snapshot" and click "Start"'); + console.log('3. Go to the JavaScript console, type leakTest() and press Enter'); + console.log('4. When you see "leakTest done" in the console, take another heap snapshot and use the Comparison option.'); +} +function runLeakTest(/*boolean*/ parallel, requestCount) { + requestCount = +requestCount || 100; + + console.log('Sending ' + requestCount + ' requests'); + + var initCount = 0; + var doneCount = 0; + + if (parallel) { + while (initCount < requestCount) doRequest(); // initCount is incremented in doRequest + } else { + doRequest(); + } + function onComplete(res) { + if (++doneCount % 10 === 0 || doneCount === requestCount) { + console.log('done #' + doneCount); + } + assert.equal(res.statusCode, 200); + assert.equal(res.headers['access-control-allow-origin'], '*'); + assert.equal(res.headers['whatever'], 'header', 'Custom header must be passed through.'); + if (!parallel && doneCount < requestCount) { + doRequest(); + } + } + function doRequest() { + http.get({ + agent: false, + host: host, + port: port, + path: '/' + no_cors_url + '1?' + (++initCount) // resource with one redirect + }, onComplete); + } +} diff --git a/test/sub-no-cors-server.js b/test/sub-no-cors-server.js new file mode 100644 index 0000000..8b1a76a --- /dev/null +++ b/test/sub-no-cors-server.js @@ -0,0 +1,31 @@ +// jshint node:true +'use strict'; + +var host = process.argv[2]; +var port = process.argv[3]; +console.log('Trying to start redirect server on ' + host + ':' + port + '...'); + +process.on('disconnect', function() { + console.log('Stopping redirect server at ' + host + ':' + port + '...\n'); + process.exit(); +}); + +var regex = /^\/(\d+)(.*)/; +require('http').createServer(function(req, res) { + // Don't use console.log, because the number of newlines would be too much. + process.stdout.write(req.url + ' '); + // Redirect a few times. E.g. /3 -> /2 -> /1 -> /0 + // Preserve any suffix (for cache breaking) + var path = regex.exec(req.url); + var count = path && +path[1]; + if (count > 0) { + res.writeHead(302, { location: '/' + (count - 1) + path[2] }); + res.end(); + } else { + res.writeHead(200, { 'whatever':'header', 'Set-Cookie': 'test=1; path=/' }); + res.end('Ok'); + } +}).listen(port, host, function() { + console.log('Started redirect server at ' + host + ':' + port + '\n'); + process.send('ready'); +}); diff --git a/test/sub-server.js b/test/sub-server.js new file mode 100644 index 0000000..73554e1 --- /dev/null +++ b/test/sub-server.js @@ -0,0 +1,42 @@ +// jshint node:true +'use strict'; + +var host = process.argv[2]; +var port = process.argv[3]; +console.log('Trying to start CORS Anywhere on ' + host + ':' + port + '...'); + +process.on('disconnect', function() { + console.log('Stopping CORS Anywhere server at ' + host + ':' + port + '...\n'); + process.exit(); +}); + +require('../lib/cors-anywhere').createServer().listen(port, host, function() { + console.log('Running CORS Anywhere on ' + host + ':' + port + '\n'); + var hasDevtoolsAgent = false; + try { + require('webkit-devtools-agent'); + hasDevtoolsAgent = true; + } catch (e) { + console.error('Failed to load webkit-devtools-agent. Memory leak testing is not available'); + console.error('Install it using npm install webkit-devtools-agent'); + } + process.send({ + hasDevtoolsAgent: hasDevtoolsAgent + }); +}); + +global.leakTest = function leakTest(parallel, requestCount) { + // parallel {boolean} If true, all requests are sent at once. + // If false, the next request is only sent after completing the previous one (default). + // requestCount {number} Number of requests, defaults to 100 + // All parameters are optional + process.send({ + action: 'leakTest', + parallel: parallel, + requestCount: requestCount + }); + console.log('Switch to the shell and watch the console for the completion message.'); +}; +global.leakTestParallel = function leakTestParallel(requestCount) { + global.leakTest(true, requestCount); +};