├── .gitattributes ├── services ├── root.js ├── auth.js ├── README.md └── todo.js ├── plugins ├── timestamp.js └── README.md ├── README.md ├── test ├── plugins │ └── timestamp.test.js ├── services │ ├── root.test.js │ ├── auth.test.js │ └── todo.test.js └── helper.js ├── .travis.yml ├── schemas ├── auth.js └── todo.js ├── .gitignore ├── app.js ├── package.json └── todo.postman_collection.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set 2 | * text=auto 3 | 4 | # Require Unix line endings 5 | * text eol=lf 6 | -------------------------------------------------------------------------------- /services/root.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = async function (fastify, opts) { 4 | fastify.get('/', async function (request, reply) { 5 | return { message: 'Hello Fastify!' } 6 | }) 7 | } 8 | -------------------------------------------------------------------------------- /plugins/timestamp.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fp = require('fastify-plugin') 4 | 5 | module.exports = fp(async (fastify, opts) => { 6 | fastify.decorate('timestamp', function () { 7 | return Date.now() 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Simple Fastify REST API Example 2 | 3 | [![Build Status](https://travis-ci.com/fastify/fastify-example-todo.svg?branch=main)](https://travis-ci.com/fastify/fastify-example-todo) 4 | 5 | 6 | > [!TIP] 7 | > This project is archived, check the new and updated [fastify demo](https://github.com/fastify/demo)! 8 | -------------------------------------------------------------------------------- /test/plugins/timestamp.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const { build } = require('../helper') 5 | 6 | test('support works standalone', async (t) => { 7 | const fastify = build(t) 8 | await fastify.ready() 9 | 10 | const ts = fastify.timestamp() 11 | t.ok(new Date(ts) > 0) 12 | }) 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "11" 5 | - "10" 6 | - "8" 7 | 8 | services: 9 | - mongodb 10 | 11 | before_script: 12 | - mongo todo-test --eval 'db.createUser({user:"dummy",pwd:"dummy",roles:["readWrite"]});' 13 | 14 | notifications: 15 | email: 16 | on_success: never 17 | on_failure: always 18 | -------------------------------------------------------------------------------- /test/services/root.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const { build } = require('../helper') 5 | 6 | test('default root route', async (t) => { 7 | const app = build(t) 8 | 9 | const res = await app.inject({ 10 | url: '/api' 11 | }) 12 | t.deepEqual(JSON.parse(res.payload), { message: 'Hello Fastify!' }) 13 | }) 14 | -------------------------------------------------------------------------------- /schemas/auth.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const token = { 4 | response: { 5 | 200: { 6 | type: 'object', 7 | properties: { 8 | token: { type: 'string' } 9 | } 10 | } 11 | }, 12 | body: { 13 | type: 'object', 14 | properties: { 15 | username: { type: 'string', minLength: 1 }, 16 | password: { type: 'string', minLength: 1 } 17 | }, 18 | required: ['username', 'password'] 19 | } 20 | } 21 | 22 | module.exports = { token } 23 | -------------------------------------------------------------------------------- /plugins/README.md: -------------------------------------------------------------------------------- 1 | # Plugins Folder 2 | 3 | Plugins define behavior that is common to all the routes in your 4 | application. Authentication, caching, templates, and all the other cross 5 | cutting concerns should be handled by plugins placed in this folder. 6 | 7 | Files in this folder are typically defined through the 8 | [`fastify-plugin`](https://github.com/fastify/fastify-plugin) module, 9 | making them non-encapsulated. They can define decorators and set hooks 10 | that will then be used in the rest of your application. 11 | 12 | Check out: 13 | 14 | * [The hitchhiker's guide to plugins](https://github.com/fastify/fastify/blob/main/docs/Plugins-Guide.md) 15 | * [Fastify decorators](https://fastify.dev/docs/latest/Decorators/). 16 | * [Fastify lifecycle](https://fastify.dev/docs/latest/Lifecycle/). 17 | -------------------------------------------------------------------------------- /services/auth.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const schemas = require('../schemas/auth') 4 | 5 | module.exports = async function (fastify, opts) { 6 | fastify.post('/token', { schema: schemas.token }, async function (request, reply) { 7 | const { username, password } = request.body 8 | 9 | const user = await this.mongo.db 10 | .collection('users') 11 | .findOne({ username, password }) 12 | 13 | if ( 14 | user == null || 15 | user.username !== username || 16 | user.password !== password 17 | ) { 18 | reply.status(401).send({ message: 'Invalid username or password' }) 19 | } else { 20 | const token = fastify.jwt.sign( 21 | { sub: user.username }, 22 | { expiresIn: '1h' } 23 | ) 24 | reply.send({ token }) 25 | } 26 | }) 27 | } 28 | 29 | module.exports.autoPrefix = '/auth' 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # 0x 40 | profile-* 41 | 42 | # mac files 43 | .DS_Store 44 | 45 | # vim swap files 46 | *.swp 47 | 48 | # webstorm 49 | .idea 50 | 51 | #tap files 52 | .tap/ 53 | 54 | # vscode 55 | .vscode 56 | *code-workspace 57 | 58 | # clinic 59 | profile* 60 | *clinic* 61 | *flamegraph* 62 | 63 | # lock files 64 | yarn.lock 65 | package-lock.json 66 | 67 | # generated code 68 | examples/typescript-server.js 69 | test/types/index.js 70 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const AutoLoad = require('fastify-autoload') 5 | 6 | module.exports = function (fastify, opts, next) { 7 | fastify 8 | .register(require('fastify-mongodb'), { 9 | url: 'mongodb://localhost/todo', 10 | ...opts.mongo 11 | }) 12 | .register(require('fastify-cors')) 13 | .register(require('fastify-helmet')) 14 | .register(require('fastify-jwt'), { 15 | secret: opts.auth ? opts.auth.secret : process.env.SECRET || 'youshouldspecifyalongsecret' 16 | }) 17 | 18 | // Do not touch the following lines 19 | 20 | // This loads all plugins defined in plugins 21 | // those should be support plugins that are reused 22 | // through your application 23 | fastify.register(AutoLoad, { 24 | dir: path.join(__dirname, 'plugins'), 25 | options: Object.assign({}, opts) 26 | }) 27 | 28 | // This loads all plugins defined in services 29 | // define your routes in one of these 30 | fastify.register(AutoLoad, { 31 | dir: path.join(__dirname, 'services'), 32 | options: Object.assign({ prefix: '/api' }, opts) 33 | }) 34 | 35 | // Make sure to call next when done 36 | next() 37 | } 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fastify-example-todo", 3 | "version": "0.0.1", 4 | "description": "A todo-app (API only) example in Fastify", 5 | "main": "app.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "pretest": "npm run lint", 11 | "test": "tap --cov test/**/*.test.js", 12 | "start": "fastify start -l info app.js", 13 | "dev": "fastify start -l info -P app.js", 14 | "lint": "standard --fix | snazzy", 15 | "mongo": "docker run -p 27017:27017 -e MONGO_INITDB_DATABASE=todo-test -e MONGO_INITDB_ROOT_USERNAME=dummy -e MONGO_INITDB_ROOT_PASSWORD=dummy --rm mongo:4" 16 | }, 17 | "keywords": [], 18 | "author": "Cemre Mengu ", 19 | "license": "MIT", 20 | "dependencies": { 21 | "fastify": "^2.0.0", 22 | "fastify-autoload": "^0.6.0", 23 | "fastify-cli": "^0.27.0", 24 | "fastify-cors": "^3.0.3", 25 | "fastify-helmet": "^3.0.0", 26 | "fastify-jwt": "^0.8.0", 27 | "fastify-mongodb": "^1.0.0", 28 | "fastify-plugin": "^1.4.0" 29 | }, 30 | "devDependencies": { 31 | "mongo-clean": "^2.0.0", 32 | "mongodb": "^3.1.10", 33 | "snazzy": "^8.0.0", 34 | "standard": "^12.0.1", 35 | "tap": "^12.1.1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /services/README.md: -------------------------------------------------------------------------------- 1 | # Services Folder 2 | 3 | Services define routes within your application. Fastify provides an 4 | easy path to a microservice architecture, in the future you might want 5 | to independently deploy some of those. 6 | 7 | In this folder you should define all the services that define the routes 8 | of your web application. 9 | Each service is a [Fastify 10 | plugin](https://fastify.dev/docs/latest/Plugins/), it is 11 | encapsulated (it can have its own independent plugins) and it is 12 | typically stored in a file; be careful to group your routes logically, 13 | e.g. all `/users` routes in a `users.js` file. We have added 14 | a `root.js` file for you with a '/' root added. 15 | 16 | If a single file become too large, create a folder and add a `index.js` file there: 17 | this file must be a Fastify plugin, and it will be loaded automatically 18 | by the application. You can now add as many files as you want inside that folder. 19 | In this way you can create complex services within a single monolith, 20 | and eventually extract them. 21 | 22 | If you need to share functionality between services, place that 23 | functionality into the `plugins` folder, and share it via 24 | [decorators](https://fastify.dev/docs/latest/Decorators/). 25 | -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // This file contains code that we reuse 4 | // between our tests. 5 | 6 | const Fastify = require('fastify') 7 | const fp = require('fastify-plugin') 8 | const App = require('../app') 9 | 10 | const clean = require('mongo-clean') 11 | const { MongoClient } = require('mongodb') 12 | const { beforeEach, tearDown } = require('tap') 13 | const url = 'mongodb://localhost:27017' 14 | const database = 'todo-test' 15 | 16 | let client 17 | 18 | beforeEach(async function () { 19 | if (!client) { 20 | client = await MongoClient.connect( 21 | url, 22 | { 23 | w: 1, 24 | useNewUrlParser: true 25 | } 26 | ) 27 | } 28 | await clean(client.db(database)) 29 | await client 30 | .db(database) 31 | .collection('users') 32 | .insertOne({ username: 'dummy', password: 'dummy' }) 33 | }) 34 | 35 | tearDown(async function () { 36 | if (client) { 37 | await client.close() 38 | client = null 39 | } 40 | }) 41 | 42 | // Fill in this config with all the configurations 43 | // needed for testing the application 44 | function config () { 45 | return { 46 | auth: { 47 | secret: 'averyverylongsecret' 48 | }, 49 | mongo: { 50 | client, 51 | database 52 | } 53 | } 54 | } 55 | 56 | // automatically build and tear down our instance 57 | function build (t) { 58 | const app = Fastify() 59 | 60 | // fastify-plugin ensures that all decorators 61 | // are exposed for testing purposes, this is 62 | // different from the production setup 63 | app.register(fp(App), config()) 64 | 65 | // tear down our app after we are done 66 | t.tearDown(app.close.bind(app)) 67 | 68 | return app 69 | } 70 | 71 | module.exports = { build } 72 | -------------------------------------------------------------------------------- /schemas/todo.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const findAll = { 4 | response: { 5 | 200: { 6 | type: 'array', 7 | items: { 8 | properties: { 9 | // do not include _id field here so that it is removed from the response 10 | // _id: { type: 'string' }, 11 | name: { type: 'string' }, 12 | timestamp: { type: 'integer' }, 13 | done: { type: 'boolean' } 14 | } 15 | } 16 | } 17 | }, 18 | querystring: { 19 | limit: { type: 'integer' }, 20 | offset: { type: 'integer' } 21 | } 22 | } 23 | 24 | const findOne = { 25 | response: { 26 | 200: { 27 | type: 'object', 28 | properties: { 29 | name: { type: 'string' }, 30 | timestamp: { type: 'integer' }, 31 | done: { type: 'boolean' } 32 | } 33 | }, 34 | 404: { 35 | type: 'object', 36 | properties: { 37 | message: { type: 'string' } 38 | } 39 | } 40 | }, 41 | params: { 42 | type: 'object', 43 | properties: { 44 | name: { type: 'string' } 45 | } 46 | } 47 | } 48 | 49 | const insertOne = { 50 | body: { 51 | type: 'object', 52 | properties: { 53 | name: { type: 'string' } 54 | } 55 | } 56 | } 57 | 58 | const updateOne = { 59 | body: { 60 | type: 'object', 61 | properties: { 62 | done: { type: 'boolean' } 63 | } 64 | }, 65 | params: { 66 | type: 'object', 67 | properties: { 68 | name: { type: 'string' } 69 | } 70 | } 71 | } 72 | 73 | const deleteOne = { 74 | params: { 75 | type: 'object', 76 | properties: { 77 | name: { type: 'string' } 78 | } 79 | } 80 | } 81 | 82 | module.exports = { findAll, findOne, insertOne, updateOne, deleteOne } 83 | -------------------------------------------------------------------------------- /test/services/auth.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const { build } = require('../helper') 5 | 6 | test('test user authentication', async (t) => { 7 | t.test('should return a token', async (t) => { 8 | const app = build(t) 9 | 10 | const res = await app.inject({ 11 | url: '/api/auth/token', 12 | method: 'POST', 13 | payload: { username: 'dummy', password: 'dummy' } 14 | }) 15 | 16 | const { token } = JSON.parse(res.payload) 17 | 18 | t.ok(token) 19 | }) 20 | 21 | t.test('should give Invalid username or password', async (t) => { 22 | const app = build(t) 23 | 24 | const res = await app.inject({ 25 | url: '/api/auth/token', 26 | method: 'POST', 27 | payload: { username: 'wrong', password: 'wrong' } 28 | }) 29 | 30 | const payload = JSON.parse(res.payload) 31 | 32 | t.notOk(payload.token) 33 | t.is(res.statusCode, 401) 34 | t.is(payload.message, 'Invalid username or password') 35 | }) 36 | 37 | test('should not accept empty username or password', async t => { 38 | const app = build(t) 39 | 40 | const res = await app.inject({ 41 | url: '/api/auth/token', 42 | method: 'POST', 43 | payload: { username: '', password: '' } 44 | }) 45 | 46 | const payload = JSON.parse(res.payload) 47 | 48 | t.notOk(payload.token) 49 | t.is(res.statusCode, 400) 50 | t.is( 51 | payload.message, 52 | `body.username should NOT be shorter than 1 characters` 53 | ) 54 | }) 55 | 56 | test('should not accept missing username or password', async t => { 57 | const app = build(t) 58 | 59 | const res = await app.inject({ 60 | url: '/api/auth/token', 61 | method: 'POST', 62 | payload: {} 63 | }) 64 | 65 | const payload = JSON.parse(res.payload) 66 | 67 | t.notOk(payload.token) 68 | t.is(res.statusCode, 400) 69 | t.is( 70 | payload.message, 71 | `body should have required property 'username'` 72 | ) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /services/todo.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const schemas = require('../schemas/todo') 4 | 5 | module.exports = async function (fastify, opts) { 6 | fastify.addHook('onRequest', async (request, reply) => { 7 | try { 8 | await request.jwtVerify() 9 | } catch (err) { 10 | reply.send(err) 11 | } 12 | }) 13 | 14 | fastify.setNotFoundHandler(function (request, reply) { 15 | reply 16 | .code(404) 17 | .type('application/json') 18 | .send({ message: 'Requested todo item does not exist' }) 19 | }) 20 | 21 | fastify.get( 22 | '/', 23 | { schema: schemas.findAll }, 24 | async function (request, reply) { 25 | const limit = parseInt(request.query.limit) || 0 26 | const offset = parseInt(request.query.offset) || 0 27 | return this.mongo.db 28 | .collection('todo') 29 | .find() 30 | .sort({ timestamp: -1 }) 31 | .skip(offset) 32 | .limit(limit) 33 | .toArray() 34 | } 35 | ) 36 | 37 | fastify.get( 38 | '/:name', 39 | { schema: schemas.findOne }, 40 | async function (request, reply) { 41 | const item = await this.mongo.db 42 | .collection('todo') 43 | .findOne({ name: request.params.name }) 44 | 45 | if (item == null) { 46 | return reply.callNotFound() 47 | } 48 | 49 | return item 50 | } 51 | ) 52 | 53 | fastify.post( 54 | '/', 55 | { schema: schemas.insertOne }, 56 | async function (request, reply) { 57 | return this.mongo.db.collection('todo').insertOne( 58 | Object.assign(request.body, { 59 | timestamp: this.timestamp(), 60 | done: false 61 | }) 62 | ) 63 | } 64 | ) 65 | 66 | fastify.put( 67 | '/:name', 68 | { schema: schemas.updateOne }, 69 | async function (request, reply) { 70 | return this.mongo.db 71 | .collection('todo') 72 | .findOneAndUpdate( 73 | { name: request.params.name }, 74 | { $set: { done: request.body.done } } 75 | ) 76 | } 77 | ) 78 | 79 | fastify.delete( 80 | '/:name', 81 | { schema: schemas.deleteOne }, 82 | async function (request, reply) { 83 | return this.mongo.db 84 | .collection('todo') 85 | .deleteOne({ name: request.params.name }) 86 | } 87 | ) 88 | } 89 | 90 | module.exports.autoPrefix = '/todo' 91 | -------------------------------------------------------------------------------- /todo.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "a68220c9-2d01-4aa6-8f2f-c061e4627036", 4 | "name": "todo", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "create", 10 | "request": { 11 | "method": "POST", 12 | "header": [ 13 | { 14 | "key": "Content-Type", 15 | "name": "Content-Type", 16 | "value": "application/json", 17 | "type": "text" 18 | } 19 | ], 20 | "body": { 21 | "mode": "raw", 22 | "raw": "{\r\n\t\"name\": \"test\"\r\n}" 23 | }, 24 | "url": { 25 | "raw": "http://localhost:3000/api/todo", 26 | "protocol": "http", 27 | "host": [ 28 | "localhost" 29 | ], 30 | "port": "3000", 31 | "path": [ 32 | "api", 33 | "todo" 34 | ] 35 | } 36 | }, 37 | "response": [] 38 | }, 39 | { 40 | "name": "read", 41 | "request": { 42 | "method": "GET", 43 | "header": [], 44 | "body": { 45 | "mode": "raw", 46 | "raw": "" 47 | }, 48 | "url": { 49 | "raw": "http://localhost:3000/api/todo/test", 50 | "protocol": "http", 51 | "host": [ 52 | "localhost" 53 | ], 54 | "port": "3000", 55 | "path": [ 56 | "api", 57 | "todo", 58 | "test" 59 | ] 60 | } 61 | }, 62 | "response": [] 63 | }, 64 | { 65 | "name": "update", 66 | "request": { 67 | "method": "PUT", 68 | "header": [ 69 | { 70 | "key": "Content-Type", 71 | "name": "Content-Type", 72 | "value": "application/json", 73 | "type": "text" 74 | } 75 | ], 76 | "body": { 77 | "mode": "raw", 78 | "raw": "{\n\t\"done\": true\n}" 79 | }, 80 | "url": { 81 | "raw": "http://localhost:3000/api/todo/test", 82 | "protocol": "http", 83 | "host": [ 84 | "localhost" 85 | ], 86 | "port": "3000", 87 | "path": [ 88 | "api", 89 | "todo", 90 | "test" 91 | ] 92 | } 93 | }, 94 | "response": [] 95 | }, 96 | { 97 | "name": "delete", 98 | "request": { 99 | "method": "DELETE", 100 | "header": [], 101 | "body": { 102 | "mode": "raw", 103 | "raw": "" 104 | }, 105 | "url": { 106 | "raw": "http://localhost:3000/api/todo/test", 107 | "protocol": "http", 108 | "host": [ 109 | "localhost" 110 | ], 111 | "port": "3000", 112 | "path": [ 113 | "api", 114 | "todo", 115 | "test" 116 | ] 117 | } 118 | }, 119 | "response": [] 120 | }, 121 | { 122 | "name": "all", 123 | "request": { 124 | "method": "GET", 125 | "header": [], 126 | "body": { 127 | "mode": "raw", 128 | "raw": "" 129 | }, 130 | "url": { 131 | "raw": "http://localhost:3000/api/todo", 132 | "protocol": "http", 133 | "host": [ 134 | "localhost" 135 | ], 136 | "port": "3000", 137 | "path": [ 138 | "api", 139 | "todo" 140 | ] 141 | } 142 | }, 143 | "response": [] 144 | }, 145 | { 146 | "name": "token", 147 | "request": { 148 | "method": "POST", 149 | "header": [ 150 | { 151 | "key": "Content-Type", 152 | "name": "Content-Type", 153 | "value": "application/json", 154 | "type": "text" 155 | } 156 | ], 157 | "body": { 158 | "mode": "raw", 159 | "raw": "{\n\t\"username\": \"dummy\",\n\t\"password\": \"dummy\"\n}" 160 | }, 161 | "url": { 162 | "raw": "http://localhost:3000/api/auth/token", 163 | "protocol": "http", 164 | "host": [ 165 | "localhost" 166 | ], 167 | "port": "3000", 168 | "path": [ 169 | "api", 170 | "auth", 171 | "token" 172 | ] 173 | } 174 | }, 175 | "response": [] 176 | } 177 | ] 178 | } -------------------------------------------------------------------------------- /test/services/todo.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const { build } = require('../helper') 5 | 6 | test('test todo list functionality', async (t) => { 7 | t.test('should create an item', async (t) => { 8 | const app = build(t) 9 | 10 | const auth = await app.inject({ 11 | url: '/api/auth/token', 12 | method: 'POST', 13 | payload: { username: 'dummy', password: 'dummy' } 14 | }) 15 | 16 | const { token } = JSON.parse(auth.payload) 17 | 18 | await app.inject({ 19 | url: '/api/todo', 20 | headers: { Authorization: `Bearer ${token}` }, 21 | method: 'POST', 22 | payload: { name: 'my-first-item' } 23 | }) 24 | 25 | const res = await app.inject({ 26 | url: '/api/todo/my-first-item', 27 | headers: { Authorization: `Bearer ${token}` } 28 | }) 29 | 30 | const payload = JSON.parse(res.payload) 31 | 32 | t.is(payload.done, false) 33 | t.is(payload.name, 'my-first-item') 34 | t.notSame(payload.timestamp, null) 35 | }) 36 | 37 | t.test('should get all items', async (t) => { 38 | const app = build(t) 39 | 40 | const auth = await app.inject({ 41 | url: '/api/auth/token', 42 | method: 'POST', 43 | payload: { username: 'dummy', password: 'dummy' } 44 | }) 45 | 46 | const { token } = JSON.parse(auth.payload) 47 | 48 | await app.inject({ 49 | url: '/api/todo', 50 | headers: { Authorization: `Bearer ${token}` }, 51 | method: 'POST', 52 | payload: { name: 'my-first-item' } 53 | }) 54 | 55 | await app.inject({ 56 | url: '/api/todo', 57 | headers: { Authorization: `Bearer ${token}` }, 58 | method: 'POST', 59 | payload: { name: 'my-second-item' } 60 | }) 61 | 62 | const res = await app.inject({ 63 | url: '/api/todo', 64 | headers: { Authorization: `Bearer ${token}` } 65 | }) 66 | 67 | const payload = JSON.parse(res.payload) 68 | 69 | t.is(payload.length, 2) 70 | 71 | t.is(payload[0].done, false) 72 | t.is(payload[0].name, 'my-second-item') 73 | t.notSame(payload[0].timestamp, null) 74 | 75 | t.is(payload[1].done, false) 76 | t.is(payload[1].name, 'my-first-item') 77 | t.notSame(payload[1].timestamp, null) 78 | }) 79 | 80 | t.test('should mark item as done', async (t) => { 81 | const app = build(t) 82 | 83 | const auth = await app.inject({ 84 | url: '/api/auth/token', 85 | method: 'POST', 86 | payload: { username: 'dummy', password: 'dummy' } 87 | }) 88 | 89 | const { token } = JSON.parse(auth.payload) 90 | 91 | await app.inject({ 92 | url: '/api/todo', 93 | headers: { Authorization: `Bearer ${token}` }, 94 | method: 'POST', 95 | payload: { name: 'my-first-item' } 96 | }) 97 | 98 | await app.inject({ 99 | url: '/api/todo/my-first-item', 100 | headers: { Authorization: `Bearer ${token}` }, 101 | method: 'PUT', 102 | payload: { done: true } 103 | }) 104 | 105 | const res = await app.inject({ 106 | url: '/api/todo', 107 | headers: { Authorization: `Bearer ${token}` } 108 | }) 109 | 110 | const payload = JSON.parse(res.payload) 111 | 112 | t.is(payload.length, 1) 113 | t.is(payload[0].done, true) 114 | t.is(payload[0].name, 'my-first-item') 115 | t.notSame(payload[0].timestamp, null) 116 | }) 117 | 118 | t.test('should delete item', async (t) => { 119 | const app = build(t) 120 | 121 | const auth = await app.inject({ 122 | url: '/api/auth/token', 123 | method: 'POST', 124 | payload: { username: 'dummy', password: 'dummy' } 125 | }) 126 | 127 | const { token } = JSON.parse(auth.payload) 128 | 129 | await app.inject({ 130 | url: '/api/todo', 131 | headers: { Authorization: `Bearer ${token}` }, 132 | method: 'POST', 133 | payload: { name: 'my-first-item' } 134 | }) 135 | 136 | await app.inject({ 137 | url: '/api/todo/my-first-item', 138 | headers: { Authorization: `Bearer ${token}` }, 139 | method: 'DELETE' 140 | }) 141 | 142 | const res = await app.inject({ 143 | url: '/api/todo', 144 | headers: { Authorization: `Bearer ${token}` } 145 | }) 146 | 147 | const payload = JSON.parse(res.payload) 148 | 149 | t.is(payload.length, 0) 150 | t.deepEquals(payload, []) 151 | }) 152 | 153 | t.test('should give 404 if requested item does not exist', async (t) => { 154 | const app = build(t) 155 | 156 | const auth = await app.inject({ 157 | url: '/api/auth/token', 158 | method: 'POST', 159 | payload: { username: 'dummy', password: 'dummy' } 160 | }) 161 | 162 | const { token } = JSON.parse(auth.payload) 163 | 164 | const res = await app.inject({ 165 | url: '/api/todo/this-does-not-exist', 166 | headers: { Authorization: `Bearer ${token}` } 167 | }) 168 | 169 | const payload = JSON.parse(res.payload) 170 | 171 | t.is(res.statusCode, 404) 172 | t.deepEquals(payload, { 173 | message: 'Requested todo item does not exist' 174 | }) 175 | }) 176 | 177 | t.test('should give jwt token error', async (t) => { 178 | const app = build(t) 179 | 180 | const res = await app.inject({ 181 | url: '/api/todo', 182 | headers: { Authorization: 'Bearer test' } 183 | }) 184 | 185 | const payload = JSON.parse(res.payload) 186 | 187 | t.is(res.statusCode, 500) 188 | t.is(payload.message, 'jwt malformed') 189 | }) 190 | }) 191 | --------------------------------------------------------------------------------