├── .travis.yml ├── test ├── mocha.opts └── routes │ └── tasks.js ├── .gitignore ├── database.js ├── package.json ├── index.js ├── LICENSE ├── README.md ├── controllers └── Tasks.js ├── models └── Tasks.js └── routes └── tasks.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require should 2 | --ui bdd 3 | --reporter dot -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | -------------------------------------------------------------------------------- /database.js: -------------------------------------------------------------------------------- 1 | /** 2 | * There's not much value to this file 3 | * It's just a database stub to simulate calls 4 | * to a db storage engine. 5 | * Pay little attention to this file in the context 6 | * of this example. 7 | **/ 8 | 9 | 'use strict'; 10 | 11 | module.exports = function() { 12 | var store = {}; 13 | 14 | function Database() {}; 15 | 16 | Database.prototype.get = function(key) { 17 | var value; 18 | return value = typeof store !== 'undefined' && store !== null ? store[key] : void 0; 19 | }; 20 | 21 | Database.prototype.set = function(key, value) { 22 | store[key] = value; 23 | return store[key]; 24 | }; 25 | 26 | // Used in tests 27 | Database.prototype.clear = function() { 28 | store = {}; 29 | }; 30 | 31 | return new Database(); 32 | }; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-restful-api-example", 3 | "version": "1.0.2", 4 | "description": "An example of a restful api built using hapi.js", 5 | "main": "index.js", 6 | "keywords": [ 7 | "hapi", 8 | "hapijs", 9 | "api", 10 | "restful", 11 | "example" 12 | ], 13 | "scripts": { 14 | "test": "NODE_ENV=test ./node_modules/.bin/lab --assert should" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/rcorral/hapi-restful-api-example.git" 19 | }, 20 | "author": "Rafael Corral", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/rcorral/hapi-restful-api-example/issues" 24 | }, 25 | "homepage": "https://github.com/rcorral/hapi-restful-api-example", 26 | "dependencies": { 27 | "boom": "^2.6.0", 28 | "hapi": "~8.0.0", 29 | "joi": "^5.0.1" 30 | }, 31 | "devDependencies": { 32 | "lab": "^5.0.2", 33 | "should": "^4.3.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Database = require('./database'); 4 | var Hapi = require('hapi'); 5 | 6 | var database = new Database(); 7 | var server = new Hapi.Server({debug: {request: ['info', 'error']}}); 8 | 9 | // Expose database 10 | if (process.env.NODE_ENV === 'test') { 11 | server.database = database; 12 | } 13 | 14 | // Create server 15 | server.connection({ 16 | host: 'localhost', 17 | port: 8000 18 | }); 19 | 20 | // Add routes 21 | var plugins = [ 22 | { 23 | register: require('./routes/tasks.js'), 24 | options: { 25 | database: database 26 | } 27 | } 28 | ]; 29 | 30 | server.register(plugins, function (err) { 31 | if (err) { throw err; } 32 | 33 | if (!module.parent) { 34 | server.start(function(err) { 35 | if (err) { throw err; } 36 | 37 | server.log('info', 'Server running at: ' + server.info.uri); 38 | }); 39 | } 40 | }); 41 | 42 | module.exports = server; 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Rafael Corral 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A Tasks API built using hapi v8.0 2 | ================================= 3 | 4 | [![Build Status](http://img.shields.io/travis/rcorral/hapi-restful-api-example.svg?style=flat)](https://travis-ci.org/rcorral/hapi-restful-api-example) 5 | [![dependency Status](https://david-dm.org/rcorral/hapi-restful-api-example.svg?style=flat)](https://david-dm.org/rcorral/hapi-restful-api-example#info=dependencies) 6 | [![devDependency Status](https://david-dm.org/rcorral/hapi-restful-api-example/dev-status.svg?style=flat)](https://david-dm.org/rcorral/hapi-restful-api-example#info=devDependencies) 7 | 8 | An example of a Restful API built using [hapi.js](http://hapijs.com/) v8.0 for storing a list of tasks. 9 | Check out the [demo](https://github.com/rcorral/hapi-restful-api-example/tree/deployment#demo). 10 | 11 | Install 12 | ------- 13 | 14 | `$ git clone git@github.com:rcorral/hapi-restful-api-example.git` 15 | `$ cd hapi-restful-api-example` 16 | `$ npm install` 17 | 18 | Run 19 | --- 20 | 21 | `$ npm index.js` 22 | 23 | Using the API 24 | ------------- 25 | 26 | #### Get tasks 27 | `$ curl -XGET http://localhost:8000/tasks` 28 | 29 | #### Get task by id 30 | `$ curl -XGET http://localhost:8000/tasks/{id}` 31 | 32 | #### Add tasks 33 | 34 | ``` 35 | $ curl -XPOST http://localhost:8000/tasks \ 36 | -H 'Content-Type: application/json' \ 37 | -d '{"task": "Play futbol."}' 38 | ``` 39 | 40 | #### Update task 41 | 42 | ``` 43 | $ curl -XPUT http://localhost:8000/tasks/{id} \ 44 | -H 'Content-Type: application/json' \ 45 | -d '{"task": "Play soccer."}' 46 | ``` 47 | 48 | #### Delete task 49 | `$ curl -XDELETE http://localhost:8000/tasks/{id}` 50 | 51 | Tests 52 | ----- 53 | 54 | `$ npm test` 55 | 56 | License 57 | ------- 58 | 59 | MIT 60 | -------------------------------------------------------------------------------- /controllers/Tasks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Boom = require('boom'); 4 | var TasksModel = require('../models/Tasks'); 5 | 6 | function TasksController(database) { 7 | this.tasksModel = new TasksModel(database); 8 | }; 9 | 10 | // [GET] /tasks 11 | TasksController.prototype.index = function(request, reply) { 12 | var start = request.query.start; 13 | var limit = request.query.limit; 14 | 15 | if (start == null) { 16 | start = 0 17 | } 18 | 19 | if (limit == null) { 20 | limit = start + 9 21 | } 22 | 23 | reply(this.tasksModel.getTasks(start, limit)); 24 | }; 25 | 26 | // [GET] /tasks/{id} 27 | TasksController.prototype.show = function(request, reply) { 28 | try { 29 | var id = request.params.id; 30 | 31 | reply(this.tasksModel.getTask(id)); 32 | } catch (e) { 33 | reply(Boom.notFound(e.message)); 34 | } 35 | }; 36 | 37 | // [POST] /tasks 38 | TasksController.prototype.store = function(request, reply) { 39 | try { 40 | var value = request.payload.task; 41 | 42 | reply(this.tasksModel.addTask(value)) 43 | .created(); 44 | } catch (e) { 45 | reply(Boom.badRequest(e.message)); 46 | } 47 | }; 48 | 49 | // [PUT] /tasks/{id} 50 | TasksController.prototype.update = function(request, reply) { 51 | try { 52 | var id = request.params.id; 53 | var task = request.payload.task; 54 | 55 | reply(this.tasksModel.updateTask(id, task)); 56 | } catch (e) { 57 | reply(Boom.notFound(e.message)); 58 | } 59 | }; 60 | 61 | // [DELETE] /tasks/{id} 62 | TasksController.prototype.destroy = function(request, reply) { 63 | try { 64 | var id = request.params.id; 65 | 66 | this.tasksModel.deleteTask(id); 67 | reply().code(204); 68 | } catch (e) { 69 | reply(Boom.notFound(e.message)); 70 | } 71 | }; 72 | 73 | module.exports = TasksController; 74 | -------------------------------------------------------------------------------- /models/Tasks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var crypto = require('crypto'); 4 | 5 | function TasksModel(database) { 6 | this.db = database; 7 | }; 8 | 9 | TasksModel.prototype.getAllTasks = function() { 10 | return this.db.get('tasks') || []; 11 | }; 12 | 13 | TasksModel.prototype.findTaskByProperty = function(prop, value) { 14 | var task, i, len; 15 | var tasks = this.getAllTasks(); 16 | 17 | for (i = 0, len = tasks.length; i < len; i++) { 18 | task = tasks[i]; 19 | if (task[prop] === value) { 20 | return task; 21 | } 22 | } 23 | 24 | return null; 25 | }; 26 | 27 | TasksModel.prototype.getTasks = function(start, limit) { 28 | var tasks = this.getAllTasks(); 29 | return tasks.slice(start, limit + 1); 30 | }; 31 | 32 | TasksModel.prototype.getTask = function(id) { 33 | var task = this.findTaskByProperty('id', id); 34 | 35 | if (!task) { 36 | throw new Error('Task doesn\'t exists.'); 37 | } 38 | 39 | return task; 40 | }; 41 | 42 | TasksModel.prototype.addTask = function(newTask) { 43 | var tasks = this.getAllTasks(); 44 | newTask = newTask.trim(); 45 | 46 | // We don't want duplicates 47 | if (this.findTaskByProperty('value', newTask)) { 48 | throw new Error('Task already exists for id: ' + task.id); 49 | } 50 | 51 | var task = { 52 | // Collisions can happen but unlikely 53 | // 1 byte to hex turns into two characters 54 | id: crypto.randomBytes(8).toString('hex'), 55 | value: newTask 56 | } 57 | tasks.push(task); 58 | 59 | this.db.set('tasks', tasks); 60 | 61 | return task; 62 | }; 63 | 64 | TasksModel.prototype.updateTask = function(id, updatedTask) { 65 | updatedTask = updatedTask.trim(); 66 | 67 | var task = this.findTaskByProperty('id', id); 68 | 69 | if (!task) { 70 | throw new Error('Task doesn\'t exists.'); 71 | } 72 | 73 | task.value = updatedTask; 74 | 75 | return task; 76 | }; 77 | 78 | TasksModel.prototype.deleteTask = function(id) { 79 | if (!this.findTaskByProperty('id', id)) { 80 | throw new Error('Task doesn\'t exists.'); 81 | } 82 | 83 | var task, i, len; 84 | var tasks = this.getAllTasks(); 85 | 86 | for (i = 0, len = tasks.length; i < len; i++) { 87 | task = tasks[i]; 88 | if (task.id === id) { 89 | // Removes task 90 | tasks.splice(i, 1); 91 | this.db.set('tasks', tasks); 92 | return; 93 | } 94 | } 95 | }; 96 | 97 | module.exports = TasksModel; 98 | -------------------------------------------------------------------------------- /routes/tasks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Tasks routes 4 | var Joi = require('joi'); 5 | var TasksController = require('../controllers/Tasks'); 6 | 7 | exports.register = function(server, options, next) { 8 | // Setup the controller 9 | var tasksController = new TasksController(options.database); 10 | 11 | // Binds all methods 12 | // similar to doing `tasksController.index.bind(tasksController);` 13 | // when declaring handlers 14 | server.bind(tasksController); 15 | 16 | // Declare routes 17 | server.route([ 18 | { 19 | method: 'GET', 20 | path: '/tasks', 21 | config: { 22 | handler: tasksController.index, 23 | validate: { 24 | query: Joi.object().keys({ 25 | start: Joi.number().min(0), 26 | limit: Joi.number().min(1) 27 | }) 28 | } 29 | } 30 | }, 31 | { 32 | method: 'GET', 33 | path: '/tasks/{id}', 34 | config: { 35 | handler: tasksController.show, 36 | validate: { 37 | params: { 38 | id: Joi.string().regex(/[a-zA-Z0-9]{16}/) 39 | } 40 | } 41 | } 42 | }, 43 | { 44 | method: 'POST', 45 | path: '/tasks', 46 | config: { 47 | handler: tasksController.store, 48 | validate: { 49 | payload: Joi.object().length(1).keys({ 50 | task: Joi.string().required().min(1).max(60) 51 | }) 52 | } 53 | } 54 | }, 55 | { 56 | method: 'PUT', 57 | path: '/tasks/{id}', 58 | config: { 59 | handler: tasksController.update, 60 | validate: { 61 | params: { 62 | id: Joi.string().regex(/[a-zA-Z0-9]{16}/) 63 | }, 64 | payload: Joi.object().length(1).keys({ 65 | task: Joi.string().required().min(1).max(60) 66 | }) 67 | } 68 | } 69 | }, 70 | { 71 | method: 'DELETE', 72 | path: '/tasks/{id}', 73 | config: { 74 | handler: tasksController.destroy, 75 | validate: { 76 | params: { 77 | id: Joi.string().regex(/[a-zA-Z0-9]{16}/) 78 | } 79 | } 80 | } 81 | } 82 | ]); 83 | 84 | next(); 85 | } 86 | 87 | exports.register.attributes = { 88 | name: 'routes-tasks', 89 | version: '1.0.1' 90 | }; 91 | -------------------------------------------------------------------------------- /test/routes/tasks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var lab = exports.lab = require('lab').script(); 4 | var describe = lab.describe; 5 | var it = lab.it; 6 | var beforeEach = lab.beforeEach; 7 | var afterEach = lab.afterEach; 8 | var server = require('../../'); 9 | var db = server.database; 10 | 11 | describe('Routes /tasks', function() { 12 | 13 | describe('GET /tasks', function() { 14 | 15 | beforeEach(function(done) { 16 | db.clear(); 17 | var options = {method: 'POST', url: '/tasks', payload: {}}; 18 | 19 | for (var i = 0; i < 20; i++) { 20 | options.payload.task = 'task#' + i; 21 | server.inject(options, function(response) { 22 | if (response.result.value === 'task#19') { 23 | done(); 24 | } 25 | }); 26 | } 27 | }); 28 | 29 | it('returns 200 HTTP status code', function(done) { 30 | var options = {method: 'GET', url: '/tasks'}; 31 | server.inject(options, function(response) { 32 | response.statusCode.should.be.exactly(200); 33 | done(); 34 | }); 35 | }); 36 | 37 | it('returns an empty array when db is empty', function(done) { 38 | db.clear(); 39 | var options = {method: 'GET', url: '/tasks'}; 40 | server.inject(options, function(response) { 41 | response.result.should.be.an.instanceOf.Array; 42 | response.result.should.eql([]); 43 | done(); 44 | }); 45 | }); 46 | 47 | it('returns 10 tasks at a time by default', function(done) { 48 | var options = {method: 'GET', url: '/tasks'}; 49 | server.inject(options, function(response) { 50 | response.result.length.should.be.exactly(10); 51 | done(); 52 | }); 53 | }); 54 | 55 | it('returns 10 tasks at a time if the limit query parameter isn\'t used', function(done) { 56 | var options = {method: 'GET', url: '/tasks?start=2'}; 57 | server.inject(options, function(response) { 58 | response.result.length.should.be.exactly(10); 59 | done(); 60 | }); 61 | }); 62 | 63 | it('returns objects for each item in results', function(done) { 64 | var options = {method: 'GET', url: '/tasks'}; 65 | server.inject(options, function(response) { 66 | for (var i = 0; i < response.result.length; i++) { 67 | response.result[i].should.be.an.instanceOf.Object; 68 | }; 69 | done(); 70 | }); 71 | }); 72 | 73 | it('returns objects that have an id and a value property', function(done) { 74 | var options = {method: 'GET', url: '/tasks'}; 75 | server.inject(options, function(response) { 76 | for (var i = 0; i < response.result.length; i++) { 77 | response.result[i].should.have.property('id'); 78 | response.result[i].should.have.property('value'); 79 | }; 80 | done(); 81 | }); 82 | }); 83 | 84 | it('shouldn\'t allow a start query parameter smaller than 0', function(done) { 85 | var options = {method: 'GET', url: '/tasks?start=-1'}; 86 | server.inject(options, function(response) { 87 | response.statusCode.should.be.exactly(400); 88 | response.result.should.be.an.instanceOf.Object; 89 | done(); 90 | }); 91 | }); 92 | 93 | it('shouldn\'t allow a limit query parameter smaller than 1', function(done) { 94 | var options = {method: 'GET', url: '/tasks?limit=0'}; 95 | server.inject(options, function(response) { 96 | response.statusCode.should.be.exactly(400); 97 | response.result.should.be.an.instanceOf.Object; 98 | done(); 99 | }); 100 | }); 101 | 102 | it('returns the right amount of results given the query', function(done) { 103 | var options = {method: 'GET', url: '/tasks?start=6&limit=10'}; 104 | server.inject(options, function(response) { 105 | response.result.length.should.be.exactly(5); 106 | done(); 107 | }); 108 | }); 109 | 110 | it('returns results within the correct limits', function(done) { 111 | var options = {method: 'GET', url: '/tasks'}; 112 | // Get first 10 objects 113 | server.inject(options, function(response) { 114 | var tasks = response.result; 115 | var options = {method: 'GET', url: '/tasks?start=2&limit=4'}; 116 | server.inject(options, function(response) { 117 | response.result.length.should.be.exactly(3); 118 | response.result[0].should.eql(tasks[2]); 119 | response.result[1].should.eql(tasks[3]); 120 | response.result[2].should.eql(tasks[4]); 121 | done(); 122 | }); 123 | }); 124 | }); 125 | 126 | }); 127 | 128 | describe('GET /tasks/{id}', function() { 129 | 130 | it('validates id in url parameter', function(done) { 131 | var options = {method: 'GET', url: '/tasks/1'}; 132 | server.inject(options, function(response) { 133 | response.statusCode.should.be.exactly(400); 134 | done(); 135 | }); 136 | }); 137 | 138 | it('returns 404 when task isn\'t found', function(done) { 139 | var options = {method: 'GET', url: '/tasks/1234567890ABCDEF'}; 140 | server.inject(options, function(response) { 141 | response.statusCode.should.be.exactly(404); 142 | done(); 143 | }); 144 | }); 145 | 146 | it('returns requested task', function(done) { 147 | var options = {method: 'POST', url: '/tasks', payload: {task: 'my task'}}; 148 | server.inject(options, function(response) { 149 | var taskID = response.result.id; 150 | var options = {method: 'GET', url: '/tasks/' + taskID}; 151 | server.inject(options, function(response) { 152 | response.result.should.be.an.instanceOf.Object; 153 | response.result.id.should.be.exactly(taskID); 154 | response.result.value.should.be.exactly('my task'); 155 | done(); 156 | }); 157 | }); 158 | }); 159 | 160 | }); 161 | 162 | describe('POST /tasks', function() { 163 | 164 | var clear = function(done) { 165 | db.clear(); 166 | done(); 167 | }; 168 | beforeEach(clear); 169 | afterEach(clear); 170 | 171 | it('fails when there\'s no payload', function(done) { 172 | var options = {method: 'POST', url: '/tasks'}; 173 | server.inject(options, function(response) { 174 | response.statusCode.should.be.exactly(400); 175 | done(); 176 | }); 177 | }); 178 | 179 | it('fails with an invalid payload', function(done) { 180 | var options = {method: 'POST', url: '/tasks', payload: {}}; 181 | server.inject(options, function(response) { 182 | response.statusCode.should.be.exactly(400); 183 | done(); 184 | }); 185 | }); 186 | 187 | it('fails when there\'s too many properties in the payload', function(done) { 188 | var options = {method: 'POST', url: '/tasks', payload: {task: 'a task', something: 'else'}}; 189 | server.inject(options, function(response) { 190 | response.statusCode.should.be.exactly(400); 191 | done(); 192 | }); 193 | }); 194 | 195 | it('fails when the task value is empty', function(done) { 196 | var options = {method: 'POST', url: '/tasks', payload: {task: ''}}; 197 | server.inject(options, function(response) { 198 | response.statusCode.should.be.exactly(400); 199 | done(); 200 | }); 201 | }); 202 | 203 | it('fails when the task property is not set in payload', function(done) { 204 | var options = {method: 'POST', url: '/tasks', payload: {something: 'else'}}; 205 | server.inject(options, function(response) { 206 | response.statusCode.should.be.exactly(400); 207 | done(); 208 | }); 209 | }); 210 | 211 | it('fails when the task value is too long', function(done) { 212 | var task = 'this is longer than 60 characters. aaaaaaaaaaaaaaaaaaaaaaaaaa'; 213 | var options = {method: 'POST', url: '/tasks', payload: {task: task}}; 214 | server.inject(options, function(response) { 215 | response.statusCode.should.be.exactly(400); 216 | done(); 217 | }); 218 | }); 219 | 220 | it('saves with a string that is 60 characters long', function(done) { 221 | var task = 'this is not longer than 60 characters. aaaaaaaaaaaaaaaaaaaaaa'; 222 | var options = {method: 'POST', url: '/tasks', payload: {task: task}}; 223 | server.inject(options, function(response) { 224 | response.statusCode.should.be.exactly(400); 225 | done(); 226 | }); 227 | }); 228 | 229 | it('returns 201 HTTP status code', function(done) { 230 | var options = {method: 'POST', url: '/tasks', payload: {task: 'my task'}}; 231 | server.inject(options, function(response) { 232 | response.statusCode.should.be.exactly(201); 233 | done(); 234 | }); 235 | }); 236 | 237 | it('returns an object after creating new task', function(done) { 238 | var options = {method: 'POST', url: '/tasks', payload: {task: 'my task'}}; 239 | server.inject(options, function(response) { 240 | response.result.should.be.an.instanceOf.Object; 241 | done(); 242 | }); 243 | }); 244 | 245 | it('returns an object with id and value properties', function(done) { 246 | var options = {method: 'POST', url: '/tasks', payload: {task: 'my task'}}; 247 | server.inject(options, function(response) { 248 | response.result.should.have.property('id'); 249 | response.result.should.have.property('value'); 250 | response.result.value.should.be.exactly('my task'); 251 | done(); 252 | }); 253 | }); 254 | 255 | it('ids are 16 characters long', function(done) { 256 | var options = {method: 'POST', url: '/tasks', payload: {task: 'my task'}}; 257 | server.inject(options, function(response) { 258 | response.result.id.length.should.be.exactly(16); 259 | done(); 260 | }); 261 | }); 262 | 263 | it('ids should be hex values', function(done) { 264 | var options = {method: 'POST', url: '/tasks', payload: {task: 'my task'}}; 265 | server.inject(options, function(response) { 266 | /[ABCDEF0-9]{16}/i.test(response.result.id).should.be.ok; 267 | done(); 268 | }); 269 | }); 270 | 271 | it('trims white space', function(done) { 272 | var options = {method: 'POST', url: '/tasks', payload: {task: ' my task '}}; 273 | server.inject(options, function(response) { 274 | response.result.value.should.be.exactly('my task'); 275 | done(); 276 | }); 277 | }); 278 | 279 | it('doesn\'t allow duplicate tasks', function(done) { 280 | var options = {method: 'POST', url: '/tasks', payload: {task: 'task1'}}; 281 | server.inject(options, function(response) { 282 | server.inject(options, function(response) { 283 | response.result.statusCode.should.be.exactly(400); 284 | done(); 285 | }); 286 | }); 287 | }); 288 | 289 | it('saves added task', function(done) { 290 | var options = {method: 'POST', url: '/tasks', payload: {task: 'my task'}}; 291 | server.inject(options, function(response) { 292 | var options = {method: 'GET', url: '/tasks'}; 293 | server.inject(options, function(response) { 294 | response.result[0].value.should.be.eql('my task'); 295 | done(); 296 | }); 297 | }); 298 | }); 299 | 300 | }); 301 | 302 | describe('PUT /tasks/{id}', function() { 303 | var taskID = null; 304 | 305 | beforeEach(function(done) { 306 | db.clear(); 307 | 308 | var options = {method: 'POST', url: '/tasks', payload: {task: 'my task'}}; 309 | server.inject(options, function(response) { 310 | taskID = response.result.id 311 | done(); 312 | }); 313 | }); 314 | 315 | it('validates id in url parameter', function(done) { 316 | var options = {method: 'PUT', url: '/tasks/1', payload: {}}; 317 | server.inject(options, function(response) { 318 | response.statusCode.should.be.exactly(400); 319 | done(); 320 | }); 321 | }); 322 | 323 | it('fails when there\'s no payload', function(done) { 324 | var options = {method: 'PUT', url: '/tasks/' + taskID}; 325 | server.inject(options, function(response) { 326 | response.statusCode.should.be.exactly(400); 327 | done(); 328 | }); 329 | }); 330 | 331 | it('fails with an invalid payload', function(done) { 332 | var options = {method: 'PUT', url: '/tasks/' + taskID, payload: {}}; 333 | server.inject(options, function(response) { 334 | response.statusCode.should.be.exactly(400); 335 | done(); 336 | }); 337 | }); 338 | 339 | it('fails when there\'s too many properties in the payload', function(done) { 340 | var options = {method: 'PUT', url: '/tasks/' + taskID, payload: {task: 'a task', something: 'else'}}; 341 | server.inject(options, function(response) { 342 | response.statusCode.should.be.exactly(400); 343 | done(); 344 | }); 345 | }); 346 | 347 | it('fails when the task value is empty', function(done) { 348 | var options = {method: 'PUT', url: '/tasks/' + taskID, payload: {task: ''}}; 349 | server.inject(options, function(response) { 350 | response.statusCode.should.be.exactly(400); 351 | done(); 352 | }); 353 | }); 354 | 355 | it('fails when the task property is not set in payload', function(done) { 356 | var options = {method: 'PUT', url: '/tasks/' + taskID, payload: {something: 'else'}}; 357 | server.inject(options, function(response) { 358 | response.statusCode.should.be.exactly(400); 359 | done(); 360 | }); 361 | }); 362 | 363 | it('fails when the task value is too long', function(done) { 364 | var task = 'this is longer than 60 characters. aaaaaaaaaaaaaaaaaaaaaaaaaa'; 365 | var options = {method: 'PUT', url: '/tasks/' + taskID, payload: {task: task}}; 366 | server.inject(options, function(response) { 367 | response.statusCode.should.be.exactly(400); 368 | done(); 369 | }); 370 | }); 371 | 372 | it('saves with a string that is 60 characters long', function(done) { 373 | var task = 'this is not longer than 60 characters. aaaaaaaaaaaaaaaaaaaaaa'; 374 | var options = {method: 'PUT', url: '/tasks/' + taskID, payload: {task: task}}; 375 | server.inject(options, function(response) { 376 | response.statusCode.should.be.exactly(400); 377 | done(); 378 | }); 379 | }); 380 | 381 | it('returns 404 when task isn\'t found', function(done) { 382 | var options = {method: 'PUT', url: '/tasks/1234567890ABCDEF', payload: {task: 'my task'}}; 383 | server.inject(options, function(response) { 384 | response.statusCode.should.be.exactly(404); 385 | done(); 386 | }); 387 | }); 388 | 389 | it('returns a status code of 200 when sucessful', function(done) { 390 | var options = {method: 'PUT', url: '/tasks/' + taskID, payload: {task: 'my new value'}}; 391 | server.inject(options, function(response) { 392 | response.statusCode.should.be.exactly(200); 393 | done(); 394 | }); 395 | }); 396 | 397 | it('returns an object', function(done) { 398 | var options = {method: 'PUT', url: '/tasks/' + taskID, payload: {task: 'my new value'}}; 399 | server.inject(options, function(response) { 400 | response.result.should.be.an.instanceOf.Object; 401 | done(); 402 | }); 403 | }); 404 | 405 | it('returns an object with id and value properties', function(done) { 406 | var options = {method: 'PUT', url: '/tasks/' + taskID, payload: {task: 'my new value'}}; 407 | server.inject(options, function(response) { 408 | response.result.should.have.property('id'); 409 | response.result.should.have.property('value'); 410 | done(); 411 | }); 412 | }); 413 | 414 | it('returned object\'s id property should be equal to the one sent in url', function(done) { 415 | var options = {method: 'PUT', url: '/tasks/' + taskID, payload: {task: 'my new value'}}; 416 | server.inject(options, function(response) { 417 | response.result.id.should.be.exactly(taskID); 418 | done(); 419 | }); 420 | }); 421 | 422 | it('updates a task', function(done) { 423 | var options = {method: 'PUT', url: '/tasks/' + taskID, payload: {task: 'my new value'}}; 424 | server.inject(options, function(response) { 425 | response.result.value.should.be.exactly('my new value'); 426 | done(); 427 | }); 428 | }); 429 | 430 | }); 431 | 432 | describe('DELETE /tasks/{id}', function() { 433 | var taskID = null; 434 | 435 | beforeEach(function(done) { 436 | db.clear(); 437 | 438 | var options = {method: 'POST', url: '/tasks', payload: {task: 'my task'}}; 439 | server.inject(options, function(response) { 440 | taskID = response.result.id 441 | done(); 442 | }); 443 | }); 444 | 445 | it('validates id in url parameter', function(done) { 446 | var options = {method: 'DELETE', url: '/tasks/1'}; 447 | server.inject(options, function(response) { 448 | response.statusCode.should.be.exactly(400); 449 | done(); 450 | }); 451 | }); 452 | 453 | it('returns 404 when task isn\'t found', function(done) { 454 | var options = {method: 'DELETE', url: '/tasks/1234567890ABCDEF'}; 455 | server.inject(options, function(response) { 456 | response.statusCode.should.be.exactly(404); 457 | done(); 458 | }); 459 | }); 460 | 461 | it('returns a status code of 200 when sucessful', function(done) { 462 | var options = {method: 'DELETE', url: '/tasks/' + taskID}; 463 | server.inject(options, function(response) { 464 | response.statusCode.should.be.exactly(204); 465 | done(); 466 | }); 467 | }); 468 | 469 | it('response should be empty when succesfull', function(done) { 470 | var options = {method: 'DELETE', url: '/tasks/' + taskID}; 471 | server.inject(options, function(response) { 472 | (response.result === null).should.be.true; 473 | done(); 474 | }); 475 | }); 476 | 477 | it('should delete task', function(done) { 478 | db.clear(); 479 | 480 | // Create 3 tasks 481 | server.inject({method: 'POST', url: '/tasks', payload: {task: 'my task'}}, function(response) { 482 | server.inject({method: 'POST', url: '/tasks', payload: {task: 'my task1'}}, function(response) { 483 | var taskID = response.result.id; 484 | server.inject({method: 'POST', url: '/tasks', payload: {task: 'my task2'}}, function(response) { 485 | // Delete one 486 | server.inject({method: 'DELETE', url: '/tasks/' + taskID}, function(response) { 487 | // Check deletion 488 | server.inject({method: 'GET', url: '/tasks'}, function(response) { 489 | response.result.length.should.be.exactly(2); 490 | response.result[0].value.should.be.exactly('my task'); 491 | response.result[1].value.should.be.exactly('my task2'); 492 | done(); 493 | }); 494 | }); 495 | }); 496 | }); 497 | }); 498 | }); 499 | 500 | }); 501 | 502 | }); 503 | --------------------------------------------------------------------------------