├── .node-version ├── Brewfile ├── .gitignore ├── .envsample ├── scripts ├── update ├── server ├── setup ├── bootstrap └── test ├── contributors.md ├── index.js ├── src ├── core │ ├── server.js │ ├── log.js │ ├── utility.js │ ├── auth.js │ └── bootstrap.js ├── todos │ ├── todo │ │ ├── model.js │ │ ├── routes.js │ │ ├── schema.js │ │ └── controller.js │ └── index.js └── shared │ ├── index.js │ └── user │ ├── model.js │ ├── routes.js │ ├── schema.js │ └── controller.js ├── .travis.yml ├── test ├── index.spec.js └── routes │ ├── todo.spec.js │ └── user.spec.js ├── LICENSE ├── package.json └── README.md /.node-version: -------------------------------------------------------------------------------- 1 | 5.0.0 2 | -------------------------------------------------------------------------------- /Brewfile: -------------------------------------------------------------------------------- 1 | brew 'nodenv' 2 | brew 'mongodb' 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .swp 2 | **.sw* 3 | *.swo 4 | *.swp 5 | .env 6 | node_modules/ 7 | .azk 8 | /coverage 9 | tmp 10 | .idea -------------------------------------------------------------------------------- /.envsample: -------------------------------------------------------------------------------- 1 | SERVER_HOST=localhost 2 | SERVER_PORT=8000 3 | DB_NAME=hapiness 4 | DB_HOST=localhost 5 | DB_PORT=27017 6 | JWT=AWESOME_SECRET 7 | -------------------------------------------------------------------------------- /scripts/update: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # script/update: Update application to run for its current checkout. 4 | 5 | set -e 6 | 7 | cd "$(dirname "$0")/.." 8 | 9 | scripts/bootstrap 10 | -------------------------------------------------------------------------------- /contributors.md: -------------------------------------------------------------------------------- 1 | ## Contributors to the Bolty project 2 | 3 | * Marcos Bérgamo 4 | * [16/11/2015] Started project, tests and scripts 5 | 6 | * Adriean Khisbe 7 | * [7/12/2015] Setup CI 8 | 9 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // load deps 4 | let server = require('./src/core/bootstrap'); 5 | 6 | server.start((err) => { 7 | if (err) { throw err; } 8 | 9 | console.log('info', 'Server Running At: ' + server.info.uri); 10 | }); 11 | -------------------------------------------------------------------------------- /src/core/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('dotenv').config({silent: true}); 4 | 5 | // Load deps 6 | const Hapi = require('hapi'); 7 | 8 | let server; 9 | 10 | module.exports = server = new Hapi.Server(); 11 | 12 | // Set the port for listening 13 | server.connection({ 14 | host: process.env.SERVER_HOST || 'localhost', 15 | port: process.env.SERVER_PORT || '8000' 16 | }); 17 | -------------------------------------------------------------------------------- /scripts/server: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # script/server: Launch the application and any extra required processes 4 | # locally. 5 | 6 | set -e 7 | 8 | cd "$(dirname "$0")/.." 9 | 10 | # ensure everything in the app is up to date. 11 | scripts/update 12 | 13 | test -z "$NODE_ENV" && 14 | NODE_ENV='development' 15 | 16 | # boot the app and any other necessary processes. 17 | node index.js 18 | 19 | 20 | -------------------------------------------------------------------------------- /scripts/setup: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # script/setup: Set up application for the first time after cloning, or set it 4 | # back to the initial first unused state. 5 | 6 | set -e 7 | 8 | cd "$(dirname "$0")/.." 9 | 10 | scripts/bootstrap 11 | 12 | echo "===> Setting up DB..." 13 | # reset database to a fresh state. 14 | mongo ${DB_NAME} --eval "db.dropDatabase();" 15 | 16 | echo "==> App is now ready to go!" 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/todos/todo/model.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Promise = require('bluebird'); 4 | 5 | const mongoose = require('k7-mongoose').mongoose; 6 | 7 | const Schema = new mongoose.Schema({ 8 | name: { 9 | type: String, 10 | required: true 11 | }, 12 | owner: { 13 | type: mongoose.Schema.Types.ObjectId, 14 | required: true, 15 | ref: 'User' 16 | }, 17 | checked: Boolean 18 | }); 19 | 20 | const TodoModel = mongoose.model('Todo', Schema); 21 | 22 | module.exports = Promise.promisifyAll(TodoModel); 23 | 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 4 5 | - 5 6 | 7 | env: 8 | - DB_NAME=hapiness DB_HOST=localhost DB_PORT=27017 JWT=awesomeSecretKeyHere EXPORT_COVERAGE=1 9 | 10 | services: 11 | - mongodb 12 | 13 | cache: 14 | directories: 15 | - node_modules 16 | 17 | notifications: 18 | webhooks: 19 | urls: 20 | - https://webhooks.gitter.im/e/b98f41fd6ac7d5740632 21 | on_success: change # options: [always|never|change] default: always 22 | on_failure: always # options: [always|never|change] default: always 23 | on_start: false # default: false 24 | -------------------------------------------------------------------------------- /test/index.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // load deps 3 | const lab = exports.lab = require('lab').script(); 4 | const App = require('../src/core/bootstrap'); 5 | global.expect = require('chai').expect; 6 | 7 | // prepare environment 8 | global. it = lab.it; 9 | global.describe = lab.describe; 10 | global.before = lab.before; 11 | global.beforeEach = lab.beforeEach; 12 | 13 | global.describe('Bootstraping app', () => { 14 | global.before(function (done) { 15 | App.start() 16 | .then((server) => { 17 | global.server = server; 18 | global.db = global.server.database; 19 | global.db['mongoose'].on('connected', () => { 20 | done(); 21 | }); 22 | }); 23 | }); 24 | }); 25 | 26 | -------------------------------------------------------------------------------- /src/core/log.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Good = require('good'); 4 | 5 | exports.register = (server, options, next) => { 6 | const opts = { 7 | opsInterval: 1000, 8 | reporters: [{ 9 | reporter: require('good-console'), 10 | events: {error: '*', log: '*', response: '*', request: '*'} 11 | }, { 12 | reporter: require('good-file'), 13 | events: {ops: '*', error: '*'}, 14 | config: { 15 | path: '../../logs', 16 | rotate: 'daily' 17 | } 18 | }] 19 | }; 20 | 21 | server.register({ 22 | register: Good, 23 | options: opts 24 | }, (err) => { 25 | return next(err); 26 | }); 27 | }; 28 | 29 | exports.register.attributes = { 30 | name: 'logs', 31 | version: '1.0.0' 32 | }; 33 | 34 | -------------------------------------------------------------------------------- /src/core/utility.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.register = (server, options, next) => { 4 | server.method('loadRoutes', loadRoutes); 5 | 6 | server.register({ 7 | register: require('hapi-boom-decorators') 8 | }, (err) => { 9 | if (!err) { 10 | return next(); 11 | } 12 | }); 13 | 14 | function loadRoutes (routes, cb) { 15 | let registerRoutes = routes.map((route) => { 16 | return { 17 | register: require(route) 18 | }; 19 | }); 20 | 21 | server.register(registerRoutes, (err) => { 22 | if (err) { 23 | cb(err); 24 | } 25 | 26 | return cb(null); 27 | }); 28 | } 29 | }; 30 | 31 | exports.register.attributes = { 32 | name: 'utility', 33 | version: '1.0.0' 34 | }; 35 | 36 | -------------------------------------------------------------------------------- /scripts/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # script/bootstrap: Resolve all dependencies that the application requires to 4 | # run. 5 | 6 | set -e 7 | 8 | [ "$TRAVIS" = true ] || cd "$(dirname "$0")/.." 9 | 10 | if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ]; then 11 | brew update 12 | 13 | brew bundle check 2>&1 >/dev/null || { 14 | echo "==> Installing Homebrew dependencies…" 15 | brew bundle 16 | } 17 | fi 18 | 19 | if [ "$TRAVIS" = true ]; then 20 | echo "==> Not installing Node.js - TRAVIS version" 21 | elif [ -f ".node-version" ] && [ -z "$(nodenv version-name 2>/dev/null)" ]; then 22 | echo "==> Installing Node.js…" 23 | nodenv install --skip-existing 24 | 25 | if [ -f "package.json" ]; then 26 | echo "==> Installing module dependencies…" 27 | npm install 28 | fi 29 | fi 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/todos/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | 7 | const basePath = __dirname; 8 | 9 | exports.register = (server, options, next) => { 10 | // register route 11 | server.methods.loadRoutes(_.compact(getFiles('routes.js')), () => { 12 | next(); 13 | }); 14 | }; 15 | 16 | exports.register.attributes = { 17 | name: 'todo', 18 | version: '1.0.0' 19 | }; 20 | 21 | function getFiles (type) { 22 | return fs.readdirSync(basePath) 23 | .map((entity) => { 24 | let root = path.join(basePath, entity, type); 25 | 26 | if (!isFile(root)) { 27 | return; 28 | } 29 | 30 | return root; 31 | }); 32 | } 33 | 34 | function isFile (root) { 35 | try { 36 | return fs.statSync(root).isFile(); 37 | } catch (err) { 38 | return false; 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /src/shared/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | 7 | const basePath = __dirname; 8 | 9 | exports.register = (server, options, next) => { 10 | // register route 11 | server.methods.loadRoutes(_.compact(getFiles('routes.js')), () => { 12 | next(); 13 | }); 14 | }; 15 | 16 | exports.register.attributes = { 17 | name: 'access', 18 | version: '1.0.0' 19 | }; 20 | 21 | function getFiles (type) { 22 | return fs.readdirSync(basePath) 23 | .map((entity) => { 24 | let root = path.join(basePath, entity, type); 25 | 26 | if (!isFile(root)) { 27 | return; 28 | } 29 | 30 | return root; 31 | }); 32 | } 33 | 34 | function isFile (root) { 35 | try { 36 | let ret = fs.statSync(root).isFile(); 37 | return ret; 38 | } catch (err) { 39 | return false; 40 | } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /src/core/auth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Promise = require('bluebird'); 4 | const jwt = require('hapi-auth-jwt2'); 5 | 6 | exports.register = (server, options, next) => { 7 | server.register(jwt, registerAuth); 8 | 9 | function registerAuth (err) { 10 | if (err) { return next(err); } 11 | 12 | server.auth.strategy('jwt', 'jwt', { 13 | key: process.env.JWT || 'stubJWT', 14 | validateFunc: validate, 15 | verifyOptions: {algorithms: [ 'HS256' ]} 16 | }); 17 | 18 | server.auth.default('jwt'); 19 | 20 | return next(); 21 | } 22 | 23 | function validate (decoded, request, cb) { 24 | const db = server.database; 25 | const User = db.User; 26 | return new Promise((resolve) => { 27 | User.findById(decoded.id) 28 | .then((user) => { 29 | if (!user) { 30 | return cb(null, false); 31 | } 32 | 33 | return cb(null, true); 34 | }); 35 | }); 36 | } 37 | }; 38 | 39 | exports.register.attributes = { 40 | name: 'auth-jwt', 41 | version: '1.0.0' 42 | }; 43 | 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Marcos Vinicius Bérgamo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "start-hapiness", 3 | "version": "1.0.0", 4 | "description": "Boilerplate for Hapi + MongoDB API :)", 5 | "repository": "https://github.com/thebergamo/start-hapiness", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "scripts/test", 9 | "bootstrap": "scripts/bootstrap", 10 | "setup": "scripts/setup", 11 | "update": "scripts/update", 12 | "start": "scripts/server" 13 | }, 14 | "author": "Marcos Bérgamo ", 15 | "license": "MIT", 16 | "dependencies": { 17 | "bcryptjs": "2.3.0", 18 | "bluebird": "3.0.5", 19 | "boom": "2.9.0", 20 | "dotenv": "2.0.0", 21 | "glob": "6.0.1", 22 | "good": "6.4.0", 23 | "good-console": "5.2.0", 24 | "good-file": "5.1.1", 25 | "hapi": "11.1.2", 26 | "hapi-auth-jwt2": "5.2.1", 27 | "hapi-boom-decorators": "1.0.2", 28 | "joi": "6.9.0", 29 | "jsonwebtoken": "5.4.1", 30 | "lodash": "4.5.1", 31 | "k7": "1.x.x", 32 | "k7-mongoose": "1.x.x", 33 | "shortid": "2.2.4" 34 | }, 35 | "devDependencies": { 36 | "chai": "3.3.0", 37 | "coveralls": "2.11.4", 38 | "lab": "6.2.0", 39 | "semistandard": "7.0.3" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/todos/todo/routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Controller = require('./controller'); 4 | const Validator = require('./schema'); 5 | 6 | exports.register = (server, options, next) => { 7 | // instantiate controller 8 | const controller = new Controller(server.database); 9 | 10 | server.bind(controller); 11 | server.route([ 12 | { 13 | method: 'GET', 14 | path: '/todo', 15 | config: { 16 | handler: controller.list, 17 | validate: Validator.list() 18 | } 19 | }, 20 | { 21 | method: 'GET', 22 | path: '/todo/{id}', 23 | config: { 24 | handler: controller.read, 25 | validate: Validator.read() 26 | } 27 | }, 28 | { 29 | method: 'POST', 30 | path: '/todo', 31 | config: { 32 | handler: controller.create, 33 | validate: Validator.create() 34 | } 35 | }, 36 | { 37 | method: 'PUT', 38 | path: '/todo/{id?}', 39 | config: { 40 | handler: controller.update, 41 | validate: Validator.update() 42 | } 43 | }, 44 | { 45 | method: 'DELETE', 46 | path: '/todo/{id?}', 47 | config: { 48 | handler: controller.destroy, 49 | validate: Validator.destroy() 50 | } 51 | } 52 | ]); 53 | 54 | next(); 55 | }; 56 | 57 | exports.register.attributes = { 58 | name: 'todo-route', 59 | version: '1.0.0' 60 | }; 61 | -------------------------------------------------------------------------------- /scripts/test: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # script/test: Run test suite for application. Optionally pass in a path to an 4 | # individual test file to run a single test. 5 | 6 | 7 | set -e 8 | 9 | cd "$(dirname "$0")/.." 10 | 11 | [ -z "$DEBUG" ] || set -x 12 | 13 | if [ "$NODE_ENV" = "test" ]; then 14 | # if executed and the environment is already set to `test`, then we want a 15 | # clean from scratch application. This almost always means a ci environment, 16 | # since we set the environment to `test` directly in `script/cibuild`. 17 | scripts/setup 18 | else 19 | # if the environment isn't set to `test`, set it to `test` and update the 20 | # application to ensure all dependencies are met as well as any other things 21 | # that need to be up to date, like db migrations. The environement not having 22 | # already been set to `test` almost always means this is being called on it's 23 | # own from a `development` environment. 24 | export NODE_ENV="test" 25 | 26 | scripts/update 27 | fi 28 | 29 | echo "===> Running linter..." 30 | 31 | ./node_modules/semistandard/bin/cmd.js 32 | 33 | echo "===> Running tests..." 34 | 35 | if [ ! -z "$EXPORT_COVERAGE" ]; then 36 | mkdir -p coverage 37 | 38 | ./node_modules/.bin/lab -c -l -t 85 -v -m 3000 -r lcov > ./coverage/lab.lcov 39 | 40 | cat ./coverage/lab.lcov | ./node_modules/coveralls/bin/coveralls.js 41 | 42 | rm -rf ./coverage 43 | else 44 | ./node_modules/.bin/lab -c -l -t 85 -v -m 3000 45 | fi 46 | -------------------------------------------------------------------------------- /src/todos/todo/schema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // load deps 4 | const Joi = require('joi'); 5 | 6 | const TodoValidator = { 7 | list, 8 | read, 9 | create, 10 | update, 11 | destroy 12 | }; 13 | 14 | module.exports = TodoValidator; 15 | 16 | function list () { 17 | return {}; 18 | } 19 | 20 | function read () { 21 | return { 22 | params: { 23 | id: Joi 24 | .string() 25 | .alphanum() 26 | .regex(/^(?=[a-f\d]{24}$)(\d+[a-f]|[a-f]+\d)/i, '_id') 27 | .required() 28 | } 29 | }; 30 | } 31 | 32 | function create () { 33 | return { 34 | payload: { 35 | name: Joi 36 | .string() 37 | .min(1) 38 | .max(30) 39 | .trim() 40 | .required(), 41 | checked: Joi 42 | .boolean() 43 | .default(false) 44 | .optional() 45 | } 46 | }; 47 | } 48 | 49 | function update () { 50 | return { 51 | params: { 52 | id: Joi 53 | .string() 54 | .alphanum() 55 | .regex(/^(?=[a-f\d]{24}$)(\d+[a-f]|[a-f]+\d)/i, '_id') 56 | .required() 57 | }, 58 | payload: { 59 | name: Joi 60 | .string() 61 | .min(1) 62 | .max(30) 63 | .trim() 64 | .optional(), 65 | checked: Joi 66 | .boolean() 67 | .default(false) 68 | .optional() 69 | } 70 | }; 71 | } 72 | 73 | function destroy () { 74 | return { 75 | params: { 76 | id: Joi 77 | .string() 78 | .alphanum() 79 | .regex(/^(?=[a-f\d]{24}$)(\d+[a-f]|[a-f]+\d)/i, '_id') 80 | .required() 81 | } 82 | }; 83 | } 84 | -------------------------------------------------------------------------------- /src/shared/user/model.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Promise = require('bluebird'); 4 | 5 | const bcrypt = require('bcryptjs'); 6 | const shortid = require('shortid'); 7 | const mongoose = require('k7-mongoose').mongoose; 8 | 9 | const Schema = new mongoose.Schema({ 10 | name: { 11 | type: String, 12 | required: true 13 | }, 14 | password: { 15 | type: String, 16 | required: true 17 | }, 18 | username: { 19 | type: String, 20 | unique: true, 21 | required: true 22 | }, 23 | email: { 24 | type: String, 25 | unique: true, 26 | required: true 27 | }, 28 | recoveryCode: { 29 | type: String, 30 | unique: true, 31 | default: shortid.generate 32 | } 33 | }); 34 | 35 | Schema.pre('save', function (next) { 36 | const user = this; 37 | if (!user.isModified('password')) return next(); 38 | 39 | user.password = hashPassword(user.password); 40 | 41 | return next(); 42 | }); 43 | 44 | Schema.pre('findOneAndUpdate', function () { 45 | const password = hashPassword(this.getUpdate().$set.password); 46 | 47 | if (!password) { 48 | return; 49 | } 50 | 51 | this.findOneAndUpdate({}, {password: password}); 52 | }); 53 | 54 | Schema.methods.validatePassword = function (requestPassword) { 55 | return bcrypt.compareSync(requestPassword, this.password); 56 | }; 57 | 58 | const UserModel = mongoose.model('User', Schema); 59 | 60 | module.exports = Promise.promisifyAll(UserModel); 61 | 62 | function hashPassword (password) { 63 | if (!password) { 64 | return false; 65 | } 66 | 67 | return bcrypt.hashSync(password, bcrypt.genSaltSync(8), null); 68 | } 69 | -------------------------------------------------------------------------------- /src/shared/user/routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Controller = require('./controller'); 4 | const Validator = require('./schema'); 5 | 6 | exports.register = (server, options, next) => { 7 | // instantiate controller 8 | const controller = new Controller(server.database); 9 | 10 | server.bind(controller); 11 | server.route([ 12 | { 13 | method: 'GET', 14 | path: '/user', 15 | config: { 16 | auth: false, 17 | handler: controller.list, 18 | validate: Validator.list() 19 | } 20 | }, 21 | { 22 | method: 'GET', 23 | path: '/user/{id}', 24 | config: { 25 | handler: controller.read, 26 | validate: Validator.read() 27 | } 28 | }, 29 | { 30 | method: 'POST', 31 | path: '/user', 32 | config: { 33 | auth: false, 34 | handler: controller.create, 35 | validate: Validator.create() 36 | } 37 | }, 38 | { 39 | method: 'POST', 40 | path: '/user/login', 41 | config: { 42 | auth: false, 43 | handler: controller.logIn, 44 | validate: Validator.logIn() 45 | } 46 | }, 47 | { 48 | method: 'PUT', 49 | path: '/user/{id?}', 50 | config: { 51 | handler: controller.update, 52 | validate: Validator.update() 53 | } 54 | }, 55 | { 56 | method: 'DELETE', 57 | path: '/user/{id?}', 58 | config: { 59 | handler: controller.destroy, 60 | validate: Validator.destroy() 61 | } 62 | } 63 | ]); 64 | 65 | next(); 66 | }; 67 | 68 | exports.register.attributes = { 69 | name: 'user-route', 70 | version: '1.0.0' 71 | }; 72 | -------------------------------------------------------------------------------- /src/todos/todo/controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function TodoController (db) { 4 | this.database = db; 5 | this.model = db.Todo; 6 | } 7 | 8 | TodoController.prototype = { 9 | list, 10 | read, 11 | create, 12 | update, 13 | destroy 14 | }; 15 | 16 | module.exports = TodoController; 17 | 18 | // [GET] /todo 19 | function list (request, reply) { 20 | const userId = request.auth.credentials.id; 21 | 22 | this.model.findAsync({owner: userId}) 23 | .then((todos) => { 24 | reply(todos); 25 | }) 26 | .catch((err) => { 27 | reply.badImplementation(err.message); 28 | }); 29 | } 30 | 31 | // [GET] /todo/{id} 32 | function read (request, reply) { 33 | const userId = request.auth.credentials.id; 34 | const id = request.params.id; 35 | 36 | this.model.findOneAsync({_id: id, owner: userId}) 37 | .then((todo) => { 38 | if (!todo) { 39 | reply.notFound(); 40 | return; 41 | } 42 | 43 | reply(todo); 44 | }) 45 | .catch((err) => { 46 | reply.badImplementation(err.message); 47 | }); 48 | } 49 | 50 | // [POST] /todo 51 | function create (request, reply) { 52 | const userId = request.auth.credentials.id; 53 | const payload = request.payload; 54 | 55 | payload.owner = userId; 56 | 57 | this.model.createAsync(payload) 58 | .then((todo) => { 59 | reply(todo).code(201); 60 | }) 61 | .catch((err) => { 62 | reply.badImplementation(err.message); 63 | }); 64 | } 65 | 66 | // [PUT] /todo/{id} 67 | function update (request, reply) { 68 | const userId = request.auth.credentials.id; 69 | const id = request.params.id; 70 | const payload = request.payload; 71 | 72 | this.model.findOneAndUpdateAsync({_id: id, owner: userId}, payload, { new: true }) 73 | .then((todo) => { 74 | reply(todo); 75 | }) 76 | .catch((err) => { 77 | reply.badImplementation(err.message); 78 | }); 79 | } 80 | 81 | // [DELETE] /todo/{id} 82 | function destroy (request, reply) { 83 | const userId = request.auth.credentials.id; 84 | const id = request.params.id; 85 | 86 | this.model.removeAsync({_id: id, owner: userId}) 87 | .then(() => { 88 | reply(); 89 | }) 90 | .catch((err) => { 91 | reply.badImplementation(err.message); 92 | }); 93 | } 94 | -------------------------------------------------------------------------------- /src/shared/user/schema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // load deps 4 | const Joi = require('joi'); 5 | 6 | const UserValidator = { 7 | list, 8 | read, 9 | create, 10 | logIn, 11 | update, 12 | destroy 13 | }; 14 | 15 | module.exports = UserValidator; 16 | 17 | function list () { 18 | return {}; 19 | } 20 | 21 | function read () { 22 | return { 23 | params: { 24 | id: Joi 25 | .string() 26 | .alphanum() 27 | .regex(/^(?=[a-f\d]{24}$)(\d+[a-f]|[a-f]+\d)/i, '_id') 28 | .required() 29 | } 30 | }; 31 | } 32 | 33 | function create () { 34 | return { 35 | payload: { 36 | name: Joi 37 | .string() 38 | .min(1) 39 | .max(30) 40 | .trim() 41 | .required(), 42 | username: Joi 43 | .string() 44 | .min(1) 45 | .max(20) 46 | .trim() 47 | .required(), 48 | email: Joi 49 | .string() 50 | .email() 51 | .required(), 52 | password: Joi 53 | .string() 54 | .min(6) 55 | .max(50) 56 | .trim() 57 | .required() 58 | } 59 | }; 60 | } 61 | 62 | function logIn () { 63 | return { 64 | payload: { 65 | email: Joi 66 | .string() 67 | .email() 68 | .required(), 69 | password: Joi 70 | .string() 71 | .trim() 72 | .required() 73 | } 74 | }; 75 | } 76 | 77 | function update () { 78 | return { 79 | params: { 80 | id: Joi 81 | .string() 82 | .alphanum() 83 | .regex(/^(?=[a-f\d]{24}$)(\d+[a-f]|[a-f]+\d)/i, '_id') 84 | .required() 85 | }, 86 | payload: { 87 | name: Joi 88 | .string() 89 | .min(1) 90 | .max(30) 91 | .trim() 92 | .optional(), 93 | username: Joi 94 | .string() 95 | .min(1) 96 | .max(20) 97 | .trim() 98 | .optional(), 99 | email: Joi 100 | .string() 101 | .email() 102 | .optional(), 103 | password: Joi 104 | .string() 105 | .min(6) 106 | .max(50) 107 | .trim() 108 | .optional() 109 | } 110 | }; 111 | } 112 | 113 | function destroy () { 114 | return { 115 | params: { 116 | id: Joi 117 | .string() 118 | .alphanum() 119 | .regex(/^(?=[a-f\d]{24}$)(\d+[a-f]|[a-f]+\d)/i, '_id') 120 | .required() 121 | } 122 | }; 123 | } 124 | -------------------------------------------------------------------------------- /src/shared/user/controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const jwt = require('jsonwebtoken'); 4 | 5 | function UserController (db) { 6 | this.database = db; 7 | this.model = db.User; 8 | } 9 | 10 | UserController.prototype = { 11 | list, 12 | read, 13 | create, 14 | logIn, 15 | update, 16 | destroy 17 | }; 18 | 19 | module.exports = UserController; 20 | 21 | // [GET] /user 22 | function list (request, reply) { 23 | this.model.findAsync({}) 24 | .then((users) => { 25 | reply(users); 26 | }) 27 | .catch((err) => { 28 | reply.badImplementation(err.message); 29 | }); 30 | } 31 | 32 | // [GET] /user/{id} 33 | function read (request, reply) { 34 | const id = request.params.id; 35 | 36 | this.model.findOneAsync({_id: id}) 37 | .then((user) => { 38 | if (!user) { 39 | reply.notFound(); 40 | return; 41 | } 42 | 43 | reply(user); 44 | }) 45 | .catch((err) => { 46 | reply.badImplementation(err.message); 47 | }); 48 | } 49 | 50 | // [POST] /user 51 | function create (request, reply) { 52 | const payload = request.payload; 53 | 54 | this.model.createAsync(payload) 55 | .then((user) => { 56 | const token = getToken(user.id); 57 | 58 | reply({ 59 | token: token 60 | }).code(201); 61 | }) 62 | .catch((err) => { 63 | reply.badImplementation(err.message); 64 | }); 65 | } 66 | 67 | // [POST] /user/login 68 | function logIn (request, reply) { 69 | const credentials = request.payload; 70 | 71 | this.model.findOneAsync({email: credentials.email}) 72 | .then((user) => { 73 | if (!user) { 74 | return reply.unauthorized('Email or Password invalid'); 75 | } 76 | 77 | if (!user.validatePassword(credentials.password)) { 78 | return reply.unauthorized('Email or Password invalid'); 79 | } 80 | 81 | const token = getToken(user.id); 82 | 83 | reply({ 84 | token: token 85 | }); 86 | }) 87 | .catch((err) => { 88 | reply.badImplementation(err.message); 89 | }); 90 | } 91 | 92 | // [PUT] /user 93 | function update (request, reply) { 94 | const id = request.params.id; 95 | const payload = request.payload; 96 | 97 | this.model.findOneAndUpdateAsync({_id: id}, {$set: payload}, {new: true}) 98 | .then((user) => { 99 | reply(user); 100 | }) 101 | .catch((err) => { 102 | reply.badImplementation(err.message); 103 | }); 104 | } 105 | 106 | // [DELETE] /user 107 | function destroy (request, reply) { 108 | const id = request.auth.credentials.id; 109 | 110 | this.model.removeAsync({_id: id}) 111 | .then(() => { 112 | reply(); 113 | }) 114 | .catch((err) => { 115 | reply.badImplementation(err.message); 116 | }); 117 | } 118 | 119 | function getToken (id) { 120 | const secretKey = process.env.JWT || 'stubJWT'; 121 | 122 | return jwt.sign({ 123 | id: id 124 | }, secretKey, {expiresIn: '18h'}); 125 | } 126 | -------------------------------------------------------------------------------- /src/core/bootstrap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Promise = require('bluebird'); 4 | 5 | const fs = Promise.promisifyAll(require('fs')); 6 | const url = require('url'); 7 | const path = require('path'); 8 | 9 | const Server = require('./server'); 10 | 11 | module.exports = {start}; 12 | 13 | function start () { 14 | return registerDatabase() 15 | .then(registerCorePlugins()) 16 | .then(registerModules) 17 | .then(startServer) 18 | .catch(catchError); 19 | } 20 | 21 | function registerDatabase () { 22 | return registerToServer({ 23 | register: require('k7'), 24 | options: { 25 | connectionString: getDatabaseURI(), 26 | models: ['src/**/model.js'], 27 | adapter: require('k7-mongoose'), 28 | events: { 29 | connected: () => { 30 | Server.log(['info', 'database'], 'Database connection is open!'); 31 | } 32 | } 33 | } 34 | }); 35 | // return registerToServer(require('./database')); 36 | } 37 | 38 | function registerCorePlugins () { 39 | return fs.readdirAsync(__dirname) 40 | .filter(filterCoreFiles) 41 | .map(getCoreFiles) 42 | .then(registerToServer); 43 | } 44 | 45 | function registerModules () { 46 | return fs.readdirAsync(path.join(__dirname, '..')) 47 | .filter(filterCoreDirectories) 48 | .map(getModules) 49 | .then(registerToServer); 50 | } 51 | 52 | function startServer () { 53 | if (process.env.NODE_ENV === 'test') { 54 | return Server; 55 | } 56 | 57 | Server.start(logStart); 58 | 59 | function logStart (err) { 60 | if (err) { 61 | throw err; 62 | } 63 | 64 | Server.log('info', 'Server running at: ' + Server.info.uri); 65 | } 66 | } 67 | 68 | function catchError (err) { 69 | Server.log('error', '==> App Error ' + err); 70 | process.exit(1); 71 | } 72 | 73 | function registerToServer (plugins) { 74 | return new Promise((resolve, reject) => { 75 | Server.register(plugins, (err) => { 76 | if (err) { 77 | return reject(err); 78 | } 79 | 80 | return resolve(); 81 | }); 82 | }); 83 | } 84 | 85 | function getCoreFiles (file) { 86 | return { 87 | register: require(path.join(__dirname, file)) 88 | }; 89 | } 90 | 91 | function getModules (dir) { 92 | return { 93 | register: require(path.join(__dirname, '..', dir)) 94 | }; 95 | } 96 | 97 | function filterCoreFiles (fileName) { 98 | try { 99 | let stat = fs.statSync(path.join(__dirname, fileName)); 100 | 101 | if (stat.isFile() && fileName.match(/^[^.]/) && ['server.js', 'bootstrap.js', 'database.js'].indexOf(fileName) === -1) { 102 | return true; 103 | } 104 | 105 | return false; 106 | } catch (err) { 107 | return false; 108 | } 109 | } 110 | 111 | function filterCoreDirectories (dirName) { 112 | try { 113 | let stat = fs.statSync(path.join(__dirname, '..', dirName)); 114 | if (stat.isDirectory() && dirName.match(/^[^.]/) && ['core'].indexOf(dirName) === -1) { 115 | return true; 116 | } 117 | 118 | return false; 119 | } catch (err) { 120 | return false; 121 | } 122 | } 123 | 124 | function getDatabaseURI () { 125 | return url.format({ 126 | protocol: 'mongodb', 127 | slashes: true, 128 | port: process.env.DB_PORT || 27017, 129 | hostname: process.env.DB_HOST || 'localhost', 130 | pathname: process.env.DB_NAME || 'project' 131 | }); 132 | } 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Start Hapiness 2 | === 3 | 4 | [![Build Status](https://travis-ci.org/thebergamo/start-hapiness.svg)](https://travis-ci.org/thebergamo/start-hapiness) 5 | [![Coverage Status](https://coveralls.io/repos/thebergamo/start-hapiness/badge.svg?branch=master&service=github)](https://coveralls.io/github/thebergamo/start-hapiness?branch=master) 6 | 7 | ### Getting Started 8 | This project is a boilerplate to help you develop your project with Hapi.js and MongoDB. 9 | 10 | #### Project Structure 11 | In root, we have the directories: `scripts`, `src`, `test`. 12 | 13 | * **scripts** are responsibles for the bash scripts, like: `bootstrap`, `setup`, `server`, `test` and `update`. You can read more about these scripts in [github/scripts-to-rule-them-all][scripts]. 14 | * **src** is the main directory of the source code, all files and directories in that directory are distributed by responsabilites. 15 | * **core** is where all plugins and important files to the bootstrap of the system. 16 | * **shared** here, are the entities shared in the system, like `user`. Common entities in the application need be here, isolated too. 17 | * **todos** if you have other specific entities, you can create a new directory, `todos` is that case. When a new scope is created you create a new directory for that scope and it will be loaded automatically in the bootstrap application. 18 | * **test** the tests of applications. we have inside this directory a directory named `routes` where all exposed routes are tested. 19 | 20 | #### Scopes and Entities 21 | Inside the directory `src`you will create a new directory when a new **scope** is requested, like `todos`. Inside that directory, will have an `index.js` in root of the scope directory, and some directories as **entities**. 22 | 23 | **Scopes** have **entities** and your application have many scopes. A **scope** are pieces of your application, in a case like a ecommerce, a common scope will be **products**. And in the scope products we'll have **entities** like **product** and **category**. 24 | Inside **entity** we can have new endpoint exported like Hapi plugins in the file `scope/entity/route.js`. You can declare the models and all of your logic in that file. But the recomended is structuring that like **MVC**. 25 | 26 | You'll have the files: `model.js`, `routes.js`, `validation.js`, `controller.js`. 27 | * `model.js` will have you model declaration. 28 | * `validation.js` wil have your schema of parameters for every route in your routes. 29 | * `controller.js` will be your controller, a Class that have yours methods for response the router endpoints. 30 | * `routes.js` will be an Hapi.js plugins exported your routes. 31 | 32 | Finally, in your `scope/index.js` will get all of your `routes.js` files in your **entities** directories. 33 | 34 | For more examples about the code, explore the source in this repository. 35 | 36 | ### Testing 37 | 38 | For testing you just need clone this repo and run `npm test` inside root folder of this project.; 39 | 40 | ### Contribute 41 | 42 | To contribute you can try to find an [issue or enchancment][issues] and try to 43 | implement it. Fork the project, implement the code, make tests, add yourself 44 | to the [contributors][contrib] list and send the PR to the master branch. 45 | 46 | ### License 47 | 48 | Copyright (c) 2015, Marcos Bérgamo 49 | 50 | Permission to use, copy, modify, and/or distribute this software for any purpose 51 | with or without fee is hereby granted, provided that the above copyright notice 52 | and this permission notice appear in all copies. 53 | 54 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 55 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 56 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 57 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 58 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 59 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 60 | THIS SOFTWARE. 61 | 62 | [issues]: https://github.com/thebergamo/start-hapiness/issues?q=is%3Aopen+is%3Aenchancement+is%3Abug 63 | [contrib]: contributors.md 64 | [scripts]: https://github.com/github/scripts-to-rule-them-all 65 | 66 | -------------------------------------------------------------------------------- /test/routes/todo.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe, beforeEach, before, it, expect, db, server */ 2 | 'use strict'; 3 | 4 | describe('Routes /todo', () => { 5 | let token; 6 | before((done) => { 7 | db.User.removeAsync({}) 8 | .then(() => { 9 | const options = { 10 | method: 'POST', 11 | url: '/user', 12 | payload: { 13 | name: 'Jack Bauer', 14 | username: 'jack_b', 15 | email: 'jbauer@24hours.com', 16 | password: '#24hoursRescuePresident' 17 | } 18 | }; 19 | 20 | server.inject(options, (response) => { 21 | token = response.result.token; 22 | done(); 23 | }); 24 | }); 25 | }); 26 | describe('GET /todo', () => { 27 | beforeEach((done) => { 28 | db.Todo.removeAsync({}) 29 | .then(() => { 30 | const options = { 31 | method: 'POST', 32 | url: '/todo', 33 | headers: {'Authorization': 'Bearer ' + token}, 34 | payload: {} 35 | }; 36 | 37 | for (let i = 0; i < 10; i++) { 38 | options.payload.name = 'TODO Task' + i; 39 | 40 | server.inject(options, (response) => {}); 41 | } 42 | done(); 43 | }); 44 | }); 45 | 46 | it('return 200 HTTP status code', (done) => { 47 | db.Todo.remove(() => { 48 | const options = { 49 | method: 'GET', 50 | url: '/todo', 51 | headers: {'Authorization': 'Bearer ' + token} 52 | }; 53 | 54 | server.inject(options, (response) => { 55 | expect(response).to.have.property('statusCode', 200); 56 | done(); 57 | }); 58 | }); 59 | }); 60 | 61 | it('returns an empty array when todo is empty', (done) => { 62 | db.Todo.remove(() => { 63 | const options = { 64 | method: 'GET', 65 | url: '/todo', 66 | headers: {'Authorization': 'Bearer ' + token} 67 | }; 68 | server.inject(options, (response) => { 69 | expect(response).to.have.property('result'); 70 | expect(response.result).to.have.length.least(0); 71 | done(); 72 | }); 73 | }); 74 | }); 75 | 76 | it('return 10 todo at a time', (done) => { 77 | const options = { 78 | method: 'GET', 79 | url: '/todo', 80 | headers: {'Authorization': 'Bearer ' + token} 81 | }; 82 | server.inject(options, (response) => { 83 | expect(response).to.have.property('result'); 84 | expect(response.result).to.have.length.least(10); 85 | for (let i = 0; i < 10; i++) { 86 | let todo = response.result[i]; 87 | expect(todo).to.have.property('name'); 88 | expect(todo.name).to.contain('TODO Task'); 89 | expect(todo).to.have.property('checked', false); 90 | } 91 | done(); 92 | }); 93 | }); 94 | }); 95 | 96 | describe('GET /todo/{id}', () => { 97 | let todo; 98 | before((done) => { 99 | db.Todo.removeAsync({}) 100 | .then(() => { 101 | const options = { 102 | method: 'POST', 103 | url: '/todo', 104 | payload: { 105 | name: 'TODO Tasky', 106 | checked: false 107 | }, 108 | headers: {'Authorization': 'Bearer ' + token} 109 | }; 110 | 111 | server.inject(options, (response) => { 112 | todo = response.result; 113 | done(); 114 | }); 115 | }); 116 | }); 117 | 118 | it('returns 200 HTTP status code', (done) => { 119 | const options = { 120 | method: 'GET', 121 | url: '/todo/' + todo._id, 122 | headers: {'Authorization': 'Bearer ' + token} 123 | }; 124 | server.inject(options, (response) => { 125 | expect(response).to.have.property('statusCode', 200); 126 | done(); 127 | }); 128 | }); 129 | 130 | it('returns 1 todo at a time', (done) => { 131 | const options = { 132 | method: 'GET', 133 | url: '/todo/' + todo._id, 134 | headers: {'Authorization': 'Bearer ' + token} 135 | }; 136 | server.inject(options, (response) => { 137 | expect(response).to.have.property('result'); 138 | expect(response.result).to.have.property('name', 'TODO Tasky'); 139 | expect(response.result).to.have.property('checked', false); 140 | done(); 141 | }); 142 | }); 143 | 144 | it('returns 400 HTTP status code when the specified id is invalid', (done) => { 145 | const options = { 146 | method: 'GET', 147 | url: '/todo/12', 148 | headers: {'Authorization': 'Bearer ' + token} 149 | }; 150 | server.inject(options, (response) => { 151 | expect(response).to.have.property('statusCode', 400); 152 | expect(response).to.have.property('result'); 153 | expect(response.result).to.have.property('statusCode', 400); 154 | expect(response.result).to.have.property('error', 'Bad Request'); 155 | expect(response.result).to.have.property('message', 'child "id" fails because ["id" with value "12" fails to match the _id pattern]'); 156 | 157 | done(); 158 | }); 159 | }); 160 | 161 | it('returns 404 HTTP status code when the specified id is not found', (done) => { 162 | const options = { 163 | method: 'GET', 164 | url: '/todo/561fd08d9607e21a7d39819d', 165 | headers: {'Authorization': 'Bearer ' + token} 166 | }; 167 | server.inject(options, (response) => { 168 | expect(response).to.have.property('statusCode', 404); 169 | expect(response).to.have.property('result'); 170 | expect(response.result).to.have.property('statusCode', 404); 171 | expect(response.result).to.have.property('error', 'Not Found'); 172 | 173 | done(); 174 | }); 175 | }); 176 | }); 177 | 178 | describe('POST /todo', () => { 179 | it('returns 400 HTTP status code when no body is sended', (done) => { 180 | const options = { 181 | method: 'POST', 182 | url: '/todo', 183 | headers: {'Authorization': 'Bearer ' + token} 184 | }; 185 | server.inject(options, (response) => { 186 | expect(response).to.have.property('statusCode', 400); 187 | expect(response).to.have.property('result'); 188 | expect(response.result).to.have.property('statusCode', 400); 189 | expect(response.result).to.have.property('error', 'Bad Request'); 190 | expect(response.result).to.have.property('message', '"value" must be an object'); 191 | done(); 192 | }); 193 | }); 194 | 195 | it('returns 400 HTTP status code when no `name` is send', (done) => { 196 | const options = { 197 | method: 'POST', 198 | url: '/todo', 199 | payload: {}, 200 | headers: {'Authorization': 'Bearer ' + token} 201 | }; 202 | server.inject(options, (response) => { 203 | expect(response).to.have.property('statusCode', 400); 204 | expect(response).to.have.property('result'); 205 | expect(response.result).to.have.property('statusCode', 400); 206 | expect(response.result).to.have.property('error', 'Bad Request'); 207 | expect(response.result).to.have.property('message', 'child "name" fails because ["name" is required]'); 208 | done(); 209 | }); 210 | }); 211 | 212 | it('returns 400 HTTP status code when `name` is empty', (done) => { 213 | const options = { 214 | method: 'POST', 215 | url: '/todo', 216 | payload: {name: ''}, 217 | headers: {'Authorization': 'Bearer ' + token} 218 | }; 219 | server.inject(options, (response) => { 220 | expect(response).to.have.property('statusCode', 400); 221 | expect(response).to.have.property('result'); 222 | expect(response.result).to.have.property('statusCode', 400); 223 | expect(response.result).to.have.property('error', 'Bad Request'); 224 | expect(response.result).to.have.property('message', 'child "name" fails because ["name" is not allowed to be empty]'); 225 | done(); 226 | }); 227 | }); 228 | 229 | it('returns 400 HTTP status code when `name` isn\'t a string', (done) => { 230 | const options = { 231 | method: 'POST', 232 | url: '/todo', 233 | payload: {name: 0}, 234 | headers: {'Authorization': 'Bearer ' + token} 235 | }; 236 | server.inject(options, (response) => { 237 | expect(response).to.have.property('statusCode', 400); 238 | expect(response).to.have.property('result'); 239 | expect(response.result).to.have.property('statusCode', 400); 240 | expect(response.result).to.have.property('error', 'Bad Request'); 241 | expect(response.result).to.have.property('message', 'child "name" fails because ["name" must be a string]'); 242 | done(); 243 | }); 244 | }); 245 | 246 | it('return 400 HTTP status code when `name` haven\'t more than 30 chars', (done) => { 247 | const options = { 248 | method: 'POST', 249 | url: '/todo', 250 | payload: {name: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'}, 251 | headers: {'Authorization': 'Bearer ' + token} 252 | }; 253 | server.inject(options, (response) => { 254 | expect(response).to.have.property('statusCode', 400); 255 | expect(response).to.have.property('result'); 256 | expect(response.result).to.have.property('statusCode', 400); 257 | expect(response.result).to.have.property('error', 'Bad Request'); 258 | expect(response.result).to.have.property('message', 'child "name" fails because ["name" length must be less than or equal to 30 characters long]'); 259 | done(); 260 | }); 261 | }); 262 | 263 | it('returns 201 HTTP status code when all data is correct', (done) => { 264 | const options = { 265 | method: 'POST', 266 | url: '/todo', 267 | payload: {name: 'Taskyet'}, 268 | headers: {'Authorization': 'Bearer ' + token} 269 | }; 270 | server.inject(options, (response) => { 271 | expect(response).to.have.property('statusCode', 201); 272 | expect(response).to.have.property('result'); 273 | expect(response.result).to.have.property('_id'); 274 | expect(response.result).to.have.property('name', 'Taskyet'); 275 | expect(response.result).to.have.property('checked', false); 276 | done(); 277 | }); 278 | }); 279 | }); 280 | 281 | describe('PUT /todo', () => { 282 | let todo; 283 | before((done) => { 284 | db.Todo.removeAsync({}) 285 | .then(() => { 286 | const options = { 287 | method: 'POST', 288 | url: '/todo', 289 | payload: { 290 | name: 'TodoList' 291 | }, 292 | headers: {'Authorization': 'Bearer ' + token} 293 | }; 294 | 295 | server.inject(options, (response) => { 296 | todo = response.result; 297 | done(); 298 | }); 299 | }); 300 | }); 301 | 302 | it('returns 400 HTTP status code when no `id` is send', (done) => { 303 | const options = { 304 | method: 'PUT', 305 | url: '/todo/', 306 | payload: {}, 307 | headers: {'Authorization': 'Bearer ' + token} 308 | }; 309 | server.inject(options, (response) => { 310 | expect(response).to.have.property('statusCode', 400); 311 | expect(response).to.have.property('result'); 312 | expect(response.result).to.have.property('statusCode', 400); 313 | expect(response.result).to.have.property('error', 'Bad Request'); 314 | expect(response.result).to.have.property('message', 'child "id" fails because ["id" is required]'); 315 | done(); 316 | }); 317 | }); 318 | 319 | it('returns 400 HTTP status code when `name` is empty', (done) => { 320 | const options = { 321 | method: 'PUT', 322 | url: '/todo/' + todo._id, 323 | payload: {name: ''}, 324 | headers: {'Authorization': 'Bearer ' + token} 325 | }; 326 | server.inject(options, (response) => { 327 | expect(response).to.have.property('statusCode', 400); 328 | expect(response).to.have.property('result'); 329 | expect(response.result).to.have.property('statusCode', 400); 330 | expect(response.result).to.have.property('error', 'Bad Request'); 331 | expect(response.result).to.have.property('message', 'child "name" fails because ["name" is not allowed to be empty]'); 332 | done(); 333 | }); 334 | }); 335 | 336 | it('returns 400 HTTP status code when `name` isn\'t a string', (done) => { 337 | const options = { 338 | method: 'PUT', 339 | url: '/todo/' + todo._id, 340 | payload: {name: 0}, 341 | headers: {'Authorization': 'Bearer ' + token} 342 | }; 343 | server.inject(options, (response) => { 344 | expect(response).to.have.property('statusCode', 400); 345 | expect(response).to.have.property('result'); 346 | expect(response.result).to.have.property('statusCode', 400); 347 | expect(response.result).to.have.property('error', 'Bad Request'); 348 | expect(response.result).to.have.property('message', 'child "name" fails because ["name" must be a string]'); 349 | done(); 350 | }); 351 | }); 352 | 353 | it('return 400 HTTP status code when `name` haven\'t more than 30 chars', (done) => { 354 | const options = { 355 | method: 'PUT', 356 | url: '/todo/' + todo._id, 357 | payload: {name: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'}, 358 | headers: {'Authorization': 'Bearer ' + token} 359 | }; 360 | server.inject(options, (response) => { 361 | expect(response).to.have.property('statusCode', 400); 362 | expect(response).to.have.property('result'); 363 | expect(response.result).to.have.property('statusCode', 400); 364 | expect(response.result).to.have.property('error', 'Bad Request'); 365 | expect(response.result).to.have.property('message', 'child "name" fails because ["name" length must be less than or equal to 30 characters long]'); 366 | done(); 367 | }); 368 | }); 369 | 370 | it('returns 200 HTTP status code when all data is correct', (done) => { 371 | const options = { 372 | method: 'PUT', 373 | url: '/todo/' + todo._id, 374 | payload: {name: 'Taskyet'}, 375 | headers: {'Authorization': 'Bearer ' + token} 376 | }; 377 | server.inject(options, (response) => { 378 | expect(response).to.have.property('statusCode', 200); 379 | expect(response).to.have.property('result'); 380 | expect(response.result).to.have.property('_id'); 381 | expect(response.result).to.have.property('name', 'Taskyet'); 382 | expect(response.result).to.have.property('checked', false); 383 | done(); 384 | }); 385 | }); 386 | }); 387 | 388 | describe('DELETE /todo/{id}', () => { 389 | let todo; 390 | before((done) => { 391 | db.Todo.removeAsync({}) 392 | .then(() => { 393 | const options = { 394 | method: 'POST', 395 | url: '/todo', 396 | headers: {'Authorization': 'Bearer ' + token}, 397 | payload: { 398 | name: 'TaskMore', 399 | checked: true 400 | } 401 | }; 402 | 403 | server.inject(options, (response) => { 404 | todo = response.result; 405 | done(); 406 | }); 407 | }); 408 | }); 409 | 410 | it('returns 400 HTTP status code when no `id` is send', (done) => { 411 | const options = { 412 | method: 'DELETE', 413 | url: '/todo/', 414 | headers: {'Authorization': 'Bearer ' + token} 415 | }; 416 | server.inject(options, (response) => { 417 | expect(response).to.have.property('statusCode', 400); 418 | expect(response).to.have.property('result'); 419 | expect(response.result).to.have.property('statusCode', 400); 420 | expect(response.result).to.have.property('error', 'Bad Request'); 421 | expect(response.result).to.have.property('message', 'child "id" fails because ["id" is required]'); 422 | done(); 423 | }); 424 | }); 425 | 426 | it('returns 200 HTTP status code when record is deleted', (done) => { 427 | const options = { 428 | method: 'DELETE', 429 | url: '/todo/' + todo._id, 430 | headers: {'Authorization': 'Bearer ' + token} 431 | }; 432 | server.inject(options, (response) => { 433 | expect(response).to.have.property('statusCode', 200); 434 | expect(response).to.have.property('result'); 435 | expect(response.result).to.be.empty; 436 | done(); 437 | }); 438 | }); 439 | }); 440 | }); 441 | -------------------------------------------------------------------------------- /test/routes/user.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe, beforeEach, before, it, expect, db, server */ 2 | 'use strict'; 3 | 4 | let jwt = require('jsonwebtoken'); 5 | const JWT = process.env.JWT || 'stubJWT'; 6 | 7 | describe('Routes /user', () => { 8 | describe('GET /user', () => { 9 | beforeEach((done) => { 10 | db.User.removeAsync({}) 11 | .then(() => { 12 | const options = { 13 | method: 'POST', 14 | url: '/user', 15 | payload: {} 16 | }; 17 | 18 | for (let i = 0; i < 5; i++) { 19 | options.payload = { 20 | name: 'User ' + i, 21 | password: '12345678', 22 | username: 'user_' + i, 23 | email: 'user_' + i + '@example.com' 24 | }; 25 | 26 | server.inject(options, (response) => {}); 27 | } 28 | done(); 29 | }); 30 | }); 31 | 32 | it('return 200 HTTP status code', (done) => { 33 | db.User.remove(() => { 34 | const options = {method: 'GET', url: '/user'}; 35 | server.inject(options, (response) => { 36 | expect(response).to.have.property('statusCode', 200); 37 | done(); 38 | }); 39 | }); 40 | }); 41 | 42 | it('return an empty array when users is empty', (done) => { 43 | db.User.remove(() => { 44 | let options = {method: 'GET', url: '/user'}; 45 | server.inject(options, (response) => { 46 | expect(response).to.have.property('result'); 47 | expect(response.result).to.have.length.least(0); 48 | done(); 49 | }); 50 | }); 51 | }); 52 | 53 | it('return 5 users at a time', (done) => { 54 | const options = {method: 'GET', url: '/user'}; 55 | server.inject(options, (response) => { 56 | expect(response).to.have.property('result'); 57 | expect(response.result).to.have.length.least(5); 58 | for (let i = 0; i < 5; i++) { 59 | let user = response.result[i]; 60 | expect(user).to.have.property('name', 'User ' + i); 61 | expect(user).to.have.property('username', 'user_' + i); 62 | expect(user).to.have.property('email', 'user_' + i + '@example.com'); 63 | } 64 | done(); 65 | }); 66 | }); 67 | }); 68 | 69 | describe('GET /user/{id}', () => { 70 | let token; 71 | let userInfo; 72 | before((done) => { 73 | db.User.removeAsync({}) 74 | .then(() => { 75 | const options = { 76 | method: 'POST', 77 | url: '/user', 78 | payload: { 79 | name: 'Jack Bauer', 80 | username: 'jack_b', 81 | email: 'jbauer@24hours.com', 82 | password: '#24hoursRescuePresident' 83 | } 84 | }; 85 | 86 | server.inject(options, (response) => { 87 | token = response.result.token; 88 | userInfo = jwt.verify(token, JWT); 89 | done(); 90 | }); 91 | }); 92 | }); 93 | 94 | describe('when user is not authenticated', () => { 95 | it('returns 401 HTTP status code', (done) => { 96 | const options = {method: 'GET', url: '/user/' + userInfo.id}; 97 | server.inject(options, (response) => { 98 | expect(response).to.have.property('statusCode', 401); 99 | done(); 100 | }); 101 | }); 102 | }); 103 | 104 | describe('when user is authenticated', () => { 105 | it('returns 200 HTTP status code', (done) => { 106 | const options = { 107 | method: 'GET', 108 | url: '/user/' + userInfo.id, 109 | headers: {'Authorization': 'Bearer ' + token} 110 | }; 111 | 112 | server.inject(options, (response) => { 113 | expect(response).to.have.property('statusCode', 200); 114 | done(); 115 | }); 116 | }); 117 | 118 | it('returns 1 user at a time', (done) => { 119 | const options = { 120 | method: 'GET', 121 | url: '/user/' + userInfo.id, 122 | headers: {'Authorization': 'Bearer ' + token} 123 | }; 124 | 125 | server.inject(options, (response) => { 126 | expect(response.result).to.have.property('name', 'Jack Bauer'); 127 | expect(response.result).to.have.property('username', 'jack_b'); 128 | expect(response.result).to.have.property('email', 'jbauer@24hours.com'); 129 | done(); 130 | }); 131 | }); 132 | 133 | it('return 400 HTTP status code when the specified id is invalid', (done) => { 134 | const options = { 135 | method: 'GET', 136 | url: '/user/12', 137 | headers: {'Authorization': 'Bearer ' + token} 138 | }; 139 | 140 | server.inject(options, (response) => { 141 | expect(response).to.have.property('statusCode', 400); 142 | expect(response).to.have.property('result'); 143 | expect(response.result).to.have.property('statusCode', 400); 144 | expect(response.result).to.have.property('error', 'Bad Request'); 145 | expect(response.result).to.have.property('message', 'child "id" fails because ["id" with value "12" fails to match the _id pattern]'); 146 | done(); 147 | }); 148 | }); 149 | 150 | it('return 404 HTTP status code when the specified id is not found', (done) => { 151 | const options = { 152 | method: 'GET', 153 | url: '/user/561fd08d9607e21a7d39819d', 154 | headers: {'Authorization': 'Bearer ' + token} 155 | }; 156 | 157 | server.inject(options, (response) => { 158 | expect(response).to.have.property('statusCode', 404); 159 | expect(response).to.have.property('result'); 160 | expect(response.result).to.have.property('statusCode', 404); 161 | expect(response.result).to.have.property('error', 'Not Found'); 162 | 163 | done(); 164 | }); 165 | }); 166 | }); 167 | }); 168 | 169 | describe('POST /user', () => { 170 | beforeEach((done) => { 171 | return db.User.removeAsync({}) 172 | .then(() => { 173 | done(); 174 | }); 175 | }); 176 | 177 | it('returns 400 HTTP status code when no body is sended', (done) => { 178 | const options = {method: 'POST', url: '/user'}; 179 | server.inject(options, (response) => { 180 | expect(response).to.have.property('statusCode', 400); 181 | expect(response).to.have.property('result'); 182 | expect(response.result).to.have.property('statusCode', 400); 183 | expect(response.result).to.have.property('error', 'Bad Request'); 184 | expect(response.result).to.have.property('message', '"value" must be an object'); 185 | done(); 186 | }); 187 | }); 188 | 189 | it('returns 400 HTTP status code when no `name` is send', (done) => { 190 | const options = {method: 'POST', url: '/user', payload: {}}; 191 | server.inject(options, (response) => { 192 | expect(response).to.have.property('statusCode', 400); 193 | expect(response).to.have.property('result'); 194 | expect(response.result).to.have.property('statusCode', 400); 195 | expect(response.result).to.have.property('error', 'Bad Request'); 196 | expect(response.result).to.have.property('message', 'child "name" fails because ["name" is required]'); 197 | done(); 198 | }); 199 | }); 200 | 201 | it('returns 400 HTTP status code when `name` is empty', (done) => { 202 | const options = {method: 'POST', url: '/user', payload: {name: ''}}; 203 | server.inject(options, (response) => { 204 | expect(response).to.have.property('statusCode', 400); 205 | expect(response).to.have.property('result'); 206 | expect(response.result).to.have.property('statusCode', 400); 207 | expect(response.result).to.have.property('error', 'Bad Request'); 208 | expect(response.result).to.have.property('message', 'child "name" fails because ["name" is not allowed to be empty]'); 209 | done(); 210 | }); 211 | }); 212 | 213 | it('returns 400 HTTP status code when `name` isn\'t a string', (done) => { 214 | const options = {method: 'POST', url: '/user', payload: {name: 0}}; 215 | server.inject(options, (response) => { 216 | expect(response).to.have.property('statusCode', 400); 217 | expect(response).to.have.property('result'); 218 | expect(response.result).to.have.property('statusCode', 400); 219 | expect(response.result).to.have.property('error', 'Bad Request'); 220 | expect(response.result).to.have.property('message', 'child "name" fails because ["name" must be a string]'); 221 | done(); 222 | }); 223 | }); 224 | 225 | it('return 400 HTTP status code when `name` haven\'t more than 30 chars', (done) => { 226 | const options = {method: 'POST', url: '/user', payload: {name: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'}}; 227 | server.inject(options, (response) => { 228 | expect(response).to.have.property('statusCode', 400); 229 | expect(response).to.have.property('result'); 230 | expect(response.result).to.have.property('statusCode', 400); 231 | expect(response.result).to.have.property('error', 'Bad Request'); 232 | expect(response.result).to.have.property('message', 'child "name" fails because ["name" length must be less than or equal to 30 characters long]'); 233 | done(); 234 | }); 235 | }); 236 | 237 | it('returns 400 HTTP status code when no `username` is send', (done) => { 238 | const options = {method: 'POST', url: '/user', payload: {name: 'Marcos'}}; 239 | server.inject(options, (response) => { 240 | expect(response).to.have.property('statusCode', 400); 241 | expect(response).to.have.property('result'); 242 | expect(response.result).to.have.property('statusCode', 400); 243 | expect(response.result).to.have.property('error', 'Bad Request'); 244 | expect(response.result).to.have.property('message', 'child "username" fails because ["username" is required]'); 245 | done(); 246 | }); 247 | }); 248 | 249 | it('returns 400 HTTP status code when `username` is empty', (done) => { 250 | const options = {method: 'POST', url: '/user', payload: {name: 'Marcos', username: ''}}; 251 | server.inject(options, (response) => { 252 | expect(response).to.have.property('statusCode', 400); 253 | expect(response).to.have.property('result'); 254 | expect(response.result).to.have.property('statusCode', 400); 255 | expect(response.result).to.have.property('error', 'Bad Request'); 256 | expect(response.result).to.have.property('message', 'child "username" fails because ["username" is not allowed to be empty]'); 257 | done(); 258 | }); 259 | }); 260 | 261 | it('returns 400 HTTP status code when `username` isn\'t a string', (done) => { 262 | const options = {method: 'POST', url: '/user', payload: {name: 'Marcos', username: 0}}; 263 | server.inject(options, (response) => { 264 | expect(response).to.have.property('statusCode', 400); 265 | expect(response).to.have.property('result'); 266 | expect(response.result).to.have.property('statusCode', 400); 267 | expect(response.result).to.have.property('error', 'Bad Request'); 268 | expect(response.result).to.have.property('message', 'child "username" fails because ["username" must be a string]'); 269 | done(); 270 | }); 271 | }); 272 | 273 | it('return 400 HTTP status code when `username` haven\'t more than 20 chars', (done) => { 274 | const options = {method: 'POST', url: '/user', payload: {name: 'Marcos', username: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'}}; 275 | server.inject(options, (response) => { 276 | expect(response).to.have.property('statusCode', 400); 277 | expect(response).to.have.property('result'); 278 | expect(response.result).to.have.property('statusCode', 400); 279 | expect(response.result).to.have.property('error', 'Bad Request'); 280 | expect(response.result).to.have.property('message', 'child "username" fails because ["username" length must be less than or equal to 20 characters long]'); 281 | done(); 282 | }); 283 | }); 284 | 285 | it('returns 400 HTTP status code when no `email` is sent', (done) => { 286 | const options = {method: 'POST', url: '/user', payload: {name: 'Marcos', username: 'marc'}}; 287 | server.inject(options, (response) => { 288 | expect(response).to.have.property('statusCode', 400); 289 | expect(response).to.have.property('result'); 290 | expect(response.result).to.have.property('statusCode', 400); 291 | expect(response.result).to.have.property('error', 'Bad Request'); 292 | expect(response.result).to.have.property('message', 'child "email" fails because ["email" is required]'); 293 | done(); 294 | }); 295 | }); 296 | 297 | it('returns 400 HTTP status code when `email` is empty', (done) => { 298 | const options = {method: 'POST', url: '/user', payload: {name: 'Marcos', username: 'marc', email: ''}}; 299 | server.inject(options, (response) => { 300 | expect(response).to.have.property('statusCode', 400); 301 | expect(response).to.have.property('result'); 302 | expect(response.result).to.have.property('statusCode', 400); 303 | expect(response.result).to.have.property('error', 'Bad Request'); 304 | expect(response.result).to.have.property('message', 'child "email" fails because ["email" is not allowed to be empty]'); 305 | done(); 306 | }); 307 | }); 308 | 309 | it('returns 400 HTTP status code when `email` isn\'t a string ', (done) => { 310 | const options = {method: 'POST', url: '/user', payload: {name: 'Marcos', username: 'marc', email: 0}}; 311 | server.inject(options, (response) => { 312 | expect(response).to.have.property('statusCode', 400); 313 | expect(response).to.have.property('result'); 314 | expect(response.result).to.have.property('statusCode', 400); 315 | expect(response.result).to.have.property('error', 'Bad Request'); 316 | expect(response.result).to.have.property('message', 'child "email" fails because ["email" must be a string]'); 317 | done(); 318 | }); 319 | }); 320 | 321 | it('returns 400 HTTP status code when `email` is invalid email', (done) => { 322 | const options = {method: 'POST', url: '/user', payload: {name: 'Marcos', username: 'marc', email: 'notanemail'}}; 323 | server.inject(options, (response) => { 324 | expect(response).to.have.property('statusCode', 400); 325 | expect(response).to.have.property('result'); 326 | expect(response.result).to.have.property('statusCode', 400); 327 | expect(response.result).to.have.property('error', 'Bad Request'); 328 | expect(response.result).to.have.property('message', 'child "email" fails because ["email" must be a valid email]'); 329 | done(); 330 | }); 331 | }); 332 | 333 | it('returns 400 HTTP status code when no `password` is sent', (done) => { 334 | const options = {method: 'POST', url: '/user', payload: {name: 'Marcos', username: 'marc', email: 'marcos@thedon.com.br'}}; 335 | server.inject(options, (response) => { 336 | expect(response).to.have.property('statusCode', 400); 337 | expect(response).to.have.property('result'); 338 | expect(response.result).to.have.property('statusCode', 400); 339 | expect(response.result).to.have.property('error', 'Bad Request'); 340 | expect(response.result).to.have.property('message', 'child "password" fails because ["password" is required]'); 341 | done(); 342 | }); 343 | }); 344 | 345 | it('returns 400 HTTP status code when `password` is empty', (done) => { 346 | const options = {method: 'POST', url: '/user', payload: {name: 'Marcos', username: 'marc', email: 'marcos@thedon.com.br', password: ''}}; 347 | server.inject(options, (response) => { 348 | expect(response).to.have.property('statusCode', 400); 349 | expect(response).to.have.property('result'); 350 | expect(response.result).to.have.property('statusCode', 400); 351 | expect(response.result).to.have.property('error', 'Bad Request'); 352 | expect(response.result).to.have.property('message', 'child "password" fails because ["password" is not allowed to be empty]'); 353 | done(); 354 | }); 355 | }); 356 | 357 | it('returns 400 HTTP status code when `password` isn\'t a string ', (done) => { 358 | const options = {method: 'POST', url: '/user', payload: {name: 'Marcos', username: 'marc', email: 'marcos@thedon.com.br', password: 0}}; 359 | server.inject(options, (response) => { 360 | expect(response).to.have.property('statusCode', 400); 361 | expect(response).to.have.property('result'); 362 | expect(response.result).to.have.property('statusCode', 400); 363 | expect(response.result).to.have.property('error', 'Bad Request'); 364 | expect(response.result).to.have.property('message', 'child "password" fails because ["password" must be a string]'); 365 | done(); 366 | }); 367 | }); 368 | 369 | it('return 400 HTTP status code when `password` haven\'t least than 6 chars', (done) => { 370 | const options = {method: 'POST', url: '/user', payload: {name: 'Marcos', username: 'marc', email: 'marcos@thedon.com.br', password: 'aaa'}}; 371 | server.inject(options, (response) => { 372 | expect(response).to.have.property('statusCode', 400); 373 | expect(response).to.have.property('result'); 374 | expect(response.result).to.have.property('statusCode', 400); 375 | expect(response.result).to.have.property('error', 'Bad Request'); 376 | expect(response.result).to.have.property('message', 'child "password" fails because ["password" length must be at least 6 characters long]'); 377 | done(); 378 | }); 379 | }); 380 | 381 | it('return 400 HTTP status code when `password` haven\'t more than 50 chars', (done) => { 382 | const options = {method: 'POST', url: '/user', payload: {name: 'Marcos', username: 'marc', email: 'marcos@thedon.com.br', password: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'}}; 383 | server.inject(options, (response) => { 384 | expect(response).to.have.property('statusCode', 400); 385 | expect(response).to.have.property('result'); 386 | expect(response.result).to.have.property('statusCode', 400); 387 | expect(response.result).to.have.property('error', 'Bad Request'); 388 | expect(response.result).to.have.property('message', 'child "password" fails because ["password" length must be less than or equal to 50 characters long]'); 389 | done(); 390 | }); 391 | }); 392 | 393 | it('returns 201 HTTP status code when all data is correct', (done) => { 394 | const options = {method: 'POST', url: '/user', payload: {name: 'Jack B', username: 'jack_b', email: 'jack_b@24h.com', password: '123456'}}; 395 | server.inject(options, (response) => { 396 | expect(response).to.have.property('statusCode', 201); 397 | expect(response).to.have.property('result'); 398 | expect(response.result).to.have.property('token'); 399 | done(); 400 | }); 401 | }); 402 | }); 403 | 404 | describe('PUT /user/{id}', () => { 405 | let userInfo; 406 | let token; 407 | before((done) => { 408 | db.User.removeAsync({}) 409 | .then(() => { 410 | const options = { 411 | method: 'POST', 412 | url: '/user', 413 | payload: { 414 | name: 'Jack Bauer', 415 | username: 'jack_b', 416 | email: 'jbauer@24hours.com', 417 | password: '#24hoursRescuePresident' 418 | } 419 | }; 420 | 421 | server.inject(options, (response) => { 422 | token = response.result.token; 423 | userInfo = jwt.verify(token, JWT); 424 | done(); 425 | }); 426 | }); 427 | }); 428 | 429 | it('returns 400 HTTP status code when `name` is empty', (done) => { 430 | const options = { 431 | method: 'PUT', 432 | url: '/user/' + userInfo.id, 433 | payload: {name: ''}, 434 | headers: {'Authorization': 'Bearer ' + token} 435 | }; 436 | server.inject(options, (response) => { 437 | expect(response).to.have.property('statusCode', 400); 438 | expect(response).to.have.property('result'); 439 | expect(response.result).to.have.property('statusCode', 400); 440 | expect(response.result).to.have.property('error', 'Bad Request'); 441 | expect(response.result).to.have.property('message', 'child "name" fails because ["name" is not allowed to be empty]'); 442 | done(); 443 | }); 444 | }); 445 | 446 | it('returns 400 HTTP status code when `name` isn\'t a string', (done) => { 447 | const options = { 448 | method: 'PUT', 449 | url: '/user/' + userInfo.id, 450 | payload: {name: 0}, 451 | headers: {'Authorization': 'Bearer ' + token} 452 | }; 453 | server.inject(options, (response) => { 454 | expect(response).to.have.property('statusCode', 400); 455 | expect(response).to.have.property('result'); 456 | expect(response.result).to.have.property('statusCode', 400); 457 | expect(response.result).to.have.property('error', 'Bad Request'); 458 | expect(response.result).to.have.property('message', 'child "name" fails because ["name" must be a string]'); 459 | done(); 460 | }); 461 | }); 462 | 463 | it('return 400 HTTP status code when `name` haven\'t more than 30 chars', (done) => { 464 | const options = { 465 | method: 'PUT', 466 | url: '/user/' + userInfo.id, 467 | payload: {name: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'}, 468 | headers: {'Authorization': 'Bearer ' + token} 469 | }; 470 | server.inject(options, (response) => { 471 | expect(response).to.have.property('statusCode', 400); 472 | expect(response).to.have.property('result'); 473 | expect(response.result).to.have.property('statusCode', 400); 474 | expect(response.result).to.have.property('error', 'Bad Request'); 475 | expect(response.result).to.have.property('message', 'child "name" fails because ["name" length must be less than or equal to 30 characters long]'); 476 | done(); 477 | }); 478 | }); 479 | 480 | it('returns 400 HTTP status code when `username` is empty', (done) => { 481 | const options = { 482 | method: 'PUT', 483 | url: '/user/' + userInfo.id, 484 | payload: {name: 'Marcos', username: ''}, 485 | headers: {'Authorization': 'Bearer ' + token} 486 | }; 487 | server.inject(options, (response) => { 488 | expect(response).to.have.property('statusCode', 400); 489 | expect(response).to.have.property('result'); 490 | expect(response.result).to.have.property('statusCode', 400); 491 | expect(response.result).to.have.property('error', 'Bad Request'); 492 | expect(response.result).to.have.property('message', 'child "username" fails because ["username" is not allowed to be empty]'); 493 | done(); 494 | }); 495 | }); 496 | 497 | it('returns 400 HTTP status code when `username` isn\'t a string', (done) => { 498 | const options = { 499 | method: 'PUT', 500 | url: '/user/' + userInfo.id, 501 | payload: {name: 'Marcos', username: 0}, 502 | headers: {'Authorization': 'Bearer ' + token} 503 | }; 504 | server.inject(options, (response) => { 505 | expect(response).to.have.property('statusCode', 400); 506 | expect(response).to.have.property('result'); 507 | expect(response.result).to.have.property('statusCode', 400); 508 | expect(response.result).to.have.property('error', 'Bad Request'); 509 | expect(response.result).to.have.property('message', 'child "username" fails because ["username" must be a string]'); 510 | done(); 511 | }); 512 | }); 513 | 514 | it('return 400 HTTP status code when `username` haven\'t more than 20 chars', (done) => { 515 | const options = { 516 | method: 'PUT', 517 | url: '/user/' + userInfo.id, 518 | payload: {name: 'Marcos', username: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'}, 519 | headers: {'Authorization': 'Bearer ' + token} 520 | }; 521 | server.inject(options, (response) => { 522 | expect(response).to.have.property('statusCode', 400); 523 | expect(response).to.have.property('result'); 524 | expect(response.result).to.have.property('statusCode', 400); 525 | expect(response.result).to.have.property('error', 'Bad Request'); 526 | expect(response.result).to.have.property('message', 'child "username" fails because ["username" length must be less than or equal to 20 characters long]'); 527 | done(); 528 | }); 529 | }); 530 | 531 | it('returns 400 HTTP status code when `email` is empty', (done) => { 532 | const options = { 533 | method: 'PUT', 534 | url: '/user/' + userInfo.id, 535 | payload: {name: 'Marcos', username: 'marc', email: ''}, 536 | headers: {'Authorization': 'Bearer ' + token} 537 | }; 538 | server.inject(options, (response) => { 539 | expect(response).to.have.property('statusCode', 400); 540 | expect(response).to.have.property('result'); 541 | expect(response.result).to.have.property('statusCode', 400); 542 | expect(response.result).to.have.property('error', 'Bad Request'); 543 | expect(response.result).to.have.property('message', 'child "email" fails because ["email" is not allowed to be empty]'); 544 | done(); 545 | }); 546 | }); 547 | 548 | it('returns 400 HTTP status code when `email` isn\'t a string ', (done) => { 549 | const options = { 550 | method: 'PUT', 551 | url: '/user/' + userInfo.id, 552 | payload: {name: 'Marcos', username: 'marc', email: 0}, 553 | headers: {'Authorization': 'Bearer ' + token} 554 | }; 555 | server.inject(options, (response) => { 556 | expect(response).to.have.property('statusCode', 400); 557 | expect(response).to.have.property('result'); 558 | expect(response.result).to.have.property('statusCode', 400); 559 | expect(response.result).to.have.property('error', 'Bad Request'); 560 | expect(response.result).to.have.property('message', 'child "email" fails because ["email" must be a string]'); 561 | done(); 562 | }); 563 | }); 564 | 565 | it('returns 400 HTTP status code when `email` is invalid email', (done) => { 566 | const options = { 567 | method: 'PUT', 568 | url: '/user/' + userInfo.id, 569 | payload: {name: 'Marcos', username: 'marc', email: 'notanemail'}, 570 | headers: {'Authorization': 'Bearer ' + token} 571 | }; 572 | server.inject(options, (response) => { 573 | expect(response).to.have.property('statusCode', 400); 574 | expect(response).to.have.property('result'); 575 | expect(response.result).to.have.property('statusCode', 400); 576 | expect(response.result).to.have.property('error', 'Bad Request'); 577 | expect(response.result).to.have.property('message', 'child "email" fails because ["email" must be a valid email]'); 578 | done(); 579 | }); 580 | }); 581 | 582 | it('returns 400 HTTP status code when `password` is empty', (done) => { 583 | const options = { 584 | method: 'PUT', 585 | url: '/user/' + userInfo.id, 586 | payload: {name: 'Marcos', username: 'marc', email: 'marcos@thedon.com.br', password: ''}, 587 | headers: {'Authorization': 'Bearer ' + token} 588 | }; 589 | server.inject(options, (response) => { 590 | expect(response).to.have.property('statusCode', 400); 591 | expect(response).to.have.property('result'); 592 | expect(response.result).to.have.property('statusCode', 400); 593 | expect(response.result).to.have.property('error', 'Bad Request'); 594 | expect(response.result).to.have.property('message', 'child "password" fails because ["password" is not allowed to be empty]'); 595 | done(); 596 | }); 597 | }); 598 | 599 | it('returns 400 HTTP status code when `password` isn\'t a string ', (done) => { 600 | const options = { 601 | method: 'PUT', 602 | url: '/user/' + userInfo.id, 603 | payload: {name: 'Marcos', username: 'marc', email: 'marcos@thedon.com.br', password: 0}, 604 | headers: {'Authorization': 'Bearer ' + token} 605 | }; 606 | server.inject(options, (response) => { 607 | expect(response).to.have.property('statusCode', 400); 608 | expect(response).to.have.property('result'); 609 | expect(response.result).to.have.property('statusCode', 400); 610 | expect(response.result).to.have.property('error', 'Bad Request'); 611 | expect(response.result).to.have.property('message', 'child "password" fails because ["password" must be a string]'); 612 | done(); 613 | }); 614 | }); 615 | 616 | it('return 400 HTTP status code when `password` haven\'t least than 6 chars', (done) => { 617 | const options = { 618 | method: 'PUT', 619 | url: '/user/' + userInfo.id, 620 | payload: {name: 'Marcos', username: 'marc', email: 'marcos@thedon.com.br', password: 'aaa'}, 621 | headers: {'Authorization': 'Bearer ' + token} 622 | }; 623 | server.inject(options, (response) => { 624 | expect(response).to.have.property('statusCode', 400); 625 | expect(response).to.have.property('result'); 626 | expect(response.result).to.have.property('statusCode', 400); 627 | expect(response.result).to.have.property('error', 'Bad Request'); 628 | expect(response.result).to.have.property('message', 'child "password" fails because ["password" length must be at least 6 characters long]'); 629 | done(); 630 | }); 631 | }); 632 | 633 | it('return 400 HTTP status code when `password` haven\'t more than 50 chars', (done) => { 634 | const options = { 635 | method: 'PUT', 636 | url: '/user/' + userInfo.id, 637 | payload: {name: 'Marcos', username: 'marc', email: 'marcos@thedon.com.br', password: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'}, 638 | headers: {'Authorization': 'Bearer ' + token} 639 | }; 640 | server.inject(options, (response) => { 641 | expect(response).to.have.property('statusCode', 400); 642 | expect(response).to.have.property('result'); 643 | expect(response.result).to.have.property('statusCode', 400); 644 | expect(response.result).to.have.property('error', 'Bad Request'); 645 | expect(response.result).to.have.property('message', 'child "password" fails because ["password" length must be less than or equal to 50 characters long]'); 646 | done(); 647 | }); 648 | }); 649 | 650 | it('returns 200 HTTP status code when all data is correct', (done) => { 651 | const options = { 652 | method: 'PUT', 653 | url: '/user/' + userInfo.id, 654 | payload: {name: 'Jack B R', username: 'jack_br', email: 'jack_br@24h.com', password: '123456vv'}, 655 | headers: {'Authorization': 'Bearer ' + token} 656 | }; 657 | server.inject(options, (response) => { 658 | expect(response).to.have.property('statusCode', 200); 659 | expect(response).to.have.property('result'); 660 | expect(response.result).to.have.property('id'); 661 | expect(response.result).to.have.property('name', 'Jack B R'); 662 | expect(response.result).to.have.property('username', 'jack_br'); 663 | expect(response.result).to.have.property('email', 'jack_br@24h.com'); 664 | done(); 665 | }); 666 | }); 667 | }); 668 | 669 | describe('POST /user/login', () => { 670 | before((done) => { 671 | db.User.removeAsync({}) 672 | .then(() => { 673 | const options = { 674 | method: 'POST', 675 | url: '/user', 676 | payload: { 677 | name: 'Jack Bauer', 678 | username: 'jack_b', 679 | email: 'jbauer@24hours.com', 680 | password: '#24hoursRescuePresident' 681 | } 682 | }; 683 | 684 | server.inject(options, (response) => { 685 | done(); 686 | }); 687 | }); 688 | }); 689 | 690 | it('returns 400 HTTP status code when no `email` is send', (done) => { 691 | const options = { 692 | method: 'POST', 693 | url: '/user/login', 694 | payload: {} 695 | }; 696 | server.inject(options, (response) => { 697 | expect(response).to.have.property('statusCode', 400); 698 | expect(response).to.have.property('result'); 699 | expect(response.result).to.have.property('statusCode', 400); 700 | expect(response.result).to.have.property('error', 'Bad Request'); 701 | expect(response.result).to.have.property('message', 'child "email" fails because ["email" is required]'); 702 | done(); 703 | }); 704 | }); 705 | 706 | it('returns 400 HTTP status code when no `password` is send', (done) => { 707 | const options = { 708 | method: 'POST', 709 | url: '/user/login', 710 | payload: {email: 'jack@24h.com'} 711 | }; 712 | server.inject(options, (response) => { 713 | expect(response).to.have.property('statusCode', 400); 714 | expect(response).to.have.property('result'); 715 | expect(response.result).to.have.property('statusCode', 400); 716 | expect(response.result).to.have.property('error', 'Bad Request'); 717 | expect(response.result).to.have.property('message', 'child "password" fails because ["password" is required]'); 718 | done(); 719 | }); 720 | }); 721 | 722 | it('returns 400 HTTP status code when `email` is invalid', (done) => { 723 | const options = { 724 | method: 'POST', 725 | url: '/user/login', 726 | payload: {email: 'jack'} 727 | }; 728 | server.inject(options, (response) => { 729 | expect(response).to.have.property('statusCode', 400); 730 | expect(response).to.have.property('result'); 731 | expect(response.result).to.have.property('statusCode', 400); 732 | expect(response.result).to.have.property('error', 'Bad Request'); 733 | expect(response.result).to.have.property('message', 'child "email" fails because ["email" must be a valid email]'); 734 | done(); 735 | }); 736 | }); 737 | 738 | it('returns 401 HTTP status code when `email` isn`t in our base', (done) => { 739 | const options = { 740 | method: 'POST', 741 | url: '/user/login', 742 | payload: {email: 'jack_b@24h.com', password: 'd'} 743 | }; 744 | server.inject(options, (response) => { 745 | expect(response).to.have.property('statusCode', 401); 746 | expect(response).to.have.property('result'); 747 | expect(response.result).to.have.property('statusCode', 401); 748 | expect(response.result).to.have.property('error', 'Unauthorized'); 749 | expect(response.result).to.have.property('message', 'Email or Password invalid'); 750 | done(); 751 | }); 752 | }); 753 | 754 | it('returns 401 HTTP status code when `password` is incorrect for this user', (done) => { 755 | const options = { 756 | method: 'POST', 757 | url: '/user/login', 758 | payload: {email: 'jbauer@24h.com', password: 'mmm'} 759 | }; 760 | server.inject(options, (response) => { 761 | expect(response).to.have.property('statusCode', 401); 762 | expect(response).to.have.property('result'); 763 | expect(response.result).to.have.property('statusCode', 401); 764 | expect(response.result).to.have.property('error', 'Unauthorized'); 765 | expect(response.result).to.have.property('message', 'Email or Password invalid'); 766 | done(); 767 | }); 768 | }); 769 | 770 | it('returns 200 HTTP status code when success login', (done) => { 771 | const options = { 772 | method: 'POST', 773 | url: '/user/login', 774 | payload: {email: 'jbauer@24hours.com', password: '#24hoursRescuePresident'} 775 | }; 776 | server.inject(options, (response) => { 777 | expect(response).to.have.property('statusCode', 200); 778 | expect(response).to.have.property('result'); 779 | expect(response.result).to.have.property('token'); 780 | done(); 781 | }); 782 | }); 783 | }); 784 | 785 | describe('DELETE /user/{id}', () => { 786 | let userInfo; 787 | let token; 788 | before((done) => { 789 | db.User.removeAsync({}) 790 | .then(() => { 791 | const options = { 792 | method: 'POST', 793 | url: '/user', 794 | payload: { 795 | name: 'Jack Bauer', 796 | username: 'jack_b', 797 | email: 'jbauer@24hours.com', 798 | password: '#24hoursRescuePresident' 799 | } 800 | }; 801 | 802 | server.inject(options, (response) => { 803 | token = response.result.token; 804 | userInfo = jwt.verify(token, JWT); 805 | done(); 806 | }); 807 | }); 808 | }); 809 | 810 | it('returns 400 HTTP status code when no `id` is send', (done) => { 811 | const options = { 812 | method: 'DELETE', 813 | url: '/user', 814 | headers: {'Authorization': 'Bearer ' + token} 815 | }; 816 | server.inject(options, (response) => { 817 | expect(response).to.have.property('statusCode', 400); 818 | expect(response).to.have.property('result'); 819 | expect(response.result).to.have.property('statusCode', 400); 820 | expect(response.result).to.have.property('error', 'Bad Request'); 821 | expect(response.result).to.have.property('message', 'child "id" fails because ["id" is required]'); 822 | done(); 823 | }); 824 | }); 825 | 826 | it('returns 200 HTTP status code when record is deleted', (done) => { 827 | const options = { 828 | method: 'DELETE', 829 | url: '/user/' + userInfo.id, 830 | headers: {'Authorization': 'Bearer ' + token} 831 | }; 832 | server.inject(options, (response) => { 833 | expect(response).to.have.property('statusCode', 200); 834 | expect(response).to.have.property('result'); 835 | expect(response.result).to.be.empty; 836 | done(); 837 | }); 838 | }); 839 | }); 840 | }); 841 | --------------------------------------------------------------------------------