├── .gitignore
├── src
└── server
│ ├── session.js
│ ├── db
│ ├── connection.js
│ ├── migrations
│ │ ├── 20171231115201_users.js
│ │ └── 20170817152841_movies.js
│ ├── queries
│ │ ├── users.js
│ │ └── movies.js
│ └── seeds
│ │ ├── movies_seed.js
│ │ └── users.js
│ ├── views
│ ├── admin.html
│ ├── status.html
│ ├── login.html
│ └── register.html
│ ├── routes
│ ├── index.js
│ ├── _helpers.js
│ ├── auth.js
│ └── movies.js
│ ├── index.js
│ └── auth.js
├── test
├── unit
│ └── sample.test.js
└── integration
│ ├── routes.index.test.js
│ ├── routes.auth.test.js
│ ├── routes.auth.stub.test.js
│ └── routes.movies.test.js
├── .travis.yml
├── knexfile.js
├── package.json
├── README.md
├── .jshintrc
└── LICENSE
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | npm-debug.log
4 |
--------------------------------------------------------------------------------
/src/server/session.js:
--------------------------------------------------------------------------------
1 | const RedisStore = require('koa-redis');
2 |
3 | module.exports = new RedisStore();
4 |
--------------------------------------------------------------------------------
/src/server/db/connection.js:
--------------------------------------------------------------------------------
1 | const environment = process.env.NODE_ENV || 'development';
2 | const config = require('../../../knexfile.js')[environment];
3 |
4 | module.exports = require('knex')(config);
5 |
--------------------------------------------------------------------------------
/src/server/views/admin.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Admin
6 |
7 |
8 | You are an admin!
9 | Logout ?
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/server/views/status.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Status
6 |
7 |
8 | You are authenticated.
9 | Logout ?
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/server/routes/index.js:
--------------------------------------------------------------------------------
1 | const Router = require('koa-router');
2 | const router = new Router();
3 |
4 | router.get('/', async (ctx) => {
5 | ctx.body = {
6 | status: 'success',
7 | message: 'hello, world!'
8 | };
9 | })
10 |
11 | module.exports = router;
12 |
--------------------------------------------------------------------------------
/test/unit/sample.test.js:
--------------------------------------------------------------------------------
1 | process.env.NODE_ENV = 'test';
2 |
3 | const chai = require('chai');
4 | const should = chai.should();
5 |
6 | describe('Sample Test', () => {
7 | it('should pass', (done) => {
8 | const sum = 1 + 2;
9 | sum.should.eql(3);
10 | sum.should.not.eql(4);
11 | done();
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language:
2 | - node_js
3 |
4 | node_js:
5 | - '9'
6 | - '8'
7 | - '7'
8 |
9 | services:
10 | - postgresql
11 | - redis-server
12 |
13 | addons:
14 | postgresql: '9.4'
15 |
16 | env:
17 | global:
18 | - NODE_ENV=test
19 | - export DATABASE_TEST_URL=postgresql://localhost/koa_api_test
20 |
21 | before_script:
22 | - psql -c 'create database koa_api_test;' -U postgres
23 |
24 | script:
25 | - npm run test
26 |
--------------------------------------------------------------------------------
/src/server/db/migrations/20171231115201_users.js:
--------------------------------------------------------------------------------
1 | exports.up = (knex, Promise) => {
2 | return knex.schema.createTable('users', (table) => {
3 | table.increments();
4 | table.string('username').unique().notNullable();
5 | table.string('password').notNullable();
6 | table.boolean('admin').notNullable().defaultTo(false);
7 | });
8 | };
9 |
10 | exports.down = (knex, Promise) => {
11 | return knex.schema.dropTable('users');
12 | };
13 |
--------------------------------------------------------------------------------
/src/server/views/login.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Login
6 |
7 |
8 | Login
9 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/server/db/migrations/20170817152841_movies.js:
--------------------------------------------------------------------------------
1 | exports.up = (knex, Promise) => {
2 | return knex.schema.createTable('movies', (table) => {
3 | table.increments();
4 | table.string('name').notNullable().unique();
5 | table.string('genre').notNullable();
6 | table.integer('rating').notNullable();
7 | table.boolean('explicit').notNullable();
8 | });
9 | };
10 |
11 | exports.down = (knex, Promise) => {
12 | return knex.schema.dropTable('movies');
13 | };
14 |
--------------------------------------------------------------------------------
/src/server/views/register.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Register
6 |
7 |
8 | Register
9 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/server/db/queries/users.js:
--------------------------------------------------------------------------------
1 | const bcrypt = require('bcryptjs');
2 | const knex = require('../connection');
3 |
4 | function getSingleUser(id) {
5 | return knex('users')
6 | .select('*')
7 | .where({ id: parseInt(id) });
8 | }
9 |
10 | function addUser(user) {
11 | const salt = bcrypt.genSaltSync();
12 | const hash = bcrypt.hashSync(user.password, salt);
13 | return knex('users')
14 | .insert({
15 | username: user.username,
16 | password: hash,
17 | })
18 | .returning('*');
19 | }
20 |
21 | module.exports = {
22 | getSingleUser,
23 | addUser,
24 | };
25 |
--------------------------------------------------------------------------------
/src/server/routes/_helpers.js:
--------------------------------------------------------------------------------
1 | const queries = require('../db/queries/users');
2 |
3 | function ensureAuthenticated(context) {
4 | return context.isAuthenticated();
5 | }
6 |
7 | function ensureAdmin(context) {
8 | return new Promise((resolve, reject) => {
9 | if (context.isAuthenticated()) {
10 | queries.getSingleUser(context.state.user.id)
11 | .then((user) => {
12 | if (user && user[0].admin) resolve(true);
13 | resolve(false);
14 | })
15 | .catch((err) => { reject(false); });
16 | }
17 | return false;
18 | });
19 | }
20 |
21 | module.exports = {
22 | ensureAuthenticated,
23 | ensureAdmin
24 | };
25 |
--------------------------------------------------------------------------------
/knexfile.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const BASE_PATH = path.join(__dirname, 'src', 'server', 'db');
4 |
5 | module.exports = {
6 | test: {
7 | client: 'pg',
8 | connection: 'postgres://localhost:5432/koa_api_test',
9 | migrations: {
10 | directory: path.join(BASE_PATH, 'migrations')
11 | },
12 | seeds: {
13 | directory: path.join(BASE_PATH, 'seeds')
14 | }
15 | },
16 | development: {
17 | client: 'pg',
18 | connection: 'postgres://localhost:5432/koa_api',
19 | migrations: {
20 | directory: path.join(BASE_PATH, 'migrations')
21 | },
22 | seeds: {
23 | directory: path.join(BASE_PATH, 'seeds')
24 | }
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/src/server/db/seeds/movies_seed.js:
--------------------------------------------------------------------------------
1 | exports.seed = (knex, Promise) => {
2 | return knex('movies').del()
3 | .then(() => {
4 | return knex('movies').insert({
5 | name: 'The Land Before Time',
6 | genre: 'Fantasy',
7 | rating: 7,
8 | explicit: false
9 | });
10 | })
11 | .then(() => {
12 | return knex('movies').insert({
13 | name: 'Jurassic Park',
14 | genre: 'Science Fiction',
15 | rating: 9,
16 | explicit: true
17 | });
18 | })
19 | .then(() => {
20 | return knex('movies').insert({
21 | name: 'Ice Age: Dawn of the Dinosaurs',
22 | genre: 'Action/Romance',
23 | rating: 5,
24 | explicit: false
25 | });
26 | });
27 | };
28 |
--------------------------------------------------------------------------------
/src/server/db/seeds/users.js:
--------------------------------------------------------------------------------
1 | const bcrypt = require('bcryptjs');
2 |
3 | exports.seed = (knex, Promise) => {
4 | return knex('users').del()
5 | .then(() => {
6 | const salt = bcrypt.genSaltSync();
7 | const hash = bcrypt.hashSync('johnson', salt);
8 | return Promise.join(
9 | knex('users').insert({
10 | username: 'jeremy',
11 | password: hash,
12 | admin: false
13 | })
14 | );
15 | })
16 | .then(() => {
17 | const salt = bcrypt.genSaltSync();
18 | const hash = bcrypt.hashSync('88776655443322', salt);
19 | return Promise.join(
20 | knex('users').insert({
21 | username: 'hank',
22 | password: hash,
23 | admin: true
24 | })
25 | );
26 | });
27 | };
28 |
--------------------------------------------------------------------------------
/test/integration/routes.index.test.js:
--------------------------------------------------------------------------------
1 | process.env.NODE_ENV = 'test';
2 |
3 | const chai = require('chai');
4 | const should = chai.should();
5 | const chaiHttp = require('chai-http');
6 | chai.use(chaiHttp);
7 |
8 | const server = require('../../src/server/index');
9 |
10 | describe('routes : index', () => {
11 |
12 | describe('GET /', () => {
13 | it('should return json', (done) => {
14 | chai.request(server)
15 | .get('/')
16 | .end((err, res) => {
17 | should.not.exist(err);
18 | res.status.should.eql(200);
19 | res.type.should.eql('application/json');
20 | res.body.status.should.equal('success');
21 | res.body.message.should.eql('hello, world!');
22 | done();
23 | });
24 | });
25 | });
26 |
27 | });
28 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "node-koa-api",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "start": "node ./src/server/index.js",
7 | "test": "./node_modules/mocha/bin/_mocha ./test/**/*.test.js"
8 | },
9 | "keywords": [],
10 | "author": "Michael Herman",
11 | "license": "ISC",
12 | "devDependencies": {
13 | "chai": "4.1.1",
14 | "chai-http": "^3.0.0",
15 | "mocha": "3.5.0",
16 | "sinon": "^4.1.5"
17 | },
18 | "dependencies": {
19 | "bcryptjs": "^2.4.3",
20 | "knex": "^0.13.0",
21 | "koa": "^2.3.0",
22 | "koa-bodyparser": "^4.2.0",
23 | "koa-passport": "^4.0.1",
24 | "koa-redis": "^3.1.1",
25 | "koa-router": "^7.2.1",
26 | "koa-session": "^5.5.1",
27 | "passport-local": "^1.0.0",
28 | "pg": "^7.1.2"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/server/db/queries/movies.js:
--------------------------------------------------------------------------------
1 | const knex = require('../connection');
2 |
3 | function getAllMovies() {
4 | return knex('movies')
5 | .select('*');
6 | }
7 |
8 | function getSingleMovie(id) {
9 | return knex('movies')
10 | .select('*')
11 | .where({ id: parseInt(id) });
12 | }
13 |
14 | function addMovie(movie) {
15 | return knex('movies')
16 | .insert(movie)
17 | .returning('*');
18 | }
19 |
20 | function updateMovie(id, movie) {
21 | return knex('movies')
22 | .update(movie)
23 | .where({ id: parseInt(id) })
24 | .returning('*');
25 | }
26 |
27 | function deleteMovie(id) {
28 | return knex('movies')
29 | .del()
30 | .where({ id: parseInt(id) })
31 | .returning('*');
32 | }
33 |
34 | module.exports = {
35 | getAllMovies,
36 | getSingleMovie,
37 | addMovie,
38 | updateMovie,
39 | deleteMovie
40 | };
41 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # RESTful API with Koa, Passport, Redis, and Postgres
2 |
3 | [](https://travis-ci.org/mjhea0/node-koa-api)
4 |
5 | ## Want to learn how to build this project?
6 |
7 | Check out the following blog posts:
8 |
9 | 1. [Building a RESTful API with Koa and Postgres](http://mherman.org/blog/2017/08/23/building-a-restful-api-with-koa-and-postgres)
10 | 1. [User Authentication with Passport and Koa](http://mherman.org/blog/2018/01/02/user-authentication-with-passport-and-koa)
11 |
12 | ## Want to use this project?
13 |
14 | 1. Fork/Clone
15 | 1. Install dependencies - `npm install`
16 | 1. Fire up Postgres and Redis on the default ports
17 | 1. Create two local Postgres databases - `koa_api` and `koa_api_test`
18 | 1. Migrate - `knex migrate:latest --env development`
19 | 1. Seed - `knex seed:run --env development`
20 | 1. Sanity check - `npm start`
21 | 1. Test - `npm test`
22 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "globals": {
3 | "esnext": true,
4 | "jasmine": false,
5 | "spyOn": false,
6 | "it": false,
7 | "console": false,
8 | "describe": false,
9 | "expect": false,
10 | "beforeEach": false,
11 | "afterEach": false,
12 | "waits": false,
13 | "waitsFor": false,
14 | "runs": false,
15 | "$": false,
16 | "confirm": false
17 | },
18 | "esnext": true,
19 | "node" : true,
20 | "browser" : true,
21 | "boss" : false,
22 | "curly": false,
23 | "debug": false,
24 | "devel": false,
25 | "eqeqeq": true,
26 | "evil": true,
27 | "forin": false,
28 | "immed": true,
29 | "laxbreak": false,
30 | "newcap": true,
31 | "noarg": true,
32 | "noempty": false,
33 | "nonew": false,
34 | "nomen": false,
35 | "onevar": true,
36 | "plusplus": false,
37 | "regexp": false,
38 | "undef": true,
39 | "sub": true,
40 | "strict": false,
41 | "white": true,
42 | "unused": false
43 | }
44 |
--------------------------------------------------------------------------------
/src/server/index.js:
--------------------------------------------------------------------------------
1 | const Koa = require('koa');
2 | const bodyParser = require('koa-bodyparser');
3 | const session = require('koa-session');
4 | const passport = require('koa-passport');
5 |
6 | const indexRoutes = require('./routes/index');
7 | const movieRoutes = require('./routes/movies');
8 | const authRoutes = require('./routes/auth');
9 | const store = require('./session');
10 |
11 | const app = new Koa();
12 | const PORT = process.env.PORT || 1337;
13 |
14 | // sessions
15 | app.keys = ['super-secret-key'];
16 | app.use(session({ store }, app));
17 |
18 | // body parser
19 | app.use(bodyParser());
20 |
21 | // authentication
22 | require('./auth');
23 | app.use(passport.initialize());
24 | app.use(passport.session());
25 |
26 | // routes
27 | app.use(indexRoutes.routes());
28 | app.use(movieRoutes.routes());
29 | app.use(authRoutes.routes());
30 |
31 | // server
32 | const server = app.listen(PORT, () => {
33 | console.log(`Server listening on port: ${PORT}`);
34 | });
35 |
36 | module.exports = server;
37 |
--------------------------------------------------------------------------------
/src/server/auth.js:
--------------------------------------------------------------------------------
1 | const passport = require('koa-passport');
2 | const LocalStrategy = require('passport-local').Strategy;
3 | const bcrypt = require('bcryptjs');
4 |
5 | const knex = require('./db/connection');
6 |
7 | const options = {};
8 |
9 | function comparePass(userPassword, databasePassword) {
10 | return bcrypt.compareSync(userPassword, databasePassword);
11 | }
12 |
13 | passport.serializeUser((user, done) => { done(null, user.id); });
14 |
15 | passport.deserializeUser((id, done) => {
16 | return knex('users').where({id}).first()
17 | .then((user) => { done(null, user); })
18 | .catch((err) => { done(err,null); });
19 | });
20 |
21 | passport.use(new LocalStrategy(options, (username, password, done) => {
22 | knex('users').where({ username }).first()
23 | .then((user) => {
24 | if (!user) return done(null, false);
25 | if (!comparePass(password, user.password)) {
26 | return done(null, false);
27 | } else {
28 | return done(null, user);
29 | }
30 | })
31 | .catch((err) => { return done(err); });
32 | }));
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Michael Herman
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 |
--------------------------------------------------------------------------------
/src/server/routes/auth.js:
--------------------------------------------------------------------------------
1 | const Router = require('koa-router');
2 | const passport = require('koa-passport');
3 | const fs = require('fs');
4 |
5 | const queries = require('../db/queries/users');
6 | const helpers = require('./_helpers');
7 |
8 | const router = new Router();
9 |
10 | router.get('/auth/register', async (ctx) => {
11 | ctx.type = 'html';
12 | ctx.body = fs.createReadStream('./src/server/views/register.html');
13 | });
14 |
15 | router.post('/auth/register', async (ctx) => {
16 | const user = await queries.addUser(ctx.request.body);
17 | return passport.authenticate('local', (err, user, info, status) => {
18 | if (user) {
19 | ctx.login(user);
20 | ctx.redirect('/auth/status');
21 | } else {
22 | ctx.status = 400;
23 | ctx.body = { status: 'error' };
24 | }
25 | })(ctx);
26 | });
27 |
28 | router.get('/auth/login', async (ctx) => {
29 | if (!helpers.ensureAuthenticated(ctx)) {
30 | ctx.type = 'html';
31 | ctx.body = fs.createReadStream('./src/server/views/login.html');
32 | } else {
33 | ctx.redirect('/auth/status');
34 | }
35 | });
36 |
37 | router.post('/auth/login', async (ctx) => {
38 | return passport.authenticate('local', (err, user, info, status) => {
39 | if (user) {
40 | ctx.login(user);
41 | ctx.redirect('/auth/status');
42 | } else {
43 | ctx.status = 400;
44 | ctx.body = { status: 'error' };
45 | }
46 | })(ctx);
47 | });
48 |
49 | router.get('/auth/logout', async (ctx) => {
50 | if (helpers.ensureAuthenticated(ctx)) {
51 | ctx.logout();
52 | ctx.redirect('/auth/login');
53 | } else {
54 | ctx.body = { success: false };
55 | ctx.throw(401);
56 | }
57 | });
58 |
59 | router.get('/auth/status', async (ctx) => {
60 | if (helpers.ensureAuthenticated(ctx)) {
61 | ctx.type = 'html';
62 | ctx.body = fs.createReadStream('./src/server/views/status.html');
63 | } else {
64 | ctx.redirect('/auth/login');
65 | }
66 | });
67 |
68 | router.get('/auth/admin', async (ctx) => {
69 | if (await helpers.ensureAdmin(ctx)) {
70 | ctx.type = 'html';
71 | ctx.body = fs.createReadStream('./src/server/views/admin.html');
72 | } else {
73 | ctx.redirect('/auth/login');
74 | }
75 | });
76 |
77 | module.exports = router;
78 |
--------------------------------------------------------------------------------
/test/integration/routes.auth.test.js:
--------------------------------------------------------------------------------
1 | process.env.NODE_ENV = 'test';
2 |
3 | const chai = require('chai');
4 | const should = chai.should();
5 | const chaiHttp = require('chai-http');
6 | chai.use(chaiHttp);
7 |
8 | const server = require('../../src/server/index');
9 | const knex = require('../../src/server/db/connection');
10 |
11 | describe('routes : auth', () => {
12 |
13 | beforeEach(() => {
14 | return knex.migrate.rollback()
15 | .then(() => { return knex.migrate.latest(); })
16 | .then(() => { return knex.seed.run(); });
17 | });
18 |
19 | afterEach(() => {
20 | return knex.migrate.rollback();
21 | });
22 |
23 | describe('GET /auth/register', () => {
24 | it('should render the register view', (done) => {
25 | chai.request(server)
26 | .get('/auth/register')
27 | .end((err, res) => {
28 | should.not.exist(err);
29 | res.redirects.length.should.eql(0);
30 | res.status.should.eql(200);
31 | res.type.should.eql('text/html');
32 | res.text.should.contain('Register ');
33 | res.text.should.contain(
34 | 'Register
');
35 | done();
36 | });
37 | });
38 | });
39 |
40 | describe('POST /auth/register', () => {
41 | it('should register a new user', (done) => {
42 | chai.request(server)
43 | .post('/auth/register')
44 | .send({
45 | username: 'michael',
46 | password: 'herman'
47 | })
48 | .end((err, res) => {
49 | should.not.exist(err);
50 | res.redirects[0].should.contain('/auth/status');
51 | done();
52 | });
53 | });
54 | });
55 |
56 | describe('GET /auth/login', () => {
57 | it('should render the login view', (done) => {
58 | chai.request(server)
59 | .get('/auth/login')
60 | .end((err, res) => {
61 | should.not.exist(err);
62 | res.redirects.length.should.eql(0);
63 | res.status.should.eql(200);
64 | res.type.should.eql('text/html');
65 | res.text.should.contain('Login ');
66 | res.text.should.contain(
67 | 'Log In
');
68 | done();
69 | });
70 | });
71 | });
72 |
73 | describe('POST /auth/login', () => {
74 | it('should login a user', (done) => {
75 | chai.request(server)
76 | .post('/auth/login')
77 | .send({
78 | username: 'jeremy',
79 | password: 'johnson'
80 | })
81 | .end((err, res) => {
82 | res.redirects[0].should.contain('/auth/status');
83 | done();
84 | });
85 | });
86 | });
87 |
88 | });
89 |
--------------------------------------------------------------------------------
/src/server/routes/movies.js:
--------------------------------------------------------------------------------
1 | const Router = require('koa-router');
2 | const queries = require('../db/queries/movies');
3 |
4 | const router = new Router();
5 | const BASE_URL = `/api/v1/movies`;
6 |
7 | router.get(BASE_URL, async (ctx) => {
8 | try {
9 | const movies = await queries.getAllMovies();
10 | ctx.body = {
11 | status: 'success',
12 | data: movies
13 | };
14 | } catch (err) {
15 | console.log(err)
16 | }
17 | })
18 |
19 | router.get(`${BASE_URL}/:id`, async (ctx) => {
20 | try {
21 | const movie = await queries.getSingleMovie(ctx.params.id);
22 | if (movie.length) {
23 | ctx.body = {
24 | status: 'success',
25 | data: movie
26 | };
27 | } else {
28 | ctx.status = 404;
29 | ctx.body = {
30 | status: 'error',
31 | message: 'That movie does not exist.'
32 | };
33 | }
34 | } catch (err) {
35 | console.log(err)
36 | }
37 | })
38 |
39 | router.post(`${BASE_URL}`, async (ctx) => {
40 | try {
41 | const movie = await queries.addMovie(ctx.request.body);
42 | if (movie.length) {
43 | ctx.status = 201;
44 | ctx.body = {
45 | status: 'success',
46 | data: movie
47 | };
48 | } else {
49 | ctx.status = 400;
50 | ctx.body = {
51 | status: 'error',
52 | message: 'Something went wrong.'
53 | };
54 | }
55 | } catch (err) {
56 | ctx.status = 400;
57 | ctx.body = {
58 | status: 'error',
59 | message: err.message || 'Sorry, an error has occurred.'
60 | };
61 | }
62 | })
63 |
64 | router.put(`${BASE_URL}/:id`, async (ctx) => {
65 | try {
66 | const movie = await queries.updateMovie(ctx.params.id, ctx.request.body);
67 | if (movie.length) {
68 | ctx.status = 200;
69 | ctx.body = {
70 | status: 'success',
71 | data: movie
72 | };
73 | } else {
74 | ctx.status = 404;
75 | ctx.body = {
76 | status: 'error',
77 | message: 'That movie does not exist.'
78 | };
79 | }
80 | } catch (err) {
81 | ctx.status = 400;
82 | ctx.body = {
83 | status: 'error',
84 | message: err.message || 'Sorry, an error has occurred.'
85 | };
86 | }
87 | })
88 |
89 | router.delete(`${BASE_URL}/:id`, async (ctx) => {
90 | try {
91 | const movie = await queries.deleteMovie(ctx.params.id);
92 | if (movie.length) {
93 | ctx.status = 200;
94 | ctx.body = {
95 | status: 'success',
96 | data: movie
97 | };
98 | } else {
99 | ctx.status = 404;
100 | ctx.body = {
101 | status: 'error',
102 | message: 'That movie does not exist.'
103 | };
104 | }
105 | } catch (err) {
106 | ctx.status = 400;
107 | ctx.body = {
108 | status: 'error',
109 | message: err.message || 'Sorry, an error has occurred.'
110 | };
111 | }
112 | })
113 |
114 | module.exports = router;
115 |
--------------------------------------------------------------------------------
/test/integration/routes.auth.stub.test.js:
--------------------------------------------------------------------------------
1 | process.env.NODE_ENV = 'test';
2 |
3 | const chai = require('chai');
4 | const passport = require('koa-passport');
5 | const should = chai.should();
6 | const chaiHttp = require('chai-http');
7 | const sinon = require('sinon');
8 | chai.use(chaiHttp);
9 |
10 | const server = require('../../src/server/index');
11 | const knex = require('../../src/server/db/connection');
12 | const queries = require('../../src/server/db/queries/users');
13 | const store = require('../../src/server/session');
14 | const helpers = require('../../src/server/routes/_helpers');
15 |
16 | describe('routes : auth - stubbed', () => {
17 |
18 | beforeEach(() => {
19 | this.ensureAuthenticated = sinon.stub(
20 | helpers, 'ensureAuthenticated'
21 | ).returns(() => {});
22 | this.ensureAdmin = sinon.stub(helpers, 'ensureAdmin');
23 | this.store = sinon.stub(store, 'set');
24 | this.authenticate = sinon.stub(passport, 'authenticate').returns(() => {});
25 | this.serialize = sinon.stub(passport, 'serializeUser').returns(() => {});
26 | this.deserialize = sinon.stub(passport, 'deserializeUser').returns(
27 | () => {});
28 | });
29 |
30 | afterEach(() => {
31 | this.authenticate.restore();
32 | this.serialize.restore();
33 | this.deserialize.restore();
34 | this.store.restore();
35 | this.ensureAuthenticated.restore();
36 | this.ensureAdmin.restore();
37 | });
38 |
39 | describe('GET /auth/register', () => {
40 | it('should render the register view', (done) => {
41 | chai.request(server)
42 | .get('/auth/register')
43 | .end((err, res) => {
44 | should.not.exist(err);
45 | res.redirects.length.should.eql(0);
46 | res.status.should.eql(200);
47 | res.type.should.eql('text/html');
48 | res.text.should.contain('Register ');
49 | res.text.should.contain(
50 | 'Register
');
51 | done();
52 | });
53 | });
54 | });
55 |
56 | describe('POST /auth/register', () => {
57 | beforeEach(() => {
58 | const user = [
59 | {
60 | id: 1,
61 | username: 'michael',
62 | password: 'something'
63 | }
64 | ];
65 | this.query = sinon.stub(queries, 'addUser').resolves(user);
66 | this.authenticate.yields(null, { id: 1 });
67 | this.serialize.yields(null, { id: 1 });
68 | this.deserialize.yields(null, { id: 1 });
69 | });
70 | afterEach(() => {
71 | this.query.restore();
72 | });
73 | it('should register a new user', (done) => {
74 | chai.request(server)
75 | .post('/auth/register')
76 | .send({
77 | username: 'michael',
78 | password: 'herman'
79 | })
80 | .end((err, res) => {
81 | res.redirects[0].should.contain('/auth/status');
82 | done();
83 | });
84 | });
85 | });
86 |
87 | describe('GET /auth/login', () => {
88 | beforeEach(() => {
89 | this.ensureAuthenticated.returns(false);
90 | });
91 | it('should render the login view if a user is not logged in', (done) => {
92 | chai.request(server)
93 | .get('/auth/login')
94 | .end((err, res) => {
95 | should.not.exist(err);
96 | res.redirects.length.should.eql(0);
97 | res.status.should.eql(200);
98 | res.type.should.eql('text/html');
99 | res.text.should.contain('Login ');
100 | res.text.should.contain(
101 | 'Log In
');
102 | done();
103 | });
104 | });
105 | });
106 |
107 | describe('GET /auth/login', () => {
108 | beforeEach(() => {
109 | this.ensureAuthenticated.returns(true);
110 | });
111 | it('should render the status view if the user is logged in', (done) => {
112 | chai.request(server)
113 | .get('/auth/login')
114 | .end((err, res) => {
115 | should.not.exist(err);
116 | res.redirects.length.should.eql(1);
117 | res.redirects[0].should.contain('/auth/status');
118 | res.status.should.eql(200);
119 | res.type.should.eql('text/html');
120 | res.text.should.contain('You are authenticated.
');
121 | done();
122 | });
123 | });
124 | });
125 |
126 | describe('POST /auth/login', () => {
127 | beforeEach(() => {
128 | this.authenticate.yields(null, { id: 1 });
129 | this.serialize.yields(null, { id: 1 });
130 | this.deserialize.yields(null, { id: 1 });
131 | });
132 | it('should login a user', (done) => {
133 | chai.request(server)
134 | .post('/auth/login')
135 | .send({
136 | username: 'jeremy',
137 | password: 'johnson'
138 | })
139 | .end((err, res) => {
140 | res.redirects[0].should.contain('/auth/status');
141 | done();
142 | });
143 | });
144 | });
145 |
146 | describe('POST /auth/login', () => {
147 | beforeEach(() => {
148 | this.authenticate.yields(null, false);
149 | });
150 | it('should not login a user if the password is incorrect', (done) => {
151 | chai.request(server)
152 | .post('/auth/login')
153 | .send({
154 | username: 'jeremy',
155 | password: 'notcorrect'
156 | })
157 | .end((err, res) => {
158 | should.exist(err);
159 | res.redirects.length.should.eql(0);
160 | res.status.should.eql(400);
161 | res.type.should.eql('application/json');
162 | res.body.status.should.eql('error');
163 | done();
164 | });
165 | });
166 | });
167 |
168 | describe('GET /auth/status', () => {
169 | beforeEach(() => {
170 | this.ensureAuthenticated.returns(true);
171 | });
172 | it('should render the status view', (done) => {
173 | chai.request(server)
174 | .get('/auth/status')
175 | .end((err, res) => {
176 | should.not.exist(err);
177 | res.redirects.length.should.eql(0);
178 | res.status.should.eql(200);
179 | res.type.should.eql('text/html');
180 | res.text.should.contain('You are authenticated.
');
181 | done();
182 | });
183 | });
184 | });
185 |
186 | describe('GET /auth/admin', () => {
187 | beforeEach(() => {
188 | this.ensureAdmin.resolves(true);
189 | });
190 | it('should render the admin view if an admin user is logged in', (done) => {
191 | chai.request(server)
192 | .get('/auth/admin')
193 | .end((err, res) => {
194 | should.not.exist(err);
195 | res.redirects.length.should.eql(0);
196 | res.status.should.eql(200);
197 | res.type.should.eql('text/html');
198 | res.text.should.contain('You are an admin!
');
199 | done();
200 | });
201 | });
202 | });
203 |
204 | describe('GET /auth/admin', () => {
205 | beforeEach(() => {
206 | this.ensureAdmin.resolves(false);
207 | this.ensureAuthenticated.returns(false);
208 | });
209 | it('should redirect to /auth/login if a user is not logged in', (done) => {
210 | chai.request(server)
211 | .get('/auth/admin')
212 | .end((err, res) => {
213 | should.not.exist(err);
214 | res.redirects.length.should.eql(1);
215 | res.redirects[0].should.contain('/auth/login');
216 | res.status.should.eql(200);
217 | res.type.should.eql('text/html');
218 | res.text.should.contain('Login ');
219 | res.text.should.contain(
220 | 'Log In
');
221 | done();
222 | });
223 | });
224 | });
225 |
226 | describe('GET /auth/admin', () => {
227 | beforeEach(() => {
228 | this.ensureAdmin.resolves(false);
229 | this.ensureAuthenticated.returns(true);
230 | });
231 | it('should redirect to /auth/status if a user is logged in but not an admin', (done) => {
232 | chai.request(server)
233 | .get('/auth/admin')
234 | .end((err, res) => {
235 | should.not.exist(err);
236 | res.redirects.length.should.eql(2);
237 | res.redirects[0].should.contain('/auth/login');
238 | res.redirects[1].should.contain('/auth/status');
239 | res.status.should.eql(200);
240 | res.type.should.eql('text/html');
241 | res.text.should.contain('You are authenticated.
');
242 | done();
243 | });
244 | });
245 | });
246 |
247 | });
248 |
--------------------------------------------------------------------------------
/test/integration/routes.movies.test.js:
--------------------------------------------------------------------------------
1 | process.env.NODE_ENV = 'test';
2 |
3 | const chai = require('chai');
4 | const should = chai.should();
5 | const chaiHttp = require('chai-http');
6 | chai.use(chaiHttp);
7 |
8 | const server = require('../../src/server/index');
9 | const knex = require('../../src/server/db/connection');
10 |
11 | describe('routes : movies', () => {
12 |
13 | beforeEach(() => {
14 | return knex.migrate.rollback()
15 | .then(() => { return knex.migrate.latest(); })
16 | .then(() => { return knex.seed.run(); });
17 | });
18 |
19 | afterEach(() => {
20 | return knex.migrate.rollback();
21 | });
22 |
23 | describe('GET /api/v1/movies', () => {
24 | it('should return all movies', (done) => {
25 | chai.request(server)
26 | .get('/api/v1/movies')
27 | .end((err, res) => {
28 | // there should be no errors
29 | should.not.exist(err);
30 | // there should be a 200 status code
31 | res.status.should.equal(200);
32 | // the response should be JSON
33 | res.type.should.equal('application/json');
34 | // the JSON response body should have a
35 | // key-value pair of {"status": "success"}
36 | res.body.status.should.eql('success');
37 | // the JSON response body should have a
38 | // key-value pair of {"data": [3 movie objects]}
39 | res.body.data.length.should.eql(3);
40 | // the first object in the data array should
41 | // have the right keys
42 | res.body.data[0].should.include.keys(
43 | 'id', 'name', 'genre', 'rating', 'explicit'
44 | );
45 | done();
46 | });
47 | });
48 | });
49 |
50 | describe('GET /api/v1/movies/:id', () => {
51 | it('should respond with a single movie', (done) => {
52 | chai.request(server)
53 | .get('/api/v1/movies/1')
54 | .end((err, res) => {
55 | // there should be no errors
56 | should.not.exist(err);
57 | // there should be a 200 status code
58 | res.status.should.equal(200);
59 | // the response should be JSON
60 | res.type.should.equal('application/json');
61 | // the JSON response body should have a
62 | // key-value pair of {"status": "success"}
63 | res.body.status.should.eql('success');
64 | // the JSON response body should have a
65 | // key-value pair of {"data": 1 movie object}
66 | res.body.data[0].should.include.keys(
67 | 'id', 'name', 'genre', 'rating', 'explicit'
68 | );
69 | done();
70 | });
71 | });
72 | it('should throw an error if the movie does not exist', (done) => {
73 | chai.request(server)
74 | .get('/api/v1/movies/9999999')
75 | .end((err, res) => {
76 | // there should an error
77 | should.exist(err);
78 | // there should be a 404 status code
79 | res.status.should.equal(404);
80 | // the response should be JSON
81 | res.type.should.equal('application/json');
82 | // the JSON response body should have a
83 | // key-value pair of {"status": "error"}
84 | res.body.status.should.eql('error');
85 | // the JSON response body should have a
86 | // key-value pair of {"message": "That movie does not exist."}
87 | res.body.message.should.eql('That movie does not exist.');
88 | done();
89 | });
90 | });
91 | });
92 |
93 | describe('POST /api/v1/movies', () => {
94 | it('should return the movie that was added', (done) => {
95 | chai.request(server)
96 | .post('/api/v1/movies')
97 | .send({
98 | name: 'Titanic',
99 | genre: 'Drama',
100 | rating: 8,
101 | explicit: true
102 | })
103 | .end((err, res) => {
104 | // there should be no errors
105 | should.not.exist(err);
106 | // there should be a 201 status code
107 | // (indicating that something was "created")
108 | res.status.should.equal(201);
109 | // the response should be JSON
110 | res.type.should.equal('application/json');
111 | // the JSON response body should have a
112 | // key-value pair of {"status": "success"}
113 | res.body.status.should.eql('success');
114 | // the JSON response body should have a
115 | // key-value pair of {"data": 1 movie object}
116 | res.body.data[0].should.include.keys(
117 | 'id', 'name', 'genre', 'rating', 'explicit'
118 | );
119 | done();
120 | });
121 | });
122 | it('should throw an error if the payload is malformed', (done) => {
123 | chai.request(server)
124 | .post('/api/v1/movies')
125 | .send({
126 | name: 'Titanic'
127 | })
128 | .end((err, res) => {
129 | // there should an error
130 | should.exist(err);
131 | // there should be a 400 status code
132 | res.status.should.equal(400);
133 | // the response should be JSON
134 | res.type.should.equal('application/json');
135 | // the JSON response body should have a
136 | // key-value pair of {"status": "error"}
137 | res.body.status.should.eql('error');
138 | // the JSON response body should have a message key
139 | should.exist(res.body.message);
140 | done();
141 | });
142 | });
143 | });
144 |
145 | describe('PUT /api/v1/movies', () => {
146 | it('should return the movie that was updated', (done) => {
147 | knex('movies')
148 | .select('*')
149 | .then((movie) => {
150 | const movieObject = movie[0];
151 | chai.request(server)
152 | .put(`/api/v1/movies/${movieObject.id}`)
153 | .send({
154 | rating: 9
155 | })
156 | .end((err, res) => {
157 | // there should be no errors
158 | should.not.exist(err);
159 | // there should be a 200 status code
160 | res.status.should.equal(200);
161 | // the response should be JSON
162 | res.type.should.equal('application/json');
163 | // the JSON response body should have a
164 | // key-value pair of {"status": "success"}
165 | res.body.status.should.eql('success');
166 | // the JSON response body should have a
167 | // key-value pair of {"data": 1 movie object}
168 | res.body.data[0].should.include.keys(
169 | 'id', 'name', 'genre', 'rating', 'explicit'
170 | );
171 | // ensure the movie was in fact updated
172 | const newMovieObject = res.body.data[0];
173 | newMovieObject.rating.should.not.eql(movieObject.rating);
174 | done();
175 | });
176 | });
177 | });
178 | it('should throw an error if the movie does not exist', (done) => {
179 | chai.request(server)
180 | .put('/api/v1/movies/9999999')
181 | .send({
182 | rating: 9
183 | })
184 | .end((err, res) => {
185 | // there should an error
186 | should.exist(err);
187 | // there should be a 404 status code
188 | res.status.should.equal(404);
189 | // the response should be JSON
190 | res.type.should.equal('application/json');
191 | // the JSON response body should have a
192 | // key-value pair of {"status": "error"}
193 | res.body.status.should.eql('error');
194 | // the JSON response body should have a
195 | // key-value pair of {"message": "That movie does not exist."}
196 | res.body.message.should.eql('That movie does not exist.');
197 | done();
198 | });
199 | });
200 | });
201 |
202 | describe('DELETE /api/v1/movies/:id', () => {
203 | it('should return the movie that was deleted', (done) => {
204 | knex('movies')
205 | .select('*')
206 | .then((movies) => {
207 | const movieObject = movies[0];
208 | const lengthBeforeDelete = movies.length;
209 | chai.request(server)
210 | .delete(`/api/v1/movies/${movieObject.id}`)
211 | .end((err, res) => {
212 | // there should be no errors
213 | should.not.exist(err);
214 | // there should be a 200 status code
215 | res.status.should.equal(200);
216 | // the response should be JSON
217 | res.type.should.equal('application/json');
218 | // the JSON response body should have a
219 | // key-value pair of {"status": "success"}
220 | res.body.status.should.eql('success');
221 | // the JSON response body should have a
222 | // key-value pair of {"data": 1 movie object}
223 | res.body.data[0].should.include.keys(
224 | 'id', 'name', 'genre', 'rating', 'explicit'
225 | );
226 | // ensure the movie was in fact deleted
227 | knex('movies').select('*')
228 | .then((updatedMovies) => {
229 | updatedMovies.length.should.eql(lengthBeforeDelete - 1);
230 | done();
231 | });
232 | });
233 | });
234 | });
235 | it('should throw an error if the movie does not exist', (done) => {
236 | chai.request(server)
237 | .delete('/api/v1/movies/9999999')
238 | .end((err, res) => {
239 | // there should an error
240 | should.exist(err);
241 | // there should be a 404 status code
242 | res.status.should.equal(404);
243 | // the response should be JSON
244 | res.type.should.equal('application/json');
245 | // the JSON response body should have a
246 | // key-value pair of {"status": "error"}
247 | res.body.status.should.eql('error');
248 | // the JSON response body should have a
249 | // key-value pair of {"message": "That movie does not exist."}
250 | res.body.message.should.eql('That movie does not exist.');
251 | done();
252 | });
253 | });
254 | });
255 |
256 | });
257 |
--------------------------------------------------------------------------------