From a964ed5fe659eb3f90fb94721e3666b767dc3767 Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Tue, 4 Feb 2014 19:04:59 +0100 Subject: [PATCH] Implement Unified Map API Closes #126 --- NEWS.md | 1 + lib/cartodb/cartodb_windshaft.js | 5 +- lib/cartodb/server_options.js | 4 + npm-shrinkwrap.json | 474 ------------- package.json | 2 +- test/acceptance/unified.js | 1142 ++++++++++++++++++++++++++++++ 6 files changed, 1152 insertions(+), 476 deletions(-) delete mode 100644 npm-shrinkwrap.json create mode 100644 test/acceptance/unified.js diff --git a/NEWS.md b/NEWS.md index 91b8d836..f1e64ad5 100644 --- a/NEWS.md +++ b/NEWS.md @@ -5,6 +5,7 @@ New features: * Add support for torque tiles (#112) * Add attributes service (#118) + * Implement Unified Map API (#127) Enhancements: diff --git a/lib/cartodb/cartodb_windshaft.js b/lib/cartodb/cartodb_windshaft.js index d098bbb8..0af28351 100644 --- a/lib/cartodb/cartodb_windshaft.js +++ b/lib/cartodb/cartodb_windshaft.js @@ -127,7 +127,10 @@ var CartodbWindshaft = function(serverOptions) { return serverOptions.userByReq(req); } - var template_baseurl = serverOptions.base_url_notable + '/template'; + // "named" is the official, "template" is for backward compatibility up to 1.6.x + //var template_baseurl = serverOptions.base_url_notable + '/(template|named)'; + var template_baseurl = '/tiles/template'; + var template_baseurl = '(?:/maps/named|/tiles/template)'; // Add a template ws.post(template_baseurl, function(req, res) { diff --git a/lib/cartodb/server_options.js b/lib/cartodb/server_options.js index 08b196f3..444a8794 100644 --- a/lib/cartodb/server_options.js +++ b/lib/cartodb/server_options.js @@ -27,7 +27,11 @@ module.exports = function(){ var me = { base_url: '/tiles/:table', + /// @deprecated with Windshaft-0.17.0 base_url_notable: '/tiles', + // "maps" is the official, while + // "tiles/layergroup" is for backward compatibility up to 1.6.x + base_url_mapconfig: '(?:/maps|/tiles/layergroup)', grainstore: { map: { // TODO: allow to specify in configuration diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json deleted file mode 100644 index da5c6117..00000000 --- a/npm-shrinkwrap.json +++ /dev/null @@ -1,474 +0,0 @@ -{ - "name": "windshaft-cartodb", - "version": "1.7.0", - "dependencies": { - "node-varnish": { - "version": "0.1.1" - }, - "underscore": { - "version": "1.3.3" - }, - "windshaft": { - "version": "0.16.0", - "dependencies": { - "grainstore": { - "version": "0.17.0", - "dependencies": { - "mapnik-reference": { - "version": "5.0.7" - }, - "millstone": { - "version": "0.6.8", - "dependencies": { - "underscore": { - "version": "1.5.2" - }, - "request": { - "version": "2.26.0", - "dependencies": { - "qs": { - "version": "0.6.6" - }, - "json-stringify-safe": { - "version": "5.0.0" - }, - "forever-agent": { - "version": "0.5.0" - }, - "tunnel-agent": { - "version": "0.3.0" - }, - "http-signature": { - "version": "0.10.0", - "dependencies": { - "assert-plus": { - "version": "0.1.2" - }, - "asn1": { - "version": "0.1.11" - }, - "ctype": { - "version": "0.5.2" - } - } - }, - "hawk": { - "version": "1.0.0", - "dependencies": { - "hoek": { - "version": "0.9.1" - }, - "boom": { - "version": "0.4.2" - }, - "cryptiles": { - "version": "0.2.2" - }, - "sntp": { - "version": "0.2.4" - } - } - }, - "aws-sign": { - "version": "0.3.0" - }, - "oauth-sign": { - "version": "0.3.0" - }, - "cookie-jar": { - "version": "0.3.0" - }, - "node-uuid": { - "version": "1.4.1" - }, - "form-data": { - "version": "0.1.2", - "dependencies": { - "combined-stream": { - "version": "0.0.4", - "dependencies": { - "delayed-stream": { - "version": "0.0.5" - } - } - }, - "async": { - "version": "0.2.10" - } - } - } - } - }, - "srs": { - "version": "0.3.9" - }, - "zipfile": { - "version": "0.4.3" - }, - "sqlite3": { - "version": "2.2.0", - "dependencies": { - "node-pre-gyp": { - "version": "0.2.6", - "dependencies": { - "nopt": { - "version": "2.1.2", - "dependencies": { - "abbrev": { - "version": "1.0.4" - } - } - }, - "npmlog": { - "version": "0.0.6", - "dependencies": { - "ansi": { - "version": "0.2.1" - } - } - }, - "semver": { - "version": "2.1.0" - }, - "tar": { - "version": "0.1.19", - "dependencies": { - "inherits": { - "version": "2.0.1" - }, - "block-stream": { - "version": "0.0.7" - }, - "fstream": { - "version": "0.1.25", - "dependencies": { - "graceful-fs": { - "version": "2.0.1" - } - } - } - } - }, - "tar-pack": { - "version": "2.0.0", - "dependencies": { - "uid-number": { - "version": "0.0.3" - }, - "once": { - "version": "1.1.1" - }, - "debug": { - "version": "0.7.4" - }, - "fstream": { - "version": "0.1.25", - "dependencies": { - "graceful-fs": { - "version": "2.0.1" - }, - "inherits": { - "version": "2.0.1" - } - } - }, - "fstream-ignore": { - "version": "0.0.7", - "dependencies": { - "minimatch": { - "version": "0.2.14", - "dependencies": { - "sigmund": { - "version": "1.0.0" - } - } - }, - "inherits": { - "version": "2.0.1" - } - } - }, - "readable-stream": { - "version": "1.0.25-1", - "dependencies": { - "string_decoder": { - "version": "0.10.25" - } - } - }, - "graceful-fs": { - "version": "1.2.3" - } - } - }, - "aws-sdk": { - "version": "2.0.0-rc9", - "dependencies": { - "xml2js": { - "version": "0.2.4", - "dependencies": { - "sax": { - "version": "0.6.0" - } - } - }, - "xmlbuilder": { - "version": "0.4.2" - } - } - }, - "rc": { - "version": "0.3.3", - "dependencies": { - "optimist": { - "version": "0.3.7", - "dependencies": { - "wordwrap": { - "version": "0.0.2" - } - } - }, - "deep-extend": { - "version": "0.2.8" - }, - "ini": { - "version": "1.1.0" - } - } - }, - "rimraf": { - "version": "2.2.6" - } - } - } - } - }, - "mime": { - "version": "1.2.11" - }, - "mkdirp": { - "version": "0.3.5" - }, - "optimist": { - "version": "0.6.0", - "dependencies": { - "wordwrap": { - "version": "0.0.2" - }, - "minimist": { - "version": "0.0.5" - } - } - } - } - } - } - }, - "generic-pool": { - "version": "2.0.4" - }, - "express": { - "version": "2.5.11", - "dependencies": { - "connect": { - "version": "1.9.2", - "dependencies": { - "formidable": { - "version": "1.0.14" - } - } - }, - "mime": { - "version": "1.2.4" - }, - "qs": { - "version": "0.4.2" - }, - "mkdirp": { - "version": "0.3.0" - } - } - }, - "tilelive": { - "version": "4.4.3", - "dependencies": { - "optimist": { - "version": "0.3.7", - "dependencies": { - "wordwrap": { - "version": "0.0.2" - } - } - }, - "sphericalmercator": { - "version": "1.0.2" - } - } - }, - "tilelive-mapnik": { - "version": "0.6.5", - "dependencies": { - "eio": { - "version": "0.2.2" - }, - "mime": { - "version": "1.2.11" - }, - "sphericalmercator": { - "version": "1.0.2" - } - } - }, - "lru-cache": { - "version": "2.3.1" - }, - "carto": { - "version": "0.9.5-cdb2", - "from": "http://github.com/CartoDB/carto/tarball/0.9.5-cdb2", - "dependencies": { - "underscore": { - "version": "1.4.4" - }, - "mapnik-reference": { - "version": "5.0.7" - }, - "xml2js": { - "version": "0.2.8", - "dependencies": { - "sax": { - "version": "0.5.8" - } - } - }, - "optimist": { - "version": "0.6.0", - "dependencies": { - "wordwrap": { - "version": "0.0.2" - }, - "minimist": { - "version": "0.0.5" - } - } - } - } - }, - "underscore.string": { - "version": "1.1.6", - "dependencies": { - "underscore": { - "version": "1.1.7" - } - } - }, - "pg": { - "version": "2.6.2", - "dependencies": { - "generic-pool": { - "version": "2.0.3" - }, - "buffer-writer": { - "version": "1.0.0" - } - } - }, - "torque.js": { - "version": "2.2.00" - } - } - }, - "step": { - "version": "0.0.5" - }, - "request": { - "version": "2.9.202" - }, - "cartodb-redis": { - "version": "0.3.0" - }, - "redis-mpool": { - "version": "0.0.3", - "dependencies": { - "generic-pool": { - "version": "2.0.4" - }, - "hiredis": { - "version": "0.1.16", - "dependencies": { - "bindings": { - "version": "1.1.1" - } - } - } - } - }, - "mapnik": { - "version": "0.7.25" - }, - "lzma": { - "version": "1.2.3" - }, - "strftime": { - "version": "0.6.2" - }, - "semver": { - "version": "1.1.4" - }, - "redis": { - "version": "0.8.6" - }, - "mocha": { - "version": "1.14.0", - "dependencies": { - "commander": { - "version": "2.0.0" - }, - "growl": { - "version": "1.7.0" - }, - "jade": { - "version": "0.26.3", - "dependencies": { - "commander": { - "version": "0.6.1" - }, - "mkdirp": { - "version": "0.3.0" - } - } - }, - "diff": { - "version": "1.0.7" - }, - "debug": { - "version": "0.7.4" - }, - "mkdirp": { - "version": "0.3.5" - }, - "glob": { - "version": "3.2.3", - "dependencies": { - "minimatch": { - "version": "0.2.14", - "dependencies": { - "lru-cache": { - "version": "2.5.0" - }, - "sigmund": { - "version": "1.0.0" - } - } - }, - "graceful-fs": { - "version": "2.0.1" - }, - "inherits": { - "version": "2.0.1" - } - } - } - } - } - } -} diff --git a/package.json b/package.json index fd47ffc6..93ebf9e9 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "dependencies": { "node-varnish": "0.1.1", "underscore" : "~1.3.3", - "windshaft" : "~0.16.0", + "windshaft" : "~0.17.0", "step": "0.0.x", "request": "2.9.202", "cartodb-redis": "~0.3.0", diff --git a/test/acceptance/unified.js b/test/acceptance/unified.js new file mode 100644 index 00000000..16f91471 --- /dev/null +++ b/test/acceptance/unified.js @@ -0,0 +1,1142 @@ +var assert = require('../support/assert'); +var tests = module.exports = {}; +var _ = require('underscore'); +var redis = require('redis'); +var querystring = require('querystring'); +var semver = require('semver'); +var mapnik = require('mapnik'); +var Step = require('step'); +var strftime = require('strftime'); +var SQLAPIEmu = require(__dirname + '/../support/SQLAPIEmu.js'); +var redis_stats_db = 5; + +require(__dirname + '/../support/test_helper'); + +var windshaft_fixtures = __dirname + '/../../node_modules/windshaft/test/fixtures'; + +var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/cartodb_windshaft'); +var serverOptions = require(__dirname + '/../../lib/cartodb/server_options'); +var server = new CartodbWindshaft(serverOptions); +server.setMaxListeners(0); + +suite('unified_api', function() { + + var redis_client = redis.createClient(global.environment.redis.port); + var sqlapi_server; + var expected_last_updated_epoch = 1234567890123; // this is hard-coded into SQLAPIEmu + var expected_last_updated = new Date(expected_last_updated_epoch).toISOString(); + + suiteSetup(function(done){ + sqlapi_server = new SQLAPIEmu(global.environment.sqlapi.port, done); + // TODO: check redis is clean ? + }); + + var template_acceptance1 = { + version: '0.0.1', + name: 'acceptance1', + auth: { method: 'open' }, + layergroup: { // SHOULD BE mapConfig ? + version: '1.0.0', + layers: [ + { options: { + sql: 'select cartodb_id, ST_Translate(the_geom_webmercator, -5e6, 0) as the_geom_webmercator from test_table limit 2 offset 2', + cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }', + cartocss_version: '2.0.2', + interactivity: 'cartodb_id' + } } + ] + } + }; + + test("can add template, returning id", function(done) { + + var errors = []; + var expected_failure = false; + var expected_tpl_id = "localhost@acceptance1"; + var post_request_1 = { + url: '/maps/named', + method: 'POST', + headers: {host: 'localhost', 'Content-Type': 'application/json' }, + data: JSON.stringify(template_acceptance1) + } + Step( + function postUnauthenticated() + { + var next = this; + assert.response(server, post_request_1, {}, + function(res) { next(null, res); }); + }, + function postTemplate(err, res) + { + if ( err ) throw err; + assert.equal(res.statusCode, 401); + var parsed = JSON.parse(res.body); + assert.ok(parsed.hasOwnProperty('error'), res.body); + err = parsed.error; + assert.ok(err.match(/only.*authenticated.*user/i), + 'Unexpected error response: ' + err); + post_request_1.url += '?api_key=1234'; + var next = this; + assert.response(server, post_request_1, {}, + function(res) { next(null, res); }); + }, + function rePostTemplate(err, res) + { + if ( err ) throw err; + assert.equal(res.statusCode, 200, res.body); + var parsedBody = JSON.parse(res.body); + var expectedBody = { template_id: expected_tpl_id }; + assert.deepEqual(parsedBody, expectedBody); + var next = this; + assert.response(server, post_request_1, {}, + function(res) { next(null, res); }); + }, + function checkFailure(err, res) + { + if ( err ) throw err; + assert.equal(res.statusCode, 400, res.body); + var parsedBody = JSON.parse(res.body); + assert.ok(parsedBody.hasOwnProperty('error'), res.body); + assert.ok(parsedBody.error.match(/already exists/i), + 'Unexpected error for pre-existing template name: ' + parsedBody.error); + return null; + }, + function finish(err) { + if ( err ) errors.push(err); + redis_client.keys("map_*|localhost", function(err, keys) { + if ( err ) errors.push(err.message); + var todrop = _.map(keys, function(m) { + if ( m.match(/^map_(tpl|crt)|/) ) + return m; + }); + if ( todrop.length != 2 ) { + errors.push(new Error("Unexpected keys in redis: " + todrop)); + } else { + if ( todrop.indexOf('map_tpl|localhost') == -1 ) { + errors.push(new Error("Missing 'map_tpl|localhost' key in redis")); + } + if ( todrop.indexOf('map_crt|localhost') == -1 ) { + errors.push(new Error("Missing 'map_crt|localhost' key in redis")); + } + } + redis_client.del(todrop, function(err) { + if ( err ) errors.push(err.message); + if ( errors.length ) { + done(new Error(errors)); + } + else done(null); + }); + }); + } + ); + }); + + test("instance endpoint should return CORS headers", function(done){ + Step(function postTemplate1(err, res) { + var next = this; + var post_request = { + url: '/maps/named?api_key=1234', + method: 'POST', + headers: {host: 'localhost.localhost', 'Content-Type': 'application/json' }, + data: JSON.stringify(template_acceptance1) + }; + assert.response(server, post_request, {}, function(res) { next(null, res); }); + }, + function testCORS() { + assert.response(server, { + url: '/maps/named/acceptance1', + method: 'OPTIONS' + },{ + status: 200, + headers: { + 'Access-Control-Allow-Headers': 'X-Requested-With, X-Prototype-Version, X-CSRF-Token, Content-Type', + 'Access-Control-Allow-Origin': '*' + } + }, function() { done(); }); + }); + }); + + + test("can list templates", function(done) { + + var errors = []; + var expected_failure = false; + var tplid1, tplid2; + Step( + function postTemplate1(err, res) + { + var next = this; + var post_request = { + url: '/maps/named?api_key=1234', + method: 'POST', + headers: {host: 'localhost', 'Content-Type': 'application/json' }, + data: JSON.stringify(template_acceptance1) + } + assert.response(server, post_request, {}, + function(res) { next(null, res); }); + }, + function postTemplate2(err, res) + { + if ( err ) throw err; + assert.equal(res.statusCode, 200, res.body); + var parsed = JSON.parse(res.body); + assert.ok(parsed.hasOwnProperty('template_id'), + "Missing 'template_id' from response body: " + res.body); + tplid1 = parsed.template_id; + + var next = this; + var backup_name = template_acceptance1.name; + template_acceptance1.name += '_new'; + var post_request = { + url: '/maps/named?api_key=1234', + method: 'POST', + headers: {host: 'localhost', 'Content-Type': 'application/json' }, + data: JSON.stringify(template_acceptance1) + } + template_acceptance1.name = backup_name; + assert.response(server, post_request, {}, + function(res) { next(null, res); }); + }, + function litsTemplatesUnauthenticated(err, res) + { + if ( err ) throw err; + assert.equal(res.statusCode, 200, res.body); + var parsed = JSON.parse(res.body); + assert.ok(parsed.hasOwnProperty('template_id'), + "Missing 'template_id' from response body: " + res.body); + tplid2 = parsed.template_id; + var next = this; + var get_request = { + url: '/maps/named', + method: 'GET', + headers: {host: 'localhost'} + } + assert.response(server, get_request, {}, + function(res) { next(null, res); }); + }, + function litsTemplates(err, res) + { + if ( err ) throw err; + assert.equal(res.statusCode, 401, res.statusCode + ': ' + res.body); + var parsed = JSON.parse(res.body); + assert.ok(parsed.hasOwnProperty('error'), + 'Missing error from response: ' + res.body); + err = parsed.error; + assert.ok(err.match(/authenticated user/), err); + var next = this; + var get_request = { + url: '/maps/named?api_key=1234', + method: 'GET', + headers: {host: 'localhost'} + } + assert.response(server, get_request, {}, + function(res) { next(null, res); }); + }, + function checkList(err, res) + { + if ( err ) throw err; + assert.equal(res.statusCode, 200, res.body); + var parsed = JSON.parse(res.body); + assert.ok(parsed.hasOwnProperty('template_ids'), + "Missing 'template_ids' from response body: " + res.body); + var ids = parsed.template_ids; + assert.equal(ids.length, 2); + assert.ok(ids.indexOf(tplid1) != -1, + 'Missing "' + tplid1 + "' from list response: " + ids.join(',')); + assert.ok(ids.indexOf(tplid2) != -1, + 'Missing "' + tplid2 + "' from list response: " + ids.join(',')); + return null; + }, + function finish(err) { + if ( err ) errors.push(err); + redis_client.keys("map_*|localhost", function(err, keys) { + if ( err ) errors.push(err.message); + var todrop = _.map(keys, function(m) { + if ( m.match(/^map_(tpl|crt)|/) ) + return m; + }); + if ( todrop.length != 2 ) { + errors.push(new Error("Unexpected keys in redis: " + todrop)); + } else { + if ( todrop.indexOf('map_tpl|localhost') == -1 ) { + errors.push(new Error("Missing 'map_tpl|localhost' key in redis")); + } + if ( todrop.indexOf('map_crt|localhost') == -1 ) { + errors.push(new Error("Missing 'map_crt|localhost' key in redis")); + } + } + redis_client.del(todrop, function(err) { + if ( err ) errors.push(err.message); + if ( errors.length ) { + done(new Error(errors)); + } + else done(null); + }); + }); + } + ); + }); + + test("can update template", function(done) { + + var errors = []; + var expected_failure = false; + var tpl_id; + Step( + function postTemplate(err, res) + { + var next = this; + var post_request = { + url: '/maps/named?api_key=1234', + method: 'POST', + headers: {host: 'localhost', 'Content-Type': 'application/json' }, + data: JSON.stringify(template_acceptance1) + } + assert.response(server, post_request, {}, + function(res) { next(null, res); }); + }, + function putMisnamedTemplate(err, res) + { + if ( err ) throw err; + assert.equal(res.statusCode, 200, res.body); + var parsed = JSON.parse(res.body); + assert.ok(parsed.hasOwnProperty('template_id'), + "Missing 'template_id' from response body: " + res.body); + tpl_id = parsed.template_id; + var backup_name = template_acceptance1.name; + template_acceptance1.name = 'changed_name'; + var put_request = { + url: '/maps/named/' + tpl_id + '/?api_key=1234', + method: 'PUT', + headers: {host: 'localhost', 'Content-Type': 'application/json' }, + data: JSON.stringify(template_acceptance1) + } + template_acceptance1.name = backup_name; + var next = this; + assert.response(server, put_request, {}, + function(res) { next(null, res); }); + }, + function putUnexistentTemplate(err, res) + { + if ( err ) throw err; + assert.equal(res.statusCode, 400, res.statusCode + ": " + res.body); + var parsedBody = JSON.parse(res.body); + assert.ok(parsedBody.hasOwnProperty('error'), res.body); + assert.ok(parsedBody.error.match(/cannot update name/i), + 'Unexpected error for invalid update: ' + parsedBody.error); + var put_request = { + url: '/maps/named/unexistent/?api_key=1234', + method: 'PUT', + headers: {host: 'localhost', 'Content-Type': 'application/json' }, + data: JSON.stringify(template_acceptance1) + } + var next = this; + assert.response(server, put_request, {}, + function(res) { next(null, res); }); + }, + function putValidTemplate(err, res) + { + if ( err ) throw err; + assert.equal(res.statusCode, 400, res.statusCode + ": " + res.body); + var parsedBody = JSON.parse(res.body); + assert.ok(parsedBody.hasOwnProperty('error'), res.body); + assert.ok(parsedBody.error.match(/cannot update name/i), + 'Unexpected error for invalid update: ' + parsedBody.error); + var put_request = { + url: '/maps/named/' + tpl_id + '/?api_key=1234', + method: 'PUT', + headers: {host: 'localhost', 'Content-Type': 'application/json' }, + data: JSON.stringify(template_acceptance1) + } + var next = this; + assert.response(server, put_request, {}, + function(res) { next(null, res); }); + }, + function checkValidUpate(err, res) + { + if ( err ) throw err; + assert.equal(res.statusCode, 200, res.statusCode + ": " + res.body); + var parsed = JSON.parse(res.body); + assert.ok(parsed.hasOwnProperty('template_id'), + "Missing 'template_id' from response body: " + res.body); + assert.equal(tpl_id, parsed.template_id); + return null; + }, + function finish(err) { + if ( err ) errors.push(err); + redis_client.keys("map_*|localhost", function(err, keys) { + if ( err ) errors.push(err.message); + var todrop = _.map(keys, function(m) { + if ( m.match(/^map_(tpl|crt)|/) ) + return m; + }); + if ( todrop.length != 2 ) { + errors.push(new Error("Unexpected keys in redis: " + todrop)); + } else { + if ( todrop.indexOf('map_tpl|localhost') == -1 ) { + errors.push(new Error("Missing 'map_tpl|localhost' key in redis")); + } + if ( todrop.indexOf('map_crt|localhost') == -1 ) { + errors.push(new Error("Missing 'map_crt|localhost' key in redis")); + } + } + redis_client.del(todrop, function(err) { + if ( err ) errors.push(err.message); + if ( errors.length ) { + done(new Error(errors)); + } + else done(null); + }); + }); + } + ); + }); + + test("can get a template by id", function(done) { + + var errors = []; + var expected_failure = false; + var tpl_id; + Step( + function postTemplate(err, res) + { + var next = this; + var post_request = { + url: '/maps/named?api_key=1234', + method: 'POST', + headers: {host: 'localhost', 'Content-Type': 'application/json' }, + data: JSON.stringify(template_acceptance1) + } + assert.response(server, post_request, {}, + function(res) { next(null, res); }); + }, + function getTemplateUnauthorized(err, res) + { + if ( err ) throw err; + assert.equal(res.statusCode, 200, res.body); + var parsed = JSON.parse(res.body); + assert.ok(parsed.hasOwnProperty('template_id'), + "Missing 'template_id' from response body: " + res.body); + tpl_id = parsed.template_id; + var get_request = { + url: '/maps/named/' + tpl_id, + method: 'GET', + headers: {host: 'localhost'} + } + var next = this; + assert.response(server, get_request, {}, + function(res) { next(null, res); }); + }, + function getTemplate(err, res) + { + if ( err ) throw err; + assert.equal(res.statusCode, 401, res.statusCode + ": " + res.body); + var parsedBody = JSON.parse(res.body); + assert.ok(parsedBody.hasOwnProperty('error'), res.body); + assert.ok(parsedBody.error.match(/only.*authenticated.*user/i), + 'Unexpected error for unauthenticated template get: ' + parsedBody.error); + var get_request = { + url: '/maps/named/' + tpl_id + '?api_key=1234', + method: 'GET', + headers: {host: 'localhost'} + } + var next = this; + assert.response(server, get_request, {}, + function(res) { next(null, res); }); + }, + function checkReturnTemplate(err, res) + { + if ( err ) throw err; + assert.equal(res.statusCode, 200, res.statusCode + ": " + res.body); + var parsed = JSON.parse(res.body); + assert.ok(parsed.hasOwnProperty('template'), + "Missing 'template' from response body: " + res.body); + assert.deepEqual(template_acceptance1, parsed.template); + return null; + }, + function finish(err) { + if ( err ) errors.push(err); + redis_client.keys("map_*|localhost", function(err, keys) { + if ( err ) errors.push(err.message); + var todrop = _.map(keys, function(m) { + if ( m.match(/^map_(tpl|crt)|/) ) + return m; + }); + if ( todrop.length != 2 ) { + errors.push(new Error("Unexpected keys in redis: " + todrop)); + } else { + if ( todrop.indexOf('map_tpl|localhost') == -1 ) { + errors.push(new Error("Missing 'map_tpl|localhost' key in redis")); + } + if ( todrop.indexOf('map_crt|localhost') == -1 ) { + errors.push(new Error("Missing 'map_crt|localhost' key in redis")); + } + } + redis_client.del(todrop, function(err) { + if ( err ) errors.push(err.message); + if ( errors.length ) { + done(new Error(errors)); + } + else done(null); + }); + }); + } + ); + }); + + test("can delete a template by id", function(done) { + + var errors = []; + var expected_failure = false; + var tpl_id; + Step( + function postTemplate(err, res) + { + var next = this; + var post_request = { + url: '/maps/named?api_key=1234', + method: 'POST', + headers: {host: 'localhost', 'Content-Type': 'application/json' }, + data: JSON.stringify(template_acceptance1) + } + assert.response(server, post_request, {}, + function(res) { next(null, res); }); + }, + function getTemplate(err, res) + { + if ( err ) throw err; + assert.equal(res.statusCode, 200, res.body); + var parsed = JSON.parse(res.body); + assert.ok(parsed.hasOwnProperty('template_id'), + "Missing 'template_id' from response body: " + res.body); + tpl_id = parsed.template_id; + var get_request = { + url: '/maps/named/' + tpl_id + '?api_key=1234', + method: 'GET', + headers: {host: 'localhost'} + } + var next = this; + assert.response(server, get_request, {}, + function(res) { next(null, res); }); + }, + function deleteTemplateUnauthorized(err, res) + { + if ( err ) throw err; + assert.equal(res.statusCode, 200, res.statusCode + ": " + res.body); + var parsed = JSON.parse(res.body); + assert.ok(parsed.hasOwnProperty('template'), + "Missing 'template' from response body: " + res.body); + assert.deepEqual(template_acceptance1, parsed.template); + var del_request = { + url: '/maps/named/' + tpl_id, + method: 'DELETE', + headers: {host: 'localhost'} + } + var next = this; + assert.response(server, del_request, {}, + function(res) { next(null, res); }); + }, + function deleteTemplate(err, res) + { + if ( err ) throw err; + assert.equal(res.statusCode, 401, res.statusCode + ": " + res.body); + var parsed = JSON.parse(res.body); + assert.ok(parsed.hasOwnProperty('error'), + "Missing 'error' from response body: " + res.body); + assert.ok(parsed.error.match(/only.*authenticated.*user/i), + 'Unexpected error for unauthenticated template get: ' + parsed.error); + var del_request = { + url: '/maps/named/' + tpl_id + '?api_key=1234', + method: 'DELETE', + headers: {host: 'localhost'} + } + var next = this; + assert.response(server, del_request, {}, + function(res) { next(null, res); }); + }, + function getMissingTemplate(err, res) + { + if ( err ) throw err; + assert.equal(res.statusCode, 204, res.statusCode + ': ' + res.body); + assert.ok(!res.body, 'Unexpected body in DELETE /template response'); + var get_request = { + url: '/maps/named/' + tpl_id + '?api_key=1234', + method: 'GET', + headers: {host: 'localhost'} + } + var next = this; + assert.response(server, get_request, {}, + function(res) { next(null, res); }); + }, + function checkGetFailure(err, res) + { + if ( err ) throw err; + assert.equal(res.statusCode, 404, res.statusCode + ': ' + res.body); + var parsed = JSON.parse(res.body); + assert.ok(parsed.hasOwnProperty('error'), + "Missing 'error' from response body: " + res.body); + assert.ok(parsed.error.match(/cannot find/i), + 'Unexpected error for missing template: ' + parsed.error); + return null; + }, + function finish(err) { + if ( err ) errors.push(err); + redis_client.keys("map_*|localhost", function(err, keys) { + if ( err ) errors.push(err.message); + var todrop = _.map(keys, function(m) { + if ( m.match(/^map_(tpl|crt)|/) ) + return m; + }); + if ( todrop.length ) { + errors.push(new Error("Unexpected keys in redis: " + todrop)); + redis_client.del(todrop, function(err) { + if ( err ) errors.push(err.message); + if ( errors.length ) { + done(new Error(errors)); + } + else done(null); + }); + } else { + if ( errors.length ) { + done(new Error(errors)); + } + else done(null); + } + }); + } + ); + }); + + test("can instanciate a template by id", function(done) { + + // This map fetches data from a private table + var template_acceptance2 = { + version: '0.0.1', + name: 'acceptance1', + auth: { method: 'token', valid_tokens: ['valid1','valid2'] }, + layergroup: { + version: '1.0.0', + layers: [ + { options: { + sql: "select * from test_table_private_1 LIMIT 0", + cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }', + cartocss_version: '2.0.2', + interactivity: 'cartodb_id' + } } + ] + } + }; + + var template_params = {}; + + var errors = []; + var expected_failure = false; + var tpl_id; + var layergroupid; + Step( + function postTemplate(err, res) + { + var next = this; + var post_request = { + url: '/maps/named?api_key=1234', + method: 'POST', + headers: {host: 'localhost', 'Content-Type': 'application/json' }, + data: JSON.stringify(template_acceptance2) + } + assert.response(server, post_request, {}, + function(res) { next(null, res); }); + }, + function instanciateNoAuth(err, res) + { + if ( err ) throw err; + assert.equal(res.statusCode, 200, res.body); + var parsed = JSON.parse(res.body); + assert.ok(parsed.hasOwnProperty('template_id'), + "Missing 'template_id' from response body: " + res.body); + tpl_id = parsed.template_id; + var post_request = { + url: '/maps/named/' + tpl_id, + method: 'POST', + headers: {host: 'localhost', 'Content-Type': 'application/json' }, + data: JSON.stringify(template_params) + } + var next = this; + assert.response(server, post_request, {}, + function(res) { next(null, res); }); + }, + function instanciateAuth(err, res) + { + if ( err ) throw err; + assert.equal(res.statusCode, 401, + 'Unexpected success instanciating template with no auth: ' + + res.statusCode + ': ' + res.body); + var parsed = JSON.parse(res.body); + assert.ok(parsed.hasOwnProperty('error'), + "Missing 'error' from response body: " + res.body); + assert.ok(parsed.error.match(/unauthorized/i), + 'Unexpected error for unauthorized instance : ' + parsed.error); + var post_request = { + url: '/maps/named/' + tpl_id + '?auth_token=valid2', + method: 'POST', + headers: {host: 'localhost', 'Content-Type': 'application/json' }, + data: JSON.stringify(template_params) + } + var next = this; + assert.response(server, post_request, {}, + function(res) { next(null, res); }); + }, + function fetchTileNoAuth(err, res) { + if ( err ) throw err; + assert.equal(res.statusCode, 200, + 'Instantiating template: ' + res.statusCode + ': ' + res.body); + var parsed = JSON.parse(res.body); + assert.ok(parsed.hasOwnProperty('layergroupid'), + "Missing 'layergroupid' from response body: " + res.body); + layergroupid = parsed.layergroupid; + assert.ok(layergroupid.match(/^localhost@/), + "Returned layergroupid does not start with signer name: " + + layergroupid); + assert.ok(parsed.hasOwnProperty('last_updated'), + "Missing 'last_updated' from response body: " + res.body); + // TODO: check value of last_updated ? + var get_request = { + url: '/maps/' + layergroupid + ':cb0/0/0/0.png', + method: 'GET', + headers: {host: 'localhost' }, + encoding: 'binary' + } + var next = this; + assert.response(server, get_request, {}, + function(res) { next(null, res); }); + }, + function fetchTileAuth(err, res) { + if ( err ) throw err; + assert.equal(res.statusCode, 401, + 'Fetching tile with no auth: ' + res.statusCode + ': ' + res.body); + var parsed = JSON.parse(res.body); + assert.ok(parsed.hasOwnProperty('error'), + "Missing 'error' from response body: " + res.body); + assert.ok(parsed.error.match(/permission denied/i), + 'Unexpected error for unauthorized instance ' + + '(expected /permission denied): ' + parsed.error); + var get_request = { + url: '/maps/' + layergroupid + '/0/0/0.png?auth_token=valid1', + method: 'GET', + headers: {host: 'localhost' }, + encoding: 'binary' + } + var next = this; + assert.response(server, get_request, {}, + function(res) { next(null, res); }); + }, + function checkTile(err, res) { + if ( err ) throw err; + assert.equal(res.statusCode, 200, + 'Unexpected error for authorized instance: ' + + res.statusCode + ' -- ' + res.body); + assert.equal(res.headers['content-type'], "image/png"); + return null; + }, + function deleteTemplate(err) + { + if ( err ) throw err; + var del_request = { + url: '/maps/named/' + tpl_id + '?api_key=1234', + method: 'DELETE', + headers: {host: 'localhost'} + } + var next = this; + assert.response(server, del_request, {}, + function(res) { next(null, res); }); + }, + function fetchTileDeleted(err, res) { + if ( err ) throw err; + assert.equal(res.statusCode, 204, + 'Deleting template: ' + res.statusCode + ':' + res.body); + var get_request = { + url: '/maps/' + layergroupid + '/0/0/0.png?auth_token=valid1', + method: 'GET', + headers: {host: 'localhost' }, + encoding: 'binary' + } + var next = this; + assert.response(server, get_request, {}, + function(res) { next(null, res); }); + }, + function checkTileDeleted(err, res) { + if ( err ) throw err; + assert.equal(res.statusCode, 401, + 'Unexpected statusCode fetch tile after signature revokal: ' + + res.statusCode + ':' + res.body); + var parsed = JSON.parse(res.body); + assert.ok(parsed.hasOwnProperty('error'), + "Missing 'error' from response body: " + res.body); + assert.ok(parsed.error.match(/permission denied/i), + 'Unexpected error for unauthorized access : ' + parsed.error); + return null; + }, + function finish(err) { + if ( err ) errors.push(err); + redis_client.keys("map_*|localhost", function(err, keys) { + if ( err ) errors.push(err.message); + var todrop = _.map(keys, function(m) { + if ( m.match(/^map_(tpl|crt)|/) ) + return m; + }); + if ( todrop.length ) { + errors.push(new Error("Unexpected keys in redis: " + todrop)); + redis_client.del(todrop, function(err) { + if ( err ) errors.push(err.message); + if ( errors.length ) { + done(new Error(errors)); + } + else done(null); + }); + } else { + if ( errors.length ) { + done(new Error(errors)); + } + else done(null); + } + }); + } + ); + }); + + test("can instanciate a template by id with open auth", function(done) { + + // This map fetches data from a private table + var template_acceptance_open = { + version: '0.0.1', + name: 'acceptance_open', + auth: { method: 'open' }, + layergroup: { + version: '1.0.0', + layers: [ + { options: { + sql: "select * from test_table_private_1 LIMIT 0", + cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }', + cartocss_version: '2.0.2', + interactivity: 'cartodb_id' + } } + ] + } + }; + + var template_params = {}; + + var errors = []; + var expected_failure = false; + var tpl_id; + var layergroupid; + Step( + function postTemplate(err, res) + { + var next = this; + var post_request = { + url: '/maps/named?api_key=1234', + method: 'POST', + headers: {host: 'localhost', 'Content-Type': 'application/json' }, + data: JSON.stringify(template_acceptance_open) + } + assert.response(server, post_request, {}, + function(res) { next(null, res); }); + }, + function instanciateNoAuth(err, res) + { + if ( err ) throw err; + assert.equal(res.statusCode, 200, res.body); + var parsed = JSON.parse(res.body); + assert.ok(parsed.hasOwnProperty('template_id'), + "Missing 'template_id' from response body: " + res.body); + tpl_id = parsed.template_id; + var post_request = { + url: '/maps/named/' + tpl_id, + method: 'POST', + headers: {host: 'localhost', 'Content-Type': 'application/json' }, + data: JSON.stringify(template_params) + } + var next = this; + assert.response(server, post_request, {}, + function(res) { next(null, res); }); + }, + function instanciateAuth(err, res) + { + if ( err ) throw err; + assert.equal(res.statusCode, 200, + 'Unexpected success instanciating template with no auth: ' + + res.statusCode + ': ' + res.body); + done(); + } + ); + }); + + test("can instanciate a template using jsonp", function(done) { + + // This map fetches data from a private table + var template_acceptance_open = { + version: '0.0.1', + name: 'acceptance_open_jsonp', + auth: { method: 'open' }, + layergroup: { + version: '1.0.0', + layers: [ + { options: { + sql: "select * from test_table_private_1 LIMIT 0", + cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }', + cartocss_version: '2.0.2', + interactivity: 'cartodb_id' + } } + ] + } + }; + + var template_params = {}; + + var errors = []; + var expected_failure = false; + var tpl_id; + var layergroupid; + Step( + function postTemplate(err, res) + { + var next = this; + var post_request = { + url: '/maps/named?api_key=1234', + method: 'POST', + headers: {host: 'localhost', 'Content-Type': 'application/json' }, + data: JSON.stringify(template_acceptance_open) + } + assert.response(server, post_request, {}, + function(res) { next(null, res); }); + }, + function instanciateNoAuth(err, res) + { + if ( err ) throw err; + assert.equal(res.statusCode, 200, res.body); + var parsed = JSON.parse(res.body); + assert.ok(parsed.hasOwnProperty('template_id'), + "Missing 'template_id' from response body: " + res.body); + tpl_id = parsed.template_id; + var post_request = { + url: '/maps/named/' + tpl_id + "/jsonp?callback=test", + method: 'GET', + headers: {host: 'localhost' } + } + var next = this; + assert.response(server, post_request, {}, + function(res) { next(null, res); }); + }, + function instanciateAuth(err, res) + { + if ( err ) throw err; + assert.equal(res.statusCode, 200, + 'Unexpected success instanciating template with no auth: ' + + res.statusCode + ': ' + res.body); + done(); + } + ); + }); + + test("can instanciate a template using jsonp with params", function(done) { + + // This map fetches data from a private table + var template_acceptance_open = { + version: '0.0.1', + name: 'acceptance_open_jsonp_params', + auth: { method: 'open' }, + /* + placeholders: { + color: { type: "css_color", default: "red" } + },*/ + layergroup: { + version: '1.0.0', + layers: [ + { options: { + sql: "select * from test_table_private_1 LIMIT 0", + cartocss: '#layer { marker-fill: <%= color %>; marker-allow-overlap:true; }', + cartocss_version: '2.0.2', + interactivity: 'cartodb_id' + } } + ] + } + }; + + var template_params = {}; + + var errors = []; + var expected_failure = false; + var tpl_id; + var layergroupid; + Step( + function postTemplate(err, res) + { + var next = this; + var post_request = { + url: '/maps/named?api_key=1234', + method: 'POST', + headers: {host: 'localhost', 'Content-Type': 'application/json' }, + data: JSON.stringify(template_acceptance_open) + } + assert.response(server, post_request, {}, + function(res) { next(null, res); }); + }, + function instanciateNoAuth(err, res) + { + if ( err ) throw err; + assert.equal(res.statusCode, 200, res.body); + var parsed = JSON.parse(res.body); + assert.ok(parsed.hasOwnProperty('template_id'), + "Missing 'template_id' from response body: " + res.body); + tpl_id = parsed.template_id; + var post_request = { + url: '/maps/named/' + tpl_id + "/jsonp?callback=test%config=" + JSON.stringify('{color:blue}'), + method: 'GET', + headers: {host: 'localhost' } + } + var next = this; + assert.response(server, post_request, {}, + function(res) { next(null, res); }); + }, + function instanciateAuth(err, res) + { + if ( err ) throw err; + assert.equal(res.statusCode, 200, + 'Unexpected success instanciating template with no auth: ' + + res.statusCode + ': ' + res.body); + done(); + } + ); + }); + + test("template instantiation raises mapviews counter", function(done) { + var layergroup = { + stat_tag: 'random_tag', + version: '1.0.0', + layers: [ + { options: { + sql: 'select 1 as cartodb_id, !pixel_height! as h, ' + + 'ST_Buffer(!bbox!, -32*greatest(!pixel_width!,!pixel_height!)) as the_geom_webmercator', + cartocss: '#layer { polygon-fill:red; }', + cartocss_version: '2.0.1' + } } + ] + }; + var template = { + version: '0.0.1', + name: 'stat_gathering', + auth: { method: 'open' }, + layergroup: layergroup + }; + var statskey = "user:localhost:mapviews"; + var redis_stats_client = redis.createClient(global.environment.redis.port); + var template_id; // will be set on template post + var now = strftime("%Y%m%d", new Date()); + var errors = []; + Step( + function clean_stats() + { + var next = this; + redis_stats_client.select(redis_stats_db, function(err) { + if ( err ) next(err); + else redis_stats_client.del(statskey+':global', next); + }); + }, + function do_post_tempate(err) + { + if ( err ) throw err; + var post_request = { + url: '/maps/named?api_key=1234', + method: 'POST', + headers: {host: 'localhost', 'Content-Type': 'application/json' }, + data: JSON.stringify(template) + } + var next = this; + assert.response(server, post_request, {}, + function(res) { next(null, res); }); + }, + function instantiateTemplate(err, res) { + if ( err ) throw err; + assert.equal(res.statusCode, 200, res.body); + template_id = JSON.parse(res.body).template_id; + var post_request = { + url: '/maps/named/' + template_id, + method: 'POST', + headers: {host: 'localhost', 'Content-Type': 'application/json' }, + data: JSON.stringify({}) + } + var next = this; + assert.response(server, post_request, {}, + function(res) { next(null, res); }); + }, + function check_global_stats(err, res) { + if ( err ) throw err; + assert.equal(res.statusCode, 200, + 'Instantiating template: ' + res.statusCode + ': ' + res.body); + var parsed = JSON.parse(res.body); + assert.ok(parsed.hasOwnProperty('layergroupid'), + "Missing 'layergroupid' from response body: " + res.body); + layergroupid = parsed.layergroupid; + redis_stats_client.zscore(statskey + ":global", now, this); + }, + function check_tag_stats(err, val) { + if ( err ) throw err; + assert.equal(val, 1, "Expected score of " + now + " in " + + statskey + ":global to be 1, got " + val); + redis_stats_client.zscore(statskey+':stat_tag:random_tag', now, this); + }, + function check_tag_stats_value(err, val) { + if ( err ) throw err; + assert.equal(val, 1, "Expected score of " + now + " in " + + statskey + ":stat_tag:" + layergroup.stat_tag + " to be 1, got " + val); + return null; + }, + function deleteTemplate(err) + { + if ( err ) throw err; + var del_request = { + url: '/maps/named/' + template_id + '?api_key=1234', + method: 'DELETE', + headers: {host: 'localhost'} + } + var next = this; + assert.response(server, del_request, {}, + function(res) { next(null, res); }); + }, + function cleanup_stats(err, res) { + if ( err ) throw err; + assert.equal(res.statusCode, 204, res.statusCode + ': ' + res.body); + if ( err ) errors.push('' + err); + redis_client.del([statskey+':global', statskey+':stat_tag:'+layergroup.stat_tag], this); + }, + function finish(err) { + if ( err ) errors.push('' + err); + if ( errors.length ) done(new Error(errors.join(','))); + else done(null); + } + ); + }); + + suiteTeardown(function(done) { + + // This test will add map_style records, like + // 'map_style|null|publicuser|my_table', + redis_client.keys("map_*", function(err, keys) { + var todrop = _.map(keys, function(m) { + if ( m.match(/^map_(tpl|crt|sig)|/) ) return m; + }); + redis_client.del(todrop, function(err) { + redis_client.select(5, function(err) { + redis_client.keys("user:localhost:mapviews*", function(err, keys) { + redis_client.del(keys, function(err) { + sqlapi_server.close(done); + }); + }); + }); + }); + }); + + }); + +}); +