donal
2018-03-26 6ef0cc963d40c64fb56ba0545c05788535a45895
WIP - first pass at modernising the api layer
31 files added
1713 ■■■■■ changed files
.buildignore 1 ●●●● patch | view | raw | blame | history
.editorconfig 21 ●●●●● patch | view | raw | blame | history
.gitattributes 1 ●●●● patch | view | raw | blame | history
.gitignore 10 ●●●●● patch | view | raw | blame | history
Dockerfile 12 ●●●●● patch | view | raw | blame | history
Gruntfile.js 363 ●●●●● patch | view | raw | blame | history
contributors.txt 2 ●●●●● patch | view | raw | blame | history
package.json 74 ●●●●● patch | view | raw | blame | history
server/.jshintrc 23 ●●●●● patch | view | raw | blame | history
server/api/todo/index.js 16 ●●●●● patch | view | raw | blame | history
server/api/todo/todo.controller.js 72 ●●●●● patch | view | raw | blame | history
server/api/todo/todo.model.js 11 ●●●●● patch | view | raw | blame | history
server/api/todo/todo.spec.js 182 ●●●●● patch | view | raw | blame | history
server/app.js 44 ●●●●● patch | view | raw | blame | history
server/components/errors/index.js 20 ●●●●● patch | view | raw | blame | history
server/config/environment/ci.js 15 ●●●●● patch | view | raw | blame | history
server/config/environment/development.js 14 ●●●●● patch | view | raw | blame | history
server/config/environment/index.js 44 ●●●●● patch | view | raw | blame | history
server/config/environment/production.js 24 ●●●●● patch | view | raw | blame | history
server/config/environment/si.js 12 ●●●●● patch | view | raw | blame | history
server/config/environment/test.js 12 ●●●●● patch | view | raw | blame | history
server/config/express.js 44 ●●●●● patch | view | raw | blame | history
server/config/local.env.sample.js 14 ●●●●● patch | view | raw | blame | history
server/config/seed.js 18 ●●●●● patch | view | raw | blame | history
server/mocks/mock-routes-config.json 87 ●●●●● patch | view | raw | blame | history
server/mocks/mock-routes.js 61 ●●●●● patch | view | raw | blame | history
server/mocks/mock-routes.spec.js 182 ●●●●● patch | view | raw | blame | history
server/routes.js 24 ●●●●● patch | view | raw | blame | history
server/views/404.html 157 ●●●●● patch | view | raw | blame | history
tasks/blanket.js 13 ●●●●● patch | view | raw | blame | history
tasks/perf-test.js 140 ●●●●● patch | view | raw | blame | history
.buildignore
New file
@@ -0,0 +1 @@
*.coffee
.editorconfig
New file
@@ -0,0 +1,21 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
# Change these settings to your own preference
indent_style = space
indent_size = 2
# We recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
.gitattributes
New file
@@ -0,0 +1 @@
* text eol=lf
.gitignore
New file
@@ -0,0 +1,10 @@
node_modules
public
.tmp
.idea
client/bower_components
dist
/server/config/local.env.js
npm-debug.log
reports
*.log
Dockerfile
New file
@@ -0,0 +1,12 @@
FROM node:4.4.7-slim
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
COPY package.json /usr/src/app/
RUN npm install --production
COPY . /usr/src/app
EXPOSE 9000
CMD [ "npm", "start" ]
Gruntfile.js
New file
@@ -0,0 +1,363 @@
// Generated on 2016-08-09 using generator-angular-fullstack 2.1.1
'use strict';
module.exports = function (grunt) {
  var localConfig;
  try {
    localConfig = require('./server/config/local.env');
  } catch(e) {
    localConfig = {};
  }
  // Load grunt tasks automatically, when needed
  require('jit-grunt')(grunt, {
    express: 'grunt-express-server'
  });
  // Time how long tasks take. Can help when optimizing build times
  require('time-grunt')(grunt);
  grunt.task.loadTasks('./tasks');
  // Define the configuration for all the tasks
  grunt.initConfig({
    // Project settings
    pkg: grunt.file.readJSON('package.json'),
    express: {
      options: {
        port: process.env.PORT || 9000
      },
      dev: {
        options: {
          script: 'server/app.js',
          debug: true
        }
      },
      prod: {
        options: {
          script: 'dist/server/app.js'
        }
      }
    },
    // Make sure code styles are up to par and there are no obvious mistakes
    jshint: {
      options: {
        reporter: require('jshint-stylish')
      },
      server: {
        options: {
          jshintrc: 'server/.jshintrc'
        },
        src: [
          'server/**/*.js',
          '!server/**/*.spec.js'
        ]
      },
      serverTest: {
        options: {
          jshintrc: 'server/.jshintrc'
        },
        src: ['server/**/*.spec.js']
      },
      ci_server: {
        options: {
          jshintrc: 'server/.jshintrc',
          reporter: require('jshint-jenkins-checkstyle-reporter'),
          reporterOutput: 'reports/server/linting/jshint-server.xml'
        },
        src: [
          'server/**/*.js',
          'server/**/*.spec.js'
        ]
      },
      bm_client: {
        options: {
          jshintrc: 'client/.jshintrc',
          reporter: require('jshint-junit-reporter'),
          reporterOutput: 'reports/client/linting/jshint-junit-client.xml'
        },
        src: [
          '<%= yeoman.client %>/app/**/*.js',
          '<%= yeoman.client %>/app/**/*.spec.js',
          '<%= yeoman.client %>/app/**/*.mock.js'
        ]
      },
      bm_server: {
        options: {
          jshintrc: 'server/.jshintrc',
          reporter: require('jshint-junit-reporter'),
          reporterOutput: 'reports/server/linting/jshint-junit-server.xml'
        },
        src: [
          'server/**/*.js',
          'server/**/*.spec.js'
        ]
      }
    },
    // Empties folders to start fresh
    clean: {
      server: '.tmp',
      karmareports: 'reports/client/karma/**',
      mochareports: 'reports/server/mocha/**',
      lint: 'reports/{server,client}/jshint/**',
      coverage: 'reports/{server,client}/coverage/**'
    },
    // Debugging with node inspector
    'node-inspector': {
      custom: {
        options: {
          'web-host': 'localhost'
        }
      }
    },
    // Use nodemon to run server in debug mode with an initial breakpoint
    nodemon: {
      debug: {
        script: 'server/app.js',
        options: {
          nodeArgs: ['--debug-brk'],
          env: {
            PORT: process.env.PORT || 9000
          },
          callback: function (nodemon) {
            nodemon.on('log', function (event) {
              console.log(event.colour);
            });
            // opens browser on initial server start
            nodemon.on('config:update', function () {
              setTimeout(function () {
                require('open')('http://localhost:8080/debug?port=5858');
              }, 500);
            });
          }
        }
      }
    },
    // Run some tasks in parallel to speed up the build process
    concurrent: {
      server: [
      ],
      test: [
      ],
      debug: {
        tasks: [
          'nodemon',
          'node-inspector'
        ],
        options: {
          logConcurrentOutput: true
        }
      },
      dist: [
      ]
    },
    mochaTest: {
      terminal: {
        options: {
          reporter: 'spec',
          require: 'tasks/blanket'
        },
        src: ['server/**/*.spec.js']
      },
      junit: {
        options: {
          reporter: 'mocha-junit-reporter',
          // require: require("blanket"),
          require: 'tasks/blanket'
        },
        src: ['server/**/*.spec.js']
      },
      html: {
        options: {
          // reporters are  ['html-cov', 'json-cov', 'travis-cov', 'mocha-lcov-reporter', 'mocha-cobertura-reporter'],
          reporter: 'html-cov',
          quiet: true,
          captureFile: 'reports/server/coverage/cobertura-report.html'
        },
        src: ['server/**/*.spec.js']
      },
      cobertura: {
        options: {
          reporter: 'mocha-cobertura-reporter',
          // use the quiet flag to suppress the mocha console output
          quiet: true,
          // specify a destination file to capture the mocha
          // output (the quiet option does not suppress this)
          captureFile: 'reports/server/coverage/cobertura-report.xml'
        },
        src: ['server/**/*.spec.js']
      },
      travis: {
        options: {
          reporter: 'travis-cov',
          quiet: false
        },
        src: ['server/**/*.spec.js']
      }
    },
    env: {
      test: {
        NODE_ENV: 'test'
      },
      dev: {
        NODE_ENV: 'development'
      },
      ci: {
        NODE_ENV: 'ci'
      },
      si: {
        NODE_ENV: 'si'
      },
      prod: {
        NODE_ENV: 'production'
      },
      jenkins: {
        MOCHA_FILE: 'reports/server/mocha/test-results.xml'
      },
      all: localConfig
    },
  });
  // Used for delaying livereload until after server has restarted
  grunt.registerTask('wait', function () {
    grunt.log.ok('Waiting for server reload...');
    var done = this.async();
    setTimeout(function () {
      grunt.log.writeln('Done waiting!');
      done();
    }, 1500);
  });
  grunt.registerTask('express-keepalive', 'Keep grunt running', function() {
    this.async();
  });
  grunt.registerTask('build-image', 'Build the image', function(imageId) {
    var shell = require("shelljs");
    grunt.log.ok('BUILDING IMAGE');
    if (!imageId) {
      grunt.fail.warn('must supply an imageId to build');
    }
    var rc = shell.exec('docker build -t todolist:' + imageId + ' -f ./dist/Dockerfile ./dist').code;
    if (rc > 0){
      grunt.fail.warn("DOCKER FAILURE")
    }
  });
  grunt.registerTask('deploy', 'deploy the node js app to a docker container and start it in the correct mode', function(target_env, build_tag) {
    grunt.log.ok('this task must run on a host that has the Docker Daemon running on it');
    var ports = {
      ci: '9001',
      si: '9002',
      production: '80'
    };
    if (target_env === undefined || build_tag === undefined){
      grunt.fail.warn('Required param not set - use grunt deploy\:\<target\>\:\<tag\>');
    } else {
      var shell = require("shelljs");
      grunt.log.ok('STOPPING AND REMOVING EXISTING CONTAINERS');
      shell.exec('docker stop todolist-'+ target_env + ' && docker rm todolist-'+ target_env);
      grunt.log.ok('DEPLOYING ' + target_env + ' CONTAINER');
      if (target_env === 'ci'){
        var rc = shell.exec('docker run -t -d --name todolist-' + target_env + ' -p ' + ports[target_env]+ ':'+ports[target_env]+' --env NODE_ENV=' + target_env + ' todolist:' + build_tag);
        if (rc > 0){
          grunt.fail.warn("DOCKER FAILURE")
        }
      } else {
        // ensure mongo is up
        var isMongo = shell.exec('docker ps | grep devops-mongo').code;
        if (isMongo > 0){
          grunt.log.ok('DEPLOYING Mongodb CONTAINER FIRST');
          shell.exec('docker run --name devops-mongo -p 27017:27017 -d mongo');
        }
        var rc = shell.exec('docker run -t -d --name todolist-' + target_env + ' --link devops-mongo:mongo.server -p '
            + ports[target_env]+ ':' + ports[target_env] + ' --env NODE_ENV=' + target_env + ' todolist:' + build_tag).code;
        if (rc > 0){
          grunt.fail.warn("DOCKER FAILURE");
        }
      }
    }
  });
  grunt.registerTask('serve', function (target) {
    if (target === 'dist') {
      return grunt.task.run(['build', 'env:all', 'env:prod', 'express:prod', 'wait', 'open', 'express-keepalive']);
    }
    if (target === 'debug') {
      return grunt.task.run([
        'clean:server',
        'env:all',
        'concurrent:server',
        'concurrent:debug'
      ]);
    }
    grunt.task.run([
      'clean:server',
      'env:all',
      'env:' + (target || 'dev'),
      'express:dev',
      'wait',
      'open',
      'watch'
    ]);
  });
  grunt.registerTask('test', function(target, environ) {
    environ = environ !== undefined ? environ : 'test';
    var usePhantom = false;
    if (environ === 'phantom') {
      environ = 'test';
      usePhantom = true;
    }
    var reporter = 'terminal';
    var coverage = 'travis';
    if (target === 'server-jenkins') {
      target = 'server';
      reporter = 'junit';
      coverage = 'cobertura';
      grunt.task.run(['env:jenkins']);
    }
    if (target === 'server') {
      return grunt.task.run([
        'clean:mochareports',
        'clean:coverage',
        'env:all',
        'env:'+environ,
        'mochaTest:' + reporter,
        'mochaTest:' + 'html',
        'mochaTest:' + coverage
      ]);
    }
    else grunt.task.run([
      'test:server'
    ]);
  });
  grunt.registerTask('build', [
    'clean:dist',
    'concurrent:dist',
  ]);
  grunt.registerTask('default', [
    'newer:jshint',
    'test',
    'build'
  ]);
};
contributors.txt
New file
@@ -0,0 +1,2 @@
William Lacy
Donal Spring
package.json
New file
@@ -0,0 +1,74 @@
{
  "name": "todolist",
  "version": "1.0.0",
  "main": "server/app.js",
  "dependencies": {
    "body-parser": "1.5.2",
    "composable-middleware": "0.3.0",
    "compression": "1.0.11",
    "connect-mongo": "0.8.2",
    "cookie-parser": "1.0.1",
    "ejs": "0.8.8",
    "errorhandler": "1.0.2",
    "express": "4.9.8",
    "express-jwt": "3.4.0",
    "express-session": "1.0.4",
    "jsonwebtoken": "5.7.0",
    "lodash": "2.4.2",
    "method-override": "1.0.2",
    "mongoose": "4.0.8",
    "morgan": "1.0.1",
    "serve-favicon": "2.0.1"
  },
  "devDependencies": {
    "blanket": "1.1.9",
    "connect-livereload": "0.4.1",
    "fs-extra": "0.30.0",
    "grunt": "0.4.5",
    "grunt-build-control": "0.4.0",
    "grunt-concurrent": "0.5.0",
    "grunt-contrib-clean": "0.5.0",
    "grunt-contrib-concat": "0.4.0",
    "grunt-contrib-copy": "0.5.0",
    "grunt-contrib-jshint": "0.10.0",
    "grunt-env": "0.4.4",
    "grunt-express-server": "0.4.19",
    "grunt-mocha-test": "0.10.2",
    "grunt-newer": "0.7.0",
    "grunt-ng-annotate": "0.2.3",
    "grunt-node-inspector": "0.4.2",
    "grunt-nodemon": "0.2.1",
    "grunt-open": "0.2.3",
    "grunt-protractor-runner": "1.2.1",
    "grunt-rev": "0.1.0",
    "grunt-svgmin": "0.4.0",
    "grunt-usemin": "2.1.1",
    "grunt-wiredep": "1.8.0",
    "jasmine-reporters": "2.2.0",
    "jit-grunt": "0.5.0",
    "jshint-jenkins-checkstyle-reporter": "0.1.2",
    "jshint-junit-reporter": "0.2.2",
    "jshint-stylish": "0.1.5",
    "mocha": "3.0.2",
    "mocha-cobertura-reporter": "1.0.4",
    "mocha-junit-reporter": "1.12.0",
    "mocha-lcov-reporter": "1.2.0",
    "open": "0.0.5",
    "q": "1.4.1",
    "request": "2.74.0",
    "requirejs": "2.1.22",
    "shelljs": "0.7.3",
    "should": "3.3.2",
    "supertest": "0.11.0",
    "time-grunt": "0.3.2",
    "travis-cov": "0.2.5"
  },
  "engines": {
    "node": ">=8.0.0"
  },
  "scripts": {
    "start": "node server/app.js",
    "test": "grunt test"
  },
  "private": true
}
server/.jshintrc
New file
@@ -0,0 +1,23 @@
{
  "node": true,
  "esnext": true,
  "bitwise": true,
  "eqeqeq": true,
  "immed": true,
  "latedef": "nofunc",
  "newcap": true,
  "noarg": true,
  "undef": true,
  "smarttabs": true,
  "asi": true,
  "noempty": true,
  "debug": false,
  "globals": {
    "describe": true,
    "it": true,
    "before": true,
    "beforeEach": true,
    "after": true,
    "afterEach": true
  }
}
server/api/todo/index.js
New file
@@ -0,0 +1,16 @@
'use strict';
var express = require('express');
var router = express.Router();
var controller = require('./todo.controller');
router.get('/', controller.index);
router.get('/:id', controller.show);
router.post('/', controller.create);
router.put('/:id', controller.update);
router.patch('/:id', controller.update);
router.delete('/:id', controller.destroy);
module.exports = router;
server/api/todo/todo.controller.js
New file
@@ -0,0 +1,72 @@
'use strict';
var _ = require('lodash');
var Todo = require('./todo.model');
// Get list of todos
exports.index = function(req, res) {
  var biscuits;
  Todo.find(function (err, todos) {
    if(err) { return handleError(res, err); }
    return res.status(200).json(todos);
  });
};
// Get a single todo
exports.show = function(req, res) {
  if (!handleObjectId(req, res)) return;
  Todo.findById(req.params.id, function (err, todo) {
    if(err) { return handleError(res, err); }
    if(!todo) { return res.status(404).send('Not Found'); }
    return res.json(todo);
  });
};
// Creates a new todo in the DB.
exports.create = function(req, res) {
  Todo.create(req.body, function(err, todo) {
    if(err) { return handleError(res, err); }
    return res.status(201).json(todo);
  });
};
// Updates an existing todo in the DB.
exports.update = function(req, res) {
  if(req.body._id) { delete req.body._id; }
  if (!handleObjectId(req, res)) return;
  Todo.findById(req.params.id.toString(), function (err, todo) {
    if (err) { return handleError(res, err); }
    if(!todo) { return res.status(404).send('Not Found'); }
    var updated = _.merge(todo, req.body);
    updated.save(function (err) {
      if (err) { return handleError(res, err); }
      return res.status(200).json(todo);
    });
  });
};
// Deletes a todo from the DB.
exports.destroy = function(req, res) {
  if (!handleObjectId(req, res)) return;
  Todo.findById(req.params.id, function (err, todo) {
    if(err) { return handleError(res, err); }
    if(!todo) { return res.status(404).send('Not Found'); }
    todo.remove(function(err) {
      if(err) { return handleError(res, err); }
      return res.status(204).send('No Content');
    });
  });
};
function handleError(res, err) {
  return res.status(500).send(err);
}
function handleObjectId(req, res) {
  // check if it is a valid ObjectID to prevent cast error
  if (!req.params || !req.params.id || !req.params.id.match(/^[0-9a-fA-F]{24}$/)) {
    res.status(400).send('not a valid mongo object id');
    return false;
  }
  return true;
}
server/api/todo/todo.model.js
New file
@@ -0,0 +1,11 @@
'use strict';
var mongoose = require('mongoose'),
    Schema = mongoose.Schema;
var TodoSchema = new Schema({
  title: String,
  completed: Boolean
});
module.exports = mongoose.model('Todo', TodoSchema);
server/api/todo/todo.spec.js
New file
@@ -0,0 +1,182 @@
'use strict';
var app = require('../../app');
var request = require('supertest');
require('should');
describe('GET /api/todos', function() {
  it('should respond with JSON array', function(done) {
    request(app)
      .get('/api/todos')
      .expect(200)
      .expect('Content-Type', /json/)
      .end(function(err, res) {
        if (err) return done(err);
        res.body.should.be.instanceof(Array);
        done();
      });
  });
});
describe('POST /api/todos', function() {
  it('should create the todo and return with the todo', function(done) {
    request(app)
      .post('/api/todos')
      .send({title: 'learn about endpoint/server side testing', completed: false})
      .expect(201)
      .expect('Content-Type', /json/)
      .end(function(err, res) {
        if (err) return done(err);
        res.body.should.have.property('_id');
        res.body.title.should.equal('learn about endpoint/server side testing');
        res.body.completed.should.equal(false);
        done();
      });
  });
});
describe('GET /api/todos/:id', function() {
  var todoId;
  beforeEach(function createObjectToUpdate(done) {
    request(app)
      .post('/api/todos')
      .send({title: 'learn about endpoint/server side testing', completed: false})
      .expect(201)
      .expect('Content-Type', /json/)
      .end(function (err, res) {
        if (err) return done(err);
        todoId = res.body._id;
        done();
      });
  });
  it('should update the todo', function (done) {
    request(app)
      .get('/api/todos/' + todoId)
      .expect(200)
      .expect('Content-Type', /json/)
      .end(function (err, res) {
        if (err) return done(err);
        res.body._id.should.equal(todoId);
        res.body.title.should.equal('learn about endpoint/server side testing');
        res.body.completed.should.equal(false);
        done();
      });
  });
  it('should return 404 for valid mongo object id that does not exist', function(done){
    request(app)
      .get('/api/todos/' + 'abcdef0123456789ABCDEF01')
      .expect(404)
      .end(function(err) {
        if (err) return done(err);
        done();
      });
  });
  it('should return 400 for invalid object ids', function(done){
    request(app)
      .get('/api/todos/' + 123)
      .expect(400)
      .end(function(err, res) {
        if (err) return done(err);
        res.text.should.equal('not a valid mongo object id')
        done();
      });
  });
});
describe('PUT /api/todos/:id', function() {
  var todoId;
  beforeEach(function createObjectToUpdate(done){
    request(app)
      .post('/api/todos')
      .send({title: 'learn about endpoint/server side testing', completed: false})
      .expect(201)
      .expect('Content-Type', /json/)
      .end(function(err, res) {
        if (err) return done(err);
        todoId = res.body._id;
        done();
      });
  });
  it('should update the todo', function(done) {
    request(app)
      .put('/api/todos/' + todoId)
      .send({title: 'LOVE endpoint/server side testing!', completed: true})
      .expect(200)
      .expect('Content-Type', /json/)
      .end(function(err, res) {
        if (err) return done(err);
        res.body.should.have.property('_id');
        res.body.title.should.equal('LOVE endpoint/server side testing!');
        res.body.completed.should.equal(true);
        done();
      });
  });
  it('should return 404 for valid mongo object id that does not exist', function(done){
    request(app)
      .put('/api/todos/' + 'abcdef0123456789ABCDEF01')
      .send({title: 'LOVE endpoint/server side testing!', completed: true})
      .expect(404)
      .end(function(err) {
        if (err) return done(err);
        done();
      });
  });
  it('should return 400 for invalid object ids', function(done){
    request(app)
      .put('/api/todos/' + 123)
      .send({title: 'LOVE endpoint/server side testing!', completed: true})
      .expect(400)
      .end(function(err, res) {
        if (err) return done(err);
        res.text.should.equal('not a valid mongo object id')
        done();
      });
  });
});
describe('DELETE /api/todos/:id', function() {
  var todoId;
  beforeEach(function createObjectToUpdate(done){
    request(app)
      .post('/api/todos')
      .send({title: 'learn about endpoint/server side testing', completed: false})
      .expect(201)
      .expect('Content-Type', /json/)
      .end(function(err, res) {
        if (err) return done(err);
        todoId = res.body._id;
        done();
      });
  });
  it('should delete the todo', function(done) {
    request(app)
      .delete('/api/todos/' + todoId)
      .expect(204)
      .end(function(err) {
        if (err) return done(err);
        done();
      });
  });
  it('should return 404 for valid mongo object id that does not exist', function(done){
    request(app)
      .delete('/api/todos/' + 'abcdef0123456789ABCDEF01')
      .expect(404)
      .end(function(err) {
        if (err) return done(err);
        done();
      });
  });
  it('should return 400 for invalid object ids', function(done){
    request(app)
      .delete('/api/todos/' + 123)
      .send({title: 'LOVE endpoint/server side testing!', completed: true})
      .expect(400)
      .end(function(err, res) {
        if (err) return done(err);
        res.text.should.equal('not a valid mongo object id')
        done();
      });
  });
});
server/app.js
New file
@@ -0,0 +1,44 @@
/**
 * Main application file
 */
'use strict';
// Set default node environment to development
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
var express = require('express');
var config = require('./config/environment');
// Populate DB with sample data
if(config.seedDB) { require('./config/seed'); }
// Setup server
var app = express();
var server = require('http').createServer(app);
require('./config/express')(app);
if (config.mocks && config.mocks.api) {
  //add stubs
  require('./mocks/mock-routes')(app);
} else {
  var mongoose = require('mongoose');
  // Connect to database
  mongoose.connect(config.mongo.uri, config.mongo.options);
  mongoose.connection.on('error', function(err) {
      console.error('MongoDB connection error: ' + err);
      process.exit(-1);
    }
  );
  require('./routes')(app);
}
// Start server
server.listen(config.port, config.ip, function () {
  console.log('Express server listening on %d, in %s mode', config.port, app.get('env'));
});
// Expose app
exports = module.exports = app;
server/components/errors/index.js
New file
@@ -0,0 +1,20 @@
/**
 * Error responses
 */
'use strict';
module.exports[404] = function pageNotFound(req, res) {
  var viewFilePath = '404';
  var statusCode = 404;
  var result = {
    status: statusCode
  };
  res.status(result.status);
  res.render(viewFilePath, function (err) {
    if (err) { return res.json(result, result.status); }
    res.render(viewFilePath);
  });
};
server/config/environment/ci.js
New file
@@ -0,0 +1,15 @@
'use strict';
// Test specific configuration
// ===========================
module.exports = {
  // MongoDB connection options
  mongo: {
    uri: 'mongodb://mongo.server/todolist-ci'
  },
  mocks: {
    api: true
  },
  seedDB: true,
  port: process.env.PORT || 9001
};
server/config/environment/development.js
New file
@@ -0,0 +1,14 @@
'use strict';
// Development specific configuration
// ==================================
module.exports = {
  // MongoDB connection options
  mongo: {
    uri: 'mongodb://mongo.server/todolist-dev'
  },
  mocks: {
    api: true
  },
  seedDB: true
};
server/config/environment/index.js
New file
@@ -0,0 +1,44 @@
'use strict';
var path = require('path');
var _ = require('lodash');
var config;
// All configurations will extend these options
// ============================================
var all = {
  env: process.env.NODE_ENV,
  // Root path of server
  root: path.normalize(__dirname + '/../../..'),
  // Server port
  port: process.env.PORT || 9000,
  // Server IP
  ip: process.env.IP || '0.0.0.0',
  // Should we populate the DB with sample data?
  seedDB: false,
  // Secret for session, you will want to change this and make it an environment variable
  secrets: {
    session: 'todolist-secret'
  },
  // MongoDB connection options
  mongo: {
    options: {
      db: {
        safe: true
      }
    }
  },
};
// Export the config object based on the NODE_ENV
// ==============================================
module.exports = _.merge(
  all,
  require('./' + process.env.NODE_ENV + '.js') || {});
server/config/environment/production.js
New file
@@ -0,0 +1,24 @@
'use strict';
// Production specific configuration
// =================================
module.exports = {
  // Server IP
  ip:       process.env.OPENSHIFT_NODEJS_IP ||
            process.env.IP ||
            '0.0.0.0',
  // Server port
  port:     process.env.OPENSHIFT_NODEJS_PORT ||
            process.env.PORT ||
            80,
  // MongoDB connection options
  mongo: {
    uri:    process.env.MONGOLAB_URI ||
            process.env.MONGOHQ_URL ||
            process.env.OPENSHIFT_MONGODB_DB_URL+process.env.OPENSHIFT_APP_NAME ||
            'mongodb://mongo.server/todolist-prod'
  },
  seedDB: true
};
server/config/environment/si.js
New file
@@ -0,0 +1,12 @@
'use strict';
// Test specific configuration
// ===========================
module.exports = {
  // MongoDB connection options
  mongo: {
    uri: 'mongodb://mongo.server/todolist-si'
  },
  seedDB: true,
  port: process.env.PORT || 9002
};
server/config/environment/test.js
New file
@@ -0,0 +1,12 @@
'use strict';
// Test specific configuration
// ===========================
module.exports = {
  // MongoDB connection options
  mongo: {
    uri: 'mongodb://mongo.server/todolist-test'
  },
  seedDB: true,
  port: process.env.PORT || 9000
};
server/config/express.js
New file
@@ -0,0 +1,44 @@
/**
 * Express configuration
 */
'use strict';
var express = require('express');
var morgan = require('morgan');
var compression = require('compression');
var bodyParser = require('body-parser');
var methodOverride = require('method-override');
var cookieParser = require('cookie-parser');
var errorHandler = require('errorhandler');
var path = require('path');
var config = require('./environment');
var passport = require('passport');
module.exports = function(app) {
  var env = app.get('env');
  app.set('views', config.root + '/server/views');
  app.engine('html', require('ejs').renderFile);
  app.set('view engine', 'html');
  app.use(compression());
  app.use(bodyParser.urlencoded({ extended: false }));
  app.use(bodyParser.json());
  app.use(methodOverride());
  app.use(cookieParser());
  app.use(passport.initialize());
  if ('production' === env  || 'si' === env  || 'ci' === env) {
    app.use(express.static(path.join(config.root, 'public')));
    app.set('appPath', path.join(config.root, 'public'));
    app.use(morgan('dev'));
  }
  if ('development' === env || 'test' === env) {
    app.use(require('connect-livereload')());
    app.use(express.static(path.join(config.root, '.tmp')));
    app.use(express.static(path.join(config.root, 'client')));
    app.set('appPath', path.join(config.root, 'client'));
    app.use(morgan('dev'));
    app.use(errorHandler()); // Error handler - has to be last
  }
};
server/config/local.env.sample.js
New file
@@ -0,0 +1,14 @@
'use strict';
// Use local.env.js for environment variables that grunt will set when the server starts locally.
// Use for your api keys, secrets, etc. This file should not be tracked by git.
//
// You will need to set these on the server you deploy to.
module.exports = {
  DOMAIN:           'http://localhost:9000',
  SESSION_SECRET:   'todolist-secret',
  // Control debug level for modules using visionmedia/debug
  DEBUG: ''
};
server/config/seed.js
New file
@@ -0,0 +1,18 @@
/**
 * Populate DB with sample data on server start
 * to disable, edit config/environment/index.js, and set `seedDB: false`
 */
'use strict';
var Todo = require('../api/todo/todo.model');
Todo.find({}).remove(function() {
  Todo.create({
    title : 'Learn some stuff about Jenkins',
    completed: true
  }, {
    title : 'Go for Coffee',
    completed: false
  });
});
server/mocks/mock-routes-config.json
New file
@@ -0,0 +1,87 @@
[
  {
    "request": {
      "url": "^/todos",
      "method": "GET"
    },
    "response": {
      "status": 200,
      "headers": {
        "Content-Type": "application/json",
        "some-arb-HEADER": "for testing"
      },
      "latency": 800,
      "body": [
        {
          "title": "Learn some stuff about Jenkins",
          "_id": "abcdef1234567890abcdef12",
          "completed": true
        },
        {
          "title": "Completed lab 1",
          "_id": "abcdef1234567890abcdef13",
          "completed": false
        }
      ]
    }
  },
  {
    "request": {
      "url": "^/todos/*",
      "method": "PUT",
      "headers": {
        "Content-Type": "application/json"
      }
    },
    "response": {
      "status": 200,
      "headers": {
        "Content-Type": "application/json"
      },
      "latency": 300,
      "body": {
        "title": "whatever",
        "_id": "abcdef1234567890abcdef12",
        "completed": true
      }
    }
  },
  {
    "request": {
      "url": "^/todos/*",
      "method": "DELETE",
      "headers": {
        "Content-Type": "application/json"
      }
    },
    "response": {
      "status": 204,
      "headers": {
        "Content-Type": "application/json"
      },
      "latency": 150,
      "body": {}
    }
  },
  {
    "request": {
      "url": "^/todos",
      "method": "POST",
      "headers": {
        "Content-Type": "application/json"
      }
    },
    "response": {
      "status": 201,
      "headers": {
        "Content-Type": "application/json"
      },
      "latency": 445,
      "body": {
        "title": "some new thing",
        "_id": "abcdef1234567890abcdef12",
        "completed": false
      }
    }
  }
]
server/mocks/mock-routes.js
New file
@@ -0,0 +1,61 @@
/**
 * Stubbed Application routes
 */
'use strict';
var express = require('express');
var routerStub = express.Router();
function mockMongoId() {
  function s4() {
    return Math.floor((1 + Math.random()) * 0x10000)
      .toString(16)
      .substring(1);
  }
  return s4() + s4() + s4() + s4() + s4() + s4();
}
routerStub.get('/todos', function (req, res) {
  // add timeout to test loading styles
  setTimeout(function () {
    return res.status(200).send([
      {"title": "Learn some stuff about Jenkins", "_id": mockMongoId(), "completed": true},
      {"title": "Go for Coffee", "_id": mockMongoId(), "completed": false}
    ]);
  }, 650);
});
routerStub.get('/todos/:id', function (req, res) {
  setTimeout(function () {
    var id = req.params.id;
    return res.status(200).send({
      "title": "Learn some stuff about Jenkins", "_id": id, "completed": true
    });
  }, 150);
});
routerStub.post('/todos', function (req, res) {
  setTimeout(function () {
    req.body._id = mockMongoId();
    return res.status(201).send(req.body);
  }, 170);
});
routerStub.put('/todos/:id', function (req, res) {
  setTimeout(function () {
    req.body._id = req.params.id;
    return res.status(200).send(req.body);
  }, 130);
});
routerStub.delete('/todos/:id', function (req, res) {
  setTimeout(function () {
    return res.status(204).send();
  }, 100);
});
module.exports = function(app) {
  app.use('/api', routerStub)
};
server/mocks/mock-routes.spec.js
New file
@@ -0,0 +1,182 @@
'use strict';
var app = require('../app');
var request = require('supertest');
require('should');
describe('GET /api/todos', function() {
  it('should respond with JSON array', function(done) {
    request(app)
      .get('/api/todos')
      .expect(200)
      .expect('Content-Type', /json/)
      .end(function(err, res) {
        if (err) return done(err);
        res.body.should.be.instanceof(Array);
        done();
      });
  });
});
describe('POST /api/todos', function() {
  it('should create the todo and return with the todo', function(done) {
    request(app)
      .post('/api/todos')
      .send({title: 'learn about endpoint/server side testing', completed: false})
      .expect(201)
      .expect('Content-Type', /json/)
      .end(function(err, res) {
        if (err) return done(err);
        res.body.should.have.property('_id');
        res.body.title.should.equal('learn about endpoint/server side testing');
        res.body.completed.should.equal(false);
        done();
      });
  });
});
describe('GET /api/todos/:id', function() {
  var todoId;
  beforeEach(function createObjectToUpdate(done) {
    request(app)
      .post('/api/todos')
      .send({title: 'learn about endpoint/server side testing', completed: false})
      .expect(201)
      .expect('Content-Type', /json/)
      .end(function (err, res) {
        if (err) return done(err);
        todoId = res.body._id;
        done();
      });
  });
  it('should update the todo', function (done) {
    request(app)
      .get('/api/todos/' + todoId)
      .expect(200)
      .expect('Content-Type', /json/)
      .end(function (err, res) {
        if (err) return done(err);
        res.body._id.should.equal(todoId);
        res.body.title.should.equal('learn about endpoint/server side testing');
        res.body.completed.should.equal(false);
        done();
      });
  });
  it('should return 404 for valid mongo object id that does not exist', function(done){
    request(app)
      .get('/api/todos/' + 'abcdef0123456789ABCDEF01')
      .expect(404)
      .end(function(err) {
        if (err) return done(err);
        done();
      });
  });
  it('should return 400 for invalid object ids', function(done){
    request(app)
      .get('/api/todos/' + 123)
      .expect(400)
      .end(function(err, res) {
        if (err) return done(err);
        res.text.should.equal('not a valid mongo object id')
        done();
      });
  });
});
describe('PUT /api/todos/:id', function() {
  var todoId;
  beforeEach(function createObjectToUpdate(done){
    request(app)
      .post('/api/todos')
      .send({title: 'learn about endpoint/server side testing', completed: false})
      .expect(201)
      .expect('Content-Type', /json/)
      .end(function(err, res) {
        if (err) return done(err);
        todoId = res.body._id;
        done();
      });
  });
  it('should update the todo', function(done) {
    request(app)
      .put('/api/todos/' + todoId)
      .send({title: 'LOVE endpoint/server side testing!', completed: true})
      .expect(200)
      .expect('Content-Type', /json/)
      .end(function(err, res) {
        if (err) return done(err);
        res.body.should.have.property('_id');
        res.body.title.should.equal('LOVE endpoint/server side testing!');
        res.body.completed.should.equal(true);
        done();
      });
  });
  it('should return 404 for valid mongo object id that does not exist', function(done){
    request(app)
      .put('/api/todos/' + 'abcdef0123456789ABCDEF01')
      .send({title: 'LOVE endpoint/server side testing!', completed: true})
      .expect(404)
      .end(function(err) {
        if (err) return done(err);
        done();
      });
  });
  it('should return 400 for invalid object ids', function(done){
    request(app)
      .put('/api/todos/' + 123)
      .send({title: 'LOVE endpoint/server side testing!', completed: true})
      .expect(400)
      .end(function(err, res) {
        if (err) return done(err);
        res.text.should.equal('not a valid mongo object id')
        done();
      });
  });
});
describe('DELETE /api/todos/:id', function() {
  var todoId;
  beforeEach(function createObjectToUpdate(done){
    request(app)
      .post('/api/todos')
      .send({title: 'learn about endpoint/server side testing', completed: false})
      .expect(201)
      .expect('Content-Type', /json/)
      .end(function(err, res) {
        if (err) return done(err);
        todoId = res.body._id;
        done();
      });
  });
  it('should delete the todo', function(done) {
    request(app)
      .delete('/api/todos/' + todoId)
      .expect(204)
      .end(function(err) {
        if (err) return done(err);
        done();
      });
  });
  it('should return 404 for valid mongo object id that does not exist', function(done){
    request(app)
      .delete('/api/todos/' + 'abcdef0123456789ABCDEF01')
      .expect(404)
      .end(function(err) {
        if (err) return done(err);
        done();
      });
  });
  it('should return 400 for invalid object ids', function(done){
    request(app)
      .delete('/api/todos/' + 123)
      .send({title: 'LOVE endpoint/server side testing!', completed: true})
      .expect(400)
      .end(function(err, res) {
        if (err) return done(err);
        res.text.should.equal('not a valid mongo object id')
        done();
      });
  });
});
server/routes.js
New file
@@ -0,0 +1,24 @@
/**
 * Main application routes
 */
'use strict';
var errors = require('./components/errors');
var path = require('path');
module.exports = function(app) {
  // Insert routes below
  app.use('/api/todos', require('./api/todo'));
  // All undefined asset or api routes should return a 404
  app.route('/:url(api|components|app|bower_components|assets)/*')
   .get(errors[404]);
  // All other routes should redirect to the index.html
  app.route('/*')
    .get(function(req, res) {
      res.sendFile(path.resolve(app.get('appPath') + '/index.html'));
    });
};
server/views/404.html
New file
@@ -0,0 +1,157 @@
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Page Not Found :(</title>
    <style>
      ::-moz-selection {
        background: #b3d4fc;
        text-shadow: none;
      }
      ::selection {
        background: #b3d4fc;
        text-shadow: none;
      }
      html {
        padding: 30px 10px;
        font-size: 20px;
        line-height: 1.4;
        color: #737373;
        background: #f0f0f0;
        -webkit-text-size-adjust: 100%;
        -ms-text-size-adjust: 100%;
      }
      html,
      input {
        font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
      }
      body {
        max-width: 500px;
        _width: 500px;
        padding: 30px 20px 50px;
        border: 1px solid #b3b3b3;
        border-radius: 4px;
        margin: 0 auto;
        box-shadow: 0 1px 10px #a7a7a7, inset 0 1px 0 #fff;
        background: #fcfcfc;
      }
      h1 {
        margin: 0 10px;
        font-size: 50px;
        text-align: center;
      }
      h1 span {
        color: #bbb;
      }
      h3 {
        margin: 1.5em 0 0.5em;
      }
      p {
        margin: 1em 0;
      }
      ul {
        padding: 0 0 0 40px;
        margin: 1em 0;
      }
      .container {
        max-width: 380px;
        _width: 380px;
        margin: 0 auto;
      }
      /* google search */
      #goog-fixurl ul {
        list-style: none;
        padding: 0;
        margin: 0;
      }
      #goog-fixurl form {
        margin: 0;
      }
      #goog-wm-qt,
      #goog-wm-sb {
        border: 1px solid #bbb;
        font-size: 16px;
        line-height: normal;
        vertical-align: top;
        color: #444;
        border-radius: 2px;
      }
      #goog-wm-qt {
        width: 220px;
        height: 20px;
        padding: 5px;
        margin: 5px 10px 0 0;
        box-shadow: inset 0 1px 1px #ccc;
      }
      #goog-wm-sb {
        display: inline-block;
        height: 32px;
        padding: 0 10px;
        margin: 5px 0 0;
        white-space: nowrap;
        cursor: pointer;
        background-color: #f5f5f5;
        background-image: -webkit-linear-gradient(rgba(255,255,255,0), #f1f1f1);
        background-image: -moz-linear-gradient(rgba(255,255,255,0), #f1f1f1);
        background-image: -ms-linear-gradient(rgba(255,255,255,0), #f1f1f1);
        background-image: -o-linear-gradient(rgba(255,255,255,0), #f1f1f1);
        -webkit-appearance: none;
        -moz-appearance: none;
        appearance: none;
        *overflow: visible;
        *display: inline;
        *zoom: 1;
      }
      #goog-wm-sb:hover,
      #goog-wm-sb:focus {
        border-color: #aaa;
        box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
        background-color: #f8f8f8;
      }
      #goog-wm-qt:hover,
      #goog-wm-qt:focus {
        border-color: #105cb6;
        outline: 0;
        color: #222;
      }
      input::-moz-focus-inner {
        padding: 0;
        border: 0;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <h1>Not found <span>:(</span></h1>
      <p>Sorry, but the page you were trying to view does not exist.</p>
      <p>It looks like this was the result of either:</p>
      <ul>
        <li>a mistyped address</li>
        <li>an out-of-date link</li>
      </ul>
      <script>
        var GOOG_FIXURL_LANG = (navigator.language || '').slice(0,2),GOOG_FIXURL_SITE = location.host;
      </script>
      <script src="//linkhelp.clients.google.com/tbproxy/lh/wm/fixurl.js"></script>
    </div>
  </body>
</html>
tasks/blanket.js
New file
@@ -0,0 +1,13 @@
/**
 * Created by donal on 19/08/2016.
 */
var path = require('path');
var srcDir = path.join(__dirname, '..', 'server');
// included as in the instructions here
// https://github.com/pghalliday/grunt-mocha-test#generating-coverage-reports
require('blanket')({
  // Only files that match the pattern will be instrumented
  pattern: srcDir
});
tasks/perf-test.js
New file
@@ -0,0 +1,140 @@
var request = require('request');
var benchrest = require('bench-rest');
var grunt = require("grunt");
var Q = require('q');
// INFO ABOUT THE STATS
// stats.main.histogram.min - the minimum time any iteration took (milliseconds)
// stats.main.histogram.max - the maximum time any iteration took (milliseconds)
// stats.main.histogram.mean - the average time any iteration took (milliseconds)
// stats.main.histogram.p95 - the amount of time that 95% of all iterations completed within (milliseconds)
var options = {
  limit: 10,     // concurrent connections
  iterations: 10000  // number of iterations to perform
};
var test = {
  domain : 'http://localhost:9000',
  dir : './reports/server/perf/',
  route : '/api/todos/',
  nfr : 60
};
var si = {
  domain : 'http://localhost:9002',
  dir : './reports/server/perf/',
  route : '/api/todos/',
  nfr : 60
};
var production = {
  domain : 'http://localhost:80',
  dir : './reports/server/perf/',
  route : '/api/todos/',
  nfr : 50
};
var test_endpoint = function (flow, options) {
  var wait = Q.defer();
  benchrest(flow, options)
    .on('error', function (err, ctxName) {
      console.error('Failed in %s with err: ', ctxName, err);
    })
    .on('end', function (stats, errorCount) {
      console.log('\n\n###### ' +flow.filename +' - ' +flow.env.domain + flow.env.route);
      console.log('Error Count', errorCount);
      console.log('Stats', stats);
      var mean_score = stats.main.histogram.mean;
      var fs = require('fs-extra');
      var file = flow.env.dir + flow.filename + '-perf-score.csv';
      fs.outputFileSync(file, 'mean,max,mix,p95\n'+  stats.main.histogram.mean +','
        + stats.main.histogram.max +','+ stats.main.histogram.min +','+ stats.main.histogram.p95);
      if (mean_score > flow.env.nfr){
        console.error('NFR EXCEEDED - ' +mean_score +' > '+flow.env.nfr);
        wait.resolve(false);
      } else {
        wait.resolve(true);
      }
    });
  return wait.promise
};
module.exports = function () {
  grunt.task.registerTask('perf-test', 'Runs the performance tests against the target env', function(target, api) {
    if (target === undefined || api === undefined){
      grunt.fail.fatal('Required param not set - use grunt perf-test\:\<target\>\:\<api\>');
    } else {
      var done = this.async();
      var create = {
        filename: 'create',
        env: {},
        main: [{
          post: si.domain + si.route,
          json: {
            title: 'Run perf-test',
            completed: false
          }
        }]
      };
      var show = {
        filename: 'show',
        env: {},
        main: [{
          get: si.domain + si.route
        }]
      };
      if (target === 'si') {
        show.env = si;
        create.env = si;
      }
      else if (target === 'production') {
        show.env = production;
        create.env = production;
      }
      else if (target === 'test') {
        show.env = test;
        create.env = test;
      } else {
        grunt.fail.fatal('Invalid target - ' + target);
        done();
      }
      grunt.log.ok("Perf tests running against " + target);
      grunt.log.ok("This may take some time .... ");
      var all_tests = [];
      // console.log(create)
      // console.log(show)
      request(show.env.domain + show.env.route, function (error, response, body) {
        if (error) {
          grunt.log.error(error);
        } else if(response.statusCode == 200) {
          if (api === 'create'){
            all_tests.push(test_endpoint(create, options));
          }
          else {
            var mongoid = JSON.parse(body)[0]._id;
            show.main[0].get = si.domain + si.route + mongoid;
            all_tests.push(test_endpoint(show, options));
          }
          Q.all(all_tests).then(function (data) {
            grunt.log.ok(data);
            if (data.indexOf(false) > -1){
              grunt.fail.fatal('FAILURE - NFR NOT ACHIEVED');
            } else {
              grunt.log.ok('SUCCESS - All NFR ACHIEVED');
              return done();
            }
          });
        } else {
          grunt.fail.fatal('FAILURE: something bad happened... there was no error from mongo but the response code was not 200')
        }
      });
    }
  });
};