diff --git a/.jshintignore b/.jshintignore new file mode 100644 index 00000000..d781eace --- /dev/null +++ b/.jshintignore @@ -0,0 +1,4 @@ +test/results/ +test/monkey/ +test/benchmark.js +test/support/ diff --git a/Makefile b/Makefile index 2bd19912..641c291a 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ test: config/environments/test.js jshint: @echo "***jshint***" - @./node_modules/.bin/jshint lib/ app.js + @./node_modules/.bin/jshint lib/ test/ app.js test-all: jshint test diff --git a/NEWS.md b/NEWS.md index 23308c58..1b00ae04 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,9 +1,22 @@ # Changelog -## 2.1.4 +## 2.2.0 Released 2015-mm-dd +Enhancements: + - jshint is run against tests + - tests moved to mocha's `describe` + +New features: + - Fastly surrogate keys invalidation for named maps + * **New configuration entry**: `fastly`. Check example configurations for more information. + - `PgQueryRunner` extracted from `QueryTablesApi` so it can be reused in new `TablesExtentApi` + - New top level element, `view`, in templates that holds attributes to identify the map scene. + - Named maps static preview in /api/v1/map/static/named/:name/:width/:height.:format endpoint + * It will be invalidated if the named map changes + * But have a Cache-Control header with a 2 hours max-age, won't be invalidated on data changes + ## 2.1.3 diff --git a/config/environments/development.js.example b/config/environments/development.js.example index 55b3739d..405c0017 100644 --- a/config/environments/development.js.example +++ b/config/environments/development.js.example @@ -181,6 +181,15 @@ var config = { ttl: 86400, layergroupTtl: 86400 // the max-age for cache-control header in layergroup responses } + // this [OPTIONAL] configuration enables invalidating by surrogate key in fastly + ,fastly: { + // whether the invalidation is enabled or not + enabled: false, + // the fastly api key + apiKey: 'wadus_api_key', + // the service that will get surrogate key invalidation + serviceId: 'wadus_service_id' + } // If useProfiler is true every response will be served with an // X-Tiler-Profile header containing elapsed timing for various // steps taken for producing the response. diff --git a/config/environments/production.js.example b/config/environments/production.js.example index 731ff573..516e18a3 100644 --- a/config/environments/production.js.example +++ b/config/environments/production.js.example @@ -175,6 +175,15 @@ var config = { ttl: 86400, layergroupTtl: 86400 // the max-age for cache-control header in layergroup responses } + // this [OPTIONAL] configuration enables invalidating by surrogate key in fastly + ,fastly: { + // whether the invalidation is enabled or not + enabled: false, + // the fastly api key + apiKey: 'wadus_api_key', + // the service that will get surrogate key invalidation + serviceId: 'wadus_service_id' + } // If useProfiler is true every response will be served with an // X-Tiler-Profile header containing elapsed timing for various // steps taken for producing the response. diff --git a/config/environments/staging.js.example b/config/environments/staging.js.example index c3c9d2a1..6495a1da 100644 --- a/config/environments/staging.js.example +++ b/config/environments/staging.js.example @@ -175,6 +175,15 @@ var config = { ttl: 86400, layergroupTtl: 86400 // the max-age for cache-control header in layergroup responses } + // this [OPTIONAL] configuration enables invalidating by surrogate key in fastly + ,fastly: { + // whether the invalidation is enabled or not + enabled: false, + // the fastly api key + apiKey: 'wadus_api_key', + // the service that will get surrogate key invalidation + serviceId: 'wadus_service_id' + } // If useProfiler is true every response will be served with an // X-Tiler-Profile header containing elapsed timing for various // steps taken for producing the response. diff --git a/config/environments/test.js.example b/config/environments/test.js.example index aa9e3a1a..39fb2dbb 100644 --- a/config/environments/test.js.example +++ b/config/environments/test.js.example @@ -177,6 +177,15 @@ var config = { ttl: 86400, layergroupTtl: 86400 // the max-age for cache-control header in layergroup responses } + // this [OPTIONAL] configuration enables invalidating by surrogate key in fastly + ,fastly: { + // whether the invalidation is enabled or not + enabled: false, + // the fastly api key + apiKey: 'wadus_api_key', + // the service that will get surrogate key invalidation + serviceId: 'wadus_service_id' + } // If useProfiler is true every response will be served with an // X-Tiler-Profile header containing elapsed timing for various // steps taken for producing the response. diff --git a/lib/cartodb/api/query_tables_api.js b/lib/cartodb/api/query_tables_api.js index 63b87158..fc0e2932 100644 --- a/lib/cartodb/api/query_tables_api.js +++ b/lib/cartodb/api/query_tables_api.js @@ -1,9 +1,5 @@ -var PSQL = require('cartodb-psql'); -var step = require('step'); - -function QueryTablesApi(pgConnection, metadataBackend) { - this.pgConnection = pgConnection; - this.metadataBackend = metadataBackend; +function QueryTablesApi(pgQueryRunner) { + this.pgQueryRunner = pgQueryRunner; } var affectedTableRegexCache = { @@ -20,7 +16,7 @@ QueryTablesApi.prototype.getAffectedTablesInQuery = function (username, sql, cal var query = 'SELECT CDB_QueryTables($windshaft$' + prepareSql(sql) + '$windshaft$)'; - this.runQuery(username, query, handleAffectedTablesInQueryRows, callback); + this.pgQueryRunner.run(username, query, handleAffectedTablesInQueryRows, callback); }; function handleAffectedTablesInQueryRows(err, rows, callback) { @@ -46,7 +42,7 @@ QueryTablesApi.prototype.getAffectedTablesAndLastUpdatedTime = function (usernam 'WHERE m.tabname = any ((SELECT tablenames from querytables)::regclass[])' ].join(' '); - this.runQuery(username, query, handleAffectedTablesAndLastUpdatedTimeRows, callback); + this.pgQueryRunner.run(username, query, handleAffectedTablesAndLastUpdatedTimeRows, callback); }; function handleAffectedTablesAndLastUpdatedTimeRows(err, rows, callback) { @@ -69,43 +65,6 @@ function handleAffectedTablesAndLastUpdatedTimeRows(err, rows, callback) { }); } - -QueryTablesApi.prototype.runQuery = function(username, query, queryHandler, callback) { - var self = this; - - var params = {}; - - step( - function setAuth() { - self.pgConnection.setDBAuth(username, params, this); - }, - function setConn(err) { - if (err) { - throw err; - } - self.pgConnection.setDBConn(username, params, this); - }, - function executeQuery(err) { - if (err) { - throw err; - } - var psql = new PSQL({ - user: params.dbuser, - pass: params.dbpass, - host: params.dbhost, - port: params.dbport, - dbname: params.dbname - }); - psql.query(query, function(err, resultSet) { - resultSet = resultSet || {}; - var rows = resultSet.rows || []; - queryHandler(err, rows, callback); - }); - } - ); -}; - - function prepareSql(sql) { return sql .replace(affectedTableRegexCache.bbox, 'ST_MakeEnvelope(0,0,0,0)') diff --git a/lib/cartodb/api/tables_extent_api.js b/lib/cartodb/api/tables_extent_api.js new file mode 100644 index 00000000..34890dbb --- /dev/null +++ b/lib/cartodb/api/tables_extent_api.js @@ -0,0 +1,54 @@ +function TablesExtentApi(pgQueryRunner) { + this.pgQueryRunner = pgQueryRunner; +} + +module.exports = TablesExtentApi; + +/** + * Given a username and a list of tables it will return the estimated extent in SRID 4326 for all the tables based on + * the_geom_webmercator (SRID 3857) column. + * + * @param {String} username + * @param {Array} tableNames The named can be schema qualified, so this accepts both `schema_name.table_name` and + * `table_name` format as valid input + * @param {Function} callback function(err, result) {Object} result with `west`, `south`, `east`, `north` + */ +TablesExtentApi.prototype.getBounds = function (username, tableNames, callback) { + var estimatedExtentSQLs = tableNames.map(function(tableName) { + var schemaTable = tableName.split('.'); + if (schemaTable.length > 1) { + return "ST_EstimatedExtent('" + schemaTable[0] + "', '" + schemaTable[1] + "', 'the_geom_webmercator')"; + } + return "ST_EstimatedExtent('" + schemaTable[0] + "', 'the_geom_webmercator')"; + }); + + var query = [ + "WITH ext as (" + + "SELECT ST_Transform(ST_SetSRID(ST_Extent(ST_Union(ARRAY[", + estimatedExtentSQLs.join(','), + "])), 3857), 4326) geom)", + "SELECT", + "ST_XMin(geom) west,", + "ST_YMin(geom) south,", + "ST_XMax(geom) east,", + "ST_YMax(geom) north", + "FROM ext" + ].join(' '); + + this.pgQueryRunner.run(username, query, handleBoundsResult, callback); +}; + +function handleBoundsResult(err, rows, callback) { + if (err){ + var msg = err.message ? err.message : err; + callback(new Error('could not fetch source tables: ' + msg)); + return; + } + var result = null; + if (rows.length > 0) { + result = { + bounds: rows[0] + }; + } + callback(null, result); +} diff --git a/lib/cartodb/backends/pg_query_runner.js b/lib/cartodb/backends/pg_query_runner.js new file mode 100644 index 00000000..ed6c88db --- /dev/null +++ b/lib/cartodb/backends/pg_query_runner.js @@ -0,0 +1,44 @@ +var PSQL = require('cartodb-psql'); +var step = require('step'); + +function PgQueryRunner(pgConnection) { + this.pgConnection = pgConnection; +} + +module.exports = PgQueryRunner; + + +PgQueryRunner.prototype.run = function(username, query, queryHandler, callback) { + var self = this; + + var params = {}; + + step( + function setAuth() { + self.pgConnection.setDBAuth(username, params, this); + }, + function setConn(err) { + if (err) { + throw err; + } + self.pgConnection.setDBConn(username, params, this); + }, + function executeQuery(err) { + if (err) { + throw err; + } + var psql = new PSQL({ + user: params.dbuser, + pass: params.dbpass, + host: params.dbhost, + port: params.dbport, + dbname: params.dbname + }); + psql.query(query, function(err, resultSet) { + resultSet = resultSet || {}; + var rows = resultSet.rows || []; + queryHandler(err, rows, callback); + }); + } + ); +}; diff --git a/lib/cartodb/cache/backend/fastly.js b/lib/cartodb/cache/backend/fastly.js new file mode 100644 index 00000000..91cf162a --- /dev/null +++ b/lib/cartodb/cache/backend/fastly.js @@ -0,0 +1,18 @@ +var FastlyPurge = require('fastly-purge'); + +function FastlyCacheBackend(apiKey, serviceId, softPurge) { + this.serviceId = serviceId; + this.fastlyPurge = new FastlyPurge(apiKey, { softPurge: softPurge || true }); +} + +module.exports = FastlyCacheBackend; + +/** + * @param cacheObject should respond to `key() -> String` method + * @param {Function} callback + */ +FastlyCacheBackend.prototype.invalidate = function(cacheObject, callback) { + this.fastlyPurge.key(this.serviceId, cacheObject.key(), callback); +}; + +module.exports = FastlyCacheBackend; diff --git a/lib/cartodb/cache/surrogate_keys_cache.js b/lib/cartodb/cache/surrogate_keys_cache.js index 53b9f45a..77147917 100644 --- a/lib/cartodb/cache/surrogate_keys_cache.js +++ b/lib/cartodb/cache/surrogate_keys_cache.js @@ -1,9 +1,11 @@ +var queue = require('queue-async'); + /** - * @param cacheBackend should respond to `invalidate(cacheObject, callback)` method + * @param {Array|Object} cacheBackends each backend backend should respond to `invalidate(cacheObject, callback)` method * @constructor */ -function SurrogateKeysCache(cacheBackend) { - this.cacheBackend = cacheBackend; +function SurrogateKeysCache(cacheBackends) { + this.cacheBackends = Array.isArray(cacheBackends) ? cacheBackends : [cacheBackends]; } module.exports = SurrogateKeysCache; @@ -22,5 +24,18 @@ SurrogateKeysCache.prototype.tag = function(response, cacheObject) { * @param {Function} callback */ SurrogateKeysCache.prototype.invalidate = function(cacheObject, callback) { - this.cacheBackend.invalidate(cacheObject, callback); + var invalidationQueue = queue(this.cacheBackends.length); + + this.cacheBackends.forEach(function(cacheBackend) { + invalidationQueue.defer(function(cacheBackend, done) { + cacheBackend.invalidate(cacheObject, done); + }, cacheBackend); + }); + + invalidationQueue.awaitAll(function(err, result) { + if (err) { + return callback(err); + } + callback(null, result); + }); }; diff --git a/lib/cartodb/cartodb_windshaft.js b/lib/cartodb/cartodb_windshaft.js index 523fdf44..b9429a2a 100644 --- a/lib/cartodb/cartodb_windshaft.js +++ b/lib/cartodb/cartodb_windshaft.js @@ -4,6 +4,11 @@ var Windshaft = require('windshaft'); var os = require('os'); var HealthCheck = require('./monitoring/health_check'); +var SurrogateKeysCache = require('./cache/surrogate_keys_cache'); +var NamedMapsCacheEntry = require('./cache/model/named_maps_entry'); +var VarnishHttpCacheBackend = require('./cache/backend/varnish_http'); +var FastlyCacheBackend = require('./cache/backend/fastly'); + if ( ! process.env.PGAPPNAME ) process.env.PGAPPNAME='cartodb_tiler'; @@ -30,28 +35,41 @@ var CartodbWindshaft = function(serverOptions) { // var template_baseurl = global.environment.base_url_templated || '(?:/maps/named|/tiles/template)'; - var SurrogateKeysCache = require('./cache/surrogate_keys_cache'), - NamedMapsCacheEntry = require('./cache/model/named_maps_entry'), - VarnishHttpCacheBackend = require('./cache/backend/varnish_http'), - varnishHttpCacheBackend = new VarnishHttpCacheBackend( - serverOptions.varnish_host, - serverOptions.varnish_http_port - ), - surrogateKeysCache = new SurrogateKeysCache(varnishHttpCacheBackend); + var surrogateKeysCacheBackends = []; + + if (serverOptions.varnish_purge_enabled) { + surrogateKeysCacheBackends.push( + new VarnishHttpCacheBackend(serverOptions.varnish_host, serverOptions.varnish_http_port) + ); + } + + if (!!serverOptions.fastly.enabled && !!serverOptions.fastly.apiKey && !!serverOptions.fastly.serviceId) { + surrogateKeysCacheBackends.push( + new FastlyCacheBackend(serverOptions.fastly.apiKey, serverOptions.fastly.serviceId) + ); + } + + var surrogateKeysCache = new SurrogateKeysCache(surrogateKeysCacheBackends); function invalidateNamedMap (owner, templateName) { + var startTime = Date.now(); surrogateKeysCache.invalidate(new NamedMapsCacheEntry(owner, templateName), function(err) { + var logMessage = JSON.stringify({ + username: owner, + type: 'named_map_invalidation', + elapsed: Date.now() - startTime, + error: !!err ? JSON.stringify(err.message) : undefined + }); if (err) { - console.warn('Cache: surrogate key invalidation failed'); + console.warn(logMessage); + } else { + console.info(logMessage); } }); } - - if (serverOptions.varnish_purge_enabled) { - ['update', 'delete'].forEach(function(eventType) { - templateMaps.on(eventType, invalidateNamedMap); - }); - } + ['update', 'delete'].forEach(function(eventType) { + templateMaps.on(eventType, invalidateNamedMap); + }); // boot var ws = new Windshaft.Server(serverOptions); @@ -72,6 +90,7 @@ var CartodbWindshaft = function(serverOptions) { '/version', // See https://github.com/CartoDB/Windshaft-cartodb/issues/176 serverOptions.base_url_mapconfig, + serverOptions.base_url_mapconfig + '/static/named/:template_id/:width/:height.:format', template_baseurl, template_baseurl + '/:template_id', template_baseurl + '/:template_id/jsonp' @@ -139,18 +158,30 @@ var CartodbWindshaft = function(serverOptions) { * Routing ******************************************************************************************************************/ - var TemplateMapsController = require('./controllers/template_maps'), - templateMapsController = new TemplateMapsController( + var NamedMapsController = require('./controllers/named_maps'), + namedMapsController = new NamedMapsController( ws, serverOptions, templateMaps, cartoData, template_baseurl, - surrogateKeysCache, - NamedMapsCacheEntry, - serverOptions.pgConnection + surrogateKeysCache ); - templateMapsController.register(ws); + namedMapsController.register(ws); + + var TablesExtentApi = require('./api/tables_extent_api'); + var tablesExtentApi = new TablesExtentApi(serverOptions.pgQueryRunner); + + var NamedStaticMapsController = require('./controllers/named_static_maps'); + var namedStaticMapsController = new NamedStaticMapsController( + ws, + serverOptions, + templateMaps, + ws.staticMapBackend, + surrogateKeysCache, + tablesExtentApi + ); + namedStaticMapsController.register(ws); /******************************************************************************************************************* * END Routing diff --git a/lib/cartodb/controllers/template_maps.js b/lib/cartodb/controllers/named_maps.js similarity index 85% rename from lib/cartodb/controllers/template_maps.js rename to lib/cartodb/controllers/named_maps.js index 29026c41..1ea98843 100644 --- a/lib/cartodb/controllers/template_maps.js +++ b/lib/cartodb/controllers/named_maps.js @@ -1,25 +1,24 @@ var step = require('step'); var assert = require('assert'); var _ = require('underscore'); +var templateName = require('../template_maps').templateName; var CdbRequest = require('../models/cdb_request'); +var NamedMapsCacheEntry = require('../cache/model/named_maps_entry'); -function TemplateMapsController(app, serverOptions, templateMaps, metadataBackend, templateBaseUrl, surrogateKeysCache, - NamedMapsCacheEntry, pgConnection) { +function NamedMapsController(app, serverOptions, templateMaps, metadataBackend, templateBaseUrl, surrogateKeysCache) { this.app = app; this.serverOptions = serverOptions; this.templateMaps = templateMaps; this.metadataBackend = metadataBackend; this.templateBaseUrl = templateBaseUrl; this.surrogateKeysCache = surrogateKeysCache; - this.NamedMapsCacheEntry = NamedMapsCacheEntry; - this.pgConnection = pgConnection; } -module.exports = TemplateMapsController; +module.exports = NamedMapsController; var cdbRequest = new CdbRequest(); -TemplateMapsController.prototype.register = function(app) { +NamedMapsController.prototype.register = function(app) { app.get(this.templateBaseUrl + '/:template_id/jsonp', this.jsonp.bind(this)); app.post(this.templateBaseUrl, this.create.bind(this)); app.put(this.templateBaseUrl + '/:template_id', this.update.bind(this)); @@ -31,7 +30,7 @@ TemplateMapsController.prototype.register = function(app) { }; // Add a template -TemplateMapsController.prototype.create = function(req, res) { +NamedMapsController.prototype.create = function(req, res) { var self = this; this.app.doCORS(res); @@ -58,7 +57,7 @@ TemplateMapsController.prototype.create = function(req, res) { }; // Update a template -TemplateMapsController.prototype.update = function(req, res) { +NamedMapsController.prototype.update = function(req, res) { var self = this; this.app.doCORS(res); @@ -89,7 +88,7 @@ TemplateMapsController.prototype.update = function(req, res) { }; // Get a specific template -TemplateMapsController.prototype.retrieve = function(req, res) { +NamedMapsController.prototype.retrieve = function(req, res) { var self = this; if (req.profiler) { @@ -128,7 +127,7 @@ TemplateMapsController.prototype.retrieve = function(req, res) { }; // Delete a specific template -TemplateMapsController.prototype.destroy = function(req, res) { +NamedMapsController.prototype.destroy = function(req, res) { var self = this; if (req.profiler) { @@ -158,7 +157,7 @@ TemplateMapsController.prototype.destroy = function(req, res) { }; // Get a list of owned templates -TemplateMapsController.prototype.list = function(req, res) { +NamedMapsController.prototype.list = function(req, res) { var self = this; if ( req.profiler ) { req.profiler.start('windshaft-cartodb.get_template_list'); @@ -185,7 +184,7 @@ TemplateMapsController.prototype.list = function(req, res) { ); }; -TemplateMapsController.prototype.instantiate = function(req, res) { +NamedMapsController.prototype.instantiate = function(req, res) { var self = this; if (req.profiler) { @@ -202,7 +201,7 @@ TemplateMapsController.prototype.instantiate = function(req, res) { ); }; -TemplateMapsController.prototype.options = function(req, res, next) { +NamedMapsController.prototype.options = function(req, res, next) { this.app.doCORS(res, "Content-Type"); return next(); }; @@ -211,7 +210,7 @@ TemplateMapsController.prototype.options = function(req, res, next) { * jsonp endpoint, allows to instantiate a template with a json call. * callback query argument is mandatory */ -TemplateMapsController.prototype.jsonp = function(req, res) { +NamedMapsController.prototype.jsonp = function(req, res) { var self = this; if (req.profiler) { @@ -239,7 +238,7 @@ TemplateMapsController.prototype.jsonp = function(req, res) { // Instantiate a template -TemplateMapsController.prototype.instantiateTemplate = function(req, res, template_params, callback) { +NamedMapsController.prototype.instantiateTemplate = function(req, res, template_params, callback) { var self = this; this.app.doCORS(res); @@ -301,7 +300,7 @@ TemplateMapsController.prototype.instantiateTemplate = function(req, res, templa res: res, profiler: req.profiler }; - self.setDBParams(cdbuser, fakereq.params, this); + self.serverOptions.setDBParams(cdbuser, fakereq.params, this); }, function setApiKey(err){ if ( req.profiler ) req.profiler.done('setDBParams'); @@ -321,7 +320,7 @@ TemplateMapsController.prototype.instantiateTemplate = function(req, res, templa var tplhash = self.templateMaps.fingerPrint(template).substring(0,8); layergroup.layergroupid = cdbuser + '@' + tplhash + '@' + layergroup.layergroupid; - self.surrogateKeysCache.tag(res, new self.NamedMapsCacheEntry(cdbuser, template.name)); + self.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(cdbuser, template.name)); return layergroup; }, @@ -329,7 +328,7 @@ TemplateMapsController.prototype.instantiateTemplate = function(req, res, templa ); }; -TemplateMapsController.prototype.finish_instantiation = function(err, response, res) { +NamedMapsController.prototype.finish_instantiation = function(err, response, res) { if (err) { var statusCode = 400; response = { error: ''+err }; @@ -342,22 +341,6 @@ TemplateMapsController.prototype.finish_instantiation = function(err, response, } }; -TemplateMapsController.prototype.setDBParams = function(cdbuser, params, callback) { - var self = this; - step( - function setAuth() { - self.pgConnection.setDBAuth(cdbuser, params, this); - }, - function setConn(err) { - if ( err ) throw err; - self.pgConnection.setDBConn(cdbuser, params, this); - }, - function finish(err) { - callback(err); - } - ); -}; - function finishFn(app, res, description, okResponse) { return function finish(err, response){ var statusCode = 200; @@ -387,14 +370,3 @@ function ifInvalidContentType(req, description) { throw new Error(description); } } - -function templateName(templateId) { - var templateIdTokens = templateId.split('@'); - var name = templateIdTokens[0]; - - if (templateIdTokens.length > 1) { - name = templateIdTokens[1]; - } - - return name; -} diff --git a/lib/cartodb/controllers/named_static_maps.js b/lib/cartodb/controllers/named_static_maps.js new file mode 100644 index 00000000..9f604a08 --- /dev/null +++ b/lib/cartodb/controllers/named_static_maps.js @@ -0,0 +1,248 @@ +var step = require('step'); +var assert = require('assert'); +var templateName = require('../template_maps').templateName; +var CdbRequest = require('../models/cdb_request'); +var NamedMapsCacheEntry = require('../cache/model/named_maps_entry'); +var _ = require('underscore'); + +function NamedStaticMapsController(app, serverOptions, templateMaps, staticMapBackend, surrogateKeysCache, + tablesExtentApi) { + this.app = app; + this.serverOptions = serverOptions; + this.templateMaps = templateMaps; + this.staticMapBackend = staticMapBackend; + this.surrogateKeysCache = surrogateKeysCache; + this.tablesExtentApi = tablesExtentApi; +} + +module.exports = NamedStaticMapsController; + +var cdbRequest = new CdbRequest(); + +NamedStaticMapsController.prototype.register = function(app) { + app.get(app.base_url_mapconfig + '/static/named/:template_id/:width/:height.:format', this.named.bind(this)); +}; + +NamedStaticMapsController.prototype.named = function(req, res) { + var self = this; + + this.app.doCORS(res); + var cdbUser = cdbRequest.userByReq(req); + + var format = req.params.format === 'jpg' ? 'jpeg' : 'png'; + + var template; + var layergroupConfig; + var layergroupId; + var fakeReq; + var cacheChannel; + + step( + function reqParams() { + self.app.req2params(req, this); + }, + function getTemplate(err) { + assert.ifError(err); + self.templateMaps.getTemplate(cdbUser, templateName(req.params.template_id), this); + }, + function checkExists(err, tpl) { + assert.ifError(err); + if (!tpl) { + var notFoundErr = new Error( + "Template '" + templateName(req.params.template_id) + "' of user '" + cdbUser + "' not found" + ); + notFoundErr.http_status = 404; + throw notFoundErr; + } + return tpl; + }, + function checkAuthorized(err, tpl) { + assert.ifError(err); + + var authorized = false; + try { + authorized = self.templateMaps.isAuthorized(tpl, req.query.auth_token); + } catch (err) { + // we catch to add http_status + var authorizationFailedErr = new Error('Failed to authorize template'); + authorizationFailedErr.http_status = 403; + throw authorizationFailedErr; + } + if ( ! authorized ) { + var unauthorizedErr = new Error('Unauthorized template instantiation'); + unauthorizedErr.http_status = 403; + throw unauthorizedErr; + } + + return tpl; + }, + function prepareParams(err, tpl) { + assert.ifError(err); + + template = tpl; + + var templateParams = {}; + if (req.query.config) { + try { + templateParams = JSON.parse(req.query.config); + } catch (e) { + throw new Error('malformed config parameter, should be a valid JSON'); + } + } + + return templateParams; + }, + function instantiateTemplate(err, templateParams) { + assert.ifError(err); + return self.templateMaps.instance(template, templateParams); + }, + function prepareLayergroup(err, layergroup) { + assert.ifError(err); + layergroupConfig = layergroup; + fakeReq = { + query: {}, + params: { + user: req.params.user + }, + headers: _.clone(req.headers), + context: _.clone(req.context), + method: req.method, + res: res, + profiler: req.profiler + }; + self.serverOptions.setDBParams(cdbUser, fakeReq.params, this); + }, + function setApiKey(err){ + assert.ifError(err); + self.app.createLayergroup(layergroupConfig, fakeReq, this); + }, + function prepareResponse(err, layergroup) { + assert.ifError(err); + + // added by createLayergroup + cacheChannel = res.header('X-Cache-Channel'); + res.removeHeader('X-Cache-Channel'); + self.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(cdbUser, template.name)); + + layergroupId = layergroup.layergroupid.split(":")[0]; + + return null; + }, + function staticImageOptions(err) { + assert.ifError(err); + getStaticImageOptions(template, this); + }, + function estimateBounds(err, imageOpts) { + assert.ifError(err); + if (imageOpts) { + return imageOpts; + } + + var defaultZoomCenter = { + zoom: 1, + center: { + lng: 0, + lat: 0 + } + }; + + var dbTables = cacheChannel.split(':'); + if (dbTables.length <= 1 || dbTables[1].length === 0) { + return defaultZoomCenter; + } + + var tableNames = dbTables[1].split(','); + if (tableNames.length === 0) { + return defaultZoomCenter; + } + + var next = this; + self.tablesExtentApi.getBounds(cdbUser, tableNames, function(err, result) { + next(null, result || defaultZoomCenter); + }); + }, + function getImage(err, imageOpts) { + assert.ifError(err); + + var staticImageReq = { + headers: _.clone(fakeReq.headers), + params: _.extend(_.clone(fakeReq.params), { + token: layergroupId, + format: req.params.format + }) + }; + + var width = +req.params.width; + var height = +req.params.height; + + if (!_.isUndefined(imageOpts.zoom) && imageOpts.center) { + self.staticMapBackend.getImage(staticImageReq, width, height, imageOpts.zoom, imageOpts.center, this); + } else { + self.staticMapBackend.getImage(staticImageReq, width, height, imageOpts.bounds, this); + } + }, + function handleImage(err, image, headers, stats) { + if (req.profiler) { + req.profiler.done('render-' + format); + req.profiler.add(stats || {}); + } + + if (err) { + if (!err.error) { + err.error = err.message; + } + self.app.sendError(res, err, self.app.findStatusCode(err), 'STATIC_VIZ_MAP', err); + } else { + res.setHeader('Content-Type', headers['Content-Type'] || 'image/' + format); + res.setHeader('Cache-Control', 'public,max-age=7200,must-revalidate'); + self.app.sendResponse(res, [image, 200]); + } + } + ); +}; + +function getStaticImageOptions(template, callback) { + if (template.view) { + var zoomCenter = templateZoomCenter(template.view); + if (zoomCenter) { + return callback(null, zoomCenter); + } + + var bounds = templateBounds(template.view); + if (bounds) { + return callback(null, bounds); + } + } + return callback(null, null); +} + +function templateZoomCenter(view) { + if (!_.isUndefined(view.zoom) && view.center) { + return { + zoom: view.zoom, + center: view.center + }; + } + return false; +} + +function templateBounds(view) { + if (view.bounds) { + var hasAllBounds = _.every(['west', 'south', 'east', 'north'], function(prop) { + return !!view.bounds[prop]; + }); + if (hasAllBounds) { + return { + bounds: { + west: view.bounds.west, + south: view.bounds.south, + east: view.bounds.east, + north: view.bounds.north + } + }; + } else { + return false; + } + } + return false; +} diff --git a/lib/cartodb/server_options.js b/lib/cartodb/server_options.js index a3f9d029..7dc1e905 100644 --- a/lib/cartodb/server_options.js +++ b/lib/cartodb/server_options.js @@ -5,6 +5,7 @@ var assert = require('assert'); var RedisPool = require('redis-mpool'); var QueryTablesApi = require('./api/query_tables_api'); +var PgQueryRunner = require('./backends/pg_query_runner'); var PgConnection = require('./backends/pg_connection'); var TemplateMaps = require('./template_maps.js'); var MapConfigNamedLayersAdapter = require('./models/mapconfig_named_layers_adapter'); @@ -25,11 +26,12 @@ var REQUEST_QUERY_PARAMS_WHITELIST = [ module.exports = function(redisPool) { redisPool = redisPool || new RedisPool(_.extend(global.environment.redis, {name: 'windshaft:server_options'})); - var cartoData = require('cartodb-redis')({ pool: redisPool }), - lzmaWorker = new LZMA(), - pgConnection = new PgConnection(cartoData), - queryTablesApi = new QueryTablesApi(pgConnection, cartoData), - cdbRequest = new CdbRequest(); + var cartoData = require('cartodb-redis')({ pool: redisPool }); + var lzmaWorker = new LZMA(); + var pgConnection = new PgConnection(cartoData); + var pgQueryRunner = new PgQueryRunner(pgConnection); + var queryTablesApi = new QueryTablesApi(pgQueryRunner); + var cdbRequest = new CdbRequest(); var rendererConfig = _.defaults(global.environment.renderer || {}, { cache_ttl: 60000, // milliseconds @@ -86,6 +88,7 @@ module.exports = function(redisPool) { varnish_http_port: global.environment.varnish.http_port, varnish_secret: global.environment.varnish.secret, varnish_purge_enabled: global.environment.varnish.purge_enabled, + fastly: global.environment.fastly || {}, cache_enabled: global.environment.cache_enabled, log_format: global.environment.log_format, useProfiler: global.environment.useProfiler @@ -100,6 +103,8 @@ module.exports = function(redisPool) { // Re-use pgConnection me.pgConnection = pgConnection; + // Re-use pgQueryRunner + me.pgQueryRunner = pgQueryRunner; var templateMaps = new TemplateMaps(redisPool, { max_user_templates: global.environment.maxUserTemplates @@ -507,6 +512,21 @@ module.exports = function(redisPool) { ); }; + me.setDBParams = function(cdbuser, params, callback) { + step( + function setAuth() { + pgConnection.setDBAuth(cdbuser, params, this); + }, + function setConn(err) { + if ( err ) throw err; + pgConnection.setDBConn(cdbuser, params, this); + }, + function finish(err) { + callback(err); + } + ); + }; + // jshint maxcomplexity:10 /** * Whitelist input and get database name & default geometry type from diff --git a/lib/cartodb/template_maps.js b/lib/cartodb/template_maps.js index c10bf0f6..12c87a06 100644 --- a/lib/cartodb/template_maps.js +++ b/lib/cartodb/template_maps.js @@ -492,3 +492,14 @@ o.fingerPrint = function(template) { .digest('hex') ; }; + +module.exports.templateName = function templateName(templateId) { + var templateIdTokens = templateId.split('@'); + var name = templateIdTokens[0]; + + if (templateIdTokens.length > 1) { + name = templateIdTokens[1]; + } + + return name; +}; diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 8ff9658a..6c203fe4 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,6 +1,6 @@ { "name": "windshaft-cartodb", - "version": "2.1.4", + "version": "2.2.0", "dependencies": { "cartodb-psql": { "version": "0.4.0", @@ -71,6 +71,302 @@ "from": "dot@~1.0.2", "resolved": "https://registry.npmjs.org/dot/-/dot-1.0.3.tgz" }, + "fastly-purge": { + "version": "1.0.0", + "from": "fastly-purge@~1.0.0", + "dependencies": { + "request": { + "version": "2.55.0", + "from": "request@^2.55.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.55.0.tgz", + "dependencies": { + "bl": { + "version": "0.9.4", + "from": "bl@~0.9.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-0.9.4.tgz", + "dependencies": { + "readable-stream": { + "version": "1.0.33", + "from": "readable-stream@~1.0.26", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.33.tgz", + "dependencies": { + "core-util-is": { + "version": "1.0.1", + "from": "core-util-is@~1.0.0", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.1.tgz" + }, + "isarray": { + "version": "0.0.1", + "from": "isarray@0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + }, + "string_decoder": { + "version": "0.10.31", + "from": "string_decoder@~0.10.x", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" + }, + "inherits": { + "version": "2.0.1", + "from": "inherits@~2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + } + } + } + } + }, + "caseless": { + "version": "0.9.0", + "from": "caseless@~0.9.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.9.0.tgz" + }, + "forever-agent": { + "version": "0.6.1", + "from": "forever-agent@~0.6.0", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" + }, + "form-data": { + "version": "0.2.0", + "from": "form-data@~0.2.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-0.2.0.tgz", + "dependencies": { + "async": { + "version": "0.9.0", + "from": "async@~0.9.0", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.0.tgz" + } + } + }, + "json-stringify-safe": { + "version": "5.0.0", + "from": "json-stringify-safe@~5.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.0.tgz" + }, + "mime-types": { + "version": "2.0.10", + "from": "mime-types@~2.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.10.tgz", + "dependencies": { + "mime-db": { + "version": "1.8.0", + "from": "mime-db@~1.8.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.8.0.tgz" + } + } + }, + "node-uuid": { + "version": "1.4.3", + "from": "node-uuid@~1.4.0", + "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.3.tgz" + }, + "qs": { + "version": "2.4.1", + "from": "qs@~2.4.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-2.4.1.tgz" + }, + "tunnel-agent": { + "version": "0.4.0", + "from": "tunnel-agent@~0.4.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.0.tgz" + }, + "tough-cookie": { + "version": "1.1.0", + "from": "tough-cookie@>=0.12.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-1.1.0.tgz" + }, + "http-signature": { + "version": "0.10.1", + "from": "http-signature@~0.10.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-0.10.1.tgz", + "dependencies": { + "assert-plus": { + "version": "0.1.5", + "from": "assert-plus@^0.1.5", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz" + }, + "asn1": { + "version": "0.1.11", + "from": "asn1@0.1.11", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.1.11.tgz" + }, + "ctype": { + "version": "0.5.3", + "from": "ctype@0.5.3", + "resolved": "https://registry.npmjs.org/ctype/-/ctype-0.5.3.tgz" + } + } + }, + "oauth-sign": { + "version": "0.6.0", + "from": "oauth-sign@~0.6.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.6.0.tgz" + }, + "hawk": { + "version": "2.3.1", + "from": "hawk@~2.3.0", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-2.3.1.tgz", + "dependencies": { + "hoek": { + "version": "2.12.0", + "from": "hoek@2.x.x", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.12.0.tgz" + }, + "boom": { + "version": "2.7.1", + "from": "boom@2.x.x", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.7.1.tgz" + }, + "cryptiles": { + "version": "2.0.4", + "from": "cryptiles@2.x.x", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.4.tgz" + }, + "sntp": { + "version": "1.0.9", + "from": "sntp@1.x.x", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz" + } + } + }, + "aws-sign2": { + "version": "0.5.0", + "from": "aws-sign2@~0.5.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.5.0.tgz" + }, + "stringstream": { + "version": "0.0.4", + "from": "stringstream@~0.0.4", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.4.tgz" + }, + "combined-stream": { + "version": "0.0.7", + "from": "combined-stream@~0.0.5", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-0.0.7.tgz", + "dependencies": { + "delayed-stream": { + "version": "0.0.5", + "from": "delayed-stream@0.0.5", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.5.tgz" + } + } + }, + "isstream": { + "version": "0.1.2", + "from": "isstream@~0.1.1", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz" + }, + "har-validator": { + "version": "1.6.1", + "from": "har-validator@^1.4.0", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-1.6.1.tgz", + "dependencies": { + "bluebird": { + "version": "2.9.25", + "from": "bluebird@^2.9.21", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.9.25.tgz" + }, + "chalk": { + "version": "1.0.0", + "from": "chalk@^1.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.0.0.tgz", + "dependencies": { + "ansi-styles": { + "version": "2.0.1", + "from": "ansi-styles@^2.0.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.0.1.tgz" + }, + "escape-string-regexp": { + "version": "1.0.3", + "from": "escape-string-regexp@^1.0.2", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.3.tgz" + }, + "has-ansi": { + "version": "1.0.3", + "from": "has-ansi@^1.0.3", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-1.0.3.tgz", + "dependencies": { + "ansi-regex": { + "version": "1.1.1", + "from": "ansi-regex@^1.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-1.1.1.tgz" + }, + "get-stdin": { + "version": "4.0.1", + "from": "get-stdin@^4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz" + } + } + }, + "strip-ansi": { + "version": "2.0.1", + "from": "strip-ansi@^2.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-2.0.1.tgz", + "dependencies": { + "ansi-regex": { + "version": "1.1.1", + "from": "ansi-regex@^1.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-1.1.1.tgz" + } + } + }, + "supports-color": { + "version": "1.3.1", + "from": "supports-color@^1.3.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-1.3.1.tgz" + } + } + }, + "commander": { + "version": "2.8.1", + "from": "commander@^2.7.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", + "dependencies": { + "graceful-readlink": { + "version": "1.0.1", + "from": "graceful-readlink@>= 1.0.0", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz" + } + } + }, + "is-my-json-valid": { + "version": "2.10.1", + "from": "is-my-json-valid@^2.10.0", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.10.1.tgz", + "dependencies": { + "generate-function": { + "version": "2.0.0", + "from": "generate-function@^2.0.0", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz" + }, + "generate-object-property": { + "version": "1.1.1", + "from": "generate-object-property@^1.1.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.1.1.tgz", + "dependencies": { + "is-property": { + "version": "1.0.2", + "from": "is-property@^1.0.0", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz" + } + } + }, + "jsonpointer": { + "version": "1.1.0", + "from": "jsonpointer@^1.1.0", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-1.1.0.tgz" + }, + "xtend": { + "version": "4.0.0", + "from": "xtend@^4.0.0", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.0.tgz" + } + } + } + } + } + } + } + } + }, "log4js": { "version": "0.6.21", "from": "https://github.com/CartoDB/log4js-node/tarball/cdb", @@ -103,7 +399,7 @@ }, "inherits": { "version": "2.0.1", - "from": "inherits@2", + "from": "inherits@2.0.1", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" } } @@ -170,9 +466,9 @@ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz" }, "windshaft": { - "version": "0.42.2", - "from": "windshaft@0.42.2", - "resolved": "https://registry.npmjs.org/windshaft/-/windshaft-0.42.2.tgz", + "version": "0.43.0", + "from": "windshaft@0.43.0", + "resolved": "https://registry.npmjs.org/windshaft/-/windshaft-0.43.0.tgz", "dependencies": { "chronograph": { "version": "0.1.0", @@ -300,16 +596,9 @@ "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.3.tgz" }, "tough-cookie": { - "version": "0.12.1", + "version": "1.1.0", "from": "tough-cookie@>=0.12.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-0.12.1.tgz", - "dependencies": { - "punycode": { - "version": "1.3.2", - "from": "punycode@>=0.2.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz" - } - } + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-1.1.0.tgz" }, "form-data": { "version": "0.1.4", @@ -2368,16 +2657,9 @@ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.0.tgz" }, "tough-cookie": { - "version": "0.12.1", + "version": "1.1.0", "from": "tough-cookie@>=0.12.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-0.12.1.tgz", - "dependencies": { - "punycode": { - "version": "1.3.2", - "from": "punycode@>=0.2.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz" - } - } + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-1.1.0.tgz" }, "http-signature": { "version": "0.10.1", diff --git a/package.json b/package.json index 9d6d4fdf..600471a7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "windshaft-cartodb", - "version": "2.1.4", + "version": "2.2.0", "description": "A map tile server for CartoDB", "keywords": [ "cartodb" @@ -24,12 +24,13 @@ "dependencies": { "underscore" : "~1.6.0", "dot": "~1.0.2", - "windshaft": "0.42.2", + "windshaft": "0.43.0", "step": "~0.0.5", "queue-async": "~1.0.7", "request": "~2.9.203", "cartodb-redis": "~0.12.1", "cartodb-psql": "~0.4.0", + "fastly-purge": "~1.0.0", "redis-mpool": "~0.3.0", "lzma": "~1.3.7", "log4js": "https://github.com/CartoDB/log4js-node/tarball/cdb" diff --git a/test/acceptance/cache/surrogate_keys_invalidation.js b/test/acceptance/cache/surrogate_keys_invalidation.js index b4f4a165..c46ab8c1 100644 --- a/test/acceptance/cache/surrogate_keys_invalidation.js +++ b/test/acceptance/cache/surrogate_keys_invalidation.js @@ -3,6 +3,7 @@ require(__dirname + '/../../support/test_helper'); var assert = require('../../support/assert'); var redis = require('redis'); var step = require('step'); +var FastlyPurge = require('fastly-purge'); var NamedMapsCacheEntry = require(__dirname + '/../../../lib/cartodb/cache/model/named_maps_entry'); var CartodbWindshaft = require(__dirname + '/../../../lib/cartodb/cartodb_windshaft'); @@ -18,6 +19,17 @@ describe('templates surrogate keys', function() { var varnishPurgeEnabled = global.environment.varnish.purge_enabled; global.environment.varnish.purge_enabled = true; + var fastlyConfig = global.environment.fastly; + var FAKE_FASTLY_API_KEY = 'fastly-api-key'; + var FAKE_FASTLY_SERVICE_ID = 'fake-service-id'; + global.environment.fastly = { + enabled: true, + // the fastly api key + apiKey: FAKE_FASTLY_API_KEY, + // the service that will get surrogate key invalidation + serviceId: FAKE_FASTLY_SERVICE_ID + }; + var serverOptions = require('../../../lib/cartodb/server_options')(); var server = new CartodbWindshaft(serverOptions); @@ -51,6 +63,7 @@ describe('templates surrogate keys', function() { var cacheEntryKey = new NamedMapsCacheEntry(templateOwner, templateName).key(); var invalidationMatchHeader = '\\b' + cacheEntryKey + '\\b'; + var fastlyPurgePath = '/service/' + FAKE_FASTLY_SERVICE_ID + '/purge/' + encodeURIComponent(cacheEntryKey); var nock = require('nock'); nock.enableNetConnect(/(127.0.0.1:5555|cartocdn.com)/); @@ -60,6 +73,8 @@ describe('templates surrogate keys', function() { global.environment.varnish.host = varnishHost; global.environment.varnish.purge_enabled = varnishPurgeEnabled; + global.environment.fastly = fastlyConfig; + nock.restore(); done(); }); @@ -109,6 +124,15 @@ describe('templates surrogate keys', function() { .matchHeader('Invalidation-Match', invalidationMatchHeader) .reply(204, ''); + var fastlyScope = nock(FastlyPurge.FASTLY_API_ENDPOINT) + .post(fastlyPurgePath) + .matchHeader('Fastly-Key', FAKE_FASTLY_API_KEY) + .matchHeader('Fastly-Soft-Purge', 1) + .matchHeader('Accept', 'application/json') + .reply(200, { + status:'ok' + }); + step( function createTemplateToUpdate() { createTemplate(this); @@ -145,6 +169,7 @@ describe('templates surrogate keys', function() { assert.deepEqual(parsedBody, expectedBody); assert.equal(scope.pendingMocks().length, 0); + assert.equal(fastlyScope.pendingMocks().length, 0); return null; }, @@ -171,6 +196,15 @@ describe('templates surrogate keys', function() { .matchHeader('Invalidation-Match', invalidationMatchHeader) .reply(204, ''); + var fastlyScope = nock(FastlyPurge.FASTLY_API_ENDPOINT) + .post(fastlyPurgePath) + .matchHeader('Fastly-Key', FAKE_FASTLY_API_KEY) + .matchHeader('Fastly-Soft-Purge', 1) + .matchHeader('Accept', 'application/json') + .reply(200, { + status:'ok' + }); + step( function createTemplateToDelete() { createTemplate(this); @@ -204,6 +238,7 @@ describe('templates surrogate keys', function() { } assert.equal(scope.pendingMocks().length, 0); + assert.equal(fastlyScope.pendingMocks().length, 0); return null; }, diff --git a/test/acceptance/health_check.js b/test/acceptance/health_check.js index 682deedd..274e9cfc 100644 --- a/test/acceptance/health_check.js +++ b/test/acceptance/health_check.js @@ -1,4 +1,4 @@ -var helper = require(__dirname + '/../support/test_helper'); +require(__dirname + '/../support/test_helper'); var assert = require('../support/assert'); var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/cartodb_windshaft'); @@ -10,7 +10,7 @@ var tilelive = {}; var HealthCheck = require('../../lib/cartodb/monitoring/health_check'); var healthCheck = new HealthCheck(metadataBackend, tilelive); -suite('health checks', function () { +describe('health checks', function () { function resetHealthConfig() { global.environment.health = { @@ -30,7 +30,7 @@ suite('health checks', function () { } }; - test('returns 200 and ok=true with enabled configuration', function (done) { + it('returns 200 and ok=true with enabled configuration', function (done) { resetHealthConfig(); assert.response(server, @@ -51,15 +51,15 @@ suite('health checks', function () { ); }); - test('error if disabled file exists', function(done) { + it('error if disabled file exists', function(done) { var fs = require('fs'); - var readFileFn = fs.readFile + var readFileFn = fs.readFile; fs.readFile = function(filename, callback) { callback(null, "Maintenance"); - } + }; - healthCheck.check(null, function(err, result) { + healthCheck.check(null, function(err/*, result*/) { assert.equal(err.message, "Maintenance"); assert.equal(err.http_status, 503); done(); @@ -68,7 +68,7 @@ suite('health checks', function () { }); - test('not err if disabled file does not exists', function(done) { + it('not err if disabled file does not exists', function(done) { resetHealthConfig(); global.environment.disabled_file = '/tmp/ftreftrgtrccre'; diff --git a/test/acceptance/multilayer_server.js b/test/acceptance/multilayer_server.js index 79b59a3c..e2427543 100644 --- a/test/acceptance/multilayer_server.js +++ b/test/acceptance/multilayer_server.js @@ -6,7 +6,7 @@ var redis = require('redis'); var _ = require('underscore'); -var QueryTablesApi = require('../../lib/cartodb/api/query_tables_api'); +var PgQueryRunner = require('../../lib/cartodb/backends/pg_query_runner'); var CartodbWindshaft = require('../../lib/cartodb/cartodb_windshaft'); var serverOptions = require('../../lib/cartodb/server_options')(); var server = new CartodbWindshaft(serverOptions); @@ -253,8 +253,8 @@ describe('tests from old api translated to multilayer', function() { it("creates layergroup fails when postgresql queries fail to figure affected tables in query", function(done) { - var runQueryFn = QueryTablesApi.prototype.runQuery; - QueryTablesApi.prototype.runQuery = function(username, query, queryHandler, callback) { + var runQueryFn = PgQueryRunner.prototype.run; + PgQueryRunner.prototype.run = function(username, query, queryHandler, callback) { return queryHandler(new Error('fake error message'), [], callback); }; @@ -272,7 +272,7 @@ describe('tests from old api translated to multilayer', function() { status: 400 }, function(res) { - QueryTablesApi.prototype.runQuery = runQueryFn; + PgQueryRunner.prototype.run = runQueryFn; assert.ok(!res.headers.hasOwnProperty('x-cache-channel')); @@ -300,8 +300,8 @@ describe('tests from old api translated to multilayer', function() { status: 200 }, function(res) { - var runQueryFn = QueryTablesApi.prototype.runQuery; - QueryTablesApi.prototype.runQuery = function(username, query, queryHandler, callback) { + var runQueryFn = PgQueryRunner.prototype.run; + PgQueryRunner.prototype.run = function(username, query, queryHandler, callback) { return queryHandler(new Error('failed to query database for affected tables'), [], callback); }; @@ -327,7 +327,7 @@ describe('tests from old api translated to multilayer', function() { }, function(res) { assert.ok(!res.headers.hasOwnProperty('x-cache-channel')); - QueryTablesApi.prototype.runQuery = runQueryFn; + PgQueryRunner.prototype.run = runQueryFn; done(); } ); diff --git a/test/acceptance/named_layers.js b/test/acceptance/named_layers.js index 9e2be330..1f1f6fa4 100644 --- a/test/acceptance/named_layers.js +++ b/test/acceptance/named_layers.js @@ -8,12 +8,11 @@ var server = new CartodbWindshaft(serverOptions); var RedisPool = require('redis-mpool'); var TemplateMaps = require('../../lib/cartodb/template_maps.js'); -var Step = require('step'); -var _ = require('underscore'); +var step = require('step'); -suite('named_layers', function() { +describe('named_layers', function() { // configure redis pool instance to use in tests - var redisPool = RedisPool(global.environment.redis); + var redisPool = new RedisPool(global.environment.redis); var templateMaps = new TemplateMaps(redisPool, { max_user_templates: global.environment.maxUserTemplates @@ -95,7 +94,7 @@ suite('named_layers', function() { } }; - suiteSetup(function(done) { + before(function(done) { global.environment.enabledFeatures = {cdbQueryTablesFromPostgres: true}; templateMaps.addTemplate(username, nestedNamedMapTemplate, function(err) { if (err) { @@ -112,7 +111,7 @@ suite('named_layers', function() { }); }); - test('should fail for non-existing template name', function(done) { + it('should fail for non-existing template name', function(done) { var layergroup = { version: '1.3.0', layers: [ @@ -125,7 +124,7 @@ suite('named_layers', function() { ] }; - Step( + step( function createLayergroup() { var next = this; assert.response(server, @@ -162,7 +161,7 @@ suite('named_layers', function() { ); }); - test('should return 403 if not properly authorized', function(done) { + it('should return 403 if not properly authorized', function(done) { var layergroup = { version: '1.3.0', @@ -178,7 +177,7 @@ suite('named_layers', function() { ] }; - Step( + step( function createLayergroup() { var next = this; assert.response(server, @@ -219,7 +218,7 @@ suite('named_layers', function() { }); - test('should return 200 and layergroup if properly authorized', function(done) { + it('should return 200 and layergroup if properly authorized', function(done) { var layergroup = { version: '1.3.0', @@ -235,7 +234,7 @@ suite('named_layers', function() { ] }; - Step( + step( function createLayergroup() { var next = this; assert.response(server, @@ -274,7 +273,7 @@ suite('named_layers', function() { }); - test('should return 400 for nested named map layers', function(done) { + it('should return 400 for nested named map layers', function(done) { var layergroup = { version: '1.3.0', @@ -288,7 +287,7 @@ suite('named_layers', function() { ] }; - Step( + step( function createLayergroup() { var next = this; assert.response(server, @@ -326,7 +325,7 @@ suite('named_layers', function() { }); - test('should return 200 and layergroup with private tables', function(done) { + it('should return 200 and layergroup with private tables', function(done) { var privateTableTemplateName = 'private_table_template'; var privateTableTemplate = { @@ -361,7 +360,7 @@ suite('named_layers', function() { ] }; - Step( + step( function createTemplate() { templateMaps.addTemplate(username, privateTableTemplate, this); }, @@ -447,7 +446,7 @@ suite('named_layers', function() { }); - test('should return 200 and layergroup with private tables and interactivity', function(done) { + it('should return 200 and layergroup with private tables and interactivity', function(done) { var privateTableTemplateNameInteractivity = 'private_table_template_interactivity'; var privateTableTemplate = { @@ -489,7 +488,7 @@ suite('named_layers', function() { ] }; - Step( + step( function createTemplate() { templateMaps.addTemplate(username, privateTableTemplate, this); }, @@ -575,7 +574,7 @@ suite('named_layers', function() { }); - test('should return 403 when private table is accessed from non named layer', function(done) { + it('should return 403 when private table is accessed from non named layer', function(done) { var layergroup = { version: '1.3.0', @@ -597,7 +596,7 @@ suite('named_layers', function() { ] }; - Step( + step( function createLayergroup() { var next = this; assert.response(server, @@ -636,7 +635,7 @@ suite('named_layers', function() { }); - suiteTeardown(function(done) { + after(function(done) { global.environment.enabledFeatures = {cdbQueryTablesFromPostgres: false}; templateMaps.delTemplate(username, nestedNamedMapTemplateName, function(err) { if (err) { diff --git a/test/acceptance/named_static_maps.js b/test/acceptance/named_static_maps.js new file mode 100644 index 00000000..f25d411d --- /dev/null +++ b/test/acceptance/named_static_maps.js @@ -0,0 +1,189 @@ +var test_helper = require('../support/test_helper'); +var RedisPool = require('redis-mpool'); +var querystring = require('querystring'); + +var assert = require('../support/assert'); +var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/cartodb_windshaft'); +var serverOptions = require(__dirname + '/../../lib/cartodb/server_options')(); +var server = new CartodbWindshaft(serverOptions); +var TemplateMaps = require('../../lib/cartodb/template_maps.js'); +var NamedMapsCacheEntry = require('../../lib/cartodb/cache/model/named_maps_entry'); + +describe('named static maps', function() { + // configure redis pool instance to use in tests + var redisPool = new RedisPool(global.environment.redis); + + var templateMaps = new TemplateMaps(redisPool, { + max_user_templates: global.environment.maxUserTemplates + }); + + var wadusLayer = { + type: 'cartodb', + options: { + sql: 'select 1 cartodb_id, null::geometry the_geom_webmercator', + cartocss: '#layer { marker-fill: <%= color %>; }', + cartocss_version: '2.3.0' + } + }; + + var username = 'localhost'; + + var templateName = 'valid_template'; + var template = { + version: '0.0.1', + name: templateName, + auth: { + method: 'open' + }, + "placeholders": { + "color": { + "type": "css_color", + "default": "#cc3300" + } + }, + layergroup: { + layers: [ + wadusLayer + ] + } + }; + + var tokenAuthTemplateName = 'auth_valid_template'; + var tokenAuthTemplate = { + version: '0.0.1', + name: tokenAuthTemplateName, + auth: { + method: 'token', + valid_tokens: ['valid1', 'valid2'] + }, + placeholders: { + color: { + "type": "css_color", + "default": "#cc3300" + } + }, + layergroup: { + layers: [ + wadusLayer + ] + } + }; + + + var namedMapLayer = { + type: 'named', + options: { + name: templateName, + config: {}, + auth_tokens: [] + } + }; + + var nestedNamedMapTemplateName = 'nested_template'; + var nestedNamedMapTemplate = { + version: '0.0.1', + name: nestedNamedMapTemplateName, + auth: { + method: 'open' + }, + layergroup: { + layers: [ + namedMapLayer + ] + } + }; + + before(function (done) { + templateMaps.addTemplate(username, nestedNamedMapTemplate, function (err) { + if (err) { + return done(err); + } + templateMaps.addTemplate(username, tokenAuthTemplate, function (err) { + if (err) { + return done(err); + } + templateMaps.addTemplate(username, template, function (err) { + return done(err); + }); + }); + }); + }); + + after(function (done) { + templateMaps.delTemplate(username, nestedNamedMapTemplateName, function (err) { + if (err) { + return done(err); + } + templateMaps.delTemplate(username, tokenAuthTemplateName, function (err) { + if (err) { + return done(err); + } + templateMaps.delTemplate(username, templateName, function (err) { + return done(err); + }); + }); + }); + }); + + function getStaticMap(name, options, callback) { + + var url = '/api/v1/map/static/named/' + name + '/640/480.png'; + if (options.params) { + url = url + '?' + querystring.stringify(options.params); + } + var requestOptions = { + url: url, + method: 'GET', + headers: { + host: username + } + }; + + console.log(url); + + var statusCode = options.status || 200; + + var expectedResponse = { + status: statusCode, + headers: { + 'Content-Type': statusCode === 200 ? 'image/png' : 'application/json; charset=utf-8' + } + }; + + assert.response(server, + requestOptions, + expectedResponse, + function (res, err) { + return callback(err, res); + } + ); + } + + it('should return a 404 error for nonexistent template name', function (done) { + var nonexistentName = 'nonexistent'; + getStaticMap(nonexistentName, { status: 404 }, function(err, res) { + assert.ok(!err); + var parsed = JSON.parse(res.body); + assert.equal(parsed.error, "Template '" + nonexistentName + "' of user '" + username + "' not found"); + done(); + }); + }); + + it('should return 403 if not properly authorized', function(done) { + getStaticMap(tokenAuthTemplateName, { status: 403 }, function(err, res) { + assert.ok(!err); + var parsed = JSON.parse(res.body); + assert.equal(parsed.error, 'Unauthorized template instantiation'); + done(); + }); + }); + + it('should return 200 if properly authorized', function(done) { + getStaticMap(tokenAuthTemplateName, { params: { auth_token: 'valid1' } }, function(err, res) { + assert.ok(!err); + test_helper.checkSurrogateKey(res, new NamedMapsCacheEntry(username, tokenAuthTemplateName).key()); + done(); + }); + }); + +}); diff --git a/test/integration/mapconfig_named_layers_datasource.js b/test/integration/mapconfig_named_layers_datasource.js index 14fbc29c..ed478461 100644 --- a/test/integration/mapconfig_named_layers_datasource.js +++ b/test/integration/mapconfig_named_layers_datasource.js @@ -1,4 +1,4 @@ -var test_helper = require('../support/test_helper'); +require('../support/test_helper'); var assert = require('assert'); var RedisPool = require('redis-mpool'); @@ -6,11 +6,8 @@ var TemplateMaps = require('../../lib/cartodb/template_maps.js'); var PgConnection = require(__dirname + '/../../lib/cartodb/backends/pg_connection'); var MapConfigNamedLayersAdapter = require('../../lib/cartodb/models/mapconfig_named_layers_adapter'); -var Step = require('step'); -var _ = require('underscore'); - // configure redis pool instance to use in tests -var redisPool = RedisPool(global.environment.redis); +var redisPool = new RedisPool(global.environment.redis); var pgConnection = new PgConnection(require('cartodb-redis')({ pool: redisPool })); var templateMaps = new TemplateMaps(redisPool, { @@ -97,8 +94,8 @@ var multipleLayersTemplate = { } }; -suite('named_layers datasources', function() { - suiteSetup(function(done) { +describe('named_layers datasources', function() { + before(function(done) { templateMaps.addTemplate(username, template, function(err) { if (err) { return done(err); @@ -224,7 +221,14 @@ suite('named_layers datasources', function() { { desc: 'with a mix of datasource and no datasource depending if layers are named or not', - config: makeNamedMapLayerConfig([simpleNamedLayer, multipleLayersNamedLayer, wadusLayer, simpleNamedLayer, wadusLayer, multipleLayersNamedLayer]), + config: makeNamedMapLayerConfig([ + simpleNamedLayer, + multipleLayersNamedLayer, + wadusLayer, + simpleNamedLayer, + wadusLayer, + multipleLayersNamedLayer + ]), test: function(err, layers, datasource, done) { assert.ok(!err); @@ -280,14 +284,16 @@ suite('named_layers datasources', function() { ]; testScenarios.forEach(function(testScenario) { - test('should return a list of layers ' + testScenario.desc, function(done) { - mapConfigNamedLayersAdapter.getLayers(username, testScenario.config.layers, pgConnection, function(err, layers, datasource) { - testScenario.test(err, layers, datasource, done); - }); + it('should return a list of layers ' + testScenario.desc, function(done) { + mapConfigNamedLayersAdapter.getLayers(username, testScenario.config.layers, pgConnection, + function(err, layers, datasource) { + testScenario.test(err, layers, datasource, done); + } + ); }); }); - suiteTeardown(function(done) { + after(function(done) { templateMaps.delTemplate(username, templateName, function(err) { if (err) { return done(err); diff --git a/test/integration/mapconfig_named_layers_expanded.js b/test/integration/mapconfig_named_layers_expanded.js index 72ab67cf..d9b5a874 100644 --- a/test/integration/mapconfig_named_layers_expanded.js +++ b/test/integration/mapconfig_named_layers_expanded.js @@ -1,4 +1,4 @@ -var testHelper = require('../support/test_helper'); +require('../support/test_helper'); var assert = require('assert'); var RedisPool = require('redis-mpool'); @@ -6,13 +6,10 @@ var TemplateMaps = require('../../lib/cartodb/template_maps.js'); var PgConnection = require(__dirname + '/../../lib/cartodb/backends/pg_connection'); var MapConfigNamedLayersAdapter = require('../../lib/cartodb/models/mapconfig_named_layers_adapter'); -var Step = require('step'); -var _ = require('underscore'); - -suite('mapconfig_named_layers_adapter', function() { +describe('mapconfig_named_layers_adapter', function() { // configure redis pool instance to use in tests - var redisPool = RedisPool(global.environment.redis); + var redisPool = new RedisPool(global.environment.redis); var pgConnection = new PgConnection(require('cartodb-redis')({ pool: redisPool })); var templateMaps = new TemplateMaps(redisPool, { @@ -138,40 +135,46 @@ suite('mapconfig_named_layers_adapter', function() { } - suiteSetup(function(done) { + before(function(done) { templateMaps.addTemplate(username, template, done); }); - test('should fail for named map layer with missing name', function(done) { + it('should fail for named map layer with missing name', function(done) { var missingNamedMapLayerConfig = makeNamedMapLayerConfig({ config: {} }); - mapConfigNamedLayersAdapter.getLayers(username, missingNamedMapLayerConfig.layers, pgConnection, function(err, layers, datasource) { - assert.ok(err); - assert.ok(!layers); - assert.ok(!datasource); - assert.equal(err.message, 'Missing Named Map `name` in layer options'); + mapConfigNamedLayersAdapter.getLayers(username, missingNamedMapLayerConfig.layers, pgConnection, + function(err, layers, datasource) { + assert.ok(err); + assert.ok(!layers); + assert.ok(!datasource); + assert.equal(err.message, 'Missing Named Map `name` in layer options'); - done(); - }); + done(); + } + ); }); - test('should fail for non-existing template name', function(done) { + it('should fail for non-existing template name', function(done) { var missingTemplateName = 'wadus'; var nonExistentNamedMapLayerConfig = makeNamedMapLayerConfig({ name: missingTemplateName }); - mapConfigNamedLayersAdapter.getLayers(username, nonExistentNamedMapLayerConfig.layers, pgConnection, function(err, layers, datasource) { - assert.ok(err); - assert.ok(!layers); - assert.ok(!datasource); - assert.equal(err.message, "Template '" + missingTemplateName + "' of user '" + username + "' not found"); + mapConfigNamedLayersAdapter.getLayers(username, nonExistentNamedMapLayerConfig.layers, pgConnection, + function(err, layers, datasource) { + assert.ok(err); + assert.ok(!layers); + assert.ok(!datasource); + assert.equal( + err.message, "Template '" + missingTemplateName + "' of user '" + username + "' not found" + ); - done(); - }); + done(); + } + ); }); - test('should fail if not properly authorized', function(done) { + it('should fail if not properly authorized', function(done) { templateMaps.addTemplate(username, tokenAuthTemplate, function(err) { if (err) { return done(err); @@ -180,18 +183,20 @@ suite('mapconfig_named_layers_adapter', function() { var nonAuthTokensNamedMapLayerConfig = makeNamedMapLayerConfig({ name: tokenAuthTemplateName }); - mapConfigNamedLayersAdapter.getLayers(username, nonAuthTokensNamedMapLayerConfig.layers, pgConnection, function(err, layers, datasource) { - assert.ok(err); - assert.ok(!layers); - assert.ok(!datasource); - assert.equal(err.message, "Unauthorized '" + tokenAuthTemplateName + "' template instantiation"); + mapConfigNamedLayersAdapter.getLayers(username, nonAuthTokensNamedMapLayerConfig.layers, pgConnection, + function(err, layers, datasource) { + assert.ok(err); + assert.ok(!layers); + assert.ok(!datasource); + assert.equal(err.message, "Unauthorized '" + tokenAuthTemplateName + "' template instantiation"); - templateMaps.delTemplate(username, tokenAuthTemplateName, done); - }); + templateMaps.delTemplate(username, tokenAuthTemplateName, done); + } + ); }); }); - test('should fail for nested named map layers', function(done) { + it('should fail for nested named map layers', function(done) { templateMaps.addTemplate(username, nestedNamedMapTemplate, function(err) { if (err) { return done(err); @@ -200,32 +205,36 @@ suite('mapconfig_named_layers_adapter', function() { var nestedNamedMapLayerConfig = makeNamedMapLayerConfig({ name: nestedNamedMapTemplateName }); - mapConfigNamedLayersAdapter.getLayers(username, nestedNamedMapLayerConfig.layers, pgConnection, function(err, layers, datasource) { - assert.ok(err); - assert.ok(!layers); - assert.ok(!datasource); - assert.equal(err.message, 'Nested named layers are not allowed'); + mapConfigNamedLayersAdapter.getLayers(username, nestedNamedMapLayerConfig.layers, pgConnection, + function(err, layers, datasource) { + assert.ok(err); + assert.ok(!layers); + assert.ok(!datasource); + assert.equal(err.message, 'Nested named layers are not allowed'); - templateMaps.delTemplate(username, nestedNamedMapTemplateName, done); - }); + templateMaps.delTemplate(username, nestedNamedMapTemplateName, done); + } + ); }); }); - test('should return an expanded list of layers for a named map layer', function(done) { + it('should return an expanded list of layers for a named map layer', function(done) { var validNamedMapMapLayerConfig = makeNamedMapLayerConfig({ name: templateName }); - mapConfigNamedLayersAdapter.getLayers(username, validNamedMapMapLayerConfig.layers, pgConnection, function(err, layers, datasource) { - assert.ok(!err); - assert.ok(layers.length, 1); - assert.ok(layers[0].type, 'cartodb'); - assert.notEqual(datasource.getLayerDatasource(0), undefined); + mapConfigNamedLayersAdapter.getLayers(username, validNamedMapMapLayerConfig.layers, pgConnection, + function(err, layers, datasource) { + assert.ok(!err); + assert.ok(layers.length, 1); + assert.ok(layers[0].type, 'cartodb'); + assert.notEqual(datasource.getLayerDatasource(0), undefined); - done(); - }); + done(); + } + ); }); - test('should return on auth=token with valid tokens provided', function(done) { + it('should return on auth=token with valid tokens provided', function(done) { templateMaps.addTemplate(username, tokenAuthTemplate, function(err) { if (err) { return done(err); @@ -235,17 +244,19 @@ suite('mapconfig_named_layers_adapter', function() { name: tokenAuthTemplateName, auth_tokens: ['valid1'] }); - mapConfigNamedLayersAdapter.getLayers(username, validAuthTokensNamedMapLayerConfig.layers, pgConnection, function(err, layers, datasource) { - assert.ok(!err); - assert.equal(layers.length, 1); - assert.notEqual(datasource.getLayerDatasource(0), undefined); + mapConfigNamedLayersAdapter.getLayers(username, validAuthTokensNamedMapLayerConfig.layers, pgConnection, + function(err, layers, datasource) { + assert.ok(!err); + assert.equal(layers.length, 1); + assert.notEqual(datasource.getLayerDatasource(0), undefined); - templateMaps.delTemplate(username, tokenAuthTemplateName, done); - }); + templateMaps.delTemplate(username, tokenAuthTemplateName, done); + } + ); }); }); - test('should return an expanded list of layers for a named map layer, multiple layers version', function(done) { + it('should return an expanded list of layers for a named map layer, multiple layers version', function(done) { templateMaps.addTemplate(username, multipleLayersTemplate, function(err) { if (err) { return done(err); @@ -255,24 +266,26 @@ suite('mapconfig_named_layers_adapter', function() { name: multipleLayersTemplateName, auth_tokens: ['valid2'] }); - mapConfigNamedLayersAdapter.getLayers(username, multipleLayersNamedMapLayerConfig.layers, pgConnection, function(err, layers, datasource) { - assert.ok(!err); - assert.equal(layers.length, 2); + mapConfigNamedLayersAdapter.getLayers(username, multipleLayersNamedMapLayerConfig.layers, pgConnection, + function(err, layers, datasource) { + assert.ok(!err); + assert.equal(layers.length, 2); - assert.equal(layers[0].type, 'mapnik'); - assert.equal(layers[0].options.cartocss, '#layer { polygon-fill: green; }'); - assert.notEqual(datasource.getLayerDatasource(0), undefined); + assert.equal(layers[0].type, 'mapnik'); + assert.equal(layers[0].options.cartocss, '#layer { polygon-fill: green; }'); + assert.notEqual(datasource.getLayerDatasource(0), undefined); - assert.equal(layers[1].type, 'cartodb'); - assert.equal(layers[1].options.cartocss, '#layer { marker-fill: red; }'); - assert.notEqual(datasource.getLayerDatasource(1), undefined); + assert.equal(layers[1].type, 'cartodb'); + assert.equal(layers[1].options.cartocss, '#layer { marker-fill: red; }'); + assert.notEqual(datasource.getLayerDatasource(1), undefined); - templateMaps.delTemplate(username, multipleLayersTemplateName, done); - }); + templateMaps.delTemplate(username, multipleLayersTemplateName, done); + } + ); }); }); - test('should replace template params with the given config', function(done) { + it('should replace template params with the given config', function(done) { templateMaps.addTemplate(username, multipleLayersTemplate, function(err) { if (err) { return done(err); @@ -289,24 +302,26 @@ suite('mapconfig_named_layers_adapter', function() { }, auth_tokens: ['valid2'] }); - mapConfigNamedLayersAdapter.getLayers(username, multipleLayersNamedMapLayerConfig.layers, pgConnection, function(err, layers, datasource) { - assert.ok(!err); - assert.equal(layers.length, 2); + mapConfigNamedLayersAdapter.getLayers(username, multipleLayersNamedMapLayerConfig.layers, pgConnection, + function(err, layers, datasource) { + assert.ok(!err); + assert.equal(layers.length, 2); - assert.equal(layers[0].type, 'mapnik'); - assert.equal(layers[0].options.cartocss, '#layer { polygon-fill: ' + polygonColor + '; }'); - assert.notEqual(datasource.getLayerDatasource(0), undefined); + assert.equal(layers[0].type, 'mapnik'); + assert.equal(layers[0].options.cartocss, '#layer { polygon-fill: ' + polygonColor + '; }'); + assert.notEqual(datasource.getLayerDatasource(0), undefined); - assert.equal(layers[1].type, 'cartodb'); - assert.equal(layers[1].options.cartocss, '#layer { marker-fill: ' + color + '; }'); - assert.notEqual(datasource.getLayerDatasource(1), undefined); + assert.equal(layers[1].type, 'cartodb'); + assert.equal(layers[1].options.cartocss, '#layer { marker-fill: ' + color + '; }'); + assert.notEqual(datasource.getLayerDatasource(1), undefined); - templateMaps.delTemplate(username, multipleLayersTemplateName, done); - }); + templateMaps.delTemplate(username, multipleLayersTemplateName, done); + } + ); }); }); - suiteTeardown(function(done) { + after(function(done) { templateMaps.delTemplate(username, templateName, done); }); }); diff --git a/test/unit/cartodb/template_maps.test.js b/test/unit/cartodb/template_maps.test.js index f92999cc..e7201e41 100644 --- a/test/unit/cartodb/template_maps.test.js +++ b/test/unit/cartodb/template_maps.test.js @@ -1,16 +1,15 @@ -var assert = require('assert') - //, _ = require('underscore') - , RedisPool = require('redis-mpool') - , TemplateMaps = require('../../../lib/cartodb/template_maps.js') - , test_helper = require('../../support/test_helper') - , Step = require('step') - , _ = require('underscore') - , tests = module.exports = {}; +require('../../support/test_helper'); -suite('template_maps', function() { +var assert = require('assert'); +var RedisPool = require('redis-mpool'); +var TemplateMaps = require('../../../lib/cartodb/template_maps.js'); +var step = require('step'); +var _ = require('underscore'); + +describe('template_maps', function() { // configure redis pool instance to use in tests - var redis_pool = RedisPool(global.environment.redis); + var redis_pool = new RedisPool(global.environment.redis); var wadusLayer = { options: { @@ -20,24 +19,12 @@ suite('template_maps', function() { } }; - var validTemplate = { - version:'0.0.1', - name: 'first', - auth: {}, - layergroup: { - layers: [ - wadusLayer - ] - } - }; - var owner = 'me'; - - test('does not accept template with unsupported version', function(done) { + it('does not accept template with unsupported version', function(done) { var tmap = new TemplateMaps(redis_pool); assert.ok(tmap); var tpl = { version:'6.6.6', name:'k', auth: {}, layergroup: {layers:[wadusLayer]} }; - Step( + step( function() { tmap.addTemplate('me', tpl, this); }, @@ -52,12 +39,12 @@ suite('template_maps', function() { ); }); - test('does not accept template with missing name', function(done) { + it('does not accept template with missing name', function(done) { var tmap = new TemplateMaps(redis_pool); assert.ok(tmap); var tpl = { version:'0.0.1', auth: {}, layergroup: {layers:[wadusLayer]} }; - Step( + step( function() { tmap.addTemplate('me', tpl, this); }, @@ -72,7 +59,7 @@ suite('template_maps', function() { ); }); - test('does not accept template with invalid name', function(done) { + it('does not accept template with invalid name', function(done) { var tmap = new TemplateMaps(redis_pool); assert.ok(tmap); var tpl = { version:'0.0.1', @@ -87,8 +74,7 @@ suite('template_maps', function() { done(new Error("Unexpected success with invalid name '" + n + "'")); } else if ( ! err.message.match(/template.*name/i) ) { - done(new Error("Unexpected error message with invalid name '" + n - + "': " + err)); + done(new Error("Unexpected error message with invalid name '" + n + "': " + err)); } else { testNext(); @@ -98,7 +84,7 @@ suite('template_maps', function() { testNext(); }); - test('does not accept template with invalid placeholder name', function(done) { + it('does not accept template with invalid placeholder name', function(done) { var tmap = new TemplateMaps(redis_pool); assert.ok(tmap); var tpl = { version:'0.0.1', @@ -115,8 +101,7 @@ suite('template_maps', function() { done(new Error("Unexpected success with invalid name '" + n + "'")); } else if ( ! err.message.match(/invalid.*name/i) ) { - done(new Error("Unexpected error message with invalid name '" + n - + "': " + err)); + done(new Error("Unexpected error message with invalid name '" + n + "': " + err)); } else { testNext(); @@ -126,7 +111,7 @@ suite('template_maps', function() { testNext(); }); - test('does not accept template with missing placeholder default', function(done) { + it('does not accept template with missing placeholder default', function(done) { var tmap = new TemplateMaps(redis_pool); assert.ok(tmap); var tpl = { version:'0.0.1', @@ -137,8 +122,7 @@ suite('template_maps', function() { done(new Error("Unexpected success with missing placeholder default")); } else if ( ! err.message.match(/missing default/i) ) { - done(new Error("Unexpected error message with missing placeholder default: " - + err)); + done(new Error("Unexpected error message with missing placeholder default: " + err)); } else { done(); @@ -146,7 +130,7 @@ suite('template_maps', function() { }); }); - test('does not accept template with missing placeholder type', function(done) { + it('does not accept template with missing placeholder type', function(done) { var tmap = new TemplateMaps(redis_pool); assert.ok(tmap); var tpl = { version:'0.0.1', @@ -157,8 +141,7 @@ suite('template_maps', function() { done(new Error("Unexpected success with missing placeholder type")); } else if ( ! err.message.match(/missing type/i) ) { - done(new Error("Unexpected error message with missing placeholder default: " - + err)); + done(new Error("Unexpected error message with missing placeholder default: " + err)); } else { done(); @@ -167,7 +150,7 @@ suite('template_maps', function() { }); // See http://github.com/CartoDB/Windshaft-cartodb/issues/128 - test('does not accept template with invalid token auth (undefined tokens)', + it('does not accept template with invalid token auth (undefined tokens)', function(done) { var tmap = new TemplateMaps(redis_pool); assert.ok(tmap); @@ -179,8 +162,7 @@ suite('template_maps', function() { done(new Error("Unexpected success with invalid token auth (undefined tokens)")); } else if ( ! err.message.match(/invalid 'token' authentication/i) ) { - done(new Error("Unexpected error message with invalid token auth (undefined tokens): " - + err)); + done(new Error("Unexpected error message with invalid token auth (undefined tokens): " + err)); } else { done(); @@ -188,14 +170,14 @@ suite('template_maps', function() { }); }); - test('add, get and delete a valid template', function(done) { + it('add, get and delete a valid template', function(done) { var tmap = new TemplateMaps(redis_pool); assert.ok(tmap); var expected_failure = false; var tpl_id; var tpl = { version:'0.0.1', name: 'first', auth: {}, layergroup: {layers:[wadusLayer]} }; - Step( + step( function() { tmap.addTemplate('me', tpl, this); }, @@ -224,15 +206,14 @@ suite('template_maps', function() { ); }); - test('add multiple templates, list them', function(done) { + it('add multiple templates, list them', function(done) { var tmap = new TemplateMaps(redis_pool); assert.ok(tmap); - var expected_failure = false; var tpl1 = { version:'0.0.1', name: 'first', auth: {}, layergroup: {layers:[wadusLayer]} }; var tpl1_id; var tpl2 = { version:'0.0.1', name: 'second', auth: {}, layergroup: {layers:[wadusLayer]} }; var tpl2_id; - Step( + step( function addTemplate1() { tmap.addTemplate('me', tpl1, this); }, @@ -283,7 +264,7 @@ suite('template_maps', function() { ); }); - test('update templates', function(done) { + it('update templates', function(done) { var tmap = new TemplateMaps(redis_pool); assert.ok(tmap); var expected_failure = false; @@ -294,7 +275,7 @@ suite('template_maps', function() { layergroup: {layers:[wadusLayer]} }; var tpl_id; - Step( + step( function addTemplate() { tmap.addTemplate(owner, tpl, this); }, @@ -323,7 +304,6 @@ suite('template_maps', function() { }, function updateUnexistentTemplate(err) { if ( err && ! expected_failure) throw err; - expected_failure = false; assert.ok(err); assert.ok(err.message.match(/unsupported.*version/i), err); tpl.version = '0.0.1'; @@ -343,7 +323,7 @@ suite('template_maps', function() { ); }); - test('instanciate templates', function() { + it('instanciate templates', function() { var tmap = new TemplateMaps(redis_pool); assert.ok(tmap); @@ -356,7 +336,7 @@ suite('template_maps', function() { color: { type: "css_color", default: "#a0fF9A" }, name: { type: "sql_literal", default: "test" }, zoom: { type: "number", default: "0" }, - test_number: { type: "number", default: 23 }, + test_number: { type: "number", default: 23 } }, layergroup: { version: '1.0.0', @@ -400,48 +380,48 @@ suite('template_maps', function() { // Invalid css_color var err = null; - try { inst = tmap.instance(tpl1, {color:'##ff00ff'}); } + try { tmap.instance(tpl1, {color:'##ff00ff'}); } catch (e) { err = e; } assert.ok(err); assert.ok(err.message.match(/invalid css_color/i), err); // Invalid css_color 2 (too few digits) - var err = null; - try { inst = tmap.instance(tpl1, {color:'#ff'}); } + err = null; + try { tmap.instance(tpl1, {color:'#ff'}); } catch (e) { err = e; } assert.ok(err); assert.ok(err.message.match(/invalid css_color/i), err); // Invalid css_color 3 (too many digits) - var err = null; - try { inst = tmap.instance(tpl1, {color:'#1234567'}); } + err = null; + try { tmap.instance(tpl1, {color:'#1234567'}); } catch (e) { err = e; } assert.ok(err); assert.ok(err.message.match(/invalid css_color/i), err); // Invalid number - var err = null; - try { inst = tmap.instance(tpl1, {zoom:'#'}); } + err = null; + try { tmap.instance(tpl1, {zoom:'#'}); } catch (e) { err = e; } assert.ok(err); assert.ok(err.message.match(/invalid number/i), err); // Invalid number 2 - var err = null; - try { inst = tmap.instance(tpl1, {zoom:'23e'}); } + err = null; + try { tmap.instance(tpl1, {zoom:'23e'}); } catch (e) { err = e; } assert.ok(err); assert.ok(err.message.match(/invalid number/i), err); // Valid number - var err = null; - try { inst = tmap.instance(tpl1, {zoom:'-.23e10'}); } + err = null; + try { tmap.instance(tpl1, {zoom:'-.23e10'}); } catch (e) { err = e; } assert.ok(!err); }); // Can set a limit on the number of user templates - test('can limit number of user templates', function(done) { + it('can limit number of user templates', function(done) { var tmap = new TemplateMaps(redis_pool, { max_user_templates: 2 }); @@ -450,7 +430,7 @@ suite('template_maps', function() { var expectErr = false; var idMe = []; var idYou = []; - Step( + step( function oneForMe() { tpl.name = 'oneForMe'; tmap.addTemplate('me', tpl, this); @@ -470,7 +450,7 @@ suite('template_maps', function() { expectErr = true; tmap.addTemplate('me', tpl, this); }, - function errForMe(err, id) { + function errForMe(err/*, id*/) { if ( err && ! expectErr ) throw err; expectErr = false; assert.ok(err); @@ -508,7 +488,7 @@ suite('template_maps', function() { expectErr = true; tmap.addTemplate('you', tpl, this); }, - function errForYou(err, id) { + function errForYou(err/*, id*/) { if ( err && ! expectErr ) throw err; expectErr = false; assert.ok(err); diff --git a/test/unit/cartodb/template_maps_auth.test.js b/test/unit/cartodb/template_maps_auth.test.js index 592e0257..76258695 100644 --- a/test/unit/cartodb/template_maps_auth.test.js +++ b/test/unit/cartodb/template_maps_auth.test.js @@ -1,11 +1,11 @@ +require('../../support/test_helper'); + var assert = require('assert'); var RedisPool = require('redis-mpool'); -var TemplateMaps = require('../../../lib/cartodb/template_maps'); -var test_helper = require('../../support/test_helper'); -var Step = require('step'); -var tests = module.exports = {}; -suite('template_maps_auth', function() { +var TemplateMaps = require('../../../lib/cartodb/template_maps'); + +describe('template_maps_auth', function() { // configure redis pool instance to use in tests var redisPool = new RedisPool(global.environment.redis), @@ -84,15 +84,15 @@ suite('template_maps_auth', function() { ]; authorizationTestScenarios.forEach(function(testScenario) { - test(testScenario.desc, function(done) { + it(testScenario.desc, function(done) { var debugMessage = testScenario.expected ? 'should be authorized' : 'unexpectedly authorized'; var result = templateMaps.isAuthorized(testScenario.template, testScenario.token); assert.equal(result, testScenario.expected, debugMessage); done(); - }) + }); }); - test("auth as 'open' string is authorized", function(done) { + it("auth as 'open' string is authorized", function(done) { var template = { name: 'wadus_template', auth: 'open' diff --git a/test/unit/cartodb/template_maps_defaults.test.js b/test/unit/cartodb/template_maps_defaults.test.js index 848c4aac..bbc70062 100644 --- a/test/unit/cartodb/template_maps_defaults.test.js +++ b/test/unit/cartodb/template_maps_defaults.test.js @@ -1,10 +1,11 @@ +require('../../support/test_helper'); + var assert = require('assert'); var RedisPool = require('redis-mpool'); var TemplateMaps = require('../../../lib/cartodb/template_maps.js'); -var test_helper = require('../../support/test_helper'); var _ = require('underscore'); -suite('template_maps', function() { +describe('template_maps', function() { var redisPool = new RedisPool(global.environment.redis), templateMaps = new TemplateMaps(redisPool); @@ -88,7 +89,7 @@ suite('template_maps', function() { ]; testScenarios.forEach(function(testScenario) { - test('adding template returns a new instance with ' + testScenario.desc, function(done) { + it('adding template returns a new instance with ' + testScenario.desc, function(done) { templateMaps.addTemplate(owner, testScenario.template, function(err, templateId, template) { assert.ok(!err, 'Unexpected error adding template: ' + (err && err.message)); diff --git a/test/unit/cartodb/valid_template_maps.test.js b/test/unit/cartodb/valid_template_maps.test.js index 09dab17c..ac66e4e3 100644 --- a/test/unit/cartodb/valid_template_maps.test.js +++ b/test/unit/cartodb/valid_template_maps.test.js @@ -1,10 +1,11 @@ +require('../../support/test_helper'); + var assert = require('assert'); var RedisPool = require('redis-mpool'); var TemplateMaps = require('../../../lib/cartodb/template_maps.js'); -var test_helper = require('../../support/test_helper'); var _ = require('underscore'); -suite('template_maps', function() { +describe('template_maps', function() { var redisPool = new RedisPool(global.environment.redis), templateMaps = new TemplateMaps(redisPool); @@ -87,7 +88,7 @@ suite('template_maps', function() { ]; testScenarios.forEach(function(testScenario) { - test(testScenario.desc, function(done) { + it(testScenario.desc, function(done) { templateMaps.addTemplate(owner, testScenario.template, function(err) {