diff --git a/Makefile b/Makefile index 795b997c..32a48859 100644 --- a/Makefile +++ b/Makefile @@ -17,8 +17,11 @@ config/environments/test.js: config.status--test check-local: config/environments/test.js ./run_tests.sh ${RUNTESTFLAGS} \ - test/unit/cartodb/*.js \ - test/acceptance/*.js + test/unit/cartodb/*.js \ + test/unit/cartodb/cache/model/*.js \ + test/integration/*.js \ + test/acceptance/*.js \ + test/acceptance/cache/*.js check-submodules: PATH="$$PATH:$(srcdir)/node_modules/.bin/"; \ diff --git a/NEWS.md b/NEWS.md index 509dbc9b..e17c8ea6 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,15 @@ -1.26.3 -- 2015-mm-dd +1.27.0 -- 2015-mm-dd -------------------- +Announcements: +- Adds default image placeholder for http renderer to use as fallback + +New features: +- `named` layers type, see [MapConfig-NamedMaps-extension](docs/MapConfig-NamedMaps-extension.md) + - Starts using datasource per layer feature from Windshaft ([2c7bc6a](https://github.com/CartoDB/Windshaft-cartodb/commit/2c7bc6adde561b20ed955b905e3c7bcd6795d128)) + +Bugfixes: +- Fixes tests with beforeEach and afterEach triggers 1.26.2 -- 2015-01-28 -------------------- diff --git a/assets/default-placeholder.png b/assets/default-placeholder.png new file mode 100644 index 00000000..de4079ef Binary files /dev/null and b/assets/default-placeholder.png differ diff --git a/assets/default-placeholder@2x.png b/assets/default-placeholder@2x.png new file mode 100644 index 00000000..a52d3513 Binary files /dev/null and b/assets/default-placeholder@2x.png differ diff --git a/config/environments/development.js.example b/config/environments/development.js.example index e28c51e6..2c391dd1 100644 --- a/config/environments/development.js.example +++ b/config/environments/development.js.example @@ -94,7 +94,13 @@ var config = { proxy: undefined, // the url for a proxy server whitelist: [ // the whitelist of urlTemplates that can be used 'http://{s}.example.com/{z}/{x}/{y}.png' - ] + ], + // image to use as placeholder when urlTemplate is not in the whitelist + // if provided the http renderer will use it instead of throw an error + fallbackImage: { + type: 'fs', // 'fs' and 'url' supported + src: __dirname + '/../../assets/default-placeholder.png' + } } } ,millstone: { diff --git a/config/environments/production.js.example b/config/environments/production.js.example index 6c4ea756..0d4e999a 100644 --- a/config/environments/production.js.example +++ b/config/environments/production.js.example @@ -88,7 +88,13 @@ var config = { proxy: undefined, // the url for a proxy server whitelist: [ // the whitelist of urlTemplates that can be used 'http://{s}.example.com/{z}/{x}/{y}.png' - ] + ], + // image to use as placeholder when urlTemplate is not in the whitelist + // if provided the http renderer will use it instead of throw an error + fallbackImage: { + type: 'fs', // 'fs' and 'url' supported + src: __dirname + '/../../assets/default-placeholder.png' + } } } ,millstone: { diff --git a/config/environments/staging.js.example b/config/environments/staging.js.example index f8da00db..0c88a9c7 100644 --- a/config/environments/staging.js.example +++ b/config/environments/staging.js.example @@ -88,7 +88,13 @@ var config = { proxy: undefined, // the url for a proxy server whitelist: [ // the whitelist of urlTemplates that can be used 'http://{s}.example.com/{z}/{x}/{y}.png' - ] + ], + // image to use as placeholder when urlTemplate is not in the whitelist + // if provided the http renderer will use it instead of throw an error + fallbackImage: { + type: 'fs', // 'fs' and 'url' supported + src: __dirname + '/../../assets/default-placeholder.png' + } } } ,millstone: { diff --git a/config/environments/test.js.example b/config/environments/test.js.example index 39bf1d15..14b78f61 100644 --- a/config/environments/test.js.example +++ b/config/environments/test.js.example @@ -90,7 +90,13 @@ var config = { 'http://{s}.example.com/{z}/{x}/{y}.png', // for testing purposes 'http://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png' - ] + ], + // image to use as placeholder when urlTemplate is not in the whitelist + // if provided the http renderer will use it instead of throw an error + fallbackImage: { + type: 'fs', // 'fs' and 'url' supported + src: __dirname + '/../../assets/default-placeholder.png' + } } } ,millstone: { diff --git a/docs/MapConfig-NamedMaps-extension.md b/docs/MapConfig-NamedMaps-extension.md new file mode 100644 index 00000000..a6418174 --- /dev/null +++ b/docs/MapConfig-NamedMaps-extension.md @@ -0,0 +1,56 @@ +# 1. Purpose + +This specification describes an extension for +[MapConfig 1.3.0](https://github.com/CartoDB/Windshaft/blob/master/doc/MapConfig-1.3.0.md) version. + + +# 2. Changes over specification + +This extension introduces a new layer type so it's possible to use a named map by its name as a layer. + +## 2.1 Named layers definition + +```javascript +{ + // REQUIRED + // string, `named` is the only supported value + type: "named", + + // REQUIRED + // object, set `named` map layers configuration + options: { + + // REQUIRED + // string, the name for the named map to use + name: "world_borders", + + // OPTIONAL + // object, the replacement values for the named map's template placeholders + // See https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/Map-API.md#instantiate-1 for more details + config: { + "color": "#000" + }, + + // OPTIONAL + // string array, the authorized tokens in case the named map has auth method set to `token` + // See https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/Map-API.md#named-maps-1 for more details + auth_tokens: [ + "token1", + "token2" + ] + } +} +``` + +## 2.2 Limitations + +1. A Named Map will not allow to have `named` type layers inside their templates layergroup's layers definition. +2. A `named` layer does not allow Named Maps form other accounts, it's only possible to use Named Maps from the very +same user account. + + +# History + +## 1.0.0 + + - Initial version diff --git a/lib/cartodb/cartodb_windshaft.js b/lib/cartodb/cartodb_windshaft.js index 22bbb074..477a4c24 100644 --- a/lib/cartodb/cartodb_windshaft.js +++ b/lib/cartodb/cartodb_windshaft.js @@ -1,11 +1,9 @@ -var _ = require('underscore') - , Step = require('step') - , Windshaft = require('windshaft') - , TemplateMaps = require('./template_maps.js') - , Cache = require('./cache_validator') - , os = require('os') - , HealthCheck = require('./monitoring/health_check') -; +var _ = require('underscore'); +var Step = require('step'); +var Windshaft = require('windshaft'); +var Cache = require('./cache_validator'); +var os = require('os'); +var HealthCheck = require('./monitoring/health_check'); if ( ! process.env['PGAPPNAME'] ) process.env['PGAPPNAME']='cartodb_tiler'; @@ -25,6 +23,8 @@ var CartodbWindshaft = function(serverOptions) { var cartoData = require('cartodb-redis')({pool: redisPool}); + var templateMaps = serverOptions.templateMaps; + if(serverOptions.cache_enabled) { console.log("cache invalidation enabled, varnish on ", serverOptions.varnish_host, ' ', serverOptions.varnish_port); Cache.init(serverOptions.varnish_host, serverOptions.varnish_port, serverOptions.varnish_secret); @@ -48,12 +48,6 @@ var CartodbWindshaft = function(serverOptions) { // var template_baseurl = global.environment.base_url_templated || '(?:/maps/named|/tiles/template)'; - var templateMapsOpts = { - max_user_templates: global.environment.maxUserTemplates - }; - var templateMaps = new TemplateMaps(redisPool, templateMapsOpts); - serverOptions.templateMaps = templateMaps; - var SurrogateKeysCache = require('./cache/surrogate_keys_cache'), NamedMapsCacheEntry = require('./cache/model/named_maps_entry'), VarnishHttpCacheBackend = require('./cache/backend/varnish_http'), diff --git a/lib/cartodb/models/mapconfig_named_layers_adapter.js b/lib/cartodb/models/mapconfig_named_layers_adapter.js new file mode 100644 index 00000000..ceb9c429 --- /dev/null +++ b/lib/cartodb/models/mapconfig_named_layers_adapter.js @@ -0,0 +1,120 @@ +var queue = require('queue-async'); +var _ = require('underscore'); +var Datasource = require('windshaft').Datasource; + +function MapConfigNamedLayersAdapter(templateMaps) { + this.templateMaps = templateMaps; +} + +module.exports = MapConfigNamedLayersAdapter; + +MapConfigNamedLayersAdapter.prototype.getLayers = function(username, layers, dbMetadata, callback) { + var self = this; + + var adaptLayersQueue = queue(layers.length); + + function adaptLayer(layer, done) { + if (isNamedTypeLayer(layer)) { + + if (!layer.options.name) { + return done(new Error('Missing Named Map `name` in layer options')); + } + + var templateName = layer.options.name; + var templateConfigParams = layer.options.config || {}; + var templateAuthTokens = layer.options.auth_tokens; + + self.templateMaps.getTemplate(username, templateName, function(err, template) { + if (err || !template) { + return done(new Error("Template '" + templateName + "' of user '" + username + "' not found")); + } + + if (self.templateMaps.isAuthorized(template, templateAuthTokens)) { + var nestedNamedLayers = template.layergroup.layers.filter(function(layer) { + return layer.type === 'named'; + }); + + if (nestedNamedLayers.length > 0) { + var nestedNamedMapsError = new Error('Nested named layers are not allowed'); + // nestedNamedMapsError.http_status = 400; + return done(nestedNamedMapsError); + } + + try { + var templateLayergroupConfig = self.templateMaps.instance(template, templateConfigParams); + return done(null, { + datasource: true, + layers: templateLayergroupConfig.layers + }); + } catch (err) { + return done(err); + } + } else { + var unauthorizedError = new Error("Unauthorized '" + templateName + "' template instantiation"); + unauthorizedError.http_status = 403; + return done(unauthorizedError); + } + }); + + } else { + return done(null, { + datasource: false, + layers: [layer] + }); + } + } + + var datasourceBuilder = new Datasource.Builder(); + + function layersAdaptQueueFinish(err, layersResults) { + if (err) { + return callback(err); + } + + if (!layersResults || layersResults.length === 0) { + return callback(new Error('Missing layers array from layergroup config')); + } + + var layers = [], + currentLayerIndex = 0; + + layersResults.forEach(function(layersResult) { + + layersResult.layers.forEach(function(layer) { + layers.push(layer); + if (layersResult.datasource) { + datasourceBuilder.withLayerDatasource(currentLayerIndex, { + user: dbAuth.dbuser + }); + } + currentLayerIndex++; + }); + + }); + + return callback(null, layers, datasourceBuilder.build()); + } + + + var dbAuth = {}; + + if (_.some(layers, isNamedTypeLayer)) { + // Lazy load dbAuth + dbMetadata.setDBAuth(username, dbAuth, function(err) { + if (err) { + return callback(err); + } + layers.forEach(function(layer) { + adaptLayersQueue.defer(adaptLayer, layer); + }); + adaptLayersQueue.awaitAll(layersAdaptQueueFinish); + }); + } else { + return callback(null, layers, datasourceBuilder.build()); + } + +}; + +function isNamedTypeLayer(layer) { + return layer.type === 'named'; +} diff --git a/lib/cartodb/server_options.js b/lib/cartodb/server_options.js index 0aadeaa7..a0f1808c 100644 --- a/lib/cartodb/server_options.js +++ b/lib/cartodb/server_options.js @@ -4,6 +4,8 @@ var QueryTablesApi = require('./api/query_tables_api'); var PgConnection = require('./backends/pg_connection'); var crypto = require('crypto'); var LZMA = require('lzma').LZMA; +var TemplateMaps = require('./template_maps.js'); +var MapConfigNamedLayersAdapter = require('./models/mapconfig_named_layers_adapter'); // This is for backward compatibility with 1.3.3 if ( _.isUndefined(global.environment.sqlapi.domain) ) { @@ -33,8 +35,10 @@ var REQUEST_QUERY_PARAMS_WHITELIST = [ ]; module.exports = function(redisPool) { - var redisOpts = redisPool ? {pool: redisPool} : global.environment.redis; - var cartoData = require('cartodb-redis')(redisOpts), + redisPool = redisPool + || require('redis-mpool')(_.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); @@ -107,6 +111,13 @@ module.exports = function(redisPool) { // Re-use pgConnection me.pgConnection = pgConnection; + var templateMaps = new TemplateMaps(redisPool, { + max_user_templates: global.environment.maxUserTemplates + }); + me.templateMaps = templateMaps; + + var mapConfigNamedLayersAdapter = new MapConfigNamedLayersAdapter(templateMaps); + /* This whole block is about generating X-Cache-Channel { */ // TODO: review lifetime of elements of this cache @@ -269,6 +280,23 @@ module.exports = function(redisPool) { }); }; + me.beforeLayergroupCreate = function(req, requestMapConfig, callback) { + mapConfigNamedLayersAdapter.getLayers(this.userByReq(req), requestMapConfig.layers, this, function(err, layers, datasource) { + if (err) { + return callback(err); + } + + if (!datasource.isEmpty()) { + setContext(req, 'queryTablesApiDatasource', _.find(datasource.layersDbParams, function(layerDbParams) { + return !!layerDbParams; + })); + } + + requestMapConfig.layers = layers; + return callback(null, requestMapConfig, datasource) + }); + }; + me.afterLayergroupCreate = function(req, mapconfig, response, callback) { var token = response.layergroupid; @@ -699,5 +727,36 @@ module.exports = function(redisPool) { ); }; + /******************************************************************************************************************* + * Private methods + ******************************************************************************************************************/ + + /** + * Handles context for a given Request object + * @param {Object|IncomingMessage} req + * @param {String} key + * @returns {*} + */ + function getContext(req, key) { + return req.context && req.context[key]; + } + + /** + * Handles context for a given Request object + * @param {Object|IncomingMessage} req + * @param {String} key + * @param {*} value + * @returns {*} The previous value + */ + function setContext(req, key, value) { + var previousValue; + if (value) { + req.context = req.context || {}; + previousValue = req.context[key]; + req.context[key] = value; + } + return previousValue; + } + return me; }; diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index f2f6d72e..8a65a9fa 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -113,6 +113,11 @@ "from": "https://github.com/Vizzuality/node-varnish/tarball/0.3.0", "resolved": "https://github.com/Vizzuality/node-varnish/tarball/0.3.0" }, + "queue-async": { + "version": "1.0.7", + "from": "queue-async@~1.0.7", + "resolved": "https://registry.npmjs.org/queue-async/-/queue-async-1.0.7.tgz" + }, "redis-mpool": { "version": "0.3.0", "from": "https://github.com/CartoDB/node-redis-mpool/tarball/0.3.0", @@ -178,24 +183,17 @@ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz" }, "windshaft": { - "version": "0.35.1", - "from": "https://github.com/CartoDB/Windshaft/tarball/0.35.1", - "resolved": "https://github.com/CartoDB/Windshaft/tarball/0.35.1", + "version": "0.36.0", + "from": "windshaft@~0.36.0", "dependencies": { "chronograph": { "version": "0.1.0", "from": "chronograph@git://github.com/CartoDB/chronographjs.git#0.1.0", "resolved": "git://github.com/CartoDB/chronographjs.git#0b8c35eee510cfa14a16be24d70533b38ecc1d2d" }, - "queue-async": { - "version": "1.0.7", - "from": "queue-async@~1.0.7", - "resolved": "https://registry.npmjs.org/queue-async/-/queue-async-1.0.7.tgz" - }, "grainstore": { - "version": "0.22.1", - "from": "https://github.com/CartoDB/grainstore/tarball/0.22.1", - "resolved": "https://github.com/CartoDB/grainstore/tarball/0.22.1", + "version": "0.23.0", + "from": "grainstore@~0.23.0", "dependencies": { "carto": { "version": "0.9.5-cdb2", @@ -1637,9 +1635,9 @@ "resolved": "https://registry.npmjs.org/connect/-/connect-1.9.2.tgz", "dependencies": { "formidable": { - "version": "1.0.16", + "version": "1.0.17", "from": "formidable@1.0.x", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.0.16.tgz" + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.0.17.tgz" } } }, diff --git a/package.json b/package.json index b9d0efda..3c6fa48a 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,9 @@ "node-varnish": "https://github.com/Vizzuality/node-varnish/tarball/0.3.0", "underscore" : "~1.6.0", "dot": "~1.0.2", - "windshaft": "https://github.com/CartoDB/Windshaft/tarball/0.35.1", + "windshaft": "~0.36.0", "step": "~0.0.5", + "queue-async": "~1.0.7", "request": "~2.9.203", "cartodb-redis": "https://github.com/CartoDB/node-cartodb-redis/tarball/0.11.0", "cartodb-psql": "https://github.com/CartoDB/node-cartodb-psql/tarball/0.4.0", diff --git a/test/acceptance/cache/surrogate_keys_invalidation.js b/test/acceptance/cache/surrogate_keys_invalidation.js index c4bb3818..118c6a63 100644 --- a/test/acceptance/cache/surrogate_keys_invalidation.js +++ b/test/acceptance/cache/surrogate_keys_invalidation.js @@ -16,9 +16,13 @@ var serverOptions = ServerOptions(); suite('templates surrogate keys', function() { - var redisClient, - sqlApiServer, - server; + var sqlApiServer; + var redisClient = redis.createClient(global.environment.redis.port); + + // Enable Varnish purge for tests + serverOptions.varnish_purge_enabled = true; + + var server = new CartodbWindshaft(serverOptions); var templateOwner = 'localhost', templateName = 'acceptance', @@ -45,19 +49,12 @@ suite('templates surrogate keys', function() { expectedBody = { template_id: expectedTemplateId }; suiteSetup(function(done) { - // Enable Varnish purge for tests - serverOptions.varnish_purge_enabled = true; - - server = new CartodbWindshaft(serverOptions); - sqlApiServer = new SqlApiEmulator(global.environment.sqlapi.port, done); - - redisClient = redis.createClient(global.environment.redis.port); }); var surrogateKeysCacheInvalidateFn = SurrogateKeysCache.prototype.invalidate; - beforeEach(function(done) { + function createTemplate(callback) { var postTemplateRequest = { url: '/tiles/template?api_key=1234', method: 'POST', @@ -90,10 +87,10 @@ suite('templates surrogate keys', function() { return true; }, function finish(err) { - done(err); + callback(err); } ); - }); + } test("update template calls surrogate keys invalidation", function(done) { var cacheEntryKey; @@ -104,7 +101,13 @@ suite('templates surrogate keys', function() { }; Step( - function putValidTemplate() { + function createTemplateToUpdate() { + createTemplate(this); + }, + function putValidTemplate(err) { + if (err) { + throw err; + } var updateTemplateRequest = { url: '/tiles/template/' + expectedTemplateId + '/?api_key=1234', method: 'PUT', @@ -163,7 +166,13 @@ suite('templates surrogate keys', function() { }; Step( - function putValidTemplate() { + function createTemplateToDelete() { + createTemplate(this); + }, + function deleteValidTemplate(err) { + if (err) { + throw err; + } var deleteTemplateRequest = { url: '/tiles/template/' + expectedTemplateId + '/?api_key=1234', method: 'DELETE', @@ -199,12 +208,10 @@ suite('templates surrogate keys', function() { ); }); - afterEach(function(done) { - SurrogateKeysCache.prototype.invalidate = surrogateKeysCacheInvalidateFn; - done(); - }); - suiteTeardown(function(done) { + SurrogateKeysCache.prototype.invalidate = surrogateKeysCacheInvalidateFn; + // Enable Varnish purge for tests + serverOptions.varnish_purge_enabled = false; sqlApiServer.close(done); }); diff --git a/test/acceptance/health_check.js b/test/acceptance/health_check.js index af8c361e..b6409574 100644 --- a/test/acceptance/health_check.js +++ b/test/acceptance/health_check.js @@ -7,7 +7,7 @@ var server = new CartodbWindshaft(serverOptions); suite('health checks', function () { - beforeEach(function (done) { + function resetHealthConfig() { global.environment.health = { enabled: true, username: 'localhost', @@ -15,8 +15,7 @@ suite('health checks', function () { x: 0, y: 0 }; - done(); - }); + } var healthCheckRequest = { url: '/health', @@ -27,13 +26,14 @@ suite('health checks', function () { }; test('returns 200 and ok=true with enabled configuration', function (done) { + resetHealthConfig(); + assert.response(server, healthCheckRequest, { status: 200 }, function (res, err) { - console.log(res.body); assert.ok(!err); var parsed = JSON.parse(res.body); @@ -47,6 +47,8 @@ suite('health checks', function () { }); test('fails for invalid user because it is not in redis', function (done) { + resetHealthConfig(); + global.environment.health.username = 'invalid'; assert.response(server, diff --git a/test/acceptance/named_layers.js b/test/acceptance/named_layers.js new file mode 100644 index 00000000..f1a67528 --- /dev/null +++ b/test/acceptance/named_layers.js @@ -0,0 +1,527 @@ +var test_helper = require('../support/test_helper'); + +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 RedisPool = require('redis-mpool'); +var TemplateMaps = require('../../lib/cartodb/template_maps.js'); + +var Step = require('step'); +var _ = require('underscore'); + +suite('named_layers', function() { + // configure redis pool instance to use in tests + var redisPool = 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 + ] + } + }; + + suiteSetup(function(done) { + global.environment.enabledFeatures = {cdbQueryTablesFromPostgres: true}; + 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); + }); + }); + }); + }); + + test('should fail for non-existing template name', function(done) { + var layergroup = { + version: '1.3.0', + layers: [ + { + type: 'named', + options: { + name: 'nonexistent' + } + } + ] + }; + + Step( + function createLayergroup() { + var next = this; + assert.response(server, + { + url: '/tiles/layergroup', + method: 'POST', + headers: { + host: 'localhost', + 'Content-Type': 'application/json' + }, + data: JSON.stringify(layergroup) + }, + { + status: 400 + }, + function(res, err) { + next(err, res); + } + ); + }, + function checkLayergroup(err, response) { + if (err) { + throw err; + } + + var parsedBody = JSON.parse(response.body); + assert.deepEqual(parsedBody, { errors: ["Template 'nonexistent' of user 'localhost' not found"] }); + + return null; + }, + function finish(err) { + done(err); + } + ); + }); + + test('should return 403 if not properly authorized', function(done) { + + var layergroup = { + version: '1.3.0', + layers: [ + { + type: 'named', + options: { + name: tokenAuthTemplateName, + config: {}, + auth_tokens: ['token1'] + } + } + ] + }; + + Step( + function createLayergroup() { + var next = this; + assert.response(server, + { + url: '/tiles/layergroup', + method: 'POST', + headers: { + host: 'localhost', + 'Content-Type': 'application/json' + }, + data: JSON.stringify(layergroup) + }, + { + status: 403 + }, + function(res, err) { + next(err, res); + } + ); + }, + function checkLayergroup(err, response) { + if (err) { + throw err; + } + + var parsedBody = JSON.parse(response.body); + assert.deepEqual( + parsedBody, + { errors: [ "Unauthorized 'auth_valid_template' template instantiation" ] } + ); + + return null; + }, + function finish(err) { + done(err); + } + ); + + }); + + test('should return 200 and layergroup if properly authorized', function(done) { + + var layergroup = { + version: '1.3.0', + layers: [ + { + type: 'named', + options: { + name: tokenAuthTemplateName, + config: {}, + auth_tokens: ['valid1'] + } + } + ] + }; + + Step( + function createLayergroup() { + var next = this; + assert.response(server, + { + url: '/tiles/layergroup', + method: 'POST', + headers: { + host: 'localhost', + 'Content-Type': 'application/json' + }, + data: JSON.stringify(layergroup) + }, + { + status: 200 + }, + function(res, err) { + next(err, res); + } + ); + }, + function checkLayergroup(err, response) { + if (err) { + throw err; + } + + var parsedBody = JSON.parse(response.body); + assert.ok(parsedBody.layergroupid); + assert.ok(parsedBody.last_updated); + + return null; + }, + function finish(err) { + done(err); + } + ); + + }); + + test('should return 400 for nested named map layers', function(done) { + + var layergroup = { + version: '1.3.0', + layers: [ + { + type: 'named', + options: { + name: nestedNamedMapTemplateName + } + } + ] + }; + + Step( + function createLayergroup() { + var next = this; + assert.response(server, + { + url: '/tiles/layergroup', + method: 'POST', + headers: { + host: 'localhost', + 'Content-Type': 'application/json' + }, + data: JSON.stringify(layergroup) + }, + { + status: 400 + }, + function(res, err) { + next(err, res); + } + ); + }, + function checkLayergroup(err, response) { + if (err) { + throw err; + } + + var parsedBody = JSON.parse(response.body); + assert.deepEqual(parsedBody, { errors: [ 'Nested named layers are not allowed' ] }); + + return null; + }, + function finish(err) { + done(err); + } + ); + + }); + + test('should return 200 and layergroup with private tables', function(done) { + + var privateTableTemplateName = 'private_table_template'; + var privateTableTemplate = { + version: '0.0.1', + name: privateTableTemplateName, + auth: { + method: 'open' + }, + layergroup: { + layers: [ + { + type: 'cartodb', + options: { + sql: 'select * from test_table_private_1', + cartocss: '#layer { marker-fill: #cc3300; }', + cartocss_version: '2.3.0' + } + } + ] + } + }; + + var layergroup = { + version: '1.3.0', + layers: [ + { + type: 'named', + options: { + name: privateTableTemplateName + } + } + ] + }; + + Step( + function createTemplate() { + templateMaps.addTemplate(username, privateTableTemplate, this); + }, + function createLayergroup(err) { + if (err) { + throw err; + } + + var next = this; + assert.response(server, + { + url: '/tiles/layergroup', + method: 'POST', + headers: { + host: 'localhost', + 'Content-Type': 'application/json' + }, + data: JSON.stringify(layergroup) + }, + { + status: 200 + }, + function(res, err) { + next(err, res); + } + ); + }, + function checkLayergroup(err, response) { + if (err) { + throw err; + } + + var parsedBody = JSON.parse(response.body); + assert.ok(parsedBody.layergroupid); + assert.ok(parsedBody.last_updated); + + return parsedBody.layergroupid; + }, + function requestTile(err, layergroupId) { + if (err) { + throw err; + } + + var next = this; + assert.response(server, + { + url: '/tiles/layergroup/' + layergroupId + '/0/0/0.png', + method: 'GET', + headers: { + host: 'localhost' + }, + encoding: 'binary' + }, + { + status: 200, + headers: { + 'content-type': 'image/png' + } + }, + function(res, err) { + next(err, res); + } + ); + }, + function handleTileResponse(err, res) { + if (err) { + throw err; + } + //assert.ok(res.headers['X-Cache-Channel']); -> https://github.com/CartoDB/Windshaft-cartodb/issues/253 + return true; + }, + function deleteTemplate(err) { + var next = this; + templateMaps.delTemplate(username, privateTableTemplate, function(/*delErr*/) { + // ignore deletion error + next(err); + }); + }, + function finish(err) { + done(err); + } + ); + + }); + + test('should return 403 when private table is accessed from non named layer', function(done) { + + var layergroup = { + version: '1.3.0', + layers: [ + { + type: 'cartodb', + options: { + sql: 'select * from test_table_private_1', + cartocss: '#layer { marker-fill: #cc3300; }', + cartocss_version: '2.3.0' + } + }, + { + type: 'named', + options: { + name: templateName + } + } + ] + }; + + Step( + function createLayergroup() { + var next = this; + assert.response(server, + { + url: '/tiles/layergroup', + method: 'POST', + headers: { + host: 'localhost', + 'Content-Type': 'application/json' + }, + data: JSON.stringify(layergroup) + }, + { + status: 403 + }, + function(res, err) { + next(err, res); + } + ); + }, + function checkLayergroup(err, response) { + if (err) { + throw err; + } + + var parsedBody = JSON.parse(response.body); + assert.ok(parsedBody.errors[0].match(/permission denied for relation test_table_private_1/)); + + return null; + }, + function finish(err) { + done(err); + } + ); + + }); + + + suiteTeardown(function(done) { + global.environment.enabledFeatures = {cdbQueryTablesFromPostgres: false}; + 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); + }); + }); + }); + }); +}); diff --git a/test/integration/mapconfig_named_layers_datasource.js b/test/integration/mapconfig_named_layers_datasource.js new file mode 100644 index 00000000..e529a860 --- /dev/null +++ b/test/integration/mapconfig_named_layers_datasource.js @@ -0,0 +1,297 @@ +var test_helper = require('../support/test_helper'); + +var assert = require('assert'); +var RedisPool = require('redis-mpool'); +var TemplateMaps = require('../../lib/cartodb/template_maps.js'); +var serverOptions = require(__dirname + '/../../lib/cartodb/server_options')(); +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 templateMaps = new TemplateMaps(redisPool, { + max_user_templates: global.environment.maxUserTemplates +}); + +var mapConfigNamedLayersAdapter = new MapConfigNamedLayersAdapter(templateMaps); + +var wadusSql = 'select 1 wadusLayer, null::geometry the_geom_webmercator'; +var wadusLayer = { + type: 'cartodb', + options: { + sql: wadusSql, + cartocss: '#layer { marker-fill: black; }', + cartocss_version: '2.3.0' + } +}; + +var wadusTemplateSql = 'select 1 wadusTemplateLayer, null::geometry the_geom_webmercator'; +var wadusTemplateLayer = { + type: 'cartodb', + options: { + sql: wadusTemplateSql, + cartocss: '#layer { marker-fill: <%= color %>; }', + cartocss_version: '2.3.0' + } +}; + +var wadusMapnikSql = 'select 1 wadusMapnikLayer, null::geometry the_geom_webmercator'; +var wadusMapnikLayer = { + type: 'mapnik', + options: { + sql: wadusMapnikSql, + cartocss: '#layer { polygon-fill: <%= polygon_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: [ + wadusTemplateLayer + ] + } +}; + +var multipleLayersTemplateName = 'multiple_valid_template'; +var multipleLayersTemplate = { + version: '0.0.1', + name: multipleLayersTemplateName, + auth: { + method: 'token', + valid_tokens: ['valid1', 'valid2'] + }, + "placeholders": { + "polygon_color": { + "type": "css_color", + "default": "green" + }, + "color": { + "type": "css_color", + "default": "red" + } + }, + layergroup: { + layers: [ + wadusMapnikLayer, + wadusTemplateLayer + ] + } +}; + +suite('named_layers datasources', function() { + suiteSetup(function(done) { + templateMaps.addTemplate(username, template, function(err) { + if (err) { + return done(err); + } + templateMaps.addTemplate(username, multipleLayersTemplate, done); + }); + }); + + function makeNamedMapLayerConfig(layers) { + return { + version: '1.3.0', + layers: layers + }; + } + + var simpleNamedLayer = { + type: 'named', + options: { + name: templateName + } + }; + + var multipleLayersNamedLayer = { + type: 'named', + options: { + name: multipleLayersTemplateName, + auth_tokens: ['valid2'] + } + }; + + var testScenarios = [ + { + desc: 'without datasource for non-named layers', + config: makeNamedMapLayerConfig([wadusLayer]), + test: function(err, layers, datasource, done) { + assert.ok(!err); + assert.equal(layers.length, 1); + + assert.equal(layers[0].type, 'cartodb'); + assert.equal(layers[0].options.sql, wadusSql); + assert.equal(datasource.getLayerDatasource(0), undefined); + + done(); + } + }, + + { + desc: 'with datasource for the named layer but not for the normal', + config: makeNamedMapLayerConfig([wadusLayer, simpleNamedLayer]), + test: function(err, layers, datasource, done) { + assert.ok(!err); + assert.equal(layers.length, 2); + + assert.equal(layers[0].type, 'cartodb'); + assert.equal(layers[0].options.sql, wadusSql); + assert.equal(datasource.getLayerDatasource(0), undefined); + + assert.equal(layers[1].type, 'cartodb'); + assert.equal(layers[1].options.sql, wadusTemplateSql); + var layerDatasource = datasource.getLayerDatasource(1); + assert.notEqual(layerDatasource, undefined); + assert.ok(layerDatasource.user); + + done(); + } + }, + + { + desc: 'with datasource for the multiple layers in the named but not for the normal', + config: makeNamedMapLayerConfig([wadusLayer, multipleLayersNamedLayer]), + test: function(err, layers, datasource, done) { + assert.ok(!err); + assert.equal(layers.length, 3); + + assert.equal(layers[0].type, 'cartodb'); + assert.equal(layers[0].options.sql, wadusSql); + assert.equal(datasource.getLayerDatasource(0), undefined); + + assert.equal(layers[1].type, 'mapnik'); + assert.equal(layers[1].options.sql, wadusMapnikSql); + var layerDatasource = datasource.getLayerDatasource(1); + assert.notEqual(layerDatasource, undefined); + assert.ok(layerDatasource.user); + + assert.equal(layers[2].type, 'cartodb'); + assert.equal(layers[2].options.sql, wadusTemplateSql); + layerDatasource = datasource.getLayerDatasource(2); + assert.notEqual(layerDatasource, undefined); + assert.ok(layerDatasource.user); + + done(); + } + }, + + { + desc: 'all with datasource because all are named', + config: makeNamedMapLayerConfig([multipleLayersNamedLayer, simpleNamedLayer]), + test: function(err, layers, datasource, done) { + assert.ok(!err); + assert.equal(layers.length, 3); + + assert.equal(layers[0].type, 'mapnik'); + assert.equal(layers[0].options.sql, wadusMapnikSql); + var layerDatasource = datasource.getLayerDatasource(0); + assert.notEqual(layerDatasource, undefined); + assert.ok(layerDatasource.user); + + assert.equal(layers[1].type, 'cartodb'); + assert.equal(layers[1].options.sql, wadusTemplateSql); + layerDatasource = datasource.getLayerDatasource(1); + assert.notEqual(layerDatasource, undefined); + assert.ok(layerDatasource.user); + + assert.equal(layers[2].type, 'cartodb'); + assert.equal(layers[2].options.sql, wadusTemplateSql); + layerDatasource = datasource.getLayerDatasource(2); + assert.notEqual(layerDatasource, undefined); + assert.ok(layerDatasource.user); + + done(); + } + }, + + { + desc: 'with a mix of datasource and no datasource depending if layers are named or not', + config: makeNamedMapLayerConfig([simpleNamedLayer, multipleLayersNamedLayer, wadusLayer, simpleNamedLayer, wadusLayer, multipleLayersNamedLayer]), + test: function(err, layers, datasource, done) { + + assert.ok(!err); + assert.equal(layers.length, 8); + + assert.equal(layers[0].type, 'cartodb'); + assert.equal(layers[0].options.sql, wadusTemplateSql); + var layerDatasource = datasource.getLayerDatasource(0); + assert.notEqual(layerDatasource, undefined); + assert.ok(layerDatasource.user); + + assert.equal(layers[1].type, 'mapnik'); + assert.equal(layers[1].options.sql, wadusMapnikSql); + layerDatasource = datasource.getLayerDatasource(1); + assert.notEqual(layerDatasource, undefined); + assert.ok(layerDatasource.user); + + assert.equal(layers[2].type, 'cartodb'); + assert.equal(layers[2].options.sql, wadusTemplateSql); + layerDatasource = datasource.getLayerDatasource(2); + assert.notEqual(layerDatasource, undefined); + assert.ok(layerDatasource.user); + + assert.equal(layers[3].type, 'cartodb'); + assert.equal(layers[3].options.sql, wadusSql); + assert.equal(datasource.getLayerDatasource(3), undefined); + + assert.equal(layers[4].type, 'cartodb'); + assert.equal(layers[4].options.sql, wadusTemplateSql); + layerDatasource = datasource.getLayerDatasource(4); + assert.notEqual(layerDatasource, undefined); + assert.ok(layerDatasource.user); + + assert.equal(layers[5].type, 'cartodb'); + assert.equal(layers[5].options.sql, wadusSql); + assert.equal(datasource.getLayerDatasource(5), undefined); + + assert.equal(layers[6].type, 'mapnik'); + assert.equal(layers[6].options.sql, wadusMapnikSql); + layerDatasource = datasource.getLayerDatasource(6); + assert.notEqual(layerDatasource, undefined); + assert.ok(layerDatasource.user); + + assert.equal(layers[7].type, 'cartodb'); + assert.equal(layers[7].options.sql, wadusTemplateSql); + layerDatasource = datasource.getLayerDatasource(7); + assert.notEqual(layerDatasource, undefined); + assert.ok(layerDatasource.user); + + done(); + } + } + ]; + + testScenarios.forEach(function(testScenario) { + test('should return a list of layers ' + testScenario.desc, function(done) { + mapConfigNamedLayersAdapter.getLayers(username, testScenario.config.layers, serverOptions, function(err, layers, datasource) { + testScenario.test(err, layers, datasource, done); + }); + }); + }); + + suiteTeardown(function(done) { + templateMaps.delTemplate(username, templateName, function(err) { + if (err) { + return done(err); + } + templateMaps.delTemplate(username, multipleLayersTemplateName, done); + }); + }); +}); diff --git a/test/integration/mapconfig_named_layers_expanded.js b/test/integration/mapconfig_named_layers_expanded.js new file mode 100644 index 00000000..26b5197a --- /dev/null +++ b/test/integration/mapconfig_named_layers_expanded.js @@ -0,0 +1,311 @@ +var testHelper = require('../support/test_helper'); + +var assert = require('assert'); +var RedisPool = require('redis-mpool'); +var TemplateMaps = require('../../lib/cartodb/template_maps.js'); +var serverOptions = require(__dirname + '/../../lib/cartodb/server_options')(); +var MapConfigNamedLayersAdapter = require('../../lib/cartodb/models/mapconfig_named_layers_adapter'); + +var Step = require('step'); +var _ = require('underscore'); + +suite('mapconfig_named_layers_adapter', function() { + + // configure redis pool instance to use in tests + var redisPool = RedisPool(global.environment.redis); + + var templateMaps = new TemplateMaps(redisPool, { + max_user_templates: global.environment.maxUserTemplates + }); + + var mapConfigNamedLayersAdapter = new MapConfigNamedLayersAdapter(templateMaps); + + 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 wadusMapnikLayer = { + type: 'mapnik', + options: { + sql: 'select 1 cartodb_id, null::geometry the_geom_webmercator', + cartocss: '#layer { polygon-fill: <%= polygon_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'] + }, + layergroup: { + layers: [ + wadusLayer + ] + } + }; + + var multipleLayersTemplateName = 'multiple_valid_template'; + var multipleLayersTemplate = { + version: '0.0.1', + name: multipleLayersTemplateName, + auth: { + method: 'token', + valid_tokens: ['valid1', 'valid2'] + }, + "placeholders": { + "polygon_color": { + "type": "css_color", + "default": "green" + }, + "color": { + "type": "css_color", + "default": "red" + } + }, + layergroup: { + layers: [ + wadusMapnikLayer, + 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 + ] + } + }; + + function makeNamedMapLayerConfig(options) { + return { + version: '1.3.0', + layers: [ + { + type: 'named', + options: options + } + ] + }; + } + + + suiteSetup(function(done) { + templateMaps.addTemplate(username, template, done); + }); + + test('should fail for named map layer with missing name', function(done) { + var missingNamedMapLayerConfig = makeNamedMapLayerConfig({ + config: {} + }); + mapConfigNamedLayersAdapter.getLayers(username, missingNamedMapLayerConfig.layers, serverOptions, 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(); + }); + }); + + test('should fail for non-existing template name', function(done) { + var missingTemplateName = 'wadus'; + var nonExistentNamedMapLayerConfig = makeNamedMapLayerConfig({ + name: missingTemplateName + }); + mapConfigNamedLayersAdapter.getLayers(username, nonExistentNamedMapLayerConfig.layers, serverOptions, 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(); + }); + }); + + test('should fail if not properly authorized', function(done) { + templateMaps.addTemplate(username, tokenAuthTemplate, function(err) { + if (err) { + return done(err); + } + + var nonAuthTokensNamedMapLayerConfig = makeNamedMapLayerConfig({ + name: tokenAuthTemplateName + }); + mapConfigNamedLayersAdapter.getLayers(username, nonAuthTokensNamedMapLayerConfig.layers, serverOptions, 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); + }); + }); + }); + + test('should fail for nested named map layers', function(done) { + templateMaps.addTemplate(username, nestedNamedMapTemplate, function(err) { + if (err) { + return done(err); + } + + var nestedNamedMapLayerConfig = makeNamedMapLayerConfig({ + name: nestedNamedMapTemplateName + }); + mapConfigNamedLayersAdapter.getLayers(username, nestedNamedMapLayerConfig.layers, serverOptions, 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); + }); + }); + }); + + test('should return an expanded list of layers for a named map layer', function(done) { + var validNamedMapMapLayerConfig = makeNamedMapLayerConfig({ + name: templateName + }); + mapConfigNamedLayersAdapter.getLayers(username, validNamedMapMapLayerConfig.layers, serverOptions, 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(); + }); + }); + + test('should return on auth=token with valid tokens provided', function(done) { + templateMaps.addTemplate(username, tokenAuthTemplate, function(err) { + if (err) { + return done(err); + } + + var validAuthTokensNamedMapLayerConfig = makeNamedMapLayerConfig({ + name: tokenAuthTemplateName, + auth_tokens: ['valid1'] + }); + mapConfigNamedLayersAdapter.getLayers(username, validAuthTokensNamedMapLayerConfig.layers, serverOptions, function(err, layers, datasource) { + assert.ok(!err); + assert.equal(layers.length, 1); + assert.notEqual(datasource.getLayerDatasource(0), undefined); + + templateMaps.delTemplate(username, tokenAuthTemplateName, done); + }); + }); + }); + + test('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); + } + + var multipleLayersNamedMapLayerConfig = makeNamedMapLayerConfig({ + name: multipleLayersTemplateName, + auth_tokens: ['valid2'] + }); + mapConfigNamedLayersAdapter.getLayers(username, multipleLayersNamedMapLayerConfig.layers, serverOptions, 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[1].type, 'cartodb'); + assert.equal(layers[1].options.cartocss, '#layer { marker-fill: red; }'); + assert.notEqual(datasource.getLayerDatasource(1), undefined); + + templateMaps.delTemplate(username, multipleLayersTemplateName, done); + }); + }); + }); + + test('should replace template params with the given config', function(done) { + templateMaps.addTemplate(username, multipleLayersTemplate, function(err) { + if (err) { + return done(err); + } + + var color = '#cc3300', + polygonColor = '#ff9900'; + + var multipleLayersNamedMapLayerConfig = makeNamedMapLayerConfig({ + name: multipleLayersTemplateName, + config: { + polygon_color: polygonColor, + color: color + }, + auth_tokens: ['valid2'] + }); + mapConfigNamedLayersAdapter.getLayers(username, multipleLayersNamedMapLayerConfig.layers, serverOptions, 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[1].type, 'cartodb'); + assert.equal(layers[1].options.cartocss, '#layer { marker-fill: ' + color + '; }'); + assert.notEqual(datasource.getLayerDatasource(1), undefined); + + templateMaps.delTemplate(username, multipleLayersTemplateName, done); + }); + }); + }); + + suiteTeardown(function(done) { + templateMaps.delTemplate(username, templateName, done); + }); +}); diff --git a/tools/flush_cache b/tools/flush_cache deleted file mode 100755 index fdf3bb83..00000000 --- a/tools/flush_cache +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env node - -var path = require('path'); -var request = require('request'); - -function usage(me, exitcode) { - console.log("Usage: " + me + " [--env ] "); - process.exit(exitcode); -} - -var node_path = process.argv.shift(); -var script_path = process.argv.shift(); -var basedir = path.dirname(script_path); -var me = path.basename(script_path); - -var ENV = 'development.js'; -var username, table; -var arg; -while ( arg = process.argv.shift() ) { - if ( arg == '--env' ) { - ENV = process.argv.shift(); - } - else if ( ! username ) { - username = arg; - } - else if ( ! table ) { - table = arg; - } - else { - console.warn("Unused parameter " + arg); - } -} - -if ( ! table ) { - usage(me, 1); -} - -global.environment = require('../config/environments/' + ENV); - -// _after_ setting global.environment -var serverOptions = require('../lib/cartodb/server_options'); - -var host = global.environment.host; -var port = global.environment.port; -var re = ''+serverOptions.re_userFromHost; -var hostname = re.replace(/^\/\^/, '') - .replace(/\/$$/, '') - .replace(/\\/g,'') - .replace(/\([^)]*\)/,username) - ; -//console.log("re: " + re); -//console.log("hostname: " + hostname); - -var url = 'http://' + host + ':' + port + '/tiles/' + table + '/flush_cache'; -request.del({ url: url, headers: { host: hostname } }, -function(err, res, body) { - if ( err ) throw err; - console.log(res.body); -});