From eab5a5734172e4c33e738d5f690446ab039fd87a Mon Sep 17 00:00:00 2001 From: Kizito Akhilome Date: Mon, 1 Oct 2018 15:56:34 +0100 Subject: [PATCH 1/4] feat(order-history): write failing tests --- tests/routes/auth.spec.js | 6 +- tests/routes/orders.spec.js | 70 +++++++++++++++++++ tests/seed/seed.js | 133 ++++++++++++++++++++++++++++++++---- 3 files changed, 194 insertions(+), 15 deletions(-) create mode 100644 tests/routes/orders.spec.js diff --git a/tests/routes/auth.spec.js b/tests/routes/auth.spec.js index 4f24026..2eadc34 100644 --- a/tests/routes/auth.spec.js +++ b/tests/routes/auth.spec.js @@ -3,13 +3,13 @@ import 'chai/register-should'; import chaiHttp from 'chai-http'; import dirtyChai from 'dirty-chai'; import app from '../../server/index'; -import { seedData, populateTables, populateUsersTable } from '../seed/seed'; +import { seedData, emptyTables, populateUsersTable } from '../seed/seed'; chai.use(chaiHttp); chai.use(dirtyChai); describe('POST /auth/signup', () => { - before(populateTables); + before(emptyTables); it('should signup a valid user successfully', (done) => { chai.request(app) @@ -100,7 +100,7 @@ describe('POST /auth/signup', () => { }); describe('POST /auth/login', () => { - beforeEach(populateTables); + beforeEach(emptyTables); beforeEach(populateUsersTable); it('should sign an existing user in', (done) => { diff --git a/tests/routes/orders.spec.js b/tests/routes/orders.spec.js new file mode 100644 index 0000000..ab40459 --- /dev/null +++ b/tests/routes/orders.spec.js @@ -0,0 +1,70 @@ +import chai from 'chai'; +import 'chai/register-should'; +import chaiHttp from 'chai-http'; +import dirtyChai from 'dirty-chai'; + +import app from '../../server/index'; +import pool from '../../server/db/config'; +import { + seedData, + emptyTablesPromise, + populateUsersTablePromise, + populateMenuTablePromise, + populateOrdersTablePromise, + generateValidToken, +} from '../seed/seed'; + +chai.use(chaiHttp); +chai.use(dirtyChai); + +describe('GET /users//orders', () => { + beforeEach(async () => { + await emptyTablesPromise; + await populateMenuTablePromise; + await populateUsersTablePromise; + await populateOrdersTablePromise; + }); + const { validUser } = seedData.users; + + it('should successfully get all orders for specified user', (done) => { + chai.request(app) + .get(`/users/${validUser.id}/orders`) + .set('x-auth', generateValidToken(validUser)) + .end((err, res) => { + if (err) done(err); + + res.status.should.eql(200); + res.body.should.have.all.keys(['status', 'message', 'orders']); + res.body.orders.should.be.an('array'); + done(); + }); + }); + + it('should return a 401 if user isn\'t authenticated', (done) => { + chai.request(app) + .get(`/users/${validUser.id}/orders`) + .set('x-auth', '') + .end((err, res) => { + if (err) done(err); + + res.status.should.eql(401); + res.body.should.have.all.keys(['status', 'message']); + res.body.status.should.eql('error'); + done(); + }); + }); + + it('should only return orders placed by specified user', (done) => { + chai.request(app) + .get(`/users/${validUser.id}/orders`) + .set('x-auth', generateValidToken(validUser)) + .end(async (err, res) => { + if (err) done(err); + + const orderCount = (await pool.query('SELECT * FROM orders WHERE author=$1', [validUser.id])).rowCount; + + res.body.orders.length.should.eql(orderCount); + done(); + }); + }); +}); diff --git a/tests/seed/seed.js b/tests/seed/seed.js index 5110f34..c2d3b7a 100644 --- a/tests/seed/seed.js +++ b/tests/seed/seed.js @@ -1,20 +1,32 @@ import bcrypt from 'bcryptjs'; +import jwt from 'jsonwebtoken'; import pool from '../../server/db/config'; +const adminEmail = 'hovkard@gmail.com'; + const seedData = { users: { admin: { + id: null, name: 'Kizito', - email: 'hovkard@gmail.com', + email: adminEmail, password: 'suppersecurepassword', confirmPassword: 'suppersecurepassword', }, validUser: { + id: null, name: 'James', email: 'daniel@james.com', password: 'pixel2user', confirmPassword: 'pixel2user', }, + validUserTwo: { + id: null, + name: 'Philip', + email: 'philip@new.man', + password: 'facilitate', + confirmPassword: 'facilitate', + }, validUserInvalidPass: { email: 'daniel@james.com', password: 'thisiswrong', @@ -46,10 +58,35 @@ const seedData = { confirmPassword: 'anEntirelyDifferentThing', }, }, + menu: [ + { name: 'Burger', price: 700 }, + { name: 'Spiced turkey', price: 1200 }, + { name: 'Interesting biscuits', price: 12500 }, + ], }; -const populateTables = async () => { - const dropUsersTableQuery = 'DROP TABLE IF EXISTS users'; +// function generateValidToken(id, name, email) { +// return jwt.sign({ +// userId: id, +// userName: name, +// userEmail: email, +// userStatus: email === adminEmail ? 'admin' : 'customer', +// }, process.env.JWT_SECRET).toString(); +// } + +function generateValidToken(userObject) { + return jwt.sign({ + userId: userObject.id, + userName: userObject.name, + userEmail: userObject.email, + userStatus: userObject.email === adminEmail ? 'admin' : 'customer', + }, process.env.JWT_SECRET).toString(); +} + +const emptyTables = async () => { + const dropUsersTableQuery = 'DROP TABLE IF EXISTS users CASCADE'; + const dropMenuTableQuery = 'DROP TABLE IF EXISTS menu CASCADE'; + const dropOrdersTableQuery = 'DROP TABLE IF EXISTS orders CASCADE'; const createUsersTableQuery = `CREATE TABLE users ( id serial PRIMARY KEY, @@ -59,25 +96,97 @@ const populateTables = async () => { is_admin BOOLEAN NOT NULL )`; + const createMenuTableQuery = `CREATE TABLE menu ( + id serial PRIMARY KEY, + food_name VARCHAR(255) NOT NULL, + food_image VARCHAR(255) NOT NULL DEFAULT 'https://i.imgur.com/yRLsidK.jpg', + price REAL NOT NULL + )`; + + const createOrdersTableQuery = `CREATE TABLE orders ( + id serial PRIMARY KEY, + item INTEGER REFERENCES menu(id), + author INTEGER REFERENCES users(id), + date DATE NOT NULL DEFAULT CURRENT_DATE, + status VARCHAR(50) NOT NULL DEFAULT 'new' + )`; + await pool.query(dropUsersTableQuery); + await pool.query(dropMenuTableQuery); + await pool.query(dropOrdersTableQuery); await pool.query(createUsersTableQuery); + await pool.query(createMenuTableQuery); + await pool.query(createOrdersTableQuery); }; +const emptyTablesPromise = new Promise((resolve) => { + resolve(emptyTables); +}); + const populateUsersTable = async () => { // hash passwords const adminHashedPassword = await bcrypt.hash(seedData.users.admin.password, 10); - const userHashedPassword = await bcrypt.hash(seedData.users.validUser.password, 10); - const insertQuery = 'INSERT INTO users(name, email, password, is_admin) VALUES($1, $2, $3, $4)'; + const userOneHashedPassword = await bcrypt.hash(seedData.users.validUser.password, 10); + const userTwoHashedPassword = await bcrypt.hash(seedData.users.validUserTwo.password, 10); + const insertQuery = 'INSERT INTO users(name, email, password, is_admin) VALUES($1, $2, $3, $4) RETURNING id'; // Admin user - await pool.query( + seedData.users.admin.id = (await pool.query( insertQuery, [seedData.users.admin.name, seedData.users.admin.email, adminHashedPassword, 't'], - ); - // Customer - await pool.query( + )).rows[0].id; + // Customer 1 + seedData.users.validUser.id = (await pool.query( + insertQuery, + [seedData.users.validUser.name, seedData.users.validUser.email, userOneHashedPassword, 'f'], + )).rows[0].id; + // Customer 2 + seedData.users.validUserTwo.id = (await pool.query( insertQuery, - [seedData.users.validUser.name, seedData.users.validUser.email, userHashedPassword, 'f'], - ); + [seedData.users.validUserTwo.name, seedData.users.validUserTwo.email, userTwoHashedPassword, 'f'], + )).rows[0].id; }; -export { seedData, populateTables, populateUsersTable }; +const populateUsersTablePromise = new Promise((resolve) => { + resolve(populateUsersTable); +}); + +const populateMenuTable = () => { + const insertQuery = 'INSERT INTO menu(food_name, price) VALUES($1, $2)'; + + seedData.menu.forEach(async (food) => { + await pool.query(insertQuery, [food.name, food.price]); + }); +}; + +const populateMenuTablePromise = new Promise((resolve) => { + resolve(populateMenuTable); +}); + +const populateOrdersTable = async () => { + const insertQuery = 'INSERT INTO orders(item, author) VALUES($1, $2)'; + const randomFoodId = Math.ceil(seedData.menu.length * Math.random()); + + const orderOnePromise = pool.query(insertQuery, [randomFoodId, seedData.users.validUser.id]); + const orderTwoPromise = pool.query(insertQuery, [randomFoodId, seedData.users.validUser.id]); + const orderThreePromise = pool.query(insertQuery, [randomFoodId, seedData.users.validUserTwo.id]); + const orderFourPromise = pool.query(insertQuery, [randomFoodId, seedData.users.validUser.id]); + + await Promise.all([orderOnePromise, orderTwoPromise, orderThreePromise, orderFourPromise]); +}; + +const populateOrdersTablePromise = new Promise((resolve) => { + resolve(populateOrdersTable); +}); + +export { + seedData, + emptyTables, + emptyTablesPromise, + populateUsersTable, + populateUsersTablePromise, + populateMenuTable, + populateMenuTablePromise, + populateOrdersTable, + populateOrdersTablePromise, + generateValidToken, +}; From 8daf6ca8a19297bb38031d23eac4964f1d31c53a Mon Sep 17 00:00:00 2001 From: Kizito Akhilome Date: Mon, 1 Oct 2018 16:18:21 +0100 Subject: [PATCH 2/4] fix(order-history): handle unhandled promise rejection in tests --- tests/routes/orders.spec.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/routes/orders.spec.js b/tests/routes/orders.spec.js index ab40459..4a648e7 100644 --- a/tests/routes/orders.spec.js +++ b/tests/routes/orders.spec.js @@ -61,10 +61,13 @@ describe('GET /users//orders', () => { .end(async (err, res) => { if (err) done(err); - const orderCount = (await pool.query('SELECT * FROM orders WHERE author=$1', [validUser.id])).rowCount; - - res.body.orders.length.should.eql(orderCount); - done(); + try { + const orderCount = (await pool.query('SELECT * FROM orders WHERE author=$1', [validUser.id])).rowCount; + res.body.orders.length.should.eql(orderCount); + done(); + } catch (error) { + done(error); + } }); }); }); From 8fdde7dcfb4cf704c7bf9f6acabd37587ee342ca Mon Sep 17 00:00:00 2001 From: Kizito Akhilome Date: Mon, 1 Oct 2018 18:28:38 +0100 Subject: [PATCH 3/4] fix(order-history): update wrong route urls --- tests/routes/orders.spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/routes/orders.spec.js b/tests/routes/orders.spec.js index 4a648e7..4af1beb 100644 --- a/tests/routes/orders.spec.js +++ b/tests/routes/orders.spec.js @@ -28,7 +28,7 @@ describe('GET /users//orders', () => { it('should successfully get all orders for specified user', (done) => { chai.request(app) - .get(`/users/${validUser.id}/orders`) + .get(`/api/v1/users/${validUser.id}/orders`) .set('x-auth', generateValidToken(validUser)) .end((err, res) => { if (err) done(err); @@ -42,7 +42,7 @@ describe('GET /users//orders', () => { it('should return a 401 if user isn\'t authenticated', (done) => { chai.request(app) - .get(`/users/${validUser.id}/orders`) + .get(`/api/v1/users/${validUser.id}/orders`) .set('x-auth', '') .end((err, res) => { if (err) done(err); @@ -56,7 +56,7 @@ describe('GET /users//orders', () => { it('should only return orders placed by specified user', (done) => { chai.request(app) - .get(`/users/${validUser.id}/orders`) + .get(`/api/v1/users/${validUser.id}/orders`) .set('x-auth', generateValidToken(validUser)) .end(async (err, res) => { if (err) done(err); From 1197f0304670858f4332149806604b96d36d1f9b Mon Sep 17 00:00:00 2001 From: Kizito Akhilome Date: Mon, 1 Oct 2018 19:15:35 +0100 Subject: [PATCH 4/4] feat(order-history): implement user order history route - add more tests for GET /users//orders - implement GET /users//orders route to make all tests pass - add seed menu script for database [Finishes #160869959] --- package.json | 3 ++- server/controllers/orderController.js | 30 +++++++++++++++++++++++++++ server/db/seed.sql | 20 ++++++++++++++++++ server/index.js | 5 ++++- server/routes/ordersRouter.js | 9 ++++++++ tests/routes/orders.spec.js | 29 +++++++++++++++++++++++++- 6 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 server/db/seed.sql create mode 100644 server/routes/ordersRouter.js diff --git a/package.json b/package.json index 97bc9d5..af4a020 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "purge-db": "echo 'DROP DATABASE IF EXISTS fastfoodfast;' | psql -U postgres && echo 'CREATE DATABASE fastfoodfast;' | psql -U postgres", "setup-schema": "psql -U postgres fastfoodfast < ./server/db/schema.sql", "config-db": "npm run purge-db && npm run setup-schema", - "setup-testdb": "echo 'DROP DATABASE IF EXISTS fastfoodfast_test;' | psql -U postgres && echo 'CREATE DATABASE fastfoodfast_test;' | psql -U postgres" + "setup-testdb": "echo 'DROP DATABASE IF EXISTS fastfoodfast_test;' | psql -U postgres && echo 'CREATE DATABASE fastfoodfast_test;' | psql -U postgres", + "seed-db": "psql -U postgres fastfoodfast < ./server/db/seed.sql" }, "engines": { "node": "8.12.0" diff --git a/server/controllers/orderController.js b/server/controllers/orderController.js index 5a594e9..67dcb5a 100644 --- a/server/controllers/orderController.js +++ b/server/controllers/orderController.js @@ -1,5 +1,6 @@ import orders from '../db/orders'; import Order from '../models/Order'; +import pool from '../db/config'; class OrderController { static getAllOrders(req, res) { @@ -56,6 +57,35 @@ class OrderController { order: orders[orderIndex], }); } + + static async getAllUserOrders(req, res) { + const { id } = req.params; + + if (Number.isNaN(Number(id))) { + return res.status(400).json({ + status: 'error', + message: 'invalid user id', + }); + } + + if (Number(id) !== req.userId) { + return res.status(403).json({ + status: 'error', + message: 'you\'re not allowed to do that', + }); + } + + try { + const userOrders = (await pool.query('SELECT * FROM orders WHERE author=$1', [id])).rows; + return res.status(200).json({ + status: 'success', + message: 'orders fetched successfully', + orders: userOrders, + }); + } catch (error) { + return res.status(500).json({ error }); + } + } } export default OrderController; diff --git a/server/db/seed.sql b/server/db/seed.sql new file mode 100644 index 0000000..e8b5ce6 --- /dev/null +++ b/server/db/seed.sql @@ -0,0 +1,20 @@ +INSERT INTO menu(food_name, food_image, price) +VALUES( + 'Tasty Prawns', + 'https://i.imgur.com/mTHYwlc.jpg', + 1250 +); + +INSERT INTO menu(food_name, food_image, price) +VALUES( + 'Turkey Wings', + 'https://i.imgur.com/Bfn1CxC.jpg', + 950 +); + +INSERT INTO menu(food_name, food_image, price) +VALUES( + 'Chicken Wings', + 'https://i.imgur.com/z490cis.jpg', + 850 +); diff --git a/server/index.js b/server/index.js index affcafa..eb41189 100644 --- a/server/index.js +++ b/server/index.js @@ -3,6 +3,7 @@ import bodyParser from 'body-parser'; import dotenv from 'dotenv'; import router from './routes/routes'; import authRouter from './routes/authRouter'; +import ordersRouter from './routes/ordersRouter'; dotenv.config(); const app = express(); @@ -17,8 +18,10 @@ app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); app.use('/api/v1', router); +// Orders routes +app.use('/api/v1', ordersRouter); // Auth routes -app.use('/api/v1/auth/', authRouter); +app.use('/api/v1/auth', authRouter); app.listen(process.env.PORT); diff --git a/server/routes/ordersRouter.js b/server/routes/ordersRouter.js new file mode 100644 index 0000000..7936d32 --- /dev/null +++ b/server/routes/ordersRouter.js @@ -0,0 +1,9 @@ +import { Router } from 'express'; +import AuthHandler from '../middleware/authHandler'; +import OrderController from '../controllers/orderController'; + +const router = new Router(); + +router.get('/users/:id/orders', AuthHandler.authorize, OrderController.getAllUserOrders); + +export default router; diff --git a/tests/routes/orders.spec.js b/tests/routes/orders.spec.js index 4af1beb..df28fd6 100644 --- a/tests/routes/orders.spec.js +++ b/tests/routes/orders.spec.js @@ -24,7 +24,7 @@ describe('GET /users//orders', () => { await populateUsersTablePromise; await populateOrdersTablePromise; }); - const { validUser } = seedData.users; + const { validUser, validUserTwo } = seedData.users; it('should successfully get all orders for specified user', (done) => { chai.request(app) @@ -70,4 +70,31 @@ describe('GET /users//orders', () => { } }); }); + + it('should return a 403 if user tries to get orders not placed by them', (done) => { + chai.request(app) + .get(`/api/v1/users/${validUserTwo.id}/orders`) + .set('x-auth', generateValidToken(validUser)) + .end((err, res) => { + if (err) done(err); + + res.status.should.eql(403); + res.body.status.should.eql('error'); + done(); + }); + }); + + it('should return a 400 if specified user id is not a number', (done) => { + chai.request(app) + .get('/api/v1/users/dontdothis/orders') + .set('x-auth', generateValidToken(validUser)) + .end((err, res) => { + if (err) done(err); + + res.status.should.eql(400); + res.body.status.should.eql('error'); + res.body.message.should.eql('invalid user id'); + done(); + }); + }); });