'use strict'; var test_helper = require('../support/test-helper'); var assert = require('../support/assert'); var CartodbWindshaft = require('../../lib/server'); var serverOptions = require('../../lib/server-options'); var LayergroupToken = require('../../lib/models/layergroup-token'); var RedisPool = require('redis-mpool'); var TemplateMaps = require('../../lib/backends/template-maps'); var step = require('step'); describe('named_layers', function () { var server; before(function () { server = new CartodbWindshaft(serverOptions); }); // 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, 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 ] } }; var keysToDelete; beforeEach(function () { keysToDelete = {}; }); afterEach(function (done) { test_helper.deleteRedisKeys(keysToDelete, done); }); beforeEach(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); }); }); }); }); afterEach(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); }); }); }); }); it('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: '/api/v1/map', 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.deepStrictEqual(parsedBody.errors, ["Template 'nonexistent' of user 'localhost' not found"]); return null; }, function finish (err) { done(err); } ); }); it('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: '/api/v1/map', 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.deepStrictEqual(parsedBody.errors, ["Unauthorized 'auth_valid_template' template instantiation"]); return null; }, function finish (err) { done(err); } ); }); it('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: '/api/v1/map', 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); keysToDelete['map_cfg|' + LayergroupToken.parse(parsedBody.layergroupid).token] = 0; keysToDelete['user:localhost:mapviews:global'] = 5; return null; }, function finish (err) { done(err); } ); }); it('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: '/api/v1/map', 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.deepStrictEqual(parsedBody.errors, ['Nested named layers are not allowed']); return null; }, function finish (err) { done(err); } ); }); it('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: '/api/v1/map', 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); keysToDelete['map_cfg|' + LayergroupToken.parse(parsedBody.layergroupid).token] = 0; keysToDelete['user:localhost:mapviews:global'] = 5; return parsedBody.layergroupid; }, function requestTile (err, layergroupId) { if (err) { throw err; } var next = this; assert.response(server, { url: '/api/v1/map/' + 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; } test_helper.checkCache(res); return true; }, function deleteTemplate (err) { var next = this; templateMaps.delTemplate(username, privateTableTemplateName, function (/* delErr */) { // ignore deletion error next(err); }); }, function finish (err) { done(err); } ); }); it('should return 200 and layergroup with private tables and interactivity', function (done) { var privateTableTemplateNameInteractivity = 'private_table_template_interactivity'; var privateTableTemplate = { version: '0.0.1', auth: { method: 'open' }, name: privateTableTemplateNameInteractivity, layergroup: { layers: [ { type: 'cartodb', options: { attributes: { columns: [ 'name' ], id: 'cartodb_id' }, cartocss: '#layer { marker-fill: #cc3300; }', cartocss_version: '2.3.0', interactivity: 'cartodb_id', sql: 'select * from test_table_private_1' } } ] } }; var layergroup = { version: '1.3.0', layers: [ { type: 'named', options: { name: privateTableTemplateNameInteractivity } } ] }; step( function createTemplate () { templateMaps.addTemplate(username, privateTableTemplate, this); }, function createLayergroup (err) { if (err) { throw err; } var next = this; assert.response(server, { url: '/api/v1/map', 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); keysToDelete['map_cfg|' + LayergroupToken.parse(parsedBody.layergroupid).token] = 0; keysToDelete['user:localhost:mapviews:global'] = 5; return parsedBody.layergroupid; }, function requestTile (err, layergroupId) { if (err) { throw err; } var next = this; assert.response(server, { url: '/api/v1/map/' + 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; } test_helper.checkCache(res); return true; }, function deleteTemplate (err) { var next = this; templateMaps.delTemplate(username, privateTableTemplateNameInteractivity, function (/* delErr */) { // ignore deletion error next(err); }); }, function finish (err) { done(err); } ); }); it('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: '/api/v1/map', 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 .+?test_table_private_1/)); return null; }, function finish (err) { done(err); } ); }); it('should return metadata for named layers', function (done) { var layergroup = { version: '1.3.0', layers: [ { type: 'plain', options: { color: '#fabada' } }, { type: 'cartodb', options: { sql: 'select * from test_table', cartocss: '#layer { marker-fill: #cc3300; }', cartocss_version: '2.3.0' } }, { type: 'named', options: { name: templateName } }, { type: 'torque', options: { sql: 'select * from test_table LIMIT 0', cartocss: 'Map { -torque-frame-count:1; -torque-resolution:1; ' + "-torque-aggregation-function:'count(*)'; -torque-time-attribute:'updated_at'; }" } } ] }; step( function createLayergroup () { var next = this; assert.response(server, { url: '/api/v1/map', 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.metadata); assert.ok(parsedBody.metadata.layers); assert.strictEqual(parsedBody.metadata.layers.length, 5); assert.strictEqual(parsedBody.metadata.layers[0].type, 'plain'); assert.strictEqual(parsedBody.metadata.layers[1].type, 'mapnik'); assert.strictEqual(parsedBody.metadata.layers[2].type, 'mapnik'); assert.strictEqual(parsedBody.metadata.layers[3].type, 'mapnik'); assert.strictEqual(parsedBody.metadata.layers[4].type, 'torque'); keysToDelete['map_cfg|' + LayergroupToken.parse(parsedBody.layergroupid).token] = 0; keysToDelete['user:localhost:mapviews:global'] = 5; return null; }, function finish (err) { done(err); } ); }); it('should work with named tiles', function (done) { var namedTilesTemplateName = 'named_tiles_template'; var namedTilesTemplate = { version: '0.0.1', name: namedTilesTemplateName, auth: { method: 'open' }, layergroup: { layers: [ namedMapLayer, { type: 'mapnik', options: { sql: 'select * from test_table_private_1', cartocss: '#layer { marker-fill: #cc3300; }', cartocss_version: '2.3.0' } } ] } }; step( function createTemplate () { templateMaps.addTemplate(username, namedTilesTemplate, this); }, function createLayergroup (err) { if (err) { throw err; } var next = this; assert.response(server, { url: '/api/v1/map/named/' + namedTilesTemplateName + '?api_key=1234', method: 'POST', headers: { host: 'localhost', 'Content-Type': 'application/json' } }, { 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); assert.strictEqual(parsedBody.metadata.layers[0].type, 'mapnik'); assert.strictEqual(parsedBody.metadata.layers[1].type, 'mapnik'); keysToDelete['map_cfg|' + LayergroupToken.parse(parsedBody.layergroupid).token] = 0; keysToDelete['user:localhost:mapviews:global'] = 5; return parsedBody.layergroupid; }, function requestTile (err, layergroupId) { if (err) { throw err; } var next = this; assert.response(server, { url: '/api/v1/map/' + layergroupId + '/all/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; } test_helper.checkCache(res); return true; }, function deleteTemplate (err) { var next = this; templateMaps.delTemplate(username, namedTilesTemplateName, function (/* delErr */) { // ignore deletion error next(err); }); }, function finish (err) { done(err); } ); }); });