Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Branch: master
Fetching contributors…

Cannot retrieve contributors at this time

539 lines (455 sloc) 17.232 kB
var filesystem = require('fs'),
util = require('util'),
querystring = require('querystring'),
colors = {
red : '\033[31m',
green : '\033[32m',
reset : '\033[0m'
};
/**
* @param {Object} options
*/
function mergeWithDefaultOptions(options) {
if(options.url == undefined)
throw new Error('property url missing from options');
if(options.port == undefined)
options.port = 80;
if(options.headers == undefined)
options.headers = {};
if(options.write == undefined)
options.write = '';
if(options.method == undefined)
options.method = 'GET';
if(options.encoding == undefined)
options.encoding = 'utf8';
if(options.keepCookies == undefined)
options.keepCookies = false;
if(typeof options.write != 'string')
options.write = querystring.stringify(options.write);
if(options.headers['Content-Length'] == undefined && typeof options.write == 'string')
options.headers['Content-Length'] = Buffer.byteLength(options.write, 'utf8');
if(typeof options.url == 'object')
options.path = querystring.stringify(options.url);
else
options.path = options.url;
if(options.headers['User-Agent'] == undefined)
options['User-Agent'] = 'node dokimon scraper';
if(options.timeout == undefined)
options.timeout = 8000;
}
/** * * * * * * * * * * * * * * * * * * * * * * * *
* @class {TestManager}
* @param {Object} config
* @param {Function} testFinishCallback - Optional
* * * * * * * * * * * * * * * * * * * * * * * * */
var TestManager = function(config, testFinishCallback) {
if( config.path === undefined )
config.path = false;
this.index = 0;
this.successes = [];
this.fails = [];
this.tests = [];
this.executed = [];
/**
* Wrapper that makes it possible to handle the
* message with something other then console.log
*
* @param {String} message
* @param {Boolean} [error]
*/
this.log = function(message, error) {
if(config.log)
config.log(message, error);
else {
if(error)
console.error(message);
else
console.log(message)
}
};
/**
* @param {Test[]} tests
*/
this.setTests = function(tests) {
this.tests = tests;
};
/**
* Runs tests starting from current position of this.index
*/
this.run = function() {
if(this.index == 0) {
this.executed = [];
}
var _self = this;
if(config.verbose)
this.log('Running scripts starting from '+this.index);
var callBlockingTest = function(currentIndex) {
if(config.verbose)
_self.log('Blocking upcoming tests on index '+currentIndex+' for '+_self.tests[currentIndex].name);
_self.index = currentIndex+1;
_self.tests[currentIndex].execute(config.host, _self, config.verbose, function() {
_self.run();
}, config.path);
};
for(var i=this.index; i < this.tests.length; i++) {
var dependingTest = this.tests[i].options.dependsOn;
if( dependingTest && this.executed.indexOf(dependingTest) == -1 ) {
this.log('# -> Running depending test "'+dependingTest+'"');
_self.current = i;
this._doRunSingleTest(dependingTest, config.verbose, function() {
if(_self.tests[_self.current].blocking) {
callBlockingTest(_self.current);
}
else {
_self.tests[_self.current].execute(config.host, _self, config.verbose, false, config.path);
_self.run();
}
});
break;
}
else {
// blocking request
if(this.tests[i].blocking) {
callBlockingTest(i);
break;
}
else
this.tests[i].execute(config.host, this, config.verbose, false, config.path);
}
}
};
/**
* Run a specific test in the test list
*
* @param {String} testName
*/
this.runTest = function(testName) {
this.executed = [];
this._doRunSingleTest(testName, config.verbose);
};
/**
* @param name
* @param verbose
* @param callback
* @private
*/
this._doRunSingleTest = function(name, verbose, callback) {
var test = this.getTestByName(name);
var dependingTest = test.options.dependsOn;
if( dependingTest && this.executed.indexOf(dependingTest) == -1 ) {
var _self = this;
this.log('# -> Running depending test "'+dependingTest+'"');
this._doRunSingleTest(dependingTest, verbose, function() {
test.execute(config.host, _self, verbose, function() {
if( typeof callback == 'function' ) {
callback();
}
}, config.path);
});
}
else {
test.execute(config.host, this, verbose, function() {
if( typeof callback == 'function' ) {
callback();
}
}, config.path);
}
};
/**
* @param {String} name
* @return {Test}
*/
this.getTestByName = function(name) {
for(var i=0; i < this.tests.length; i++) {
if(this.tests[i].name == name) {
return this.tests[i];
}
}
throw new Error('Test "'+name+'" not found');
};
/**
* @param {Object} success
*/
this.registerSuccessfulTest = function(success) {
this.successes.push(success);
this.log('* '+success.name + ' successfull');
_checkIfFinished(this);
};
/**
* @param {Object} fail
* @param {Error} err - Optional
*/
this.registerFailedTest = function(fail, err) {
this.fails.push(fail);
this.log('- '+fail.name + " FAILED! \n"+fail.message, true);
if(err)
this.log(err);
this.log('');
_checkIfFinished(this);
};
/**
* Get the correct path to script. Throws error if not exists
*
* @param {String} name
* @return {String}
*/
this.scriptNameToPath = function(name) {
var testFileName = name.substring(0, config.testdir.length) == config.testdir ? name : config.testdir+'/'+name;
if(!_hasDokimonExtension(testFileName))
testFileName += TestManager.TEST_EXTENSION;
return testFileName;
};
/**
* Get a list of all available scripts in given directory
*
* @param {String} path
* @return {Array}
*/
this.loadScriptsInDir = function(path) {
var scripts = [];
filesystem.readdirSync(path).forEach(function(f) {
if(_hasDokimonExtension(f))
scripts.push(f.substr(0, f.length - TestManager.TEST_EXTENSION.length));
});
return scripts;
};
/**
* Will output test result to client when test i finished
*
* @access private
* @param {TestManager} manager
*/
var _checkIfFinished = function(manager) {
if(manager.tests.length == (manager.successes.length + manager.fails.length)) {
if(manager.fails.length == 0) {
manager.log("\n------------------------------\n"+colors.green+"Everything is fine :)"+colors.reset);
manager.log('Executed a total of '+manager.successes.length+' successfull tests');
}
else {
manager.log("\n------------------------------\n"+colors.red+"All is not well :("+colors.reset, true);
manager.log(manager.successes.length+' test'+(manager.successes.length > 1 ? 's':'')+' was successfull', true);
manager.log(manager.fails.length+' test'+(manager.fails.length > 1 ? 's':'')+' failed', true);
for(var i=0; i < manager.fails.length; i++)
manager.log(" - "+manager.fails[i].name+" ("+manager.fails[i].message+")", true);
}
manager.log("");
if(typeof testFinishCallback == 'function')
testFinishCallback(manager);
}
};
/**
* @access private
* @param {String} path
* @return {Boolean}
*/
var _hasDokimonExtension = function(path) {
return path.substr(-1 * TestManager.TEST_EXTENSION.length) == TestManager.TEST_EXTENSION;
};
};
/**
* Extension for scripts containing tests
*/
TestManager.TEST_EXTENSION = '.djs';
/** * * * * * * * * * * * * * * * * * * * * * * * *
* @class {Test}
* @param {String} testName - Name of this test
* @param {Object} options - requires url, optional is port:Number, method:String, write:String and headers:Object
* @param {Function} callback - function(response, body)
* @param {Boolean} blocking - Optional, whether or not this test is supposed to finish before running any other tests
* * * * * * * * * * * * * * * * * * * * * * * * */
var Test = function(testName, options, callback, blocking) {
mergeWithDefaultOptions(options);
this.options = options;
this.blocking = blocking;
this.name = testName;
/**
* @param {TestManager} manager
* @param {String} host
* @param {Boolean} [verbose] - Optional
* @param {Function} [callback] - Optional, will be called when request is finished and test callback is called
*/
this.execute = function(host, manager, verbose, callback, basePath) {
var exeFinishCallback = function() {
manager.executed.push(testName);
if( typeof callback == 'function' )
callback();
};
options.host = host;
if( basePath && options.path.indexOf(basePath) !== 0)
options.path = basePath+options.path;
if(options.keepCookies && Test.lastCookies)
options.headers['Cookie'] = Test.lastCookies;
if(verbose)
manager.log("About to request "+host+" for "+testName+" with \n"+_formatJSON(options)+"\n");
var _self = this;
var req = require('http').request(options, function(res) {
var collectedBody = '';
res.setEncoding(options.encoding);
res.on('data', function (body) {
collectedBody += body;
});
res.on('end', function() {
if(verbose)
manager.log(testName+" recieved response from "+options.host+options.path+" "+_formatJSON(res.headers)+"\n");
if(res.headers['set-cookie'] != undefined)
Test.lastCookies = _setToGetCookies(res.headers['set-cookie'], Test.lastCookies ? Test.lastCookies:'');
var loc = res.headers.location == undefined ? res.headers.Location : res.headers.location;
if(loc) {
if( loc.indexOf('/') !== 0 )
loc = '/'+loc;
if(verbose)
manager.log('Redirecting '+testName+' to '+loc);
options.method = 'GET';
options.headers['Content-Length'] = 0;
options.write = '';
options.path = loc;
if(options.path.indexOf(options.host) > -1) {
options.path = options.path.split(options.host)[1];
options.url = options.path;
}
_self.execute(options.host, manager, verbose, exeFinishCallback, basePath);
}
else {
res.url = options.path;
try {
_self.runCallback(res, collectedBody, manager);
manager.registerSuccessfulTest({name : testName, message:''});
exeFinishCallback();
}
catch(e) {
manager.registerFailedTest({name : testName, message:e.message}, e);
exeFinishCallback();
}
}
});
});
req.on('error', function(e) {
manager.registerFailedTest({name : testName, message:''}, e);
exeFinishCallback();
});
if(req.connection != undefined) {
req.connection.setTimeout(options.timeout, function() {
manager.registerFailedTest({name: testName, message : 'Timeout after '+options.timeout+' ms'}, '');
req.abort();
exeFinishCallback();
});
}
req.write(options.write);
req.end();
};
/**
* Runs the test callback when request is finished
* @param {Object} res
* @param {String} collectedBody
* @param {TestManager} manager
*/
this.runCallback = function(res, collectedBody,manager) {
callback(res, collectedBody, manager);
};
/**
* @access private
* @param {Array} responseCookies
* @param {String} currentCookies
* @return {String}
*/
var _setToGetCookies = function(responseCookies, currentCookies) {
var currentCookiesObj = {};
var currentCookieParts = currentCookies.split(';');
var cookies = '', i;
// Build up an object with all current cookies
for(i=0; i < currentCookieParts.length; i++) {
var p = currentCookieParts[i].split('=');
if(p.length > 1)
currentCookiesObj[p[0].trim()] = p[1].trim();
}
// Go through repsonse cookies and determine which
// has expired or new cookies
var now = new Date().getTime();
for(i=0; i < responseCookies.length; i++) {
var cookieParts = responseCookies[i].split(';');
var cookieName = cookieParts[0].split('=')[0];
if(typeof currentCookiesObj[cookieName] != 'undefined') {
// maybe expired
for(var j=0; j < cookieParts.length; j++) {
var cookieArgs = cookieParts[j].split('=');
if(cookieArgs[0] == 'expires') {
var d = new Date(cookieArgs[0]);
if(d.getTime() < now) {
currentCookiesObj[cookieName] = false;
}
}
}
}
cookies += '; '+cookieParts[0];
}
// Append previous cookies (which is not deleted by expire argument)
Object.keys(currentCookiesObj).forEach(function(k) {
var val = currentCookiesObj[k];
if(val)
cookies += '; '+k+'='+val;
});
return cookies.length == 0 ? '': cookies.substr(2, cookies.length);
};
/**
* @access private
* @param {Object} json
* @return {String}
*/
var _formatJSON = function(json) {
var str = JSON.stringify(json);
if(str == '[object Object]') {
str = '';
for(var x in json) {
var type = typeof(json[x]);
str += type +" : "+( type == 'string' || type == 'number' ? json[x]:json[x].toString())+"\n";
}
}
return str.replace(/\,\"/g, ",\n\"");
};
};
/**
* @var {Boolean|String}
*/
Test.lastCookies = false;
/** * * * * * * * * * * * * * * * * * * * * * * * *
* @class {TestFormPost}
* @param {String} testName - Name of this test
* @param {Object} options - requires url, optional is port:Number, method:String, write:String and headers:Object
* @param {Function} callback - function(response, body)
* @param {Boolean} blocking - Optional, whether or not this test is supposed to finish before running any other tests
* * * * * * * * * * * * * * * * * * * * * * * * */
var TestFormPost = function(testName, options, callback, blocking) {
TestFormPost.super_.call(this, testName, options, callback, blocking);
options.headers['Content-Type'] = 'application/x-www-form-urlencoded';
if(typeof(options.write) == 'object')
options.write = querystring.stringify(options.write);
options.headers['Content-Length'] = Buffer.byteLength(options.write, 'utf8');
options.method = 'POST';
};
util.inherits(TestFormPost, Test);
module.exports = {
/**
* @param {Object} config
* @return {TestManager}
*/
TestManager : TestManager,
/**
* @return {Test}
*/
Test : Test,
/**
* @return {TestFormPost}
*/
TestFormPost: TestFormPost,
/**
* @param {Test} t
* @param {Object} config
* @param {Function} testFinishCallback - Optional
*/
runTest : function(t, config, testFinishCallback) {
var manager = new TestManager(config, testFinishCallback);
manager.setTests([t]);
manager.run();
},
TEST_EXTENSION : TestManager.TEST_EXTENSION
};
Jump to Line
Something went wrong with that request. Please try again.