├── .gitignore ├── README.md ├── config └── index.js ├── db └── mongoose.js ├── middleware └── auth.js ├── models ├── admin │ └── admin.js └── employee │ ├── employee.js │ ├── employeeMutations.js │ ├── employeeQueries.js │ └── employeeType.js ├── package.json ├── routes ├── graphql.js └── rest.js ├── sample.env ├── seed ├── index.js └── seedInfo.js ├── server.js └── test ├── admin.test.js ├── graphql.test.js ├── rest.test.js └── setUpDB.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | data 4 | .env* 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Koa, GraphQL App 2 | 3 | # Checklist 4 | 5 | - [X] Create a simple model on mongoose 6 | - [X] Create a REST CRUD (create, read, update, delete) for the model created using koajs 7 | - [X] it should be open sourced on your github repo 8 | 9 | # Extras 10 | - [X] Create a GraphQL Type for the model created, and expose it in a GraphQL endpoint 11 | - [X] Add tests using [Jest](https://jest-everywhere.now.sh) 12 | - [X] Add authentication 13 | - [ ] Add docker support 14 | 15 | 16 | # Prerequisites 17 | * Node >= 7.7.3 18 | * MongoDB >= 3.2.12 19 | 20 | # Getting Started 21 | * Clone this repo - `git clone git@github.com:yasserhussain1110/koa-graphql-app.git` 22 | * cd into cloned repo 23 | * Copy sample .env file - `cp sample.env .env` 24 | * Fill in .env values 25 | * Install npm dependencies - `npm i` 26 | * Start mongodb server 27 | * Seed the database - `npm run seed` 28 | * Start server - `npm run start` 29 | * (Optional) Test application - `npm run test` 30 | 31 | # Details 32 | 33 | ## Models 34 | 35 | * **[Employee](/models/employee/employee.js)** - This model created using Mongoose is manipulated 36 | using the Project's Rest API and GraphQL. 37 | 38 | * **[Admin](/models/admin/admin.js)** - Admin model is used for 39 | [authenticating](/middleware/auth.js) requests. 40 | 41 | 42 | ## Routes 43 | 44 | * **[REST](/routes/rest.js)** - Requests can be sent to update/fetch data via ordinary REST API. 45 | * **[GraphQL](/routes/graphql.js)** - Requests can be sent to update/fetch data via GraphQL. 46 | 47 | 48 | ## Tests 49 | This project used Facebook's [Jest](https://jest-everywhere.now.sh) Framework for testing 50 | purposes. 51 | [Tests](/test) cover both REST API and GraphQL endpoints. 52 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | const env = process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 2 | 3 | if (env === 'development') { 4 | require('dotenv').config(); 5 | } else if(env === 'test') { 6 | require('dotenv').config({path: '.env.test'}); 7 | } else if (env === 'production') { 8 | require('dotenv').config({path: '.env.prod'}); 9 | } 10 | -------------------------------------------------------------------------------- /db/mongoose.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const mongoDbURI = process.env.MONGODB_URI || 'mongodb://localhost:27017/koa-app'; 4 | 5 | mongoose.Promise = global.Promise; 6 | mongoose.connect(mongoDbURI, {useMongoClient: true}); 7 | 8 | module.exports = mongoose; 9 | -------------------------------------------------------------------------------- /middleware/auth.js: -------------------------------------------------------------------------------- 1 | const Admin = require('../models/admin/admin'); 2 | 3 | const auth = async (ctx, next) => { 4 | const token = ctx.headers['x-auth']; 5 | const admin = await Admin.findByToken(token); 6 | if (!admin) { 7 | ctx.status = 401; 8 | return; 9 | } 10 | 11 | ctx.request.admin = admin; 12 | ctx.request.token = token; 13 | await next(); 14 | }; 15 | 16 | module.exports = auth; 17 | -------------------------------------------------------------------------------- /models/admin/admin.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const _ = require('lodash'); 3 | const Schema = mongoose.Schema; 4 | const bcrypt = require('bcryptjs'); 5 | const jwt = require('jsonwebtoken'); 6 | 7 | const {JWT_SECRET_KEY} = process.env; 8 | 9 | const AdminSchema = new Schema({ 10 | username: { 11 | type: String, 12 | required: true, 13 | minlength: 5 14 | }, 15 | 16 | password: { 17 | type: String, 18 | required: true, 19 | minlength: 6 20 | }, 21 | 22 | tokens: [{ 23 | token: { 24 | type: String, 25 | required: true 26 | } 27 | }] 28 | }); 29 | 30 | AdminSchema.pre('save', async function (next) { 31 | const admin = this; 32 | if (admin.isModified('password')) { 33 | const salt = await bcrypt.genSalt(10); 34 | admin.password = await bcrypt.hash(admin.password, salt); 35 | } 36 | next(); 37 | }); 38 | 39 | AdminSchema.methods.generateAuthToken = async function () { 40 | const admin = this; 41 | const tokenString = jwt.sign({_id: admin._id.toHexString()}, JWT_SECRET_KEY); 42 | admin.tokens.push(tokenString); 43 | 44 | await admin.save(); 45 | return tokenString; 46 | }; 47 | 48 | AdminSchema.statics.findByToken = function (tokenString) { 49 | const Admin = this; 50 | let decoded; 51 | 52 | try { 53 | decoded = jwt.verify(tokenString, JWT_SECRET_KEY); 54 | } catch (e) { 55 | return null; 56 | } 57 | 58 | const {_id} = decoded; 59 | 60 | return Admin.findOne({ 61 | _id, 62 | 'tokens.token': tokenString 63 | }); 64 | }; 65 | AdminSchema.methods.toJSON = function () { 66 | const admin = this; 67 | return _.omit(admin.toObject(), ['__v']); 68 | }; 69 | 70 | const Admin = mongoose.model('Admin', AdminSchema); 71 | 72 | module.exports = Admin; 73 | -------------------------------------------------------------------------------- /models/employee/employee.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const _ = require('lodash'); 3 | const Schema = mongoose.Schema; 4 | 5 | const EmployeeSchema = new Schema({ 6 | name: { 7 | type: String, 8 | required: true, 9 | minlength: 5, 10 | maxlength: 100 11 | }, 12 | salary: { 13 | type: Number, 14 | required: true 15 | }, 16 | address: { 17 | type: String, 18 | required: true, 19 | minlength: 10, 20 | maxlength: 200 21 | }, 22 | senior: { 23 | type: Boolean, 24 | required: true 25 | }, 26 | workExperience: { 27 | type: Number, 28 | required: true 29 | } 30 | }); 31 | 32 | EmployeeSchema.methods.toJSON = function () { 33 | const employee = this; 34 | return _.omit(employee.toObject(), ['__v']); 35 | }; 36 | 37 | const Employee = mongoose.model('Employee', EmployeeSchema); 38 | 39 | module.exports = Employee; 40 | 41 | module.exports.getListOfEmployees = () => { 42 | return Employee.find(); 43 | }; 44 | 45 | module.exports.getUserById = (root, {_id}) => { 46 | return Employee.findById({_id}); 47 | }; 48 | 49 | module.exports.addEmployee = (root, employeeInfo) => { 50 | const newUser = new Employee(employeeInfo); 51 | return newUser.save(); 52 | }; 53 | 54 | module.exports.updateEmployee = async (root, employeeInfo) => { 55 | const employee = await Employee.findById(employeeInfo._id); 56 | const employeeDoc = _.extend(employee, _.omit(employeeInfo, ['_id'])); 57 | return employeeDoc.save(); 58 | }; 59 | 60 | module.exports.deleteEmployee = async (root, {_id}) => { 61 | const employee = await Employee.findById(_id); 62 | return employee.remove(); 63 | }; 64 | -------------------------------------------------------------------------------- /models/employee/employeeMutations.js: -------------------------------------------------------------------------------- 1 | const { 2 | GraphQLString, 3 | GraphQLInt, 4 | GraphQLFloat, 5 | GraphQLNonNull, 6 | GraphQLBoolean, 7 | GraphQLID 8 | } = require('graphql'); 9 | 10 | const employeeType = require('./employeeType'); 11 | const employee = require('./employee'); 12 | 13 | module.exports = { 14 | addEmployee: { 15 | type: employeeType, 16 | args: { 17 | name: { 18 | name: 'name', 19 | type: new GraphQLNonNull(GraphQLString) 20 | }, 21 | salary: { 22 | name: 'salary', 23 | type: new GraphQLNonNull(GraphQLFloat) 24 | }, 25 | address: { 26 | name: 'address', 27 | type: new GraphQLNonNull(GraphQLString) 28 | }, 29 | senior: { 30 | name: 'senior', 31 | type: new GraphQLNonNull(GraphQLBoolean) 32 | }, 33 | workExperience: { 34 | name: 'workExperience', 35 | type: new GraphQLNonNull(GraphQLInt) 36 | } 37 | }, 38 | resolve: employee.addEmployee 39 | }, 40 | updateEmployee: { 41 | type: employeeType, 42 | args: { 43 | _id: { 44 | type: GraphQLID 45 | }, 46 | name: { 47 | name: 'name', 48 | type: new GraphQLNonNull(GraphQLString) 49 | }, 50 | salary: { 51 | name: 'salary', 52 | type: new GraphQLNonNull(GraphQLFloat) 53 | }, 54 | address: { 55 | name: 'address', 56 | type: new GraphQLNonNull(GraphQLString) 57 | }, 58 | senior: { 59 | name: 'senior', 60 | type: new GraphQLNonNull(GraphQLBoolean) 61 | }, 62 | workExperience: { 63 | name: 'workExperience', 64 | type: new GraphQLNonNull(GraphQLInt) 65 | } 66 | }, 67 | resolve: employee.updateEmployee 68 | }, 69 | deleteEmployee: { 70 | type: employeeType, 71 | args: { 72 | _id: { 73 | type: GraphQLID 74 | } 75 | }, 76 | resolve: employee.deleteEmployee 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /models/employee/employeeQueries.js: -------------------------------------------------------------------------------- 1 | const { 2 | GraphQLList, 3 | GraphQLID 4 | } = require('graphql'); 5 | 6 | const employeeType = require('./employeeType'); 7 | const employee = require('./employee'); 8 | 9 | module.exports = { 10 | employees: { 11 | type: new GraphQLList(employeeType), 12 | resolve: employee.getListOfEmployees 13 | }, 14 | employee: { 15 | type: employeeType, 16 | args: { 17 | _id: { 18 | type: GraphQLID 19 | } 20 | }, 21 | resolve: employee.getUserById 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /models/employee/employeeType.js: -------------------------------------------------------------------------------- 1 | const { 2 | GraphQLObjectType, 3 | GraphQLString, 4 | GraphQLInt, 5 | GraphQLNonNull, 6 | GraphQLID, 7 | GraphQLFloat, 8 | GraphQLBoolean 9 | } = require('graphql'); 10 | 11 | module.exports = new GraphQLObjectType({ 12 | name: 'Employee', 13 | description: 'Employee object', 14 | fields: () => ({ 15 | _id: { 16 | type: new GraphQLNonNull(GraphQLID) 17 | }, 18 | name: { 19 | type: new GraphQLNonNull(GraphQLString) 20 | }, 21 | salary: { 22 | type: new GraphQLNonNull(GraphQLFloat) 23 | }, 24 | address: { 25 | type: new GraphQLNonNull(GraphQLString) 26 | }, 27 | senior: { 28 | type: new GraphQLNonNull(GraphQLBoolean) 29 | }, 30 | workExperience: { 31 | type: new GraphQLNonNull(GraphQLInt) 32 | } 33 | }) 34 | }); 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koa-graphql", 3 | "version": "1.0.0", 4 | "description": "Koa Graphql App", 5 | "main": "server.js", 6 | "scripts": { 7 | "seed": "node seed", 8 | "start": "node server", 9 | "test": "jest test/*.test.js" 10 | }, 11 | "keywords": [], 12 | "author": "Yasser Hussain", 13 | "license": "MIT", 14 | "dependencies": { 15 | "bcryptjs": "^2.4.3", 16 | "dotenv": "^4.0.0", 17 | "graphql": "^0.11.7", 18 | "jsonwebtoken": "^8.1.0", 19 | "koa": "^2.4.1", 20 | "koa-body": "^2.5.0", 21 | "koa-graphql": "^0.7.3", 22 | "koa-router": "^7.3.0", 23 | "koajs": "^1.0.0", 24 | "lodash": "^4.17.4", 25 | "mongoose": "^4.13.2" 26 | }, 27 | "devDependencies": { 28 | "jest": "^21.2.1", 29 | "supertest": "^3.0.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /routes/graphql.js: -------------------------------------------------------------------------------- 1 | const Router = require('koa-router'); 2 | const graphqlHTTP = require('koa-graphql'); 3 | const employeeMutations = require('../models/employee/employeeMutations'); 4 | const employeeQueries = require('../models/employee/employeeQueries'); 5 | const {GraphQLObjectType, GraphQLSchema} = require('graphql'); 6 | 7 | const RootQuery = new GraphQLObjectType({ 8 | name: 'Query', 9 | description: 'Realize Root Query', 10 | fields: () => ({ 11 | employees: employeeQueries.employees, 12 | employee: employeeQueries.employee 13 | }) 14 | }); 15 | 16 | const RootMutation = new GraphQLObjectType({ 17 | name: 'Mutation', 18 | description: 'Realize Root Mutations', 19 | fields: () => ({ 20 | addEmployee: employeeMutations.addEmployee, 21 | updateEmployee: employeeMutations.updateEmployee, 22 | deleteEmployee: employeeMutations.deleteEmployee 23 | }) 24 | }); 25 | 26 | const MyGraphQLSchema = new GraphQLSchema({ 27 | query: RootQuery, 28 | mutation: RootMutation 29 | }); 30 | 31 | 32 | const router = new Router(); 33 | 34 | router.all('/graphql', graphqlHTTP({ 35 | schema: MyGraphQLSchema, 36 | graphiql: true 37 | })); 38 | 39 | 40 | module.exports = router; 41 | -------------------------------------------------------------------------------- /routes/rest.js: -------------------------------------------------------------------------------- 1 | const Router = require('koa-router'); 2 | const Employee = require('../models/employee/employee'); 3 | 4 | const router = Router({ 5 | prefix: '/employees' 6 | }); 7 | 8 | const sendEmployees = async ctx => { 9 | try { 10 | const employees = await Employee.find(); 11 | ctx.body = employees; 12 | } catch (e) { 13 | ctx.status = 400; 14 | } 15 | }; 16 | 17 | router.get('/', sendEmployees); 18 | 19 | const sendOneEmployee = async ctx => { 20 | try { 21 | const employee = await Employee.findById(ctx.params._id); 22 | if (employee) { 23 | ctx.body = employee; 24 | } else { 25 | ctx.status = 404; 26 | } 27 | } catch (e) { 28 | ctx.status = 400; 29 | } 30 | }; 31 | 32 | router.get('/:_id', sendOneEmployee); 33 | 34 | const createEmployee = async ctx => { 35 | try { 36 | const employee = await new Employee(ctx.request.body).save(); 37 | ctx.body = employee; 38 | } catch (e) { 39 | ctx.status = 400; 40 | } 41 | }; 42 | 43 | router.post('/', createEmployee); 44 | 45 | const updateEmployee = async ctx => { 46 | const employeeId = ctx.params._id; 47 | try { 48 | const employee = await Employee.findById(employeeId); 49 | if (!employee) { 50 | ctx.status = 404; 51 | return; 52 | } 53 | const updatedEmployee = await Object.assign(employee, ctx.request.body).save(); 54 | ctx.body = updatedEmployee; 55 | } catch (e) { 56 | ctx.status = 400; 57 | } 58 | }; 59 | 60 | router.patch('/:_id', updateEmployee); 61 | 62 | const deleteEmployee = async ctx => { 63 | const employeeId = ctx.params._id; 64 | try { 65 | const employee = await Employee.findById(employeeId); 66 | if (!employee) { 67 | ctx.status = 404; 68 | return; 69 | } 70 | const removedEmployee = await employee.remove(); 71 | ctx.body = removedEmployee; 72 | ctx.status = 200; 73 | } catch (e) { 74 | ctx.status = 400; 75 | } 76 | }; 77 | 78 | router.delete('/:_id', deleteEmployee); 79 | 80 | module.exports = router; 81 | -------------------------------------------------------------------------------- /sample.env: -------------------------------------------------------------------------------- 1 | PORT=YOUR_PORT 2 | MONGODB_URI=YOUR_MONGODB_URI 3 | JWT_SECRET_KEY=YOUR_JWT_SECRET_KEY 4 | -------------------------------------------------------------------------------- /seed/index.js: -------------------------------------------------------------------------------- 1 | require('../config'); 2 | const mongoose = require('../db/mongoose'); 3 | 4 | const {insertEmployeesInDB, insertAdminsInDB} = require('./seedInfo'); 5 | 6 | (async () => { 7 | await Promise.all([insertEmployeesInDB(), insertAdminsInDB()]); 8 | mongoose.connection.close(); 9 | })(); 10 | -------------------------------------------------------------------------------- /seed/seedInfo.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Employee = require('../models/employee/employee'); 3 | const Admin = require('../models/admin/admin'); 4 | const jwt = require('jsonwebtoken'); 5 | 6 | const {JWT_SECRET_KEY} = process.env; 7 | 8 | const EMPLOYEES = [{ 9 | _id: mongoose.Types.ObjectId().toString(), 10 | name: 'John Doe', 11 | salary: 100000, 12 | address: 'Bangalore, India', 13 | senior: true, 14 | workExperience: 10 15 | }, { 16 | name: 'Samuel Johnson', 17 | salary: 60000, 18 | address: 'Paris, France', 19 | senior: false, 20 | workExperience: 2 21 | }]; 22 | 23 | const insertEmployeesInDB = async () => { 24 | await Employee.remove({}); 25 | Employee.insertMany(EMPLOYEES); 26 | }; 27 | 28 | const SYS_ADMIN_ID = mongoose.Types.ObjectId().toString(); 29 | const ADMINS = [{ 30 | _id: SYS_ADMIN_ID, 31 | username: 'Sys Admin', 32 | password: 'somepassword', 33 | tokens: [{ 34 | token: jwt.sign({_id: SYS_ADMIN_ID}, JWT_SECRET_KEY) 35 | }] 36 | }]; 37 | 38 | const insertAdminsInDB = async () => { 39 | await Admin.remove(); 40 | await Promise.all( 41 | ADMINS.map(admin => new Admin(admin).save()) 42 | ); 43 | }; 44 | 45 | module.exports = { 46 | EMPLOYEES, 47 | ADMINS, 48 | insertEmployeesInDB, 49 | insertAdminsInDB 50 | }; 51 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | require('./config'); 2 | require('./db/mongoose'); 3 | 4 | const auth = require('./middleware/auth'); 5 | const Koa = require('koa'); 6 | const bodyParser = require('koa-body'); 7 | 8 | const app = new Koa(); 9 | 10 | app.use(bodyParser()); 11 | app.use(auth); 12 | 13 | const rest = require('./routes/rest'); 14 | const graphql = require('./routes/graphql'); 15 | 16 | app.use(rest.routes()); 17 | app.use(graphql.routes()).use(graphql.allowedMethods()); 18 | 19 | const server = app.listen(process.env.PORT); 20 | 21 | module.exports = server; 22 | 23 | 24 | -------------------------------------------------------------------------------- /test/admin.test.js: -------------------------------------------------------------------------------- 1 | const server = require('../server'); 2 | require('./setUpDB')(__filename); 3 | const request = require('supertest'); 4 | const mongoose = require('mongoose'); 5 | const Admin = require('../models/admin/admin'); 6 | const bcrypt = require('bcryptjs'); 7 | const {ADMINS} = require('../seed/seedInfo'); 8 | const _ = require('lodash'); 9 | 10 | beforeAll(async () => { 11 | await Admin.remove(); 12 | }); 13 | 14 | beforeEach(async () => { 15 | const admin = new Admin(ADMINS[0]); 16 | await admin.save(); 17 | }); 18 | 19 | afterEach(async () => { 20 | await Admin.remove(); 21 | }); 22 | 23 | afterAll(async () => { 24 | try { 25 | server.close(); 26 | mongoose.connection.close(); 27 | } catch (e) { 28 | console.log(e); 29 | throw e; 30 | } 31 | }); 32 | 33 | describe('Testing Admin model', () => { 34 | test('able to save admin', async done => { 35 | try { 36 | const admin = await new Admin({ 37 | username: 'some-admin', 38 | password: 'something' 39 | }).save(); 40 | 41 | expect(admin).toBeDefined(); 42 | expect(admin.toJSON()).toMatchObject({username: 'some-admin', tokens: []}); 43 | done(); 44 | } catch (e) { 45 | done(e); 46 | } 47 | }); 48 | 49 | test('admin password is being hashed', async done => { 50 | try { 51 | const admin = await new Admin({ 52 | username: 'some-admin', 53 | password: 'something' 54 | }).save(); 55 | expect(await bcrypt.compare('something', admin.password)).toBe(true); 56 | done(); 57 | } catch (e) { 58 | done(e); 59 | } 60 | }); 61 | 62 | test('able to fetch admin by token', async done => { 63 | try { 64 | const firstAdmin = ADMINS[0]; 65 | const token = firstAdmin.tokens[0].token; 66 | const admin = JSON.parse(JSON.stringify(await Admin.findByToken(token))); 67 | expect(_.omit(admin, ['password'])).toMatchObject(_.omit(firstAdmin, ['password'])); 68 | done(); 69 | } catch (e) { 70 | done(e); 71 | } 72 | }); 73 | }); 74 | 75 | 76 | describe('Testing admin authentication', () => { 77 | test('should get unathorized error', async () => { 78 | const response = await request(server).get('/employees'); 79 | expect(response.status).toBe(401); 80 | }) 81 | }); 82 | -------------------------------------------------------------------------------- /test/graphql.test.js: -------------------------------------------------------------------------------- 1 | const server = require('../server'); 2 | require('./setUpDB')(__filename); 3 | const request = require('supertest'); 4 | const Admin = require('../models/admin/admin'); 5 | const mongoose = require('mongoose'); 6 | const _ = require('lodash'); 7 | const Employee = require('../models/employee/employee'); 8 | const {EMPLOYEES, ADMINS} = require('../seed/seedInfo'); 9 | 10 | beforeAll(async () => { 11 | await Admin.remove(); 12 | const admin = new Admin(ADMINS[0]); 13 | await admin.save(); 14 | await Employee.remove(); 15 | }); 16 | 17 | beforeEach(async () => { 18 | try { 19 | await Employee.insertMany(EMPLOYEES); 20 | } catch (e) { 21 | console.log(e); 22 | throw e; 23 | } 24 | }); 25 | 26 | afterEach(async () => { 27 | try { 28 | await Employee.remove(); 29 | } catch (e) { 30 | console.log(e); 31 | throw e; 32 | } 33 | }); 34 | 35 | afterAll(async () => { 36 | await Admin.remove(); 37 | await Employee.remove(); 38 | server.close(); 39 | mongoose.connection.close(); 40 | }); 41 | 42 | describe('GraphQL Queries', () => { 43 | test('fetch all employees', async () => { 44 | const ADMIN_TOKEN = ADMINS[0].tokens[0].token; 45 | const response = 46 | await request(server) 47 | .post('/graphql') 48 | .set('x-auth', ADMIN_TOKEN) 49 | .send({ 50 | query: `{employees {name, address}}` 51 | }); 52 | expect(response.status).toBe(200); 53 | expect(response.body.data.employees).toBeDefined(); 54 | expect(response.body.data.employees.length).toBe(EMPLOYEES.length); 55 | EMPLOYEES.forEach((emp, i) => { 56 | expect(emp).toMatchObject(response.body.data.employees[i]); 57 | }); 58 | }); 59 | 60 | test('fetch an employee', async () => { 61 | const ADMIN_TOKEN = ADMINS[0].tokens[0].token; 62 | const response = 63 | await request(server) 64 | .post('/graphql') 65 | .set('x-auth', ADMIN_TOKEN) 66 | .send({ 67 | query: `{employee (_id: "${EMPLOYEES[0]._id}") {name}}` 68 | }); 69 | expect(response.status).toBe(200); 70 | expect(response.body.data.employee).toBeDefined(); 71 | expect(EMPLOYEES[0]).toMatchObject(response.body.data.employee); 72 | }); 73 | }); 74 | 75 | 76 | describe('GraphQL Mutation', () => { 77 | test('create a new employee', async () => { 78 | const ADMIN_TOKEN = ADMINS[0].tokens[0].token; 79 | const response = 80 | await request(server) 81 | .post('/graphql') 82 | .set('x-auth', ADMIN_TOKEN) 83 | .send({ 84 | query: `mutation { 85 | addEmployee( 86 | name: "Sergey Boyanovitch", 87 | salary: 200000, 88 | address: "Kiev, Ukraine", 89 | senior: true, 90 | workExperience: 23 91 | ) 92 | { 93 | _id 94 | } 95 | }` 96 | }); 97 | expect(response.status).toBe(200); 98 | expect(response.body.data.addEmployee).toBeDefined(); 99 | expect(response.body.data.addEmployee._id).toBeDefined(); 100 | const employees = await Employee.find(); 101 | expect(employees.length).toBe(EMPLOYEES.length + 1); 102 | }); 103 | 104 | test('update an employee', async () => { 105 | const ADMIN_TOKEN = ADMINS[0].tokens[0].token; 106 | const response = 107 | await request(server) 108 | .post('/graphql') 109 | .set('x-auth', ADMIN_TOKEN) 110 | .send({ 111 | query: `mutation { 112 | updateEmployee( 113 | _id: "${EMPLOYEES[0]._id}", 114 | name: "Sergey Boyanovitch", 115 | salary: 200000, 116 | address: "Kiev, Ukraine", 117 | senior: true, 118 | workExperience: 23 119 | ) 120 | { 121 | _id 122 | } 123 | }` 124 | }); 125 | 126 | expect(response.status).toBe(200); 127 | expect(response.body.data.updateEmployee).toBeDefined(); 128 | expect(response.body.data.updateEmployee._id).toBe(EMPLOYEES[0]._id); 129 | const employees = await Employee.find(); 130 | expect(employees.length).toBe(EMPLOYEES.length); 131 | const updateEmployee = JSON.parse(JSON.stringify(await Employee.findById(EMPLOYEES[0]._id))); 132 | expect(updateEmployee).toEqual({ 133 | _id: EMPLOYEES[0]._id, 134 | name: "Sergey Boyanovitch", 135 | salary: 200000, 136 | address: "Kiev, Ukraine", 137 | senior: true, 138 | workExperience: 23 139 | }); 140 | }); 141 | 142 | test('delete an employee', async () => { 143 | const ADMIN_TOKEN = ADMINS[0].tokens[0].token; 144 | const response = 145 | await request(server) 146 | .post('/graphql') 147 | .set('x-auth', ADMIN_TOKEN) 148 | .send({ 149 | query: `mutation { 150 | deleteEmployee( 151 | _id: "${EMPLOYEES[0]._id}" 152 | ) 153 | { 154 | _id 155 | } 156 | }` 157 | }); 158 | 159 | expect(response.status).toBe(200); 160 | expect(response.body.data.deleteEmployee).toBeDefined(); 161 | expect(response.body.data.deleteEmployee._id).toBe(EMPLOYEES[0]._id); 162 | const employees = await Employee.find(); 163 | expect(employees.length).toBe(EMPLOYEES.length - 1); 164 | const updateEmployee = await Employee.findById(EMPLOYEES[0]._id); 165 | expect(updateEmployee).toBeNull(); 166 | }); 167 | }); 168 | -------------------------------------------------------------------------------- /test/rest.test.js: -------------------------------------------------------------------------------- 1 | const server = require('../server'); 2 | require('./setUpDB')(__filename); 3 | const request = require('supertest'); 4 | const Admin = require('../models/admin/admin'); 5 | const mongoose = require('mongoose'); 6 | const _ = require('lodash'); 7 | const Employee = require('../models/employee/employee'); 8 | const {EMPLOYEES, ADMINS} = require('../seed/seedInfo'); 9 | 10 | beforeAll(async () => { 11 | await Admin.remove(); 12 | const admin = new Admin(ADMINS[0]); 13 | await admin.save(); 14 | await Employee.remove(); 15 | }); 16 | 17 | beforeEach(async () => { 18 | try { 19 | await Employee.insertMany(EMPLOYEES); 20 | } catch (e) { 21 | console.log(e); 22 | throw e; 23 | } 24 | }); 25 | 26 | afterEach(async () => { 27 | try { 28 | await Employee.remove(); 29 | } catch (e) { 30 | console.log(e); 31 | throw e; 32 | } 33 | }); 34 | 35 | afterAll(async () => { 36 | await Admin.remove(); 37 | await Employee.remove(); 38 | server.close(); 39 | mongoose.connection.close(); 40 | }); 41 | 42 | describe('GET /employees', () => { 43 | test('should get all employees', async () => { 44 | const ADMIN_TOKEN = ADMINS[0].tokens[0].token; 45 | const response = await request(server).get('/employees').set('x-auth', ADMIN_TOKEN); 46 | expect(response.status).toBe(200); 47 | expect(response.body.map(r => _.omit(r, ['_id']))).toEqual(EMPLOYEES.map(e => _.omit(e, ['_id']))); 48 | expect(EMPLOYEES[0]._id).toBe(response.body[0]._id); 49 | }); 50 | }); 51 | 52 | describe('GET /employees/:_id', () => { 53 | test('should get a particular employee', async () => { 54 | const ADMIN_TOKEN = ADMINS[0].tokens[0].token; 55 | const response = await request(server).get(`/employees/${EMPLOYEES[0]._id}`).set('x-auth', ADMIN_TOKEN); 56 | expect(response.status).toBe(200); 57 | expect(response.body).toEqual(EMPLOYEES[0]); 58 | }); 59 | 60 | test('should get 404 for non-existent employee', async () => { 61 | const ADMIN_TOKEN = ADMINS[0].tokens[0].token; 62 | const nonExistentID = '5a0ac5f420a8da291b551e4b'; 63 | const response = await request(server).get(`/employees/${nonExistentID}`).set('x-auth', ADMIN_TOKEN); 64 | expect(response.status).toBe(404); 65 | }); 66 | }); 67 | 68 | describe('POST /employees', () => { 69 | test('should be able to create a new employee', async () => { 70 | const ADMIN_TOKEN = ADMINS[0].tokens[0].token; 71 | const newEmployee = { 72 | name: 'Emanuel Borzanov', 73 | salary: 40000, 74 | address: 'St. Petersburg, Russia', 75 | senior: false, 76 | workExperience: 1 77 | }; 78 | const response = await request(server).post('/employees').set('x-auth', ADMIN_TOKEN).send(newEmployee); 79 | expect(response.status).toBe(200); 80 | expect(_.omit(response.body, ['_id'])).toEqual(newEmployee); 81 | const employees = await Employee.find(); 82 | expect(employees.length).toBe(3); 83 | }); 84 | 85 | test('employee validation must fail if any field is absent', async () => { 86 | const ADMIN_TOKEN = ADMINS[0].tokens[0].token; 87 | const newEmployee = { 88 | name: 'Emanuel Borzanov', 89 | salary: 40000, 90 | address: 'St. Petersburg, Russia', 91 | senior: false 92 | }; 93 | const response = await request(server).post('/employees').set('x-auth', ADMIN_TOKEN).send(newEmployee); 94 | expect(response.status).toBe(400); 95 | }); 96 | }); 97 | 98 | describe('PATCH /employees/:_id', () => { 99 | test('should be able to update salary field of employee', async () => { 100 | const ADMIN_TOKEN = ADMINS[0].tokens[0].token; 101 | const updatedFields = { 102 | salary: 200000 103 | }; 104 | const response = 105 | await request(server) 106 | .patch(`/employees/${EMPLOYEES[0]._id}`) 107 | .set('x-auth', ADMIN_TOKEN) 108 | .send(updatedFields); 109 | expect(response.status).toBe(200); 110 | expect(response.body.salary).toBe(updatedFields.salary); 111 | }); 112 | 113 | test('will get 404 while attempting to update non-existent employee', async () => { 114 | const ADMIN_TOKEN = ADMINS[0].tokens[0].token; 115 | const newEmployee = { 116 | name: 'Emanuel Borzanov', 117 | salary: 40000, 118 | address: 'St. Petersburg, Russia', 119 | senior: false, 120 | workExperience: 23 121 | }; 122 | const response = 123 | await request(server) 124 | .patch('/employees/5a0ac5f420a8da291b551e4b') 125 | .set('x-auth', ADMIN_TOKEN) 126 | .send(newEmployee); 127 | expect(response.status).toBe(404); 128 | }); 129 | }); 130 | 131 | describe('DELETE /employees/:_id', () => { 132 | test('should be able to remove employee', async () => { 133 | const ADMIN_TOKEN = ADMINS[0].tokens[0].token; 134 | const response = 135 | await request(server) 136 | .delete(`/employees/${EMPLOYEES[0]._id}`) 137 | .set('x-auth', ADMIN_TOKEN); 138 | expect(response.status).toBe(200); 139 | expect(response.body).toEqual(EMPLOYEES[0]); 140 | const employees = await Employee.find(); 141 | expect(employees.length).toBe(EMPLOYEES.length - 1); 142 | }); 143 | 144 | test('will get 404 while removing employee which does not exist', async () => { 145 | const ADMIN_TOKEN = ADMINS[0].tokens[0].token; 146 | const response = 147 | await request(server) 148 | .delete('/employees/5a0ac5f420a8da291b551e4b') 149 | .set('x-auth', ADMIN_TOKEN); 150 | expect(response.status).toBe(404); 151 | const employees = await Employee.find(); 152 | expect(employees.length).toBe(EMPLOYEES.length); 153 | }); 154 | }); 155 | -------------------------------------------------------------------------------- /test/setUpDB.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const mongoDbURI = process.env.MONGODB_URI || 'mongodb://localhost:27017/koa-app-test'; 3 | const path = require('path'); 4 | 5 | const getFileName = parentFilePath => path.basename(parentFilePath).split('.test.js')[0]; 6 | 7 | module.exports = parentFilePath => { 8 | const dbURL = mongoDbURI + '-' + getFileName(parentFilePath); 9 | mongoose.connect(dbURL, {useMongoClient: true}); 10 | }; 11 | 12 | --------------------------------------------------------------------------------