From 512aa1822636ed47e4b5bfd4fabda646bde5c120 Mon Sep 17 00:00:00 2001 From: Matt Hampel Date: Tue, 6 Oct 2015 17:48:59 -0700 Subject: [PATCH 1/3] Update to Express 4 --- lib/controllers/features.js | 34 +++--- lib/controllers/forms.js | 16 +-- lib/controllers/orgs.js | 14 +-- lib/controllers/parcels.js | 28 ++--- lib/controllers/responses.js | 213 +++++++++++++++++++---------------- lib/controllers/stats.js | 26 ++--- lib/controllers/surveys.js | 38 +++---- lib/controllers/users.js | 42 +++---- lib/models/Survey.js | 2 +- lib/postgres.js | 4 +- lib/routes.js | 15 ++- lib/server.js | 45 ++++---- newrelic.js | 2 +- package.json | 33 ++++-- test/data/fixtures.js | 16 ++- test/features.js | 1 + test/lib/router.js | 8 +- test/stats.js | 14 ++- test/users.js | 29 ++++- 19 files changed, 327 insertions(+), 253 deletions(-) diff --git a/lib/controllers/features.js b/lib/controllers/features.js index 41590c8..2097043 100644 --- a/lib/controllers/features.js +++ b/lib/controllers/features.js @@ -65,7 +65,7 @@ exports.useCache = function useCache(req, res, next) { if (cache.get(req.url) === etag) { res.set('ETag', etag); - res.send(304); + res.sendStatus(304); cacheStats.hits += 1; } @@ -101,7 +101,7 @@ exports.getById = function getById(req, response) { // Require thie id if (id === undefined || source === undefined) { - response.send(413); + response.sendStatus(413); return; } @@ -119,7 +119,7 @@ exports.getById = function getById(req, response) { if (error) { console.log('postgres database error: ' + error.message); console.log(error); - response.send(500); + response.sendStatus(500); return; } @@ -160,11 +160,13 @@ exports.getById = function getById(req, response) { .on('end', function (result) { if (error) { console.log(error); - response.send(500); + response.sendStatus(500); } else { // If there's no data, we want the client to check back. // If there is data, we want the client to use its local cache for a // while without hitting the network. + // + console.log("Got results in getById", result); if (result.rows.length > 0) { // 3600 seconds = 1 hour response.set('Cache-Control', 'max-age=3600'); @@ -207,20 +209,20 @@ exports.get = function get(req, response) { // Require a filter if (bbox === undefined && (lon === undefined || lat === undefined)) { - response.send(413); + response.sendStatus(413); return; } // Don't allow both filters at once if (bbox !== undefined && (lon !== undefined || lat !== undefined)) { - response.send(400); + response.sendStatus(400); return; } // Require either a type or a source if (type === undefined && source === undefined) { - response.send(400); + response.sendStatus(400); return; } @@ -231,7 +233,7 @@ exports.get = function get(req, response) { coordString = bbox.split(','); if (coordString.length !== 4) { - response.send(400); + response.sendStatus(400); return; } @@ -242,7 +244,7 @@ exports.get = function get(req, response) { // Make sure the conversion worked if (isNaN(coords[i])) { - response.send(400); + response.sendStatus(400); return; } } @@ -256,7 +258,7 @@ exports.get = function get(req, response) { lon = parseFloat(lon); if (isNaN(lat) || isNaN(lon)) { - response.send(400); + response.sendStatus(400); return; } geometry = 'POINT(' + lon + ' ' + lat + ')'; @@ -289,7 +291,7 @@ exports.get = function get(req, response) { if (error) { console.log('postgres database error: ' + error.message); console.log(error); - response.send(500); + response.sendStatus(500); return; } @@ -330,7 +332,7 @@ exports.get = function get(req, response) { .on('end', function (result) { if (error) { console.log(error); - response.send(500); + response.sendStatus(500); } else { // If there's no data, we want the client to check back. // If there is data, we want the client to use its local cache for a @@ -362,7 +364,7 @@ exports.listSources = function listSources(req, res) { if ((lat !== undefined && lon === undefined) || (lat === undefined && lon !== undefined)) { - res.send(400, { + res.status(400).send({ name: 'BadRequestError', message: 'Must specify both lon and lat arguments.' }); @@ -392,7 +394,7 @@ exports.listSources = function listSources(req, res) { queryConfig = { text: [ 'SELECT DISTINCT source AS name, type FROM features', - 'WHERE geom && ST_Expand(ST_SetSRID(ST_Point($1, $2),4326), .07)::geography', + 'WHERE geom && ST_Expand(ST_SetSRID(ST_Point($1, $2),4326), .07)::geography' ].join('\n'), values: [lon, lat], name: 'featureNearbySourcesQuery' @@ -403,7 +405,7 @@ exports.listSources = function listSources(req, res) { if (error) { console.log('postgres database error: ' + error.message); console.log(error); - res.send(500); + res.sendStatus(500); return; } @@ -426,7 +428,7 @@ exports.listSources = function listSources(req, res) { .on('end', function (result) { if (error) { console.log(error); - res.send(500); + res.sendStatus(500); } else { res.send({ sources: result.rows diff --git a/lib/controllers/forms.js b/lib/controllers/forms.js index 1227c15..4ee7ba1 100644 --- a/lib/controllers/forms.js +++ b/lib/controllers/forms.js @@ -41,7 +41,7 @@ exports.get = function get(req, res) { .exec(function (error, form) { if (util.handleError(error)) { return; } if (form === null) { - res.send(404); + res.sendStatus(404); } else { res.send({ form: form }); } @@ -54,7 +54,7 @@ exports.post = function post(req, res) { var surveyId = req.params.surveyId; if (!data || !util.isArray(data)) { - res.send(400); + res.sendStatus(400); return; } @@ -86,19 +86,19 @@ exports.post = function post(req, res) { }); if (errors.length > 0) { - res.send(400, errors[0]); + res.status(400).send(errors[0]); return; } Form.create(rawForms, function (error) { if (error) { - res.send(500); + res.sendStatus(500); return; } var output = Array.prototype.slice.call(arguments, 1).map(function (doc) { return doc.toObject(); }); - res.send(201, { forms: output }); + res.status(201).send({ forms: output }); }); }; @@ -113,9 +113,9 @@ exports.put = function put(req, res) { .exec(function (error, form) { if (util.handleError(error)) { return; } if (form === null) { - res.send(404); + res.sendStatus(404); } else if (form.type !== modifiedForm.type) { - res.send(400, { + res.status(400).send({ name: 'SyntaxError', message: 'You cannot change the form type' }); @@ -133,7 +133,7 @@ exports.put = function put(req, res) { form.save(function (error, doc) { if (error) { - res.send(500); + res.sendStatus(500); return; } diff --git a/lib/controllers/orgs.js b/lib/controllers/orgs.js index 12ea202..11884bc 100644 --- a/lib/controllers/orgs.js +++ b/lib/controllers/orgs.js @@ -13,7 +13,7 @@ exports.list = function list(req, res) { if (user) { // We currently only allow users to see their own orgs. if (user !== req.user._id) { - res.send(403); + res.sendStatus(403); return; } return exports.listForCurrentUser(req, res); @@ -40,7 +40,7 @@ exports.get = function get(req, res) { if (util.handleError(error, res)) { return; } if (!org) { - res.send(404); + res.sendStatus(404); return; } res.send({ org: org }); @@ -58,17 +58,17 @@ exports.post = function post(req, res) { if (error) { if (error.code === 11000) { // Mongo duplicate key error - res.send(400, { + res.status(400).send({ type: 'OrgExistsError', message: 'An organization with that name already exists' }); } else { console.log(error); - res.send(500); + res.sendStatus(500); } return; } - res.send(201, { orgs: docs }); + res.status(201).send({ orgs: docs }); }); }; @@ -82,13 +82,13 @@ exports.put = function put(req, res) { // We didn't find an org with that ID. if (!doc) { - res.send(404); + res.sendStatus(404); return; } // Make sure the user is authorized. if (doc.users.indexOf(req.user._id) === -1) { - res.send(403); + res.sendStatus(403); return; } diff --git a/lib/controllers/parcels.js b/lib/controllers/parcels.js index 2b4db2c..1e0b70d 100644 --- a/lib/controllers/parcels.js +++ b/lib/controllers/parcels.js @@ -5,7 +5,7 @@ * ================================================== * Parcels * ================================================== - * + * * Serves parcel data from the geo database. */ @@ -29,10 +29,10 @@ function bboxToPolygon(bbox) { } -/* +/* * Make values returned by the database neater. - */ -function clean(val) { + */ +function clean(val) { if(val !== null) { return val.trim(); } @@ -101,11 +101,11 @@ function getParcels(req, response, outputBuilder) { var output; var error; var i; - + // Require a filter if (bbox === undefined && (lon === undefined || lat === undefined)) { - response.send(413); + response.sendStatus(413); return; } @@ -114,7 +114,7 @@ function getParcels(req, response, outputBuilder) { // Don't allow both filters at once if (lon !== undefined || lat !== undefined) { - response.send(400); + response.sendStatus(400); return; } @@ -122,7 +122,7 @@ function getParcels(req, response, outputBuilder) { coordString = bbox.split(','); if (coordString.length !== 4) { - response.send(400); + response.sendStatus(400); return; } @@ -133,7 +133,7 @@ function getParcels(req, response, outputBuilder) { // Make sure the conversion worked if (isNaN(coords[i])) { - response.send(400); + response.sendStatus(400); return; } } @@ -151,7 +151,7 @@ function getParcels(req, response, outputBuilder) { lon = parseFloat(lon); if (isNaN(lat) || isNaN(lon)) { - response.send(400); + response.sendStatus(400); return; } @@ -161,11 +161,11 @@ function getParcels(req, response, outputBuilder) { name: 'parcelPointQuery' }; } - + getClient(function (error, client, done) { if (error) { console.log('ERROR: ' + error.message); - response.send(500); + response.sendStatus(500); return; } @@ -191,7 +191,7 @@ function getParcels(req, response, outputBuilder) { .on('end', function (result) { if (error) { console.log(error); - response.send(500); + response.sendStatus(500); } else { // If there's no data, we want the client to check back. // If there is data, we want the client to use its local cache for a @@ -212,7 +212,7 @@ exports.useCache = function useCache(req, res, next) { if (etag !== undefined && cache.get(req.url) === etag) { res.set('ETag', etag); - res.send(304); + res.sendStatus(304); return; } diff --git a/lib/controllers/responses.js b/lib/controllers/responses.js index 3a97184..432de3e 100644 --- a/lib/controllers/responses.js +++ b/lib/controllers/responses.js @@ -5,10 +5,13 @@ var _ = require('lodash'); var async = require('async'); var cuid = require('cuid'); var knox = require('knox'); + var logfmt = require('logfmt'); var makeSlug = require('slugs'); var mongoose = require('mongoose'); +var multer = require('multer'); var Promise = require('bluebird'); +var multerS3 = require('multer-s3'); var util = require('../util'); var Response = require('../models/Response'); @@ -18,6 +21,8 @@ var tasks = require('../tasks'); Promise.promisify(Response); +var MAX_FILES = 20; // Max files allowed in a single response + var client = knox.createClient({ key: settings.s3_key, secret: settings.s3_secret, @@ -30,7 +35,24 @@ var exportClient = knox.createClient({ bucket: settings.exportBucket }); -var uploadDir = settings.s3_dir; +var uploader = multer({ + storage: multerS3({ + bucket: settings.s3_bucket, + secretAccessKey: settings.s3_secret, + accessKeyId: settings.s3_key, + dirname: settings.s3_dir, + region: 'us-east-1', + key: function(req, file, callback) { + console.log('info at=responses event=file_received name=' + file.originalname); + var split = file.originalname.split('.'); + var extension = split.slice(-1)[0]; + var unique = cuid.slug(); + var fileName = 'tmp/' + unique + '.' + extension; + callback(null, fileName); + } + }) +}).any(); + var uploadPrefix = 'http://' + settings.s3_bucket + '.s3.amazonaws.com/'; // How long an export file remains valid @@ -200,7 +222,7 @@ exports.list = function list(req, res) { // box, we require paging parameters. // TODO: enforce an upper bound on the number of object we will send back if (paging === null && objectId === undefined && bbox === undefined) { - res.send(400, { + res.status(400).send({ name: 'QueryError', message: 'You must specify startIndex and count query parameters to avoid excessive server load' }); @@ -222,7 +244,7 @@ exports.list = function list(req, res) { var coords = bbox.split(','); if (coords.length !== 4) { // There need to be four points. - res.send(400, { + res.status(400).send({ name: 'QueryError', message: 'You must specify 4 points for a bbox parameter' }); @@ -354,7 +376,7 @@ exports.get = function get(req, res) { .exec(function (error, doc) { if (util.handleError(error, res)) { return; } if (doc === null) { - res.send(404); + res.sendStatus(404); } else { var response = formatForAPI(doc.getSingleEntry(new mongoose.Types.ObjectId(req.params.responseId))); res.send({ response: response }); @@ -383,7 +405,7 @@ exports.patch = function patch(req, res) { $set: set }, function(error, numCompleted) { if (util.handleError(error, res)) { return; } - res.send(204); + res.sendStatus(204); }); }; @@ -406,7 +428,7 @@ exports.put = function patch(req, res) { $set: set }, function(error, numCompleted) { if (util.handleError(error, res)) { return; } - res.send(204); + res.sendStatus(204); }); }; @@ -426,7 +448,7 @@ exports.del = function del(req, res) { new: false }).then(function (doc) { if (!doc) { - res.send(404); + res.sendStatus(404); return; } @@ -507,11 +529,11 @@ exports.del = function del(req, res) { .finally(function () { // If we encounter an error, we will still have deleted the original // entry, so we want to send a 204 back no matter what. - res.send(204); + res.sendStatus(204); }); }).catch(function (error) { logfmt.error(error); - res.send(500); + res.sendStatus(500); }); }; @@ -540,7 +562,7 @@ function makeFilename(orig, surveyId, objectName, objectId) { }; } - console.log('info at=responses event=file_received name=' + orig); + console.log('info at=responses event=making_file_name name=' + orig); // Get the date var today = new Date(); @@ -562,7 +584,7 @@ function makeFilename(orig, surveyId, objectName, objectId) { var extension = split.slice(-1)[0]; // Construct the name - var name = uploadDir + '/' + surveyId + '/' + + var name = '/' + surveyId + '/' + year + '-' + month + '-' + date + '-' + objectSlug + '-' + unique + @@ -929,111 +951,110 @@ function saveResponses(res, data, surveyId) { if (error) { if (error.name === 'ValidationError') { console.log('error at=save_response issue=invalid_data survey=' + surveyId); - res.send(400, error); + res.status(400).send(error); } else { console.log('error at=save_response issue=unknown_error error_name=' + error.name + ' survey=' + surveyId); console.log(error); - res.send(500); + res.sendStatus(500); } return; } - res.send(201, { responses: docs }); + res.status(201).send({ responses: docs }); }); } exports.post = function post(req, res) { - var data = req.body.responses; - var surveyId = req.params.surveyId; + uploader(req, res, function(error) { + var data = req.body.responses; + var surveyId = req.params.surveyId; - // If this is a request with files, the response (only one is allowed) - // arrives as a string - var files = req.files; - if (files) { - if (!req.body.data) { - console.log('Error: We received no response data, only file data.'); - res.send(400); - return; - } - - try { - data = JSON.parse(req.body.data).responses; - } catch (e) { - res.send(400); - console.log(e); - } - - // You can only add 1 response if you're attaching a file - if (data.length !== 1) { - res.send(400); - return; + if (error) { + console.log("Error uploading files", error); + res.sendStatus(500); } - } - - if (!util.isArray(data)) { - res.send(400); - return; - } - // Save each of the files - // Note: we assume that all files being uploaded are images - var savedFilePaths = []; - if (files) { - var fileList = []; - var key; - for (key in files) { - if (files.hasOwnProperty(key)) { - fileList.push(files[key]); + // If this is a request with files, the response (only one is allowed) + // arrives as a string + // TODO: may need to move this above the multer handler + var files = req.files; + if (files) { + if (!req.body.data) { + console.log('Error: We received no response data, only file data.'); + res.sendStatus(400); + return; } - } - async.each(fileList, function(file, callback) { - // Generate a name for the file. We use a collision-resistant name so - // that we can just send the file to S3 without worrying about - // overwriting another object. - var fileName; try { - fileName = makeFilename(file.name, surveyId, data[0].geo_info.humanReadableName, data[0].object_id); + data = JSON.parse(req.body.data).responses; } catch (e) { - return callback(e); + res.sendStatus(400); + console.log(e); } - console.log('info at=responses event=saving_file name=' + fileName); + // You can only add 1 response if you're attaching a file + if (data.length !== 1) { + res.sendStatus(400); + return; + } + } - // Save the file to S3 - client.putFile(file.path, fileName, function (error, response) { + if (!util.isArray(data)) { + res.sendStatus(400); + return; + } + + // Save each of the files + // Note: we assume that all files being uploaded are images + var savedFilePaths = []; + if (files) { + async.each(files, function(file, callback) { + // Generate a name for the file. We use a collision-resistant name so + // that we can just send the file to S3 without worrying about + // overwriting another object. + var fileName; + try { + fileName = makeFilename(file.originalname, surveyId, data[0].geo_info.humanReadableName, data[0].object_id); + } catch (e) { + return callback(e); + } + + console.log('info at=responses event=saving_file name=' + fileName); + + // Save the file to S3 + client.copyFile(file.key, fileName, function (error, response) { + if (error) { + callback(error); + return; + } + if (200 === response.statusCode) { + console.log('info at=responses event=saved_file url=' + uploadPrefix + fileName); + savedFilePaths.push(uploadPrefix + fileName); + callback(); + } else { + callback(new Error('Received status code ' + response.statusCode + ' trying to save a file to S3')); + } + }); + }, function callback(error) { if (error) { - callback(error); + if (error.name === 'FileNamingError') { + res.status(400).send(error); + } else { + console.log(error); + res.sendStatus(500); + } return; } - if (200 === response.statusCode) { - console.log('info at=responses event=saved_file url=' + uploadPrefix + fileName); - savedFilePaths.push(uploadPrefix + fileName); - callback(); - } else { - callback(new Error('Received status code ' + response.statusCode + ' trying to save a file to S3')); - } + // Once we have all of the files saved, attach the list of filenames + // to the first response object + data[0].files = savedFilePaths; + saveResponses(res, data, surveyId); }); - }, function callback(error) { - if (error) { - if (error.name === 'FileNamingError') { - res.send(400, error); - } else { - console.log(error); - res.send(500); - } - return; - } - // Once we have all of the files saved, attach the list of filenames - // to the first response object - data[0].files = savedFilePaths; + } else { + // No files + // Boring, much less work to do. saveResponses(res, data, surveyId); - }); - } else { - // No files - // Boring, much less work to do. - saveResponses(res, data, surveyId); - } - + } + }); }; // Redirect the client to a signed S3 URL for the export file. @@ -1059,7 +1080,7 @@ function handleExport(req, res, type) { if (util.handleError(error, res)) { return; } if (!doc) { - res.send(404); + res.sendStatus(404); return; } @@ -1068,7 +1089,7 @@ function handleExport(req, res, type) { name += '-latest-only'; } - if (req.user && _.contains(doc.users, req.user._id)) { + if (req.user && _.includes(doc.users, req.user._id)) { open = false; } @@ -1117,7 +1138,7 @@ function handleExport(req, res, type) { function (next) { // Send a 202, indicating that we are processing the request, and the // client should check back. - res.send(202); + res.sendStatus(202); next(); } ], function (error) { @@ -1151,7 +1172,7 @@ function handleExport(req, res, type) { if (metadata.requested > now - exportRequestDuration) { // If the file doesn't exist, but we requested it recently, then tell // the client to wait. - res.send(202); + res.sendStatus(202); console.log(util.format('info at=export event=202 reason=waiting_on_export request_time=%d now=%d request_duration=%d', metadata.requested, now, exportRequestDuration)); } else { // If the file doesn't exist, and we requested it a while ago, then @@ -1164,7 +1185,7 @@ function handleExport(req, res, type) { if (head.statusCode !== 200) { console.log(util.format('error at=export issue=unexpected_s3_status status_code=%d export_name=%s', head.statusCode, name)); - res.send(500); + res.sendStatus(500); return; } diff --git a/lib/controllers/stats.js b/lib/controllers/stats.js index e34deca..9f98837 100644 --- a/lib/controllers/stats.js +++ b/lib/controllers/stats.js @@ -60,7 +60,7 @@ function decodeKey(key) { function encodeStats(stats, encoder) { var ret = {}; var questions = Object.keys(stats); - + var encode = encoder || encodeKey; questions.forEach(function (question) { @@ -341,7 +341,7 @@ exports.stats = function stats(req, res) { }); }).then(function (stats) { if (!stats) { - res.send(404); + res.sendStatus(404); return; } @@ -350,7 +350,7 @@ exports.stats = function stats(req, res) { }); }).catch(function (error) { logfmt.error(error); - res.send(500); + res.sendStatus(500); }); }; @@ -435,7 +435,7 @@ exports.activity = function activity(req, res) { } if (clientError) { - res.send(400, clientError); + res.status(400).send(clientError); return; } @@ -460,7 +460,7 @@ exports.activity = function activity(req, res) { try { polygon = JSON.parse(intersects); } catch (e) { - res.send(400, { + res.status(400).send({ name: 'QueryError', message: 'Invalid encoded GeoJSON value for "intersects"' }); @@ -540,18 +540,18 @@ exports.activity = function activity(req, res) { return Survey.countAsync({ id: surveyId }) .then(function (count) { if (count === 0) { - res.send(404); + res.sendStatus(404); } else { - res.send(200, data); + res.status(200).send(data); } }); } - res.send(200, data); + res.status(200).send(data); }).catch(function (error) { console.log('error at=stats issue=mongoose_aggregate_error'); console.log(error); - res.send(500); + res.sendStatus(500); }); }; @@ -647,9 +647,9 @@ exports.byRange = function byRange(req, res) { return Survey.countAsync({ id: surveyId }) .then(function (count) { if (count === 0) { - res.send(404); + res.sendStatus(404); } else { - res.send(200, data); + res.status(200).send(data); } }); } @@ -704,10 +704,10 @@ exports.byRange = function byRange(req, res) { data.stats.activity = activity; - res.send(200, data); + res.status(200).send(data); }).catch(function (error) { console.log('error at=stats issue=mongoose_aggregate_error'); console.log(error); - res.send(500); + res.sendStatus(500); }); }; diff --git a/lib/controllers/surveys.js b/lib/controllers/surveys.js index fd0e3f1..82d75da 100644 --- a/lib/controllers/surveys.js +++ b/lib/controllers/surveys.js @@ -17,7 +17,7 @@ exports.list = function list(req, res) { users: req.user._id }; }else { - res.send(401); + res.sendStatus(401); return; } @@ -47,10 +47,10 @@ exports.get = function get(req, res) { return Promise.join(countPromise, boundsPromise) .then(function () { // In case the count or bounds promises resolve before we cancel them. - res.send(404); + res.sendStatus(404); }) .catch(Promise.CancellationError, function (e) { - res.send(404); + res.sendStatus(404); }); } @@ -62,7 +62,7 @@ exports.get = function get(req, res) { }).catch(function (error) { console.log(error); console.log(error.stack); - res.send(500); + res.sendStatus(500); }); }; @@ -114,23 +114,23 @@ exports.addUser = function addUser(req, res) { var survey = results.survey; if (error) { - res.send({ name: 'UserQueryError', message: 'We were not able to add the user at this time' }, 500); + res.status(500).send({ name: 'UserQueryError', message: 'We were not able to add the user at this time' }); return; } if(!user) { - res.send({ name: 'UserNotFoundError', message: 'User not found' }, 400); + res.status(400).send({ name: 'UserNotFoundError', message: 'User not found' }); return; } // Add the user to the survey users - if(! __.contains(survey.users, user._id)) { + if(! __.includes(survey.users, user._id)) { survey.users.push(user._id.toString()); } // Save the survey Survey.update({ id: surveyId}, { users: survey.users}, function(err, doc) { if (err) { - res.send({ name: 'SurveySaveError', message: "We were not able to add the user at this time" }, 500); + res.status(500).send({ name: 'SurveySaveError', message: "We were not able to add the user at this time" }); return; } res.send({ survey: doc }); @@ -163,11 +163,11 @@ exports.removeUser = function addUser(req, res) { var survey = results.survey; if (error) { - res.send({ name: 'UserQueryError', message: 'We were not able to add the user at this time' }, 500); + res.status(500).send({ name: 'UserQueryError', message: 'We were not able to add the user at this time' }); return; } if(!user) { - res.send({ name: 'UserNotFoundError', message: 'User not found' }, 400); + res.status(400).send({ name: 'UserNotFoundError', message: 'User not found' }); return; } @@ -177,10 +177,10 @@ exports.removeUser = function addUser(req, res) { //Save the survey Survey.update({ id: surveyId }, { users: survey.users }, function(error, success) { if (error) { - res.send({ name: 'SurveySaveError', message: "We were not able to remove the user at this time" }, 500); + res.status(500).send({ name: 'SurveySaveError', message: "We were not able to remove the user at this time" }); return; } - res.send(204); + res.sendStatus(204); return; }); }); @@ -201,9 +201,9 @@ exports.post = function post(req, response) { itemError = error; console.log(error); if (error.name === 'ValidationError') { - response.send(400, error); + response.status(400).send(error); } else { - response.send(500); + response.sendStatus(500); } return; } @@ -215,7 +215,7 @@ exports.post = function post(req, response) { var name = req.user.name; var link = 'https://' + req.headers.host + '/#surveys/' + output[0].slug; console.log('info at=surveys event=created user="' + name + '" link="' + link + '"'); - response.send(201, { surveys: output }); + response.status(201).send({ surveys: output }); }); }; @@ -227,7 +227,7 @@ exports.put = function put(req, response) { Survey.findOneAndUpdate({ id: surveyId }, survey, function (error, updated) { if (error) { console.log(error); - response.send(500); + response.sendStatus(500); return; } @@ -243,11 +243,11 @@ exports.del = function put(req, response) { Survey.remove({ id: surveyId }, function (error, updated) { if (error) { console.log(error); - response.send(500); + response.sendStatus(500); return; } - response.send(204); + response.sendStatus(204); }); }; @@ -262,7 +262,7 @@ exports.getSlug = function getSlug(req, res) { .exec(function (error, survey) { if (util.handleError(error, res)) { return; } if (survey === null) { - res.send(404); + res.sendStatus(404); } else { res.send({ survey: survey.id }); } diff --git a/lib/controllers/users.js b/lib/controllers/users.js index 4c41c68..4b8ad32 100644 --- a/lib/controllers/users.js +++ b/lib/controllers/users.js @@ -122,7 +122,7 @@ exports.login = function login(req, res, next) { if (info) { // TODO: coordinate a change with the dashboard, so that we can send JSON // over the wire. - res.send(400, info.message); + res.status(400).send(info.message); } })(req, res, next); }; @@ -142,7 +142,7 @@ exports.post = function post(req, res) { if (error) { if (error.code === 11000) { // Mongo duplicate key error - res.send(400, { + res.status(400).send({ type: 'AccountExistsError', message: 'An account with that email already exists' }); @@ -160,13 +160,13 @@ exports.post = function post(req, res) { message = 'Missing fields'; } - res.send(400, { + res.status(400).send({ name: 'UserCreationError', message: message }); } else { console.log(error); - res.send(500); + res.sendStatus(500); } return; } @@ -175,7 +175,7 @@ exports.post = function post(req, res) { if (error) { // TODO: If this is unexpected, we should probably return status code 500. console.log('error at=users_create issue=unknown type=' + error.name + ' message="' + error.message + '"'); - res.send(401); + res.sendStatus(401); return; } res.send(doc.toObject()); @@ -200,7 +200,7 @@ exports.get = function get(req, res) { exports.forgotPassword = function forgotPassword(req, response, next) { var email = req.body.user.email; if (email === undefined) { - response.send({ name: 'PasswordResetError', message: 'Email required' }, 400); + response.status(400).send({ name: 'PasswordResetError', message: 'Email required' }); return; } @@ -214,7 +214,7 @@ exports.forgotPassword = function forgotPassword(req, response, next) { return next(error); } if(!user) { - response.send({ name: 'PasswordResetError', message: 'Account not found' }, 400); + response.status(400).send({ name: 'PasswordResetError', message: 'Account not found' }); return; } @@ -258,7 +258,7 @@ exports.forgotPassword = function forgotPassword(req, response, next) { if(error){ return next(error); } - response.send(200, {}); + response.sendStatus(200); }); }); }); @@ -278,15 +278,15 @@ exports.resetPassword = function resetPassword(req, response, next) { var password = req.body.reset.password; if (!email) { - response.send({ name: 'PasswordResetError', message: 'Email required' }, 400); + response.status(400).send({ name: 'PasswordResetError', message: 'Email required' }); return; } if (!token) { - response.send({ name: 'PasswordResetError', message: 'Password reset code required' }, 400); + response.status(400).send({ name: 'PasswordResetError', message: 'Password reset code required' }); return; } if (!password) { - response.send({ name: 'PasswordResetError', message: 'Password required' }, 400); + response.status(400).send({ name: 'PasswordResetError', message: 'Password required' }); return; } @@ -297,26 +297,26 @@ exports.resetPassword = function resetPassword(req, response, next) { } if (user === null) { - response.send({ + response.status(400).send({ name: 'BadRequestError', message: 'Account not found' - }, 400); + }); return; } // Check that the user has a reset object if (user.reset === undefined) { - response.send({ name: 'PasswordResetError', message: 'Reset token required' }, 400); + response.status(400).send({ name: 'PasswordResetError', message: 'Reset token required' }); return; } // Make sure the token matches if (!bcrypt.compareSync(token, user.reset.hashedToken)) { // TODO: Should this be a 403 instead of 400? - response.send({ + response.status(400).send({ name: 'BadRequestError', message: 'Invalid reset token' - }, 400); + }); return; } @@ -324,10 +324,10 @@ exports.resetPassword = function resetPassword(req, response, next) { var now = new Date().getTime(); var expiry = user.reset.expiry; if (expiry.getTime() < now) { - response.send({ + response.status(400).send({ name: 'BadRequestError', message: 'Token expired' - }, 400); + }); return; } @@ -343,7 +343,7 @@ exports.resetPassword = function resetPassword(req, response, next) { req.logIn(user, function(error) { if (error) { console.log('error at=users_reset_password issue=unknown type=' + error.name + ' message="' + error.message + '"'); - response.send(500); + response.sendStatus(500); } response.redirect('/api/user'); return; @@ -365,7 +365,7 @@ exports.ensureAuthenticated = function ensureAuthenticated(req, res, next) { return next(); } - res.send(401, { + res.status(401).send({ type: 'AuthError', message: 'You must be logged in to access that resource' }); @@ -375,7 +375,7 @@ exports.ensureAuthenticated = function ensureAuthenticated(req, res, next) { exports.ensureSurveyAccess = function ensurePermissions(req, res, next) { Survey.findIfOwnedByUser(req.params.surveyId, req.user._id, function(error, survey) { if(!survey) { - res.send(error.code); + res.sendStatus(error.code); return; } return next(); diff --git a/lib/models/Survey.js b/lib/models/Survey.js index 23d0fd6..943f373 100644 --- a/lib/models/Survey.js +++ b/lib/models/Survey.js @@ -192,7 +192,7 @@ surveySchema.pre('save', function setId(next) { var survey = surveys[0]; - if (__.contains(survey.users, userId)) { + if (__.includes(survey.users, userId)) { survey.users = undefined; // for security cb(null, survey); return; diff --git a/lib/postgres.js b/lib/postgres.js index 7e12d34..e178b8b 100644 --- a/lib/postgres.js +++ b/lib/postgres.js @@ -5,11 +5,11 @@ * ================================================== * Postgresql client * ================================================== - * + * * Provides access to the geo database */ -var pg = require('pg').native; +var pg = require('pg'); var settings = require('../settings'); var connectionString = settings.psqlConnectionString; diff --git a/lib/routes.js b/lib/routes.js index 12471a3..fb46796 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -1,6 +1,11 @@ /*jslint node: true */ 'use strict'; +var cuid = require('cuid'); +var multer = require('multer'); +var multerS3 = require('multer-s3'); + +var settings = require('../settings'); var stats = require('./controllers/stats'); var surveys = require('./controllers/surveys'); var responses = require('./controllers/responses'); @@ -19,9 +24,10 @@ function formatHelper(format) { function enforceHTTPS(req, res, next) { if (!req.secure) { - res.send(400); + res.sendStatus(400); return; } + next(); } @@ -33,13 +39,13 @@ exports.setup = function setup(app) { app.get('/api/surveys/:surveyId', surveys.get); app.post('/api/surveys', session, users.ensureAuthenticated, surveys.post); app.put('/api/surveys/:surveyId', session, users.ensureAuthenticated, users.ensureSurveyAccess, surveys.put); - app.del('/api/surveys/:surveyId', session, users.ensureAuthenticated, users.ensureSurveyAccess, surveys.del); + app.delete('/api/surveys/:surveyId', session, users.ensureAuthenticated, users.ensureSurveyAccess, surveys.del); app.get('/api/slugs/:slug', surveys.getSlug); // Survey users app.get('/api/surveys/:surveyId/users', session, users.ensureAuthenticated, users.ensureSurveyAccess, surveys.users); app.put('/api/surveys/:surveyId/users/:email', session, users.ensureAuthenticated, users.ensureSurveyAccess, surveys.addUser); - app.del('/api/surveys/:surveyId/users/:email', session, users.ensureAuthenticated, users.ensureSurveyAccess, surveys.removeUser); + app.delete('/api/surveys/:surveyId/users/:email', session, users.ensureAuthenticated, users.ensureSurveyAccess, surveys.removeUser); // Stats app.get('/api/surveys/:surveyId/stats', stats.stats); @@ -53,8 +59,7 @@ exports.setup = function setup(app) { app.patch('/api/surveys/:surveyId/responses/:responseId', session, users.ensureAuthenticated, users.ensureSurveyAccess, responses.patch); app.put('/api/surveys/:surveyId/responses/:responseId', session, users.ensureAuthenticated, users.ensureSurveyAccess, responses.put); app.post('/api/surveys/:surveyId/responses', responses.post); - app.del('/api/surveys/:surveyId/responses/:responseId', session, users.ensureAuthenticated, users.ensureSurveyAccess, responses.del); - app.post('/api/surveys/:surveyId/responses', responses.post); + app.delete('/api/surveys/:surveyId/responses/:responseId', session, users.ensureAuthenticated, users.ensureSurveyAccess, responses.del); app.get('/api/surveys/:surveyId/responses.csv', session, responses.handleCSV); app.get('/api/surveys/:surveyId/responses.kml', session, responses.handleKML); app.get('/api/surveys/:surveyId/responses.zip', session, responses.handleShapefile); diff --git a/lib/server.js b/lib/server.js index e4dddae..b1c9132 100644 --- a/lib/server.js +++ b/lib/server.js @@ -9,11 +9,17 @@ var http = require('http'); var _ = require('lodash'); var async = require('async'); +var bodyParser = require('body-parser'); +var cookieParser = require('cookie-parser'); +var compression = require('compression'); var express = require('express'); var logfmt = require('logfmt'); -var MongoStore = require('connect-mongo')(express); -var s3 = require('connect-s3'); +var methodOverride = require('method-override'); var passport = require('passport'); +var session = require('express-session'); +var MongoStore = require('connect-mongo')(session); + +var s3 = require('connect-s3'); var mongo = require('./mongo'); var settings = require('../settings'); @@ -51,12 +57,12 @@ function createApp(db) { app.set('json spaces', 0); // Allow Heroku to handle SSL for us - app.enable('trust proxy'); + app.set('trust proxy', true); // Allows clients to simulate DELETE and PUT // (some clients don't support those verbs) // http://stackoverflow.com/questions/8378338/what-does-connect-js-methodoverride-do - app.use(express.methodOverride()); + app.use(methodOverride()); // Skip parsing the body for requests that we'll proxy to the tileserver app.use('/tiles', util.rawParser); @@ -77,17 +83,15 @@ function createApp(db) { } }); - app.use(express.json()); - app.use(express.urlencoded()); - - // Keep extensions, so we can properly guess MIME types later. - // TODO: We should get the MIME type from the upload headers. - app.use(express.multipart({ - keepExtensions: true + app.use(bodyParser.json()); + app.use(bodyParser.urlencoded({ + extended: true })); // Let's compress everything! - app.use(express.compress()); + app.use(compression({ + threshold: 10 // bytes + })); // Add common headers app.use(function(req, res, next) { @@ -102,15 +106,18 @@ function createApp(db) { function setupAuth(app, db) { // Authentication-related middleware - app.use(express.cookieParser()); + app.use(cookieParser()); // TODO: test using a spy to make we only interact with the database when we // intend to use the session information. - var expressSession = express.session({ + // + var expressSession = session({ + resave: false, // don't resave session of not modified + saveUninitialized: false, // don't create session until something stored secret: settings.secret, store: new MongoStore({ mongooseConnection: db, - maxAge: 300000 + touchAfter: 24 * 3600 // time period in seconds // collection: 'sessions' [default] }) }); @@ -119,12 +126,12 @@ function setupAuth(app, db) { // persistent login sessions (recommended). var passportMiddleware = passport.initialize(); var passportSession = passport.session(); - var session = [expressSession, passportMiddleware, passportSession]; + var sessionMiddleware = [expressSession, passportMiddleware, passportSession]; // We don't want to apply the session middleware to every route, since that // can add unnecessary overhead. Instead, we save a reference to the // middlware for use in the routes module. - app.set('session-middleware', session); + app.set('session-middleware', sessionMiddleware); } function setupRoutes(app) { @@ -185,8 +192,8 @@ function run(cb) { db.removeAllListeners(); } - console.log(_.template('info at=server event=starting port=${port}', { - port: settings.port, + console.log(_.template('info at=server event=starting port=${port}')({ + port: settings.port })); if (!cb) { diff --git a/newrelic.js b/newrelic.js index 032af23..7236128 100644 --- a/newrelic.js +++ b/newrelic.js @@ -13,7 +13,7 @@ exports.config = { * Your New Relic license key. */ license_key : process.env.NEW_RELIC_LICENSE_KEY, - logging : { + logging : { /** * Level at which to log. 'trace' is most useful to New Relic when diagnosing * issues with the agent, 'info' and higher will impose the least overhead on diff --git a/package.json b/package.json index 4f7cf78..1660f63 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "survey-api", - "version": "0.1.0", + "version": "0.2.0", "private": true, "engines": { - "node": "0.10.x", + "node": "4.2.x", "npm": "1.4.x" }, "scripts": { @@ -11,29 +11,38 @@ }, "dependencies": { "async": ">=0.2", - "bcrypt": ">= 0.5", + "bcrypt": ">= 0.8.5", "bluebird": "^2.3.2", - "connect-mongo": "~0.6.0", + "body-parser": "~1.14.1", + "compression": "^1.6.1", + "connect-busboy": "0.0.2", + "connect-mongo": "~1.1.0", + "connect-multiparty": "^2.0.0", "connect-s3": "~0.3.0", + "cookie-parser": "1.4.x", "cuid": "^1.2.4", "ejs": "~0.8.3", - "express": "3.0.x", + "express": "4.x", + "express-session": "^1.11.3", "knox": "~0.9.1", - "lodash": "~1.0.1", + "lodash": "~3.10", "logfmt": "^1.2.0", "lru-cache": "2.2.2", + "method-override": "2.3.x", "moment": "2.8.x", "moment-range": "1.0.x", "mongoose": "3.8.16", + "multer": "^1.1.0", + "multer-s3": "git@github.com:badunk/multer-s3.git", "newrelic": "^1.15.1", - "nodemailer": "0.7.1", "node-resque": "^0.8.4", "node-uuid": "1.3.3", - "passport": "0.1.15", - "passport-local": "0.1.6", - "pg": "~2.8.2", + "nodemailer": "0.7.1", + "passport": "0.3.2", + "passport-local": "1.0.0", + "pg": "^4.5.1", "redis": "^0.10.1", - "request": "2.21.x", + "request": "~2.67.0", "slugs": "0.1.2", "turf": "^2.0.0" }, @@ -42,6 +51,6 @@ "mocha": "1.21.x", "mongodb": ">= 1.2.8", "should": "~3.3", - "http-proxy": "~0.10.2" + "http-proxy": "~1.13" } } diff --git a/test/data/fixtures.js b/test/data/fixtures.js index c57ca99..d32edb9 100644 --- a/test/data/fixtures.js +++ b/test/data/fixtures.js @@ -2,6 +2,10 @@ /*globals suite, test, setup, suiteSetup, suiteTeardown, done, teardown */ 'use strict'; +// Ignore an invalid self-signed cert +// http://stackoverflow.com/questions/10888610/ignore-invalid-self-signed-ssl-certificate-in-node-js-with-https-request +process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; + // Libraries var request = require('request'); var makeSlug = require('slugs'); @@ -70,7 +74,7 @@ fixtures.users = [{ fixtures.clearUsers = function(callback) { User.remove({}, function(error, result){ if(error) { - console.log(error); + console.log("Error clearing users", error); } callback(); }); @@ -100,7 +104,7 @@ fixtures.addUser = function addUser(name, done) { }, function (error, response, user) { if (error) { return done(error); } if (response.statusCode !== 200) { - return done(new Error('Received an incorrect status from the API')); + return done(new Error('Received an incorrect status from the API: ' + response.statusCode)); } user.password = data.password; done(null, jar, user._id, user); @@ -119,11 +123,11 @@ fixtures.setupUser = function(callback) { var idA; fixtures.clearUsers(function(){ - // Create one user request.post({ url: USER_URL, - json: fixtures.users[0], + body: fixtures.users[0], + json: true, jar: jarA }, function (error, response, body) { @@ -131,6 +135,10 @@ fixtures.setupUser = function(callback) { callback(error, null); } + if (response.statusCode !== 200) { + callback(new Error("Error creating user: " + response.statusCode)); + } + idA = body._id; // Create a second user diff --git a/test/features.js b/test/features.js index d8914bb..56d98a1 100644 --- a/test/features.js +++ b/test/features.js @@ -92,6 +92,7 @@ suite('Features', function () { test('Get a parcel by id', function (done) { getJSON('/features/detroit-parcels/10004927.', function (error, parsed) { + console.log("Got parsed parcels", parsed); checkParcels(parsed); parsed.features.length.should.equal(1); parsed.features.forEach(function (feature) { diff --git a/test/lib/router.js b/test/lib/router.js index a83b493..212307b 100644 --- a/test/lib/router.js +++ b/test/lib/router.js @@ -12,9 +12,10 @@ var server = require('../../lib/server'); var key = fs.readFileSync(__dirname + '/../data/test-key.pem', 'utf8'); var cert = fs.readFileSync(__dirname + '/../data/test-cert.pem', 'utf8'); -var proxy = new httpProxy.HttpProxy({ +var proxy = httpProxy.createProxyServer({ + xfwd: true, target: { - host: 'localhost', + host: 'localhost', port: settings.port } }); @@ -32,6 +33,7 @@ console.log = function (str) { }; var router; + module.exports = { run: function start(done) { // Start the real server. @@ -43,7 +45,7 @@ module.exports = { key: key, cert: cert }, function (req, res) { - proxy.proxyRequest(req, res); + proxy.web(req, res); }).listen(settings.testSecurePort, function (error) { console.log('info at=test_https_router event=listening port=' + settings.testSecurePort); done(error); diff --git a/test/stats.js b/test/stats.js index dec6136..ef37600 100644 --- a/test/stats.js +++ b/test/stats.js @@ -236,7 +236,7 @@ suite('Stats', function () { url: BASEURL + '/surveys/' + this.surveyId + '/responses/' + this.responseId, - jar: this.userAJar, + jar: this.userAJar }); }).spread(function (response, body) { response.statusCode.should.equal(204); @@ -644,7 +644,7 @@ suite('Stats', function () { // 24 hours of data at 6-minute resolution (but we return sparse data) data.stats.activity.length.should.below((now - start) / resolution + 1); - var entries = _(this.responses).pluck('entries').flatten(); + var entries = _(this.responses).map('entries').flatten(); var trueCount = entries.filter(function (entry) { return entry.created > start; }).value().length; @@ -710,7 +710,7 @@ suite('Stats', function () { point[0] < east && point[1] > south && point[1] < north); - }).pluck('entries') + }).map('entries') .flatten() .filter(function (entry) { return entry.created > start && entry.created <= now; @@ -765,7 +765,7 @@ suite('Stats', function () { // 24 hours of data at 6-minute resolution (but we return sparse data) data.stats.activity.length.should.below((now - start) / resolution + 1); - var entries = _(this.responses).pluck('entries').flatten(); + var entries = _(this.responses).map('entries').flatten(); var trueCount = entries.filter(function (entry) { return entry.created > start; }).value().length; @@ -811,7 +811,7 @@ suite('Stats', function () { data.stats.resolution.should.equal(resolution); var entries = _(this.responses) - .pluck('entries') + .map('entries') .flatten() .filter(function (entry) { return (entry.created > start && @@ -1092,6 +1092,7 @@ suite('Stats', function () { data.stats.should.have.property('activity'); var entries = _(this.responses).pluck('entries').flatten(); + var trueCount = entries.size(); data.stats.total.should.equal(trueCount); @@ -1107,6 +1108,7 @@ suite('Stats', function () { return year === trueYear && month === trueMonth; }).value().length; + item.count.should.equal(trueCount); }); @@ -1133,7 +1135,7 @@ suite('Stats', function () { data.stats.should.have.property('activity'); var entries = _(this.responses).pluck('entries').flatten(); - entries = entries.where({ responses: { 'evenodd': 'even'}}); + entries = entries.filter({ responses: { 'evenodd': 'even'}}); var trueCount = entries.size(); data.stats.total.should.equal(trueCount); diff --git a/test/users.js b/test/users.js index e059913..1ef61ef 100644 --- a/test/users.js +++ b/test/users.js @@ -123,9 +123,7 @@ suite('Users -', function () { fixtures.clearUsers(function (error) { should.not.exist(error); User.create(generateUser(), function(error, userOne) { - // console.log("First user ", userOne); User.create(generateUser(), function(error, userTwo){ - // console.log("Second user ", userTwo); should.exist(error); done(); }); @@ -271,6 +269,7 @@ suite('Users -', function () { async.series([ // Clear users fixtures.clearUsers, + // Add a new user function (next) { fixtures.addUser('API Login Tester', function (error, newJar, newId, newUser) { @@ -279,18 +278,28 @@ suite('Users -', function () { next(); }); }, + // Logout, just to be sure function (next) { request.get({ url: BASE_LOGOUT_URL }, next); }, + // Try logging in function (next) { // Then, let's log in. - request.post({url: LOGIN_URL, json: user}, function (error, response, body) { + var jar = request.jar(); + request.post({ + url: LOGIN_URL, + json: user, + jar: jar + }, function (error, response, body) { should.not.exist(error); response.statusCode.should.equal(302); - request.get({url: HTTP_USER_URL}, function (error, response, body){ + request.get({ + url: HTTP_USER_URL, + jar: jar + }, function (error, response, body){ should.not.exist(error); response.statusCode.should.equal(200); response.should.be.json; @@ -341,11 +350,19 @@ suite('Users -', function () { user.email = user.email.toUpperCase(); // Try logging in - request.post({url: LOGIN_URL, json: user}, function (error, response, body) { + var jar = request.jar(); + request.post({ + url: LOGIN_URL, + json: user, + jar: jar + }, function (error, response, body) { should.not.exist(error); response.statusCode.should.equal(302); - request.get({url: HTTP_USER_URL}, function (error, response, body){ + request.get({ + url: HTTP_USER_URL, + jar: jar + }, function (error, response, body){ should.not.exist(error); response.statusCode.should.equal(200); response.should.be.json; From 556bac43a42fdd79b04e2c972cd05e719b52f4cc Mon Sep 17 00:00:00 2001 From: Matt Hampel Date: Thu, 17 Mar 2016 18:31:17 -0400 Subject: [PATCH 2/3] Specify multer-s3 version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1660f63..9a76f63 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "moment-range": "1.0.x", "mongoose": "3.8.16", "multer": "^1.1.0", - "multer-s3": "git@github.com:badunk/multer-s3.git", + "multer-s3": "^1.4.1", "newrelic": "^1.15.1", "node-resque": "^0.8.4", "node-uuid": "1.3.3", From a5b00c24dae2dd2809b9d5012abc98d29abdad2e Mon Sep 17 00:00:00 2001 From: Matt Hampel Date: Thu, 17 Mar 2016 18:38:41 -0400 Subject: [PATCH 3/3] Use newer compiler on Travis --- .travis.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9c17e0b..886272d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,19 +1,31 @@ language: node_js node_js: - - "0.10" + - "4" +compiler: clang-3.6 services: - mongodb - redis-server addons: + apt: + sources: + - llvm-toolchain-precise-3.6 + - ubuntu-toolchain-r-test + packages: + - clang-3.6 + - g++-4.8 postgresql: "9.3" +sudo: true + before_script: - openssl req -x509 -nodes -days 730 -newkey rsa:1024 -subj "/C=US/ST=MI/L=Detroit/O=Dis/CN=www.example.com" -keyout test/data/test-key.pem -out test/data/test-cert.pem - psql -U postgres -c "create extension postgis" - psql -c 'create database travis_ci_test;' -U postgres - psql -d travis_ci_test -a -f test/data/features-db.sql - psql -d travis_ci_test -a -f test/data/schema.sql + env: global: + - CXX=clang-3.6 - secure: "TFbmH2rbf/t/1nMkpS09QcVWbI5AdWqQSDRmuwhIent88AxfdlY4mgtviDdtDZYDN4l/zwBf442ocmiImrCi7ZJsvLBpPtqHs0mDGzOCilVPxIM71Si3BSY3QWjPTG1WSSEHpsFUXz8pWr2hlAGFi6maUEo/KcDHf8xrcJuZHq8=" - secure: "AV7+wVMYrm4f6vu5NmFts8tARo7eDFDI8E3Rwb2swIlLJnAlc/YUcxZ7Mbiav+Ops7zcWNwgl5oYlNXwPj+AVdrQ9WnxVE+FgKDzkTC/4EDl2h/tqVEOkwVJ4D5EFt5NsF47GvnuTo3v7dTfpXUwXLPqIfgYDQIs3hx43f/kHFM=" - secure: "ETzchDuzsHT1PTpWdFJjlz/xydsZQkFPVcY6vcL62T6VZXSxZ2Ev15RBzpl5dM1Kr/8VlLH8MZiW33uXrnryAjtgsVDZmbF2ZarpRxk8bKTLf4UuqYh0aqnmzMivkGkMijgCVM82UgjNUYD0L/+B0Y+mEmXKiCHqBDLXw3oB5uQ=" @@ -33,4 +45,3 @@ env: - TILESERVER_BASE=http://example.com - TEST_SECURE_PORT=8888 - REDIS_URL=http://localhost:6379 -sudo: false