From 79bac9e3d7a91a2535b54d52220b3fa46012f9ff Mon Sep 17 00:00:00 2001 From: Ibbus93 Date: Mon, 27 Jan 2020 12:18:34 +0100 Subject: [PATCH 1/2] Refactor of the project with test --- package.json | 7 +++-- server.js | 9 +++--- src/app.js | 22 ++++++-------- src/config/dynamodb.config.js | 22 ++++++-------- src/controllers/item.controller.js | 35 +++++++++++++++------ src/errors/index.js | 5 +++ src/models/item.model.js | 32 ++++++++----------- src/models/schemas/item.schema.js | 20 ++++++++++++ src/routes/index.js | 13 ++++++++ src/routes/route.index.js | 9 ------ src/validators/item.validators.js | 5 +++ tests/item.controller.test.js | 49 ++++++++++++++++++++++++++++++ tests/mocks/index.js | 25 +++++++++++++++ tests/server.test.js | 8 ++--- tests/validators.test.js | 34 +++++++++++++++++++++ 15 files changed, 220 insertions(+), 75 deletions(-) create mode 100644 src/errors/index.js create mode 100644 src/models/schemas/item.schema.js create mode 100644 src/routes/index.js delete mode 100644 src/routes/route.index.js create mode 100644 src/validators/item.validators.js create mode 100644 tests/item.controller.test.js create mode 100644 tests/mocks/index.js create mode 100644 tests/validators.test.js diff --git a/package.json b/package.json index 2d3f74b..bec8828 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "index.js", "scripts": { "start": "nodemon --exec babel-node server.js", - "test": "DEBUG=server:debug NODE_ENV=test mocha --require @babel/register --reporter spec --exit tests/ --exec babel-node", + "test": "DEBUG=server:debug NODE_ENV=test serverless-bundle test", "cover": "nyc npm run test" }, "repository": { @@ -29,6 +29,8 @@ "dynamoose": "^1.11.1", "express": "^4.17.1", "express-joi-validation": "^4.0.1", + "http-status-codes": "^1.4.0", + "lodash": "^4.17.15", "source-map-support": "^0.5.13" }, "devDependencies": { @@ -42,9 +44,8 @@ "babel-loader": "^8.0.6", "babel-plugin-source-map-support": "^2.1.1", "babel-watch": "^7.0.0", - "chai": "^4.2.0", "debug": "^4.1.1", - "mocha": "^6.2.1", + "jest": "^25.1.0", "nodemon": "^1.19.4", "nyc": "^14.1.1", "serverless-bundle": "^1.2.5", diff --git a/server.js b/server.js index 601afbe..3bf6dcd 100644 --- a/server.js +++ b/server.js @@ -6,9 +6,10 @@ const server = http.createServer(app); const PORT = process.env.NODE_PORT || 5000; const listen = server.listen(PORT, () => { - debug(`server is running on port ${PORT}`); - console.log(`Listening to port ${PORT}`); + if (process.env.NODE_ENV !== 'test') { + debug(`server is running on port ${PORT}`); + console.log(`Listening to port ${PORT}`); + } }); - -module.exports.port = listen.address().port; \ No newline at end of file +module.exports.port = listen.address().port; diff --git a/src/app.js b/src/app.js index ae7d065..f41c3ad 100644 --- a/src/app.js +++ b/src/app.js @@ -1,26 +1,22 @@ - -// app.js +/** app.js **/ const express = require('express'); const app = express(); -//require('./config/dynamodb.config'); - -const corsOptions = { - origin: '*', - methods: 'DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT', - allowedHeaders: 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token' -}; +/** CORS option if not using the express ones **/ +// const corsOptions = { +// origin: '*', +// methods: 'DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT', +// allowedHeaders: 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token' +// }; // Integrate CORS options for all routes app.use(express.json()); - // Injecting routes -require('./routes/route.index.js')(app); +require('./routes/index.js')(app); app.get('/', (req, res) => { return res.status(200).send({'message': 'YAY! Congratulations! Your first endpoint is working'}); }); - -module.exports = app; \ No newline at end of file +module.exports = app; diff --git a/src/config/dynamodb.config.js b/src/config/dynamodb.config.js index e1cf710..0a18121 100644 --- a/src/config/dynamodb.config.js +++ b/src/config/dynamodb.config.js @@ -1,25 +1,23 @@ 'use strict'; -import { ENV } from './env.config' - - +// External libraries const dynamoose = require('dynamoose'); -dynamoose.setDefaults( - { - create: false, - waitForActive: false - }); +// Config +import { ENV } from './env.config' + +dynamoose.setDefaults({ + create: false, + waitForActive: false +}); dynamoose.AWS.config.update({ region: "eu-west-1" }); -if(!ENV.DYNAMO_DB.IS_OFFLINE) { +if (!ENV.DYNAMO_DB.IS_OFFLINE) { console.log("is local"); dynamoose.local('http://localhost:4569') } - - -module.exports = dynamoose; \ No newline at end of file +export default dynamoose; diff --git a/src/controllers/item.controller.js b/src/controllers/item.controller.js index 5e41ed9..a986e54 100644 --- a/src/controllers/item.controller.js +++ b/src/controllers/item.controller.js @@ -1,15 +1,30 @@ -import * as Item from '../models/item.model' +/** External libraries **/ +import { INTERNAL_SERVER_ERROR, NOT_FOUND, OK } from 'http-status-codes'; +// import * as _ from 'lodash'; -const Get = async (req, res) => { - console.log(req.params); +import { isEmpty } from 'lodash'; - await Item.get(req.params.ID).then((data) => { - console.log(data); - res.json(data); - }); +/** Item model **/ +import Item from '../models/item.model' +import { ErrItemNotFound } from '../errors'; -}; +const ItemController = { + Get: async (req, res) => { + const { ID } = req.params; + + try { + const result = await Item.Get(ID); -module.exports = { - Get + if (isEmpty(result)) { + return res.status(NOT_FOUND).json(ErrItemNotFound); + } + + return res.status(OK).json(result); + } catch (e) { + console.error(e); + return res.status(INTERNAL_SERVER_ERROR).json(e); + } + } }; + +export default ItemController; diff --git a/src/errors/index.js b/src/errors/index.js new file mode 100644 index 0000000..7ad9fbd --- /dev/null +++ b/src/errors/index.js @@ -0,0 +1,5 @@ +/** ID param not defined **/ +export const ErrIdNotDefined = 'ErrorIdNotDefined'; + +/** Item not found**/ +export const ErrItemNotFound = 'ErrItemNotFound'; diff --git a/src/models/item.model.js b/src/models/item.model.js index d3abd1d..e30372c 100644 --- a/src/models/item.model.js +++ b/src/models/item.model.js @@ -1,23 +1,17 @@ -/** Third Party **/ -const dynamoose = require('../config/dynamodb.config'); +/** Item Dynamoose Schema **/ +import ItemSchema from './schemas/item.schema'; -/** Env **/ -import { ENV } from '../config/env.config'; +/** Errors **/ +import { ErrIdNotDefined } from '../errors'; -const Schema = dynamoose.Schema; +const ItemModel = { + Get: async (ID) => { + if (!ID) { + throw Error(ErrIdNotDefined); + } -const ItemSchema = new Schema({ - set_ID: { - type: String, - hashKey: true - }, - created_at: String, - updated_at: String, - data: String + return await ItemSchema.get(ID); + } +}; -}, { - useNativeBooleans: true, - useDocumentTypes: true, -}); - -module.exports = dynamoose.model(ENV.DYNAMO_DB.ITEM_TABLE, ItemSchema); \ No newline at end of file +export default ItemModel; diff --git a/src/models/schemas/item.schema.js b/src/models/schemas/item.schema.js new file mode 100644 index 0000000..581330c --- /dev/null +++ b/src/models/schemas/item.schema.js @@ -0,0 +1,20 @@ +/** Dynamoose config **/ +import dynamoose from '../../config/dynamodb.config'; + +/** ENV config **/ +import { ENV } from '../../config/env.config'; + +const ItemSchema = new dynamoose.Schema({ + ID: { + type: String, + hashKey: true + }, + createdAt: String, + updatedAt: String, + data: String +}, { + useNativeBooleans: true, + useDocumentTypes: true, +}); + +export default dynamoose.model(ENV.DYNAMO_DB.ITEM_TABLE, ItemSchema); diff --git a/src/routes/index.js b/src/routes/index.js new file mode 100644 index 0000000..7ca993e --- /dev/null +++ b/src/routes/index.js @@ -0,0 +1,13 @@ +/** External libraries **/ +import { createValidator } from 'express-joi-validation'; +const validator = createValidator({}); + +/** Controllers **/ +import ItemController from '../controllers/item.controller'; + +/** Validators **/ +import { itemParamsSchema } from '../validators/item.validators'; + +module.exports = (app) => { + app.get('/api/item/:ID', validator.params(itemParamsSchema), ItemController.Get); +}; diff --git a/src/routes/route.index.js b/src/routes/route.index.js deleted file mode 100644 index fa28792..0000000 --- a/src/routes/route.index.js +++ /dev/null @@ -1,9 +0,0 @@ - - -/** Controllers **/ -import * as ItemController from '../controllers/item.controller'; - -module.exports = (app) => { - app.get('/api/item/:ID', ItemController.Get); - -}; \ No newline at end of file diff --git a/src/validators/item.validators.js b/src/validators/item.validators.js new file mode 100644 index 0000000..4a7ff17 --- /dev/null +++ b/src/validators/item.validators.js @@ -0,0 +1,5 @@ +const Joi = require('@hapi/joi'); + +export const itemParamsSchema = Joi.object({ + ID: Joi.string().guid().required() +}); diff --git a/tests/item.controller.test.js b/tests/item.controller.test.js new file mode 100644 index 0000000..6e57993 --- /dev/null +++ b/tests/item.controller.test.js @@ -0,0 +1,49 @@ +import ItemController from '../src/controllers/item.controller'; + +jest.mock('../src/models/item.model', () => ({ + Get: jest.fn() +})); + +import ItemModel from '../src/models/item.model'; +import { Item, ItemID } from './mocks'; +import { ErrItemNotFound } from '../src/errors'; + +describe('Item Controller', () => { + const params = { ID: ItemID }; + let mockRequest, mockResponse; + + beforeEach(() => { + mockRequest = require('./mocks').mockRequest; + mockResponse = require('./mocks').mockResponse; + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('[GET] Should return the item', async () => { + ItemModel.Get.mockImplementation(() => Item); + + const req = mockRequest({ params }); + const res = mockResponse(); + + await ItemController.Get(req, res); + + expect(ItemModel.Get).toBeCalledTimes(1); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith(Item); + }); + + it('[GET] Should return 404 if the item does not exist', async () => { + ItemModel.Get.mockImplementation(() => {}); + + const req = mockRequest({ params }); + const res = mockResponse(); + + await ItemController.Get(req, res); + + expect(ItemModel.Get).toBeCalledTimes(1); + expect(res.status).toHaveBeenCalledWith(404); + expect(res.json).toHaveBeenCalledWith(ErrItemNotFound); + }); +}); diff --git a/tests/mocks/index.js b/tests/mocks/index.js new file mode 100644 index 0000000..f593d38 --- /dev/null +++ b/tests/mocks/index.js @@ -0,0 +1,25 @@ +const ItemID = 'b9894650-f038-46e5-94e2-1686e755ebb9'; + +const Item = { + ID: ItemID, + data: 'This is a mocked Item', + createdAt: "2020-01-20T10:00:00.000Z", + updatedAt: "2020-01-20T10:00:00.000Z" +}; + +const mockRequest = (data) => ({ ...data }); +const mockResponse = (data) => { + const res = data || {}; + + res.status = jest.fn().mockReturnValue(res); + res.json = jest.fn().mockReturnValue(res); + + return res; +}; + +export { + Item, + ItemID, + mockRequest, + mockResponse +}; diff --git a/tests/server.test.js b/tests/server.test.js index 00ee708..fe849a6 100644 --- a/tests/server.test.js +++ b/tests/server.test.js @@ -1,9 +1,7 @@ -import { expect } from 'chai'; import server from '../server'; describe('Server', ()=>{ - it('tests that server is running current port', async () => { - expect(server.port).to.equal(5000) - + it('Is running in current port', () => { + expect(server.port).toEqual(5000) }) -}); \ No newline at end of file +}); diff --git a/tests/validators.test.js b/tests/validators.test.js new file mode 100644 index 0000000..ef46b93 --- /dev/null +++ b/tests/validators.test.js @@ -0,0 +1,34 @@ +import { itemParamsSchema } from '../src/validators/item.validators'; +import { ItemID } from './mocks'; + +describe('Item Joi validators', () => { + const allowed = { ID: ItemID }; + const notAllowed = { ID: '1' }; + const empty = {}; + + describe('Params validation', () => { + it('Should validate the UUID', () => { + const { error } = itemParamsSchema.validate(allowed); + + expect(error).toBeUndefined(); + }); + + it('Should invalidate with UUID not valid', () => { + const { error } = itemParamsSchema.validate(notAllowed); + const errorTypes = error.details.map(el => el.type); + + expect(error).toBeDefined(); + expect(errorTypes.length).toBeGreaterThan(0); + expect(errorTypes.includes('string.guid')).toBe(true); + }); + + it('Should invalidate if empty', () => { + const { error } = itemParamsSchema.validate(empty); + const errorTypes = error.details.map(el => el.type); + + expect(error).toBeDefined(); + expect(errorTypes.length).toBeGreaterThan(0); + expect(errorTypes.includes('any.required')).toBe(true); + }) + }) +}); From 2cc31862b78222fcb05d9b2f0ed97f356b65e3d5 Mon Sep 17 00:00:00 2001 From: Ibbus93 Date: Mon, 27 Jan 2020 12:20:25 +0100 Subject: [PATCH 2/2] Edit README.md --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 5eae25a..f30292f 100644 --- a/README.md +++ b/README.md @@ -27,9 +27,7 @@ ecosystem and it's good to archieve a proof-of concepts. * `Webpack` and `webpack-serverless` if you want to use the latest ecma version, use loaders and manage the bundle of your code. -* `Mocha` testing framework - -* `Chai` Our TDD/BDD assertion library +* `Jest` Our TDD/BDD assertion library * `SuperTest` HTTP server testing library