diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..da23d0d --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# Deployed apps should consider commenting this line out: +# see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git +node_modules diff --git a/app.js b/app.js new file mode 100644 index 0000000..ccae063 --- /dev/null +++ b/app.js @@ -0,0 +1,42 @@ +const express = require('express'), + path = require('path'), + favicon = require('serve-favicon'), + logger = require('morgan'), + cookieParser = require('cookie-parser'), + bodyParser = require('body-parser'), + routes = require('./routes/index'); + +var app = express(); + +// view engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'jade'); + +// uncomment after placing your favicon in /public +//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); +app.use(logger('dev')); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: false })); +app.use(cookieParser()); + +app.use('/', routes); + +// catch 404 and forward to error handler +app.use(function(req, res, next) { + var err = new Error('Not Found'); + err.status = 404; + next(err); +}); + +// error handlers +if (app.get('env') === 'development') { + app.use(function(err, req, res, next) { + res.status(err.status || 500); + }); +} + +app.use(function(err, req, res, next) { + res.status(err.status || 500); +}); + +module.exports = app; diff --git a/bin/start b/bin/start new file mode 100644 index 0000000..e0447a2 --- /dev/null +++ b/bin/start @@ -0,0 +1,15 @@ +#!/usr/bin/env node +'use strict'; + +let forever = require('forever-monitor'); +var child = new (forever.Monitor)('./bin/www'); + +child.on('restart', function() { + console.log('Forever restarting script for ' + child.times + ' time'); +}); + +child.on('exit:code', function(code) { + console.log('Forever detected script exited with code ' + code); +}); + +child.start(); diff --git a/bin/www b/bin/www new file mode 100755 index 0000000..6a61b0e --- /dev/null +++ b/bin/www @@ -0,0 +1,90 @@ +#!/usr/bin/env node + +/** + * Module dependencies. + */ + +var app = require('../app'); +var debug = require('debug')('FamilyFriendly:server'); +var http = require('http'); + +/** + * Get port from environment and store in Express. + */ + +var port = normalizePort(process.env.PORT || '3000'); +app.set('port', port); + +/** + * Create HTTP server. + */ + +var server = http.createServer(app); + +/** + * Listen on provided port, on all network interfaces. + */ + +server.listen(port); +server.on('error', onError); +server.on('listening', onListening); + +/** + * Normalize a port into a number, string, or false. + */ + +function normalizePort(val) { + var port = parseInt(val, 10); + + if (isNaN(port)) { + // named pipe + return val; + } + + if (port >= 0) { + // port number + return port; + } + + return false; +} + +/** + * Event listener for HTTP server "error" event. + */ + +function onError(error) { + if (error.syscall !== 'listen') { + throw error; + } + + var bind = typeof port === 'string' + ? 'Pipe ' + port + : 'Port ' + port; + + // handle specific listen errors with friendly messages + switch (error.code) { + case 'EACCES': + console.error(bind + ' requires elevated privileges'); + process.exit(1); + break; + case 'EADDRINUSE': + console.error(bind + ' is already in use'); + process.exit(1); + break; + default: + throw error; + } +} + +/** + * Event listener for HTTP server "listening" event. + */ + +function onListening() { + var addr = server.address(); + var bind = typeof addr === 'string' + ? 'pipe ' + addr + : 'port ' + addr.port; + debug('Listening on ' + bind); +} diff --git a/config/index.js b/config/index.js new file mode 100644 index 0000000..db92be6 --- /dev/null +++ b/config/index.js @@ -0,0 +1,10 @@ +var nconf = require('nconf'); + +var environment = process.env.NODE_ENV || 'local'; + +nconf + .argv() + .env() + .file({ file: './config/' + environment + '.json' }); + +module.exports = nconf; diff --git a/config/local.json b/config/local.json new file mode 100644 index 0000000..0db3279 --- /dev/null +++ b/config/local.json @@ -0,0 +1,3 @@ +{ + +} diff --git a/helpers/moviedb-helper.js b/helpers/moviedb-helper.js new file mode 100644 index 0000000..085e0eb --- /dev/null +++ b/helpers/moviedb-helper.js @@ -0,0 +1,42 @@ +'use strict'; +const tmdb = require('moviedb')('755ceee170d06dc11c2bd9f646014c97'), + log = require('../logger'); + +function getMoviesFromSearch(movieName, req) { + + return new Promise((resolve, reject) => { + + if(!movieName) + reject(); + + try { + tmdb.searchMovie({query: movieName}, function(error, response) { + resolve(response); + }); + } catch (err) { + log.error({err: err}, 'Problem querying the search endpoint'); + reject(new Error('Problem querying the search endpoint')); + } + }); +} + +function getMovieById(movieId, req) { + return new Promise((resolve, reject) => { + if(!movieId) + reject(); + + try { + tmdb.movieInfo({id: movieId}, function(error, response) { + resolve(response); + }); + } catch (err) { + log.error({err: err}, 'Problem getting movie information by id'); + reject(new Error('Problem getting movie information by id')); + } + }); +} + +module.exports = { + getMoviesFromSearch, + getMovieById +} diff --git a/logger.js b/logger.js new file mode 100644 index 0000000..f4274a8 --- /dev/null +++ b/logger.js @@ -0,0 +1,63 @@ +var bunyan = require('bunyan'), + package = require('./package.json'), + path = require('path'); + +//get from package file +var applicationName = package.name; +var logPath = path.join(__dirname,'./logs/'); + +var logPaths = { + fatal: logPath + applicationName + '-fatal.log', + error: logPath + applicationName + '-error.log', + warn: logPath + applicationName + '-warn.log', + info: logPath + applicationName + '-info.log' +}; + +var log = bunyan.createLogger({ + name: applicationName, + serializers: bunyan.stdSerializers, + streams: [ + { + stream: process.stdout + }, + { + //"fatal" (60): The service/app is going to stop or become unusable now. + level: 'fatal', + path: logPaths.fatal, + period: '1d', // daily rotation + count: 5, // keep 5 back copies + type: 'rotating-file' + }, + { + //"error" (50): Fatal for a particular request, but the service/app continues servicing other requests + level: 'error', + path: logPaths.error, + period: '1d', // daily rotation + count: 5, // keep 5 back copies + type: 'rotating-file' + }, + { + //"warn" (40): A note on something that should probably be looked at by an operator eventually. + level : 'warn', + path : logPaths.warn, + period: '1d', // daily rotation + count: 5, // keep 5 back copies + type: 'rotating-file' + }, + { + //"info" (30): Detail on regular operation. + level: 'info', + path: logPaths.info, + period: '1d', // daily rotation + count: 5, // keep 5 back copies + type: 'rotating-file' + } + ] +}); + +module.exports = log; +module.exports.stream = { + write: function(message, encoding){ + log.info(message); + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..35cd447 --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "FamilyFriendly", + "version": "0.0.1", + "private": true, + "scripts": { + "start": "node bin/start >> logs/forever.log", + "test": "node_modules/.bin/istanbul test ./node_modules/mocha/bin/_mocha -- specs -R spec --recursive --timeout 1000" + }, + "dependencies": { + "body-parser": "~1.13.2", + "bunyan": "^1.6.0", + "cookie-parser": "~1.3.5", + "debug": "~2.2.0", + "express": "~4.9.8", + "forever-monitor": "^1.7.0", + "istanbul": "^0.4.2", + "jade": "^1.11.0", + "mocha": "^2.4.5", + "morgan": "^1.6.1", + "moviedb": "^0.2.2", + "nconf": "^0.8.4", + "nock": "^7.0.2", + "path": "^0.12.7", + "request": "^2.69.0", + "serve-favicon": "~2.3.0", + "should": "^8.2.2", + "supertest": "^1.2.0" + } +} diff --git a/routes/index.js b/routes/index.js new file mode 100644 index 0000000..f2cd835 --- /dev/null +++ b/routes/index.js @@ -0,0 +1,36 @@ +const express = require('express'), + router = express.Router(), + log = require('../logger'), + movieDbService = require('../helpers/moviedb-helper'); + +router.get('/', function(req, res) { + res.sendStatus(200); +}); + +router.get('/search/:moviename', function(req, res) { + + movieDbService.getMoviesFromSearch(req.params.moviename, req) + .then(movies => { + res.json(movies); + }) + .catch(err => { + log.error({metaData: req.metaData, err: err}, 'An error occurred trying to retrieve movies'); + }); +}); + +router.get('/movie/:movieid', function(req, res) { + movieDbService.getMovieById(req.params.movieid, req) + .then(movie => { + res.json(movie); + }) + .catch(err => { + log.error({metaData: req.metaData, err: err}, 'An error occured trying to retrieve the movie information by its id'); + }); +}); + +// TODO: implement versioning and output +router.get('/api/:version', function(req, res) { + res.send(req.params.version); +}); + +module.exports = router; diff --git a/specs/global-spec.js b/specs/global-spec.js new file mode 100644 index 0000000..ee7d176 --- /dev/null +++ b/specs/global-spec.js @@ -0,0 +1,6 @@ +require('should'); +const nock = require('nock'); + +beforeEach(function() { + nock.cleanAll(); +}); diff --git a/specs/helpers/server.js b/specs/helpers/server.js new file mode 100644 index 0000000..1d12bb0 --- /dev/null +++ b/specs/helpers/server.js @@ -0,0 +1,28 @@ +'use strict'; +const app = require('../../app.js'), + port = 3000, + host = 'http://localhost'; + +var runningServer; + +module.exports = { + url: host + ':' + port, + start: function() { + return new Promise((resolve, reject) => { + return app + .then(() => { + runningServer = app.listen(port, function(){ + console.log('test server running on: ' + port); + resolve(); + }); + }) + .catch(() => { + console.log('Error setting up the server', err); + reject(); + }); + }); + }, + stop: function() { + runningServer.close(); + } +} diff --git a/specs/unit/services/movieDb-spec.js b/specs/unit/services/movieDb-spec.js new file mode 100644 index 0000000..05c8869 --- /dev/null +++ b/specs/unit/services/movieDb-spec.js @@ -0,0 +1,64 @@ +'use strict'; +const should = require('should'), + nock = require('nock'), + server = require('../../helpers/server'), + movieDbHelper = require('../../../helpers/moviedb-helper'); + +describe('movieDb specs', function() { + this.timeout(5000); + + describe('Search the movie called Transformers and get an object back of some movies from it', function() { + it('should resolve when receiving a list of movies and send back a 200 status', function(done) { + + nock(server.url) + .get('/search/Transformers') + .reply(200, [{ + original_title: 'Transformers', + title: 'Transformers', + id: 1858, + release_date: '2007-06-27' + }]); + + var mockRequest = {}; + + movieDbHelper.getMoviesFromSearch('Transformers', mockRequest) + .then(movies => { + movies.results[0].original_title.should.eql('Transformers'); + movies.results[0].title.should.eql('Transformers'); + movies.results[0].id.should.eql(1858); + movies.results[0].release_date.should.eql('2007-06-27'); + + setTimeout(done, 5000); + done(); + }) + .catch(done); + }); + }) + + describe('Get movie information about Transformers using the ID of the movie', function() { + it('should resolve when receiving movie information about Transformers and send back a 200 status', function(done) { + nock(server.url) + .get('/movie/1858') + .reply(200, [{ + original_title: 'Transformers', + imdb_id: 'tt0418279', + adult: false, + status: 'Released' + }]); + + var mockRequest = {}; + + movieDbHelper.getMovieById(1858, mockRequest) + .then(movie => { + movie.original_title.should.eql('Transformers'); + movie.imdb_id.should.eql('tt0418279'); + movie.adult.should.eql(false); + movie.status.should.eql('Released'); + + setTimeout(done, 5000); + done(); + }) + .catch(done); + }); + }); +});