diff --git a/test/acceptance/cache_validator.js b/test/acceptance/cache_validator.js index b03f6d7e..9599b680 100644 --- a/test/acceptance/cache_validator.js +++ b/test/acceptance/cache_validator.js @@ -1,4 +1,4 @@ -var assert = require('assert'); +var assert = require('../support/assert'); var net = require('net'); require(__dirname + '/../test_helper'); var CacheValidator = require(__dirname + '/../../lib/cartodb/cache_validator'); @@ -27,14 +27,18 @@ function VarnishEmu(on_cmd_recieved, test_callback) { }); } -tests['should call purge on varnish when invalidate database'] = function() { - var varnish = new VarnishEmu( - function(cmds) { - assert.ok(cmds.length == 1); - assert.equal('purge obj.http.X-Cache-Channel ~ \"^test_db:(.*test_cache.*)|(table)$\"\n', cmds[0].toString('utf8')); - }, - function(){ - CacheValidator.init('localhost', 1337); - CacheValidator.invalidate_db('test_db', 'test_cache'); +suite('cache_validator', function() { + + test('should call purge on varnish when invalidate database', function(done) { + var varnish = new VarnishEmu(function(cmds) { + assert.ok(cmds.length == 1); + assert.equal('purge obj.http.X-Cache-Channel ~ \"^test_db:(.*test_cache.*)|(table)$\"\n', cmds[0].toString('utf8')); + done(); + }, + function() { + CacheValidator.init('localhost', 1337); + CacheValidator.invalidate_db('test_db', 'test_cache'); + }); }); -} + +}); diff --git a/test/acceptance/server.js b/test/acceptance/server.js index 423f050b..744e6811 100644 --- a/test/acceptance/server.js +++ b/test/acceptance/server.js @@ -1,200 +1,199 @@ -var assert = require('assert'); +var assert = require('../support/assert'); var tests = module.exports = {}; var _ = require('underscore'); var querystring = require('querystring'); require(__dirname + '/../test_helper'); var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/cartodb_windshaft'); -var serverOptions = require(__dirname + '/../../lib/cartodb/server_options'); +var serverOptions = require(__dirname + '/../support/server_options'); var server = new CartodbWindshaft(serverOptions); -tests['true'] = function() { - assert.ok(true); -}; - -tests["get call to server returns 200"] = function(){ - assert.response(server, { - url: '/', - method: 'GET' - },{ - status: 200 +suite('server', function() { + + test("get call to server returns 200", function(done){ + assert.response(server, { + url: '/', + method: 'GET' + },{ + status: 200 + }, function() { done(); }); }); -}; - -tests["get'ing blank style returns default style"] = function(){ - assert.response(server, { - headers: {host: 'vizzuality.localhost.lan'}, - url: '/tiles/my_table/style', - method: 'GET' - },{ - status: 200, - body: '{"style":"#my_table {marker-fill: #FF6600;marker-opacity: 1;marker-width: 8;marker-line-color: white;marker-line-width: 3;marker-line-opacity: 0.9;marker-placement: point;marker-type: ellipse;marker-allow-overlap: true;}"}' + + test("get'ing blank style returns default style", function(done){ + assert.response(server, { + headers: {host: 'vizzuality.localhost.lan'}, + url: '/tiles/my_table/style', + method: 'GET' + },{ + status: 200, + body: '{"style":"#my_table {marker-fill: #FF6600;marker-opacity: 1;marker-width: 8;marker-line-color: white;marker-line-width: 3;marker-line-opacity: 0.9;marker-placement: point;marker-type: ellipse;marker-allow-overlap: true;}"}' + }, function() { done(); }); }); -}; - -tests["post'ing no style returns 400 with errors"] = function(){ - assert.response(server, { - headers: {host: 'vizzuality.localhost.lan'}, - url: '/tiles/my_table/style', - method: 'POST' - },{ - status: 400, - body: '{"error":"must send style information"}' + + test("post'ing no style returns 400 with errors", function(done){ + assert.response(server, { + headers: {host: 'vizzuality.localhost.lan'}, + url: '/tiles/my_table/style', + method: 'POST' + },{ + status: 400, + body: '{"error":"must send style information"}' + }, function() { done(); }); }); -}; - -tests["post'ing bad style returns 400 with error"] = function(){ - assert.response(server, { - url: '/tiles/my_table3/style', - method: 'POST', - headers: {host: 'vizzuality.localhost.lan', 'Content-Type': 'application/x-www-form-urlencoded' }, - data: querystring.stringify({style: '#my_table3{backgxxxxxround-color:#fff;}'}) - },{ - status: 500, - body: JSON.stringify(['style.mss:1:11 Unrecognized rule: backgxxxxxround-color']) + + test("post'ing bad style returns 400 with error", function(done){ + assert.response(server, { + url: '/tiles/my_table3/style', + method: 'POST', + headers: {host: 'vizzuality.localhost.lan', 'Content-Type': 'application/x-www-form-urlencoded' }, + data: querystring.stringify({style: '#my_table3{backgxxxxxround-color:#fff;}'}) + },{ + status: 500, + body: JSON.stringify(['style.mss:1:11 Unrecognized rule: backgxxxxxround-color']) + }, function() { done(); }); }); -}; - -tests["post'ing multiple bad styles returns 400 with error array"] = function(){ - assert.response(server, { - url: '/tiles/my_table4/style', - method: 'POST', - headers: {host: 'vizzuality.localhost.lan', 'Content-Type': 'application/x-www-form-urlencoded' }, - data: querystring.stringify({style: '#my_table4{backgxxxxxround-color:#fff;foo:bar}'}) - },{ - status: 500, - body: JSON.stringify([ 'style.mss:1:11 Unrecognized rule: backgxxxxxround-color', 'style.mss:1:38 Unrecognized rule: foo' ]) + + test("post'ing multiple bad styles returns 400 with error array", function(done){ + assert.response(server, { + url: '/tiles/my_table4/style', + method: 'POST', + headers: {host: 'vizzuality.localhost.lan', 'Content-Type': 'application/x-www-form-urlencoded' }, + data: querystring.stringify({style: '#my_table4{backgxxxxxround-color:#fff;foo:bar}'}) + },{ + status: 500, + body: JSON.stringify([ 'style.mss:1:11 Unrecognized rule: backgxxxxxround-color', 'style.mss:1:38 Unrecognized rule: foo' ]) + }, function() { done(); }); }); -}; - -tests["post'ing good style returns 200"] = function(){ - assert.response(server, { - url: '/tiles/my_table5/style', - method: 'POST', - headers: {host: 'vizzuality.localhost.lan', 'Content-Type': 'application/x-www-form-urlencoded' }, - data: querystring.stringify({style: '#my_table5{background-color:#fff;}'}) - },{ - status: 200 + + test("post'ing good style returns 200", function(done){ + assert.response(server, { + url: '/tiles/my_table5/style', + method: 'POST', + headers: {host: 'vizzuality.localhost.lan', 'Content-Type': 'application/x-www-form-urlencoded' }, + data: querystring.stringify({style: '#my_table5{background-color:#fff;}'}) + },{ + status: 200 + }, function() { done(); }); }); -}; + + test("post'ing good style returns 200 then getting returns original style", function(done){ + var style = '#my_table5{background-color:#fff;}'; + assert.response(server, { + url: '/tiles/my_table5/style', + method: 'POST', + headers: {host: 'vizzuality.localhost.lan', 'Content-Type': 'application/x-www-form-urlencoded' }, + data: querystring.stringify({style: style}) + },{ + status: 200 + }, function() { -tests["post'ing good style returns 200 then getting returns original style"] = function(){ - var style = '#my_table5{background-color:#fff;}'; - assert.response(server, { - url: '/tiles/my_table5/style', - method: 'POST', - headers: {host: 'vizzuality.localhost.lan', 'Content-Type': 'application/x-www-form-urlencoded' }, - data: querystring.stringify({style: style}) - },{ - status: 200 + assert.response(server, { + headers: {host: 'vizzuality.localhost.lan'}, + url: '/tiles/my_table5/style', + method: 'GET' + },{ + status: 200, + body: JSON.stringify({style: style}) + }, function() { done(); }); + + }); + }); - - - assert.response(server, { - headers: {host: 'vizzuality.localhost.lan'}, - url: '/tiles/my_table5/style', - method: 'GET' - },{ - status: 200, - body: JSON.stringify({style: style}) + + test("get'ing blank infowindow returns blank", function(done){ + assert.response(server, { + headers: {host: 'vizzuality.localhost.lan'}, + url: '/tiles/my_tablez/infowindow', + method: 'GET' + },{ + status: 200, + body: '{"infowindow":null}' + }, function() { done(); }); }); -}; - -tests["get'ing blank infowindow returns blank"] = function(){ - assert.response(server, { - headers: {host: 'vizzuality.localhost.lan'}, - url: '/tiles/my_tablez/infowindow', - method: 'GET' - },{ - status: 200, - body: '{"infowindow":null}' + + test("get'ing blank infowindow with callback returns blank with callback", function(done){ + assert.response(server, { + headers: {host: 'vizzuality.localhost.lan'}, + url: '/tiles/my_tablez/infowindow?callback=simon', + method: 'GET' + },{ + status: 200, + body: 'simon({"infowindow":null});' + }, function() { done(); }); }); -}; - -tests["get'ing blank infowindow with callback returns blank with callback"] = function(){ - assert.response(server, { - headers: {host: 'vizzuality.localhost.lan'}, - url: '/tiles/my_tablez/infowindow?callback=simon', - method: 'GET' - },{ - status: 200, - body: 'simon({"infowindow":null});' + + + test("get'ing completed infowindow with callback returns information with callback", function(done){ + assert.response(server, { + headers: {host: 'vizzuality.localhost.lan'}, + url: '/tiles/my_table/infowindow?callback=simon', + method: 'GET' + },{ + status: 200, + body: 'simon({"infowindow":"this, that, the other"});' + }, function() { done(); }); }); -}; - - -tests["get'ing completed infowindow with callback returns information with callback"] = function(){ - assert.response(server, { - headers: {host: 'vizzuality.localhost.lan'}, - url: '/tiles/my_table/infowindow?callback=simon', - method: 'GET' - },{ - status: 200, - body: 'simon({"infowindow":"this, that, the other"});' + + test("get'ing a tile with default style should return an image", function(done){ + assert.response(server, { + headers: {host: 'vizzuality.localhost.lan'}, + url: '/tiles/gadm4/6/31/24.png?geom_type=polygon', + method: 'GET' + },{ + status: 200, + headers: { 'Content-Type': 'image/png' } + }, function() { done(); }); }); -}; - -tests["get'ing a tile with default style should return an image"] = function(){ - assert.response(server, { - headers: {host: 'vizzuality.localhost.lan'}, - url: '/tiles/gadm4/6/31/24.png?geom_type=polygon', - method: 'GET' - },{ - status: 200, - headers: { 'Content-Type': 'image/png' } + + + test("get'ing a json with default style should return an grid", function(done){ + assert.response(server, { + headers: {host: 'vizzuality.localhost.lan'}, + url: '/tiles/gadm4/6/31/24.grid.json', + method: 'GET' + },{ + status: 200, + headers: { 'Content-Type': 'text/javascript; charset=utf-8; charset=utf-8' } + }, function() { done(); }); }); -}; - - -tests["get'ing a json with default style should return an grid"] = function(){ - assert.response(server, { - headers: {host: 'vizzuality.localhost.lan'}, - url: '/tiles/gadm4/6/31/24.grid.json', - method: 'GET' - },{ - status: 200, - headers: { 'Content-Type': 'text/javascript; charset=utf-8; charset=utf-8' } + + test("get'ing a json with default style and sql should return a constrained grid", function(done){ + var sql = querystring.stringify({sql: "SELECT * FROM gadm4 WHERE codineprov = '08'"}) + assert.response(server, { + headers: {host: 'vizzuality.localhost.lan'}, + url: '/tiles/gadm4/6/31/24.grid.json?' + sql, + method: 'GET' + },{ + status: 200, + headers: { 'Content-Type': 'text/javascript; charset=utf-8; charset=utf-8' } + }, function() { done(); }); }); -}; - -tests["get'ing a json with default style and sql should return a constrained grid"] = function(){ - var sql = querystring.stringify({sql: "SELECT * FROM gadm4 WHERE codineprov = '08'"}) - assert.response(server, { - headers: {host: 'vizzuality.localhost.lan'}, - url: '/tiles/gadm4/6/31/24.grid.json?' + sql, - method: 'GET' - },{ - status: 200, - headers: { 'Content-Type': 'text/javascript; charset=utf-8; charset=utf-8' } + + + test("get'ing a tile with default style and sql should return a constrained image", function(done){ + var sql = querystring.stringify({sql: "SELECT * FROM gadm4 WHERE codineprov = '08'"}); + assert.response(server, { + headers: {host: 'vizzuality.localhost.lan'}, + url: '/tiles/gadm4/6/31/24.png?' + sql, + method: 'GET' + },{ + status: 200, + headers: { 'Content-Type': 'image/png' } + }, function() { done(); }); }); -}; - - -tests["get'ing a tile with default style and sql should return a constrained image"] = function(){ - var sql = querystring.stringify({sql: "SELECT * FROM gadm4 WHERE codineprov = '08'"}); - assert.response(server, { - headers: {host: 'vizzuality.localhost.lan'}, - url: '/tiles/gadm4/6/31/24.png?' + sql, - method: 'GET' - },{ - status: 200, - headers: { 'Content-Type': 'image/png' } + + + test("get'ing a tile with default style and complex sql should return a constrained image", function(done){ + var sql = querystring.stringify({sql: "SELECT * FROM gadm4 WHERE codineprov = '08' AND codccaa > 60"}) + assert.response(server, { + headers: {host: 'vizzuality.localhost.lan'}, + url: '/tiles/gadm4/6/31/24.png?' + sql, + method: 'GET' + },{ + status: 200, + headers: { 'Content-Type': 'image/png' } + }, function() { done(); }); }); -}; - - -tests["get'ing a tile with default style and complex sql should return a constrained image"] = function(){ - var sql = querystring.stringify({sql: "SELECT * FROM gadm4 WHERE codineprov = '08' AND codccaa > 60"}) - assert.response(server, { - headers: {host: 'vizzuality.localhost.lan'}, - url: '/tiles/gadm4/6/31/24.png?' + sql, - method: 'GET' - },{ - status: 200, - headers: { 'Content-Type': 'image/png' } - }); -}; - - - + +}); diff --git a/test/support/assert.js b/test/support/assert.js new file mode 100644 index 00000000..bd1c45ce --- /dev/null +++ b/test/support/assert.js @@ -0,0 +1,200 @@ +// Cribbed from the ever prolific Konstantin Kaefer +// https://github.com/mapbox/tilelive-mapnik/blob/master/test/support/assert.js + +var fs = require('fs'); +var http = require('http'); +var path = require('path'); +var exec = require('child_process').exec; + +var assert = module.exports = exports = require('assert'); + +assert.imageEqualsFile = function(buffer, file_b, callback) { + if (!callback) callback = function(err) { if (err) throw err; }; + file_b = path.resolve(file_b); + var file_a = '/tmp/' + (Math.random() * 1e16); + var err = fs.writeFileSync(file_a, buffer, 'binary'); + if (err) throw err; + + exec('compare -metric PSNR "' + file_a + '" "' + + file_b + '" /dev/null', function(err, stdout, stderr) { + if (err) { + fs.unlinkSync(file_a); + callback(err); + } else { + stderr = stderr.trim(); + if (stderr === 'inf') { + fs.unlinkSync(file_a); + callback(null); + } else { + var similarity = parseFloat(stderr); + var err = new Error('Images not equal(' + similarity + '): ' + + file_a + ' ' + file_b); + err.similarity = similarity; + callback(err); + } + } + }); +}; + + +/** + * Assert response from `server` with + * the given `req` object and `res` assertions object. + * + * @param {Server} server + * @param {Object} req + * @param {Object|Function} res + * @param {String} msg + */ +assert.response = function(server, req, res, msg){ + var port = 5555; + function check(){ + try { + server.__port = server.address().port; + server.__listening = true; + } catch (err) { + process.nextTick(check); + return; + } + if (server.__deferred) { + server.__deferred.forEach(function(args){ + assert.response.apply(assert, args); + }); + server.__deferred = null; + } + } + + // Check that the server is ready or defer + if (!server.fd) { + server.__deferred = server.__deferred || []; + server.listen(server.__port = port++, '127.0.0.1', check); + } else if (!server.__port) { + server.__deferred = server.__deferred || []; + process.nextTick(check); + } + + // The socket was created but is not yet listening, so keep deferring + if (!server.__listening) { + server.__deferred.push(arguments); + return; + } + + // Callback as third or fourth arg + var callback = typeof res === 'function' + ? res + : typeof msg === 'function' + ? msg + : function(){}; + + // Default messate to test title + if (typeof msg === 'function') msg = null; + msg = msg || assert.testTitle; + msg += '. '; + + // Pending responses + server.__pending = server.__pending || 0; + server.__pending++; + + // Create client + if (!server.fd) { + server.listen(server.__port = port++, '127.0.0.1', issue); + } else { + issue(); + } + + function issue(){ + + // Issue request + var timer, + method = req.method || 'GET', + status = res.status || res.statusCode, + data = req.data || req.body, + requestTimeout = req.timeout || 0, + encoding = req.encoding || 'utf8'; + + var request = http.request({ + host: '127.0.0.1', + port: server.__port, + path: req.url, + method: method, + headers: req.headers + }); + + var check = function() { + if (--server.__pending === 0) { + server.close(); + server.__listening = false; + } + }; + + // Timeout + if (requestTimeout) { + timer = setTimeout(function(){ + check(); + delete req.timeout; + assert.fail(msg + 'Request timed out after ' + requestTimeout + 'ms.'); + }, requestTimeout); + } + + if (data) request.write(data); + + request.on('response', function(response){ + response.body = ''; + response.setEncoding(encoding); + response.on('data', function(chunk){ response.body += chunk; }); + response.on('end', function(){ + if (timer) clearTimeout(timer); + + // Assert response body + if (res.body !== undefined) { + var eql = res.body instanceof RegExp + ? res.body.test(response.body) + : res.body === response.body; + assert.ok( + eql, + msg + 'Invalid response body.\n' + + ' Expected: ' + res.body + '\n' + + ' Got: ' + response.body + ); + } + + // Assert response status + if (typeof status === 'number') { + assert.equal( + response.statusCode, + status, + msg + 'Invalid response status code.\n' + + ' Expected: [green]{' + status + '}\n' + + ' Got: [red]{' + response.statusCode + '}' + ); + } + + // Assert response headers + if (res.headers) { + var keys = Object.keys(res.headers); + for (var i = 0, len = keys.length; i < len; ++i) { + var name = keys[i], + actual = response.headers[name.toLowerCase()], + expected = res.headers[name], + eql = expected instanceof RegExp + ? expected.test(actual) + : expected == actual; + assert.ok( + eql, + msg + 'Invalid response header [bold]{' + name + '}.\n' + + ' Expected: [green]{' + expected + '}\n' + + ' Got: [red]{' + actual + '}' + ); + } + } + + // Callback + callback(response); + check(); + }); + }); + + request.end(); + } +}; + diff --git a/test/support/server_options.js b/test/support/server_options.js new file mode 100644 index 00000000..aac0f1ca --- /dev/null +++ b/test/support/server_options.js @@ -0,0 +1,126 @@ +var _ = require('underscore') + , Step = require('step') + , cartoData = require('../../lib/cartodb/carto_data'); + +module.exports = function(){ + var me = { + base_url: '/tiles/:table', + grainstore: {datasource: global.environment.postgres}, + redis: global.environment.redis, + enable_cors: global.environment.enable_cors, + varnish_host: global.environment.varnish.host, + varnish_port: global.environment.varnish.port, + cache_enabled: global.environment.cache_enabled, + log_format: '[:date] :req[X-Real-IP] \033[90m:method\033[0m \033[36m:req[Host]:url\033[0m \033[90m:status :response-time ms -> :res[Content-Type]\033[0m' + }; + + /** + * Whitelist input and get database name & default geometry type from + * subdomain/user metadata held in CartoDB Redis + * @param req - standard express request obj. Should have host & table + * @param callback + */ + me.req2params = function(req, callback){ + + // Whitelist query parameters and attach format + var good_query = ['sql', 'geom_type', 'cache_buster','callback', 'interactivity', 'map_key', 'style']; + var bad_query = _.difference(_.keys(req.query), good_query); + + _.each(bad_query, function(key){ delete req.query[key]; }); + req.params = _.extend({}, req.params); // shuffle things as request is a strange array/object + + // bring all query values onto req.params object + _.extend(req.params, req.query); + + // for cartodb, ensure interactivity is cartodb_id or user specified + req.params.interactivity = req.params.interactivity || 'cartodb_id'; + + Step( + function getPrivacy(){ + cartoData.authorize(req, this); + }, + function gatekeep(err, data){ + if(err) throw err; + if(data === "0") throw new Error("Sorry, you are unauthorized"); + return data; + }, + function getDatabase(err, data){ + if(err) throw err; + + cartoData.getDatabase(req, this); + }, + function getGeometryType(err, data){ + if (err) throw err; + _.extend(req.params, {dbname:data}); + + cartoData.getGeometryType(req, this); + }, + function finishSetup(err, data){ + if (!_.isNull(data)) + _.extend(req.params, {geom_type: data}); + + callback(err, req); + } + ); + }; + + /** + * Little helper method to get the current list of infowindow variables and return to client + * @param req + * @param callback + */ + me.getInfowindow = function(req, callback){ + var that = this; + + Step( + function(){ + that.req2params(req, this); + }, + function(err, data){ + if (err) throw err; + cartoData.getInfowindow(data, callback); + } + ); + }; + + /** + * Little helper method to get map metadata and return to client + * @param req + * @param callback + */ + me.getMapMetadata = function(req, callback){ + var that = this; + + Step( + function(){ + that.req2params(req, this); + }, + function(err, data){ + if (err) throw err; + cartoData.getMapMetadata(data, callback); + } + ); + }; + + /** + * Helper to clear out tile cache on request + * @param req + * @param callback + */ + me.flushCache = function(req, Cache, callback){ + var that = this; + + Step( + function(){ + that.req2params(req, this); + }, + function(err, data){ + if (err) throw err; + Cache.invalidate_db(req.params.dbname, req.params.table); + callback(null, true); + } + ); + }; + + return me; +}(); diff --git a/test/unit/cartodb/redis_pool.test.js b/test/unit/cartodb/redis_pool.test.js index eec0ee7b..0833db09 100644 --- a/test/unit/cartodb/redis_pool.test.js +++ b/test/unit/cartodb/redis_pool.test.js @@ -1,54 +1,65 @@ -var assert = require('assert') +var assert = require('../../support/assert') , _ = require('underscore') , RedisPool = require('../../../lib/cartodb/redis_pool') , tests = module.exports = {}; -// configure redis pool instance to use in tests -var test_opts = { - max: 10, - idleTimeoutMillis: 1, - reapIntervalMillis: 1 -}; +suite('redis_pool', function() { -var redis_pool = new RedisPool(test_opts); + // configure redis pool instance to use in tests + var test_opts = { + max: 10, + idleTimeoutMillis: 1, + reapIntervalMillis: 1 + }; + + var redis_pool = new RedisPool(test_opts); -tests['truth'] = function(){ - assert.ok(true, 'it is'); -}; + test('RedisPool object exists', function(done){ + assert.ok(RedisPool); + done(); + }); + + test('RedisPool can create new redis_pool objects with default settings', function(done){ + var redis_pool = new RedisPool(); + done(); + }); + + test('RedisPool can create new redis_pool objects with specific settings', function(done){ + var redis_pool = new RedisPool(_.extend({host:'127.0.0.1', port: '6379'}, test_opts)); + done(); + }); + + + test('pool object has an acquire function', function(done){ + var found=false; + var functions = _.functions(redis_pool); + for (var i=0; i