├── services ├── search │ └── .gitkeep ├── authentication │ ├── .gitkeep │ ├── .gitignore │ ├── .env │ ├── .dockerignore │ ├── .eslintrc.yml │ ├── Dockerfile │ ├── src │ │ ├── routes │ │ │ └── auth.routes.js │ │ ├── models │ │ │ └── auth.model.js │ │ ├── app.js │ │ ├── server.js │ │ ├── environment │ │ │ └── config.js │ │ ├── message-bus │ │ │ └── recieve │ │ │ │ └── user.added.js │ │ └── controllers │ │ │ └── auth.controller.js │ ├── .snyk │ └── package.json ├── user-management │ ├── .gitkeep │ ├── .dockerignore │ ├── .eslintrc.yml │ ├── .gitignore │ ├── src │ │ ├── middlewares │ │ │ ├── __mocks__ │ │ │ │ └── jwt.js │ │ │ └── jwt.js │ │ ├── app.js │ │ ├── server.js │ │ ├── models │ │ │ ├── user.model.js │ │ │ └── __mocks__ │ │ │ │ └── user.model.js │ │ ├── controllers │ │ │ ├── __mocks__ │ │ │ │ └── user.controller.js │ │ │ └── user.controller.js │ │ ├── routes │ │ │ └── user.routes.js │ │ ├── environment │ │ │ └── config.js │ │ └── message-bus │ │ │ └── send │ │ │ └── user.added.js │ ├── __mocks__ │ │ ├── winston.js │ │ └── amqp-ts-async.js │ ├── tests │ │ └── unit │ │ │ ├── __snapshots__ │ │ │ ├── user.added.message.send.test.js.snap │ │ │ ├── user.controller.test.js.snap │ │ │ └── config.test.js.snap │ │ │ ├── config.test.js │ │ │ ├── app.test.js │ │ │ ├── user.model.test.js │ │ │ ├── server.test.js │ │ │ ├── user.routes.test.js │ │ │ ├── user.added.message.send.test.js │ │ │ └── user.controller.test.js │ ├── Dockerfile │ └── package.json ├── media-management │ └── .gitkeep ├── articles-management │ ├── .eslintignore │ ├── .dockerignore │ ├── .eslintrc.yml │ ├── .gitignore │ ├── src │ │ ├── middlewares │ │ │ ├── __mocks__ │ │ │ │ └── jwt.js │ │ │ └── jwt.js │ │ ├── message-bus │ │ │ └── send │ │ │ │ ├── __mocks__ │ │ │ │ └── article.added.js │ │ │ │ └── article.added.js │ │ ├── app.js │ │ ├── server.js │ │ ├── models │ │ │ ├── article.model.js │ │ │ └── __mocks__ │ │ │ │ └── article.model.js │ │ ├── controllers │ │ │ ├── __mocks__ │ │ │ │ └── article.controller.js │ │ │ └── article.controller.js │ │ ├── routes │ │ │ └── article.routes.js │ │ └── environment │ │ │ └── config.js │ ├── __mocks__ │ │ ├── winston.js │ │ └── amqp-ts-async.js │ ├── tests │ │ └── unit │ │ │ ├── __snapshots__ │ │ │ └── article.added.message.send.test.js.snap │ │ │ ├── config.test.js │ │ │ ├── app.test.js │ │ │ ├── article.model.test.js │ │ │ ├── server.test.js │ │ │ ├── article.routes.test.js │ │ │ ├── article.added.message.send.test.js │ │ │ └── article.controller.test.js │ ├── Dockerfile │ └── package.json ├── events-management │ ├── .dockerignore │ ├── .gitignore │ ├── .eslintrc.yml │ ├── src │ │ ├── middlewares │ │ │ ├── __mocks__ │ │ │ │ └── jwt.js │ │ │ └── jwt.js │ │ ├── app.js │ │ ├── server.js │ │ ├── models │ │ │ ├── event.model.js │ │ │ └── __mocks__ │ │ │ │ └── event.model.js │ │ ├── controllers │ │ │ ├── __mocks__ │ │ │ │ └── event.controller.js │ │ │ └── event.controller.js │ │ ├── routes │ │ │ └── event.routes.js │ │ └── environment │ │ │ └── config.js │ ├── Dockerfile │ ├── tests │ │ └── unit │ │ │ ├── config.test.js │ │ │ ├── app.test.js │ │ │ ├── event.model.test.js │ │ │ ├── server.test.js │ │ │ ├── event.routes.test.js │ │ │ └── event.controller.test.js │ └── package.json └── notification │ ├── .gitignore │ ├── .eslintrc.yml │ ├── src │ ├── subscriptions │ │ ├── __mocks__ │ │ │ └── article.added.js │ │ └── article.added.js │ ├── modules │ │ └── email │ │ │ ├── __mocks__ │ │ │ ├── email.js │ │ │ └── email.templates.js │ │ │ ├── email.js │ │ │ └── email.templates.js │ ├── message-controllers │ │ ├── __mocks__ │ │ │ └── articles.js │ │ └── articles.js │ ├── server.js │ └── environment │ │ └── config.js │ ├── __mocks__ │ ├── winston.js │ ├── koa.js │ ├── nodemailer.js │ └── amqp-ts-async.js │ ├── Dockerfile │ ├── tests │ └── unit │ │ ├── __snapshots__ │ │ ├── config.test.js.snap │ │ ├── email.test.js.snap │ │ └── email.templates.test.js.snap │ │ ├── email.templates.test.js │ │ ├── config.test.js │ │ ├── server.test.js │ │ ├── article.message.controller.test.js │ │ ├── email.test.js │ │ └── article.added.subscription.test.js │ └── package.json ├── .githooks ├── pre-commit.d │ ├── notification-service │ ├── user-management-service │ ├── events-management-service │ └── articles-management-service ├── pre-push.d │ ├── notification-service │ ├── user-management-service │ ├── events-management-service │ └── articles-management-service ├── pre-push └── pre-commit ├── run_all_tests ├── LICENSE.md ├── docker-compose.yml └── README.md /services/search/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /services/authentication/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /services/user-management/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /services/media-management/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /services/authentication/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /services/articles-management/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /services/authentication/.env: -------------------------------------------------------------------------------- 1 | PORT="8081" 2 | MESSAGE_BUS="amqp://localhost" -------------------------------------------------------------------------------- /services/authentication/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | .env -------------------------------------------------------------------------------- /services/events-management/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | .env -------------------------------------------------------------------------------- /services/user-management/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | .env -------------------------------------------------------------------------------- /services/articles-management/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | .env -------------------------------------------------------------------------------- /services/notification/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | .env 4 | coverage -------------------------------------------------------------------------------- /services/authentication/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: airbnb-base 2 | env: 3 | jest: true 4 | 5 | -------------------------------------------------------------------------------- /services/events-management/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | .env 4 | coverage -------------------------------------------------------------------------------- /services/notification/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: airbnb-base 2 | env: 3 | jest: true 4 | 5 | -------------------------------------------------------------------------------- /services/articles-management/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: airbnb-base 2 | env: 3 | jest: true 4 | 5 | -------------------------------------------------------------------------------- /services/articles-management/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | .env 4 | coverage -------------------------------------------------------------------------------- /services/events-management/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: airbnb-base 2 | env: 3 | jest: true 4 | 5 | -------------------------------------------------------------------------------- /services/user-management/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: airbnb-base 2 | env: 3 | jest: true 4 | 5 | -------------------------------------------------------------------------------- /services/user-management/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | .env 4 | coverage 5 | -------------------------------------------------------------------------------- /.githooks/pre-commit.d/notification-service: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd ./services/notification 4 | npm run lint -------------------------------------------------------------------------------- /.githooks/pre-push.d/notification-service: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd ./services/notification 4 | npm run testOnly -------------------------------------------------------------------------------- /services/notification/src/subscriptions/__mocks__/article.added.js: -------------------------------------------------------------------------------- 1 | module.exports.start = jest.fn(); 2 | -------------------------------------------------------------------------------- /.githooks/pre-commit.d/user-management-service: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd ./services/user-management 4 | npm run lint -------------------------------------------------------------------------------- /.githooks/pre-push.d/user-management-service: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd ./services/user-management 4 | npm run testOnly -------------------------------------------------------------------------------- /.githooks/pre-commit.d/events-management-service: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd ./services/events-management 4 | npm run lint -------------------------------------------------------------------------------- /.githooks/pre-push.d/events-management-service: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd ./services/events-management 4 | npm run testOnly -------------------------------------------------------------------------------- /.githooks/pre-commit.d/articles-management-service: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd ./services/articles-management 4 | npm run lint -------------------------------------------------------------------------------- /.githooks/pre-push.d/articles-management-service: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd ./services/articles-management 4 | npm run testOnly -------------------------------------------------------------------------------- /services/events-management/src/middlewares/__mocks__/jwt.js: -------------------------------------------------------------------------------- 1 | const jwt = jest.fn((ctx, next) => { 2 | next(); 3 | }); 4 | 5 | module.exports = jwt; 6 | -------------------------------------------------------------------------------- /services/user-management/src/middlewares/__mocks__/jwt.js: -------------------------------------------------------------------------------- 1 | const jwt = jest.fn((ctx, next) => { 2 | next(); 3 | }); 4 | 5 | module.exports = jwt; 6 | -------------------------------------------------------------------------------- /services/articles-management/src/middlewares/__mocks__/jwt.js: -------------------------------------------------------------------------------- 1 | const jwt = jest.fn((ctx, next) => { 2 | next(); 3 | }); 4 | 5 | module.exports = jwt; 6 | -------------------------------------------------------------------------------- /services/notification/__mocks__/winston.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | module.exports.info = jest.fn((message) => { 4 | }); 5 | 6 | module.exports.error = jest.fn((message) => { 7 | }); 8 | -------------------------------------------------------------------------------- /services/user-management/__mocks__/winston.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | module.exports.info = jest.fn((message) => { 4 | }); 5 | 6 | module.exports.error = jest.fn((message) => { 7 | }); 8 | -------------------------------------------------------------------------------- /services/articles-management/__mocks__/winston.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | module.exports.info = jest.fn((message) => { 4 | }); 5 | 6 | module.exports.error = jest.fn((message) => { 7 | }); 8 | -------------------------------------------------------------------------------- /services/articles-management/src/middlewares/jwt.js: -------------------------------------------------------------------------------- 1 | const koaJwt = require('koa-jwt'); 2 | const config = require('../environment/config'); 3 | 4 | module.exports = koaJwt({ 5 | secret: config.jwtsecret, 6 | }); 7 | -------------------------------------------------------------------------------- /services/events-management/src/middlewares/jwt.js: -------------------------------------------------------------------------------- 1 | const koaJwt = require('koa-jwt'); 2 | const config = require('../environment/config'); 3 | 4 | module.exports = koaJwt({ 5 | secret: config.jwtsecret, 6 | }); 7 | -------------------------------------------------------------------------------- /services/user-management/src/middlewares/jwt.js: -------------------------------------------------------------------------------- 1 | const koaJwt = require('koa-jwt'); 2 | const config = require('../environment/config'); 3 | 4 | module.exports = koaJwt({ 5 | secret: config.jwtsecret, 6 | }); 7 | -------------------------------------------------------------------------------- /services/notification/__mocks__/koa.js: -------------------------------------------------------------------------------- 1 | const appListen = jest.fn(() => { 2 | 3 | }); 4 | 5 | module.exports = jest.fn(() => ({ 6 | listen: appListen, 7 | })); 8 | 9 | module.exports.appListen = appListen; 10 | -------------------------------------------------------------------------------- /services/notification/src/modules/email/__mocks__/email.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | const email = {}; 3 | 4 | email.sendArticleAddedEmail = jest.fn((message) => { 5 | }); 6 | 7 | module.exports = email; 8 | -------------------------------------------------------------------------------- /services/user-management/tests/unit/__snapshots__/user.added.message.send.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Send Should correcly parse the json object 1`] = `mockConstructor {}`; 4 | -------------------------------------------------------------------------------- /services/notification/src/message-controllers/__mocks__/articles.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | const controller = {}; 3 | 4 | controller.added = jest.fn(message => true); 5 | 6 | module.exports = controller; 7 | -------------------------------------------------------------------------------- /services/articles-management/tests/unit/__snapshots__/article.added.message.send.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Send Should correcly parse the json object 1`] = `mockConstructor {}`; 4 | -------------------------------------------------------------------------------- /services/notification/src/modules/email/__mocks__/email.templates.js: -------------------------------------------------------------------------------- 1 | const templates = {}; 2 | 3 | templates.GetRenderedArticleAddedEmailHtml = jest.fn((title, description) => `${title}, ${description}`); 4 | 5 | module.exports = templates; 6 | -------------------------------------------------------------------------------- /services/articles-management/src/message-bus/send/__mocks__/article.added.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | const addedEventMessage = {}; 3 | 4 | addedEventMessage.send = (article) => { 5 | }; 6 | 7 | module.exports = addedEventMessage; 8 | -------------------------------------------------------------------------------- /run_all_tests: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd ./services/articles-management 4 | npm test 5 | cd - 6 | 7 | cd ./services/events-management 8 | npm test 9 | cd - 10 | 11 | cd ./services/user-management 12 | npm test 13 | cd - 14 | 15 | cd ./services/notification 16 | npm test 17 | cd - -------------------------------------------------------------------------------- /services/notification/__mocks__/nodemailer.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | const lib = {}; 4 | 5 | const sendMail = jest.fn(options => true); 6 | 7 | lib.createTransport = jest.fn(options => ({ 8 | sendMail, 9 | })); 10 | 11 | module.exports = lib; 12 | module.exports.sendMail = sendMail; 13 | -------------------------------------------------------------------------------- /services/user-management/tests/unit/__snapshots__/user.controller.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`findById should return user when correct id is passed 1`] = ` 4 | Object { 5 | "emailAddress": "test@email.com", 6 | "firstName": "Test User", 7 | "id": 123, 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /services/notification/src/server.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const logger = require('winston'); 3 | const Koa = require('koa'); 4 | const config = require('./environment/config'); 5 | 6 | require('./subscriptions/article.added').start(); 7 | 8 | const app = new Koa(); 9 | app.listen(); 10 | logger.info(config.startedMessage); 11 | -------------------------------------------------------------------------------- /services/notification/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use Node v11.2 as the base image. 2 | FROM node:11.2.0-alpine 3 | 4 | #Set the working directory 5 | WORKDIR /usr/app 6 | 7 | # Copy everything in current directory to /server folder 8 | ADD . /server 9 | 10 | # Install dependencies 11 | RUN cd /server; \ 12 | npm install 13 | 14 | # Run node 15 | CMD ["node", "/server/src/server.js"] -------------------------------------------------------------------------------- /services/authentication/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use Node v11.2 as the base image. 2 | FROM node:11.2.0-alpine 3 | 4 | #Set the working directory 5 | WORKDIR /usr/app 6 | 7 | # Copy everything in current directory to /server folder 8 | ADD . /server 9 | 10 | # Install dependencies 11 | RUN cd /server; \ 12 | npm install 13 | 14 | EXPOSE 3000 15 | 16 | # Run node 17 | CMD ["node", "/server/src/server.js"] -------------------------------------------------------------------------------- /services/articles-management/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use Node v11.2 as the base image. 2 | FROM node:11.2.0-alpine 3 | 4 | #Set the working directory 5 | WORKDIR /usr/app 6 | 7 | # Copy everything in current directory to /server folder 8 | ADD . /server 9 | 10 | # Install dependencies 11 | RUN cd /server; \ 12 | npm install 13 | 14 | EXPOSE 3000 15 | 16 | # Run node 17 | CMD ["node", "/server/src/server.js"] -------------------------------------------------------------------------------- /services/events-management/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use Node v11.2 as the base image. 2 | FROM node:11.2.0-alpine 3 | 4 | #Set the working directory 5 | WORKDIR /usr/app 6 | 7 | # Copy everything in current directory to /server folder 8 | ADD . /server 9 | 10 | # Install dependencies 11 | RUN cd /server; \ 12 | npm install 13 | 14 | EXPOSE 3000 15 | 16 | # Run node 17 | CMD ["node", "/server/src/server.js"] -------------------------------------------------------------------------------- /services/user-management/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use Node v11.2 as the base image. 2 | FROM node:11.2.0-alpine 3 | 4 | #Set the working directory 5 | WORKDIR /usr/app 6 | 7 | # Copy everything in current directory to /server folder 8 | ADD . /server 9 | 10 | # Install dependencies 11 | RUN cd /server; \ 12 | npm install 13 | 14 | EXPOSE 3000 15 | 16 | # Run node 17 | CMD ["node", "/server/src/server.js"] -------------------------------------------------------------------------------- /services/authentication/src/routes/auth.routes.js: -------------------------------------------------------------------------------- 1 | const KoaRouter = require('koa-router'); 2 | const config = require('../environment/config'); 3 | const authController = require('../controllers/auth.controller'); 4 | 5 | const api = 'auth'; 6 | 7 | const router = new KoaRouter(); 8 | 9 | router.prefix(`/${config.baseAPIRoute}/${api}`); 10 | 11 | // POST /api/auth 12 | router.post('/', authController.authenticate); 13 | 14 | module.exports = router; 15 | -------------------------------------------------------------------------------- /services/notification/tests/unit/__snapshots__/config.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`config should load default values 1`] = ` 4 | Object { 5 | "email": Object { 6 | "adminEmailID": "", 7 | "password": "", 8 | "service": "", 9 | "username": "", 10 | }, 11 | "environment": "dev", 12 | "messagebus": "amqp://guest:guest@localhost", 13 | "name": "Notification Service", 14 | "startedMessage": "Notification Service is running", 15 | } 16 | `; 17 | -------------------------------------------------------------------------------- /services/authentication/src/models/auth.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const AuthSchema = new mongoose.Schema({ 4 | createdDate: { type: Date, default: Date.now }, 5 | updatedDate: { type: Date, default: Date.now }, 6 | emailAddress: { type: String, require: true }, 7 | password: { type: String, require: true }, 8 | role: { type: String, require: true, default: 'regular' }, 9 | status: { type: String, default: 'active' }, 10 | }); 11 | 12 | module.exports = mongoose.model('Auth', AuthSchema); 13 | -------------------------------------------------------------------------------- /services/notification/src/message-controllers/articles.js: -------------------------------------------------------------------------------- 1 | const logger = require('winston'); 2 | const emailModule = require('../modules/email/email'); 3 | 4 | const controller = {}; 5 | 6 | controller.added = async (message) => { 7 | try { 8 | const content = JSON.parse(message.content.toString()); 9 | await emailModule.sendArticleAddedEmail(content); 10 | } catch (err) { 11 | logger.error(`Error in handling the recieved message - article.added - ${err}`); 12 | } 13 | }; 14 | 15 | module.exports = controller; 16 | -------------------------------------------------------------------------------- /services/notification/src/environment/config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | name: 'Notification Service', 3 | messagebus: process.env.MESSAGE_BUS || 'amqp://guest:guest@localhost', 4 | environment: process.env.ENVIRONMENT || 'dev', 5 | email: { 6 | service: process.env.EMAIL_SERVICE || '', 7 | username: process.env.EMAIL_ID || '', 8 | password: process.env.EMAIL_PASSWORD || '', 9 | adminEmailID: process.env.ADMIN_EMAIL || '', 10 | }, 11 | }; 12 | 13 | config.startedMessage = `${config.name} is running`; 14 | 15 | module.exports = config; 16 | -------------------------------------------------------------------------------- /services/notification/tests/unit/email.templates.test.js: -------------------------------------------------------------------------------- 1 | const template = require('../../src/modules/email/email.templates'); 2 | 3 | describe('Article Added Email Template', () => { 4 | test('Should return correct email template', () => { 5 | const html = template.GetRenderedArticleAddedEmailHtml('Test Name', 'Test Text'); 6 | expect(html).toMatchSnapshot(); 7 | }); 8 | 9 | test('Should return correct email template when no data is passed', () => { 10 | const html = template.GetRenderedArticleAddedEmailHtml(); 11 | expect(html).toMatchSnapshot(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /services/articles-management/tests/unit/config.test.js: -------------------------------------------------------------------------------- 1 | const config = require('../../src/environment/config'); 2 | 3 | describe('config', () => { 4 | test('should load default values', () => { 5 | expect(config.name).not.toBeNull(); 6 | expect(config.port).not.toBeNull(); 7 | expect(config.baseAPIRoute).not.toBeNull(); 8 | expect(config.environment).not.toBeNull(); 9 | expect(config.messagebus).not.toBeNull(); 10 | expect(config.db.uri).not.toBeNull(); 11 | expect(config.db.username).not.toBeNull(); 12 | expect(config.db.password).not.toBeNull(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /services/events-management/tests/unit/config.test.js: -------------------------------------------------------------------------------- 1 | const config = require('../../src/environment/config'); 2 | 3 | describe('config', () => { 4 | test('should load default values', () => { 5 | expect(config.name).not.toBeNull(); 6 | expect(config.port).not.toBeNull(); 7 | expect(config.baseAPIRoute).not.toBeNull(); 8 | expect(config.environment).not.toBeNull(); 9 | expect(config.messagebus).not.toBeNull(); 10 | expect(config.db.uri).not.toBeNull(); 11 | expect(config.db.username).not.toBeNull(); 12 | expect(config.db.password).not.toBeNull(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /services/authentication/src/app.js: -------------------------------------------------------------------------------- 1 | // Import the required npm packages 2 | const Koa = require('koa'); 3 | const Logger = require('koa-logger'); 4 | const Helmet = require('koa-helmet'); 5 | const BodyParser = require('koa-bodyparser'); 6 | 7 | // Get the API routes file 8 | const authRouter = require('./routes/auth.routes'); 9 | 10 | // Init Koa API App 11 | const app = new Koa(); 12 | app.use(Logger()); 13 | app.use(BodyParser()); 14 | app.use(Helmet()); 15 | 16 | // Setup the API routes 17 | app.use(authRouter.routes()).use(authRouter.allowedMethods({ throw: true })); 18 | 19 | module.exports = app; 20 | -------------------------------------------------------------------------------- /services/notification/tests/unit/config.test.js: -------------------------------------------------------------------------------- 1 | const config = require('../../src/environment/config'); 2 | 3 | describe('config', () => { 4 | test('should load default values', () => { 5 | expect(config.name).not.toBeNull(); 6 | expect(config.environment).not.toBeNull(); 7 | expect(config.messagebus).not.toBeNull(); 8 | expect(config.email.service).not.toBeNull(); 9 | expect(config.email.username).not.toBeNull(); 10 | expect(config.email.password).not.toBeNull(); 11 | expect(config.email.adminEmailID).not.toBeNull(); 12 | expect(config).toMatchSnapshot(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /services/user-management/src/app.js: -------------------------------------------------------------------------------- 1 | // Import the required npm packages 2 | const Koa = require('koa'); 3 | const Logger = require('koa-logger'); 4 | const Helmet = require('koa-helmet'); 5 | const BodyParser = require('koa-bodyparser'); 6 | 7 | // Get the API routes file 8 | const userRouter = require('./routes/user.routes'); 9 | 10 | // Init Koa API App 11 | const app = new Koa(); 12 | app.use(Logger()); 13 | app.use(BodyParser()); 14 | app.use(Helmet()); 15 | 16 | // Setup the API routes 17 | app.use(userRouter.routes()).use(userRouter.allowedMethods({ throw: true })); 18 | 19 | module.exports = app; 20 | -------------------------------------------------------------------------------- /services/events-management/src/app.js: -------------------------------------------------------------------------------- 1 | // Import the required npm packages 2 | const Koa = require('koa'); 3 | const Logger = require('koa-logger'); 4 | const Helmet = require('koa-helmet'); 5 | const BodyParser = require('koa-bodyparser'); 6 | 7 | // Get the API routes file 8 | const eventRouter = require('./routes/event.routes'); 9 | 10 | // Init Koa API App 11 | const app = new Koa(); 12 | app.use(Logger()); 13 | app.use(BodyParser()); 14 | app.use(Helmet()); 15 | 16 | // Setup the API routes 17 | app.use(eventRouter.routes()).use(eventRouter.allowedMethods({ throw: true })); 18 | 19 | module.exports = app; 20 | -------------------------------------------------------------------------------- /services/articles-management/src/app.js: -------------------------------------------------------------------------------- 1 | // Import the required npm packages 2 | const Koa = require('koa'); 3 | const Logger = require('koa-logger'); 4 | const Helmet = require('koa-helmet'); 5 | const BodyParser = require('koa-bodyparser'); 6 | 7 | // Get the API routes file 8 | const articleRouter = require('./routes/article.routes'); 9 | 10 | // Init Koa API App 11 | const app = new Koa(); 12 | app.use(Logger()); 13 | app.use(BodyParser()); 14 | app.use(Helmet()); 15 | 16 | // Setup the API routes 17 | app.use(articleRouter.routes()).use(articleRouter.allowedMethods({ throw: true })); 18 | 19 | module.exports = app; 20 | -------------------------------------------------------------------------------- /services/authentication/.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.19.0 3 | ignore: {} 4 | # patches apply the minimum changes required to fix a vulnerability 5 | patch: 6 | SNYK-JS-LODASH-567746: 7 | - winston > async > lodash: 8 | patched: '2020-08-04T12:02:15.835Z' 9 | - mongoose > async > lodash: 10 | patched: '2020-08-04T12:02:15.835Z' 11 | - amqp-ts-async > winston > async > lodash: 12 | patched: '2020-08-04T12:02:15.835Z' 13 | - amqp-ts-async > @types/winston > winston > async > lodash: 14 | patched: '2020-08-04T12:02:15.835Z' 15 | -------------------------------------------------------------------------------- /services/user-management/src/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | // Init the environment variables and server configurations 4 | require('dotenv').config(); 5 | 6 | // Import the required packages 7 | const Mongoose = require('mongoose'); 8 | const config = require('./environment/config'); 9 | const app = require('./app'); 10 | 11 | // Init Database Connection 12 | Mongoose.connect(config.db.uri, { user: config.db.username, pass: config.db.password }); 13 | Mongoose.connection.on('error', console.error); 14 | 15 | // Run the API Server 16 | app.listen(config.port, () => { 17 | console.log(config.startedMessage); 18 | }); 19 | -------------------------------------------------------------------------------- /services/user-management/tests/unit/config.test.js: -------------------------------------------------------------------------------- 1 | const config = require('../../src/environment/config'); 2 | 3 | describe('config', () => { 4 | test('should load default values', () => { 5 | expect(config.name).not.toBeNull(); 6 | expect(config.port).not.toBeNull(); 7 | expect(config.baseAPIRoute).not.toBeNull(); 8 | expect(config.environment).not.toBeNull(); 9 | expect(config.messagebus).not.toBeNull(); 10 | expect(config.db.uri).not.toBeNull(); 11 | expect(config.db.username).not.toBeNull(); 12 | expect(config.db.password).not.toBeNull(); 13 | expect(config).toMatchSnapshot(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /services/articles-management/src/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | // Init the environment variables and server configurations 4 | require('dotenv').config(); 5 | 6 | // Import the required packages 7 | const Mongoose = require('mongoose'); 8 | const config = require('./environment/config'); 9 | const app = require('./app'); 10 | 11 | // Init Database Connection 12 | Mongoose.connect(config.db.uri, { user: config.db.username, pass: config.db.password }); 13 | Mongoose.connection.on('error', console.error); 14 | 15 | // Run the API Server 16 | app.listen(config.port, () => { 17 | console.log(config.startedMessage); 18 | }); 19 | -------------------------------------------------------------------------------- /services/events-management/src/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | // Init the environment variables and server configurations 4 | require('dotenv').config(); 5 | 6 | // Import the required packages 7 | const Mongoose = require('mongoose'); 8 | const config = require('./environment/config'); 9 | const app = require('./app'); 10 | 11 | // Init Database Connection 12 | Mongoose.connect(config.db.uri, { user: config.db.username, pass: config.db.password }); 13 | Mongoose.connection.on('error', console.error); 14 | 15 | // Run the API Server 16 | app.listen(config.port, () => { 17 | console.log(config.startedMessage); 18 | }); 19 | -------------------------------------------------------------------------------- /services/articles-management/src/models/article.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const ArticleSchema = new mongoose.Schema({ 4 | authorUID: { type: String, require: true }, 5 | createdDate: { type: Date, default: Date.now }, 6 | updatedDate: { type: Date, default: Date.now }, 7 | title: { type: String, require: true }, 8 | description: { type: String, require: true }, 9 | body: { type: String, require: true }, 10 | meta: { 11 | likes: { type: Number, default: 0 }, 12 | }, 13 | status: { type: Number }, 14 | imagesUID: [String], 15 | tags: [String], 16 | }); 17 | 18 | module.exports = mongoose.model('Article', ArticleSchema); 19 | -------------------------------------------------------------------------------- /services/user-management/src/models/user.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const UserSchema = new mongoose.Schema({ 4 | createdDate: { type: Date, default: Date.now }, 5 | updatedDate: { type: Date, default: Date.now }, 6 | firstName: { type: String, require: true }, 7 | lastName: { type: String, require: true }, 8 | emailAddress: { type: String, require: true }, 9 | description: { type: String, require: true }, 10 | meta: { 11 | likes: { type: Number, default: 0 }, 12 | }, 13 | role: { type: String, require: true, default: 'regular' }, 14 | imagesUID: [String], 15 | tags: [String], 16 | }); 17 | 18 | module.exports = mongoose.model('User', UserSchema); 19 | -------------------------------------------------------------------------------- /services/events-management/src/models/event.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const EventSchema = new mongoose.Schema({ 4 | authorUID: { type: String, require: true }, 5 | createdDate: { type: Date, default: Date.now }, 6 | updatedDate: { type: Date, default: Date.now }, 7 | eventDate: { type: Date, require: true }, 8 | title: { type: String, require: true }, 9 | description: { type: String, require: true }, 10 | body: { type: String, require: true }, 11 | meta: { 12 | attending: { type: Number, default: 0 }, 13 | }, 14 | status: { type: Number }, 15 | imagesUID: [String], 16 | tags: [String], 17 | }); 18 | 19 | module.exports = mongoose.model('Event', EventSchema); 20 | -------------------------------------------------------------------------------- /services/notification/tests/unit/server.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | jest.mock('../../src/subscriptions/article.added'); 3 | jest.mock('koa'); 4 | jest.mock('winston'); 5 | 6 | const koa = require('koa'); 7 | const winston = require('winston'); 8 | const articleAddedSubscription = require('../../src/subscriptions/article.added'); 9 | const config = require('../../src/environment/config'); 10 | 11 | test('Server works', async () => { 12 | require('../../src/server'); 13 | expect(koa.appListen).toBeCalledTimes(1); 14 | expect(winston.info).toBeCalledTimes(1); 15 | expect(winston.info).toBeCalledWith(config.startedMessage); 16 | expect(articleAddedSubscription.start).toBeCalledTimes(1); 17 | }); 18 | -------------------------------------------------------------------------------- /services/user-management/src/controllers/__mocks__/user.controller.js: -------------------------------------------------------------------------------- 1 | const userController = {}; 2 | 3 | userController.find = jest.fn(async (ctx) => { 4 | ctx.status = 200; 5 | ctx.body = ''; 6 | }); 7 | 8 | userController.findById = jest.fn(async (ctx) => { 9 | ctx.status = 200; 10 | ctx.body = ctx.params.id; 11 | }); 12 | 13 | userController.add = jest.fn(async (ctx) => { 14 | ctx.status = 200; 15 | ctx.body = ''; 16 | }); 17 | 18 | userController.update = jest.fn(async (ctx) => { 19 | ctx.status = 200; 20 | ctx.body = ctx.params.id; 21 | }); 22 | 23 | userController.delete = jest.fn(async (ctx) => { 24 | ctx.status = 200; 25 | ctx.body = ctx.params.id; 26 | }); 27 | 28 | module.exports = userController; 29 | -------------------------------------------------------------------------------- /services/events-management/src/controllers/__mocks__/event.controller.js: -------------------------------------------------------------------------------- 1 | const eventController = {}; 2 | 3 | eventController.find = jest.fn(async (ctx) => { 4 | ctx.status = 200; 5 | ctx.body = ''; 6 | }); 7 | 8 | eventController.findById = jest.fn(async (ctx) => { 9 | ctx.status = 200; 10 | ctx.body = ctx.params.id; 11 | }); 12 | 13 | eventController.add = jest.fn(async (ctx) => { 14 | ctx.status = 200; 15 | ctx.body = ''; 16 | }); 17 | 18 | eventController.update = jest.fn(async (ctx) => { 19 | ctx.status = 200; 20 | ctx.body = ctx.params.id; 21 | }); 22 | 23 | eventController.delete = jest.fn(async (ctx) => { 24 | ctx.status = 200; 25 | ctx.body = ctx.params.id; 26 | }); 27 | 28 | module.exports = eventController; 29 | -------------------------------------------------------------------------------- /services/authentication/src/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | // Init the environment variables and server configurations 4 | require('dotenv').config(); 5 | 6 | // Import the required packages 7 | const Mongoose = require('mongoose'); 8 | const config = require('./environment/config'); 9 | const app = require('./app'); 10 | 11 | // Init Database Connection 12 | Mongoose.connect(config.db.uri, { user: config.db.username, pass: config.db.password }); 13 | Mongoose.connection.on('error', console.error); 14 | 15 | // Start Listening to Subscribed Events 16 | require('./message-bus/recieve/user.added').start(); 17 | 18 | // Run the API Server 19 | app.listen(config.port, () => { 20 | console.log(config.startedMessage); 21 | }); 22 | -------------------------------------------------------------------------------- /services/user-management/tests/unit/app.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable arrow-body-style */ 2 | /* eslint-disable no-unused-vars */ 3 | /* eslint-disable global-require */ 4 | 5 | const Koa = require('koa'); 6 | const userRouter = require('../../src/routes/user.routes'); 7 | 8 | const mockUserRoutes = jest.fn(() => { return async (ctx, next) => {}; }); 9 | userRouter.routes = mockUserRoutes; 10 | 11 | describe('Koa App', () => { 12 | test('Should return valid koa application', () => { 13 | const app = require('../../src/app'); 14 | expect(app).toBeInstanceOf(Koa); 15 | }); 16 | 17 | test('Should have user routes', () => { 18 | require('../../src/app'); 19 | expect(mockUserRoutes.mock.calls.length).toBe(1); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /services/events-management/tests/unit/app.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable arrow-body-style */ 2 | /* eslint-disable no-unused-vars */ 3 | /* eslint-disable global-require */ 4 | 5 | const Koa = require('koa'); 6 | const eventRouter = require('../../src/routes/event.routes'); 7 | 8 | const mockEventRoutes = jest.fn(() => { return async (ctx, next) => {}; }); 9 | eventRouter.routes = mockEventRoutes; 10 | 11 | describe('Koa App', () => { 12 | test('Should return valid koa application', () => { 13 | const app = require('../../src/app'); 14 | expect(app).toBeInstanceOf(Koa); 15 | }); 16 | 17 | test('Should have article routes', () => { 18 | require('../../src/app'); 19 | expect(mockEventRoutes.mock.calls.length).toBe(1); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /services/articles-management/src/controllers/__mocks__/article.controller.js: -------------------------------------------------------------------------------- 1 | const articleController = {}; 2 | 3 | articleController.find = jest.fn(async (ctx) => { 4 | ctx.status = 200; 5 | ctx.body = ''; 6 | }); 7 | 8 | articleController.findById = jest.fn(async (ctx) => { 9 | ctx.status = 200; 10 | ctx.body = ctx.params.id; 11 | }); 12 | 13 | articleController.add = jest.fn(async (ctx) => { 14 | ctx.status = 200; 15 | ctx.body = ''; 16 | }); 17 | 18 | articleController.update = jest.fn(async (ctx) => { 19 | ctx.status = 200; 20 | ctx.body = ctx.params.id; 21 | }); 22 | 23 | articleController.delete = jest.fn(async (ctx) => { 24 | ctx.status = 200; 25 | ctx.body = ctx.params.id; 26 | }); 27 | 28 | module.exports = articleController; 29 | -------------------------------------------------------------------------------- /services/articles-management/tests/unit/app.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable arrow-body-style */ 2 | /* eslint-disable no-unused-vars */ 3 | /* eslint-disable global-require */ 4 | 5 | const Koa = require('koa'); 6 | const articleRouter = require('../../src/routes/article.routes'); 7 | 8 | const mockArticleRoutes = jest.fn(() => { return async (ctx, next) => {}; }); 9 | articleRouter.routes = mockArticleRoutes; 10 | 11 | describe('Koa App', () => { 12 | test('Should return valid koa application', () => { 13 | const app = require('../../src/app'); 14 | expect(app).toBeInstanceOf(Koa); 15 | }); 16 | 17 | test('Should have article routes', () => { 18 | require('../../src/app'); 19 | expect(mockArticleRoutes.mock.calls.length).toBe(1); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /.githooks/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Running Pre-Push hook" 4 | 5 | # This script should be saved in a git repo as a hook file, e.g. .git/hooks/pre-receive. 6 | # It looks for scripts in the .git/hooks/pre-receive.d directory and executes them in order, 7 | # passing along stdin. If any script exits with a non-zero status, this script exits. 8 | 9 | script_dir=$(dirname $0) 10 | hook_name=$(basename $0) 11 | 12 | hook_dir="$script_dir/$hook_name.d" 13 | 14 | if [[ -d $hook_dir ]]; then 15 | stdin=$(cat /dev/stdin) 16 | 17 | for hook in $hook_dir/*; do 18 | echo "Running $hook_name/$hook hook" 19 | echo "$stdin" | $hook "$@" 20 | 21 | exit_code=$? 22 | 23 | if [ $exit_code != 0 ]; then 24 | exit $exit_code 25 | fi 26 | done 27 | fi 28 | 29 | exit 0 -------------------------------------------------------------------------------- /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Running Pre-Commit hook" 4 | 5 | # This script should be saved in a git repo as a hook file, e.g. .git/hooks/pre-receive. 6 | # It looks for scripts in the .git/hooks/pre-receive.d directory and executes them in order, 7 | # passing along stdin. If any script exits with a non-zero status, this script exits. 8 | 9 | script_dir=$(dirname $0) 10 | hook_name=$(basename $0) 11 | 12 | hook_dir="$script_dir/$hook_name.d" 13 | 14 | if [[ -d $hook_dir ]]; then 15 | stdin=$(cat /dev/stdin) 16 | 17 | for hook in $hook_dir/*; do 18 | echo "Running $hook_name/$hook hook" 19 | echo "$stdin" | $hook "$@" 20 | 21 | exit_code=$? 22 | 23 | if [ $exit_code != 0 ]; then 24 | exit $exit_code 25 | fi 26 | done 27 | fi 28 | 29 | exit 0 30 | 31 | -------------------------------------------------------------------------------- /services/authentication/src/environment/config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | name: 'Authentication Service', 3 | baseAPIRoute: 'api', 4 | port: process.env.PORT || 8080, 5 | messagebus: process.env.MESSAGE_BUS || 'amqp://rabbitmq', 6 | environment: process.env.ENVIRONMENT || 'dev', 7 | db: { 8 | uri: process.env.DB_URI || 'mongodb://chalumuv-localnewsapplication.documents.azure.com:10255/?ssl=true&replicaSet=globaldb', 9 | username: process.env.DB_USERNAME || 'chalumuv-localnewsapplication', 10 | password: process.env.DB_PASSWORD || 'TlJ7hnd7iRck25fUFFWYgfJFdK2oSH1N2kbBQjFzb66nqFx486JP6eaCKAQrlyn3Cnwxn6MzJtF5ABeyN9CKYQ==', 11 | }, 12 | jwtsecret: 'yoursecretkey', 13 | }; 14 | 15 | config.startedMessage = `${config.name} is running on port ${config.port}/`; 16 | 17 | module.exports = config; 18 | -------------------------------------------------------------------------------- /services/user-management/src/routes/user.routes.js: -------------------------------------------------------------------------------- 1 | const KoaRouter = require('koa-router'); 2 | const config = require('../environment/config'); 3 | const usersController = require('../controllers/user.controller'); 4 | const jwt = require('../middlewares/jwt'); 5 | 6 | const api = 'users'; 7 | 8 | const router = new KoaRouter(); 9 | 10 | router.prefix(`/${config.baseAPIRoute}/${api}`); 11 | 12 | // GET /api/users 13 | router.get('/', usersController.find); 14 | 15 | // POST /api/users 16 | router.post('/', jwt, usersController.add); 17 | 18 | // GET /api/users/id 19 | router.get('/:id', usersController.findById); 20 | 21 | // PUT /api/users/id 22 | router.put('/:id', jwt, usersController.update); 23 | 24 | // DELETE /api/users/id 25 | router.delete('/:id', jwt, usersController.delete); 26 | 27 | module.exports = router; 28 | -------------------------------------------------------------------------------- /services/user-management/tests/unit/__snapshots__/config.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`config should load default values 1`] = ` 4 | Object { 5 | "baseAPIRoute": "api", 6 | "db": Object { 7 | "password": "TlJ7hnd7iRck25fUFFWYgfJFdK2oSH1N2kbBQjFzb66nqFx486JP6eaCKAQrlyn3Cnwxn6MzJtF5ABeyN9CKYQ==", 8 | "uri": "mongodb://chalumuv-localnewsapplication.documents.azure.com:10255/?ssl=true&replicaSet=globaldb", 9 | "username": "chalumuv-localnewsapplication", 10 | }, 11 | "environment": "dev", 12 | "jwtsecret": "yoursecretkey", 13 | "messageTimeout": 500, 14 | "messagebus": "amqp://rabbitmq", 15 | "name": "User Management Service", 16 | "port": 8080, 17 | "services": Object {}, 18 | "startedMessage": "User Management Service is running on port 8080/", 19 | } 20 | `; 21 | -------------------------------------------------------------------------------- /services/notification/tests/unit/__snapshots__/email.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Email Options Shoud set the correct email credentials 1`] = ` 4 | Object { 5 | "auth": Object { 6 | "pass": "", 7 | "user": "", 8 | }, 9 | "service": "", 10 | } 11 | `; 12 | 13 | exports[`Send Article Added Email Shoud send email when correct details are provided 1`] = ` 14 | Object { 15 | "from": "", 16 | "html": "Test Article, Test Text", 17 | "subject": "LocalNewsApplication - New Article Added!", 18 | "to": "", 19 | } 20 | `; 21 | 22 | exports[`Send Article Added Email Shoud throw an error when an exception occurs 1`] = ` 23 | Object { 24 | "from": "", 25 | "html": "Test Article, Test Text", 26 | "subject": "LocalNewsApplication - New Article Added!", 27 | "to": "", 28 | } 29 | `; 30 | -------------------------------------------------------------------------------- /services/events-management/src/routes/event.routes.js: -------------------------------------------------------------------------------- 1 | const KoaRouter = require('koa-router'); 2 | const config = require('../environment/config'); 3 | const eventsController = require('../controllers/event.controller'); 4 | const jwt = require('../middlewares/jwt'); 5 | 6 | const api = 'events'; 7 | 8 | const router = new KoaRouter(); 9 | 10 | router.prefix(`/${config.baseAPIRoute}/${api}`); 11 | 12 | // GET /api/events 13 | router.get('/', eventsController.find); 14 | 15 | // POST /api/events 16 | router.post('/', jwt, eventsController.add); 17 | 18 | // GET /api/events/id 19 | router.get('/:id', eventsController.findById); 20 | 21 | // PUT /api/events/id 22 | router.put('/:id', jwt, eventsController.update); 23 | 24 | // DELETE /api/events/id 25 | router.delete('/:id', jwt, eventsController.delete); 26 | 27 | module.exports = router; 28 | -------------------------------------------------------------------------------- /services/authentication/src/message-bus/recieve/user.added.js: -------------------------------------------------------------------------------- 1 | const logger = require('winston'); 2 | const amqp = require('amqp-ts-async'); 3 | const config = require('../../environment/config'); 4 | const authController = require('../../controllers/auth.controller'); 5 | 6 | const exchangeName = 'user.added'; 7 | const queueName = ''; 8 | 9 | const connection = new amqp.Connection(config.messagebus); 10 | const exchange = connection.declareExchange(exchangeName, 'fanout', { durable: false }); 11 | const queue = connection.declareQueue(queueName, { exclusive: true }); 12 | queue.bind(exchange); 13 | 14 | module.exports = { 15 | start: () => { 16 | try { 17 | queue.activateConsumer(authController.add); 18 | } catch (err) { 19 | logger.error(`Error Listening to ${exchangeName}, ${queueName}: ${err}`); 20 | } 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /services/events-management/src/environment/config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | name: 'Events Management Service', 3 | baseAPIRoute: 'api', 4 | port: process.env.PORT || 8080, 5 | messagebus: process.env.MESSAGE_BUS || 'amqp://rabbitmq', 6 | environment: process.env.ENVIRONMENT || 'dev', 7 | db: { 8 | uri: process.env.DB_URI || 'mongodb://chalumuv-localnewsapplication.documents.azure.com:10255/?ssl=true&replicaSet=globaldb', 9 | username: process.env.DB_USERNAME || 'chalumuv-localnewsapplication', 10 | password: process.env.DB_PASSWORD || 'TlJ7hnd7iRck25fUFFWYgfJFdK2oSH1N2kbBQjFzb66nqFx486JP6eaCKAQrlyn3Cnwxn6MzJtF5ABeyN9CKYQ==', 11 | }, 12 | services: { 13 | }, 14 | jwtsecret: 'yoursecretkey', 15 | }; 16 | 17 | config.startedMessage = `${config.name} is running on port ${config.port}/`; 18 | 19 | module.exports = config; 20 | -------------------------------------------------------------------------------- /services/articles-management/src/routes/article.routes.js: -------------------------------------------------------------------------------- 1 | const KoaRouter = require('koa-router'); 2 | const config = require('../environment/config'); 3 | const articleController = require('../controllers/article.controller'); 4 | const jwt = require('../middlewares/jwt'); 5 | 6 | const api = 'articles'; 7 | 8 | const router = new KoaRouter(); 9 | 10 | router.prefix(`/${config.baseAPIRoute}/${api}`); 11 | 12 | // GET /api/articles 13 | router.get('/', articleController.find); 14 | 15 | // POST /api/articles 16 | router.post('/', jwt, articleController.add); 17 | 18 | // GET /api/articles/id 19 | router.get('/:id', articleController.findById); 20 | 21 | // PUT /api/articles/id 22 | router.put('/:id', jwt, articleController.update); 23 | 24 | // DELETE /api/articles/id 25 | router.delete('/:id', jwt, articleController.delete); 26 | 27 | module.exports = router; 28 | -------------------------------------------------------------------------------- /services/notification/src/subscriptions/article.added.js: -------------------------------------------------------------------------------- 1 | const logger = require('winston'); 2 | const amqp = require('amqp-ts-async'); 3 | const config = require('../environment/config'); 4 | const articleMessageController = require('../message-controllers/articles'); 5 | 6 | const exchangeName = 'articles.added'; 7 | const queueName = ''; 8 | 9 | const connection = new amqp.Connection(config.messagebus); 10 | const exchange = connection.declareExchange(exchangeName, 'fanout', { durable: false }); 11 | const queue = connection.declareQueue(queueName, { exclusive: true }); 12 | queue.bind(exchange); 13 | 14 | module.exports = { 15 | start: () => { 16 | try { 17 | queue.activateConsumer(articleMessageController.added); 18 | } catch (err) { 19 | logger.error(`Error Listening to ${exchangeName}, ${queueName}: ${err}`); 20 | } 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /services/user-management/src/environment/config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | name: 'User Management Service', 3 | baseAPIRoute: 'api', 4 | port: process.env.PORT || 8080, 5 | messagebus: process.env.MESSAGE_BUS || 'amqp://rabbitmq', 6 | environment: process.env.ENVIRONMENT || 'dev', 7 | db: { 8 | uri: process.env.DB_URI || 'mongodb://chalumuv-localnewsapplication.documents.azure.com:10255/?ssl=true&replicaSet=globaldb', 9 | username: process.env.DB_USERNAME || 'chalumuv-localnewsapplication', 10 | password: process.env.DB_PASSWORD || 'TlJ7hnd7iRck25fUFFWYgfJFdK2oSH1N2kbBQjFzb66nqFx486JP6eaCKAQrlyn3Cnwxn6MzJtF5ABeyN9CKYQ==', 11 | }, 12 | services: { 13 | }, 14 | messageTimeout: 500, 15 | jwtsecret: 'yoursecretkey', 16 | }; 17 | 18 | config.startedMessage = `${config.name} is running on port ${config.port}/`; 19 | 20 | module.exports = config; 21 | -------------------------------------------------------------------------------- /services/articles-management/src/environment/config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | name: 'Article Management Service', 3 | baseAPIRoute: 'api', 4 | port: process.env.PORT || 8080, 5 | messagebus: process.env.MESSAGE_BUS || 'amqp://rabbitmq', 6 | environment: process.env.ENVIRONMENT || 'dev', 7 | db: { 8 | uri: process.env.DB_URI || 'mongodb://chalumuv-localnewsapplication.documents.azure.com:10255/?ssl=true&replicaSet=globaldb', 9 | username: process.env.DB_USERNAME || 'chalumuv-localnewsapplication', 10 | password: process.env.DB_PASSWORD || 'TlJ7hnd7iRck25fUFFWYgfJFdK2oSH1N2kbBQjFzb66nqFx486JP6eaCKAQrlyn3Cnwxn6MzJtF5ABeyN9CKYQ==', 11 | }, 12 | services: { 13 | }, 14 | messageTimeout: 500, 15 | jwtsecret: 'yoursecretkey', 16 | }; 17 | 18 | config.startedMessage = `${config.name} is running on port ${config.port}/`; 19 | 20 | module.exports = config; 21 | -------------------------------------------------------------------------------- /services/user-management/src/message-bus/send/user.added.js: -------------------------------------------------------------------------------- 1 | const logger = require('winston'); 2 | const amqp = require('amqp-ts-async'); 3 | const config = require('../../environment/config'); 4 | 5 | const exchangeName = 'user.added'; 6 | 7 | module.exports = { 8 | send: (user) => { 9 | try { 10 | if (!user) { 11 | throw new Error('Sould send a valid user to message queue'); 12 | } 13 | const connection = new amqp.Connection(config.messagebus); 14 | const exchange = connection.declareExchange(exchangeName, 'fanout', { durable: false }); 15 | const message = new amqp.Message(JSON.stringify(user)); 16 | exchange.send(message); 17 | setTimeout(() => { 18 | connection.close(); 19 | }, config.messageTimeout); 20 | } catch (err) { 21 | logger.error(`Error Sending Article Added Event to ${exchangeName}: ${err}`); 22 | } 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /services/notification/__mocks__/amqp-ts-async.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable arrow-body-style */ 2 | /* eslint-disable no-unused-vars */ 3 | 4 | 5 | const bind = jest.fn((exchange) => { 6 | }); 7 | 8 | const activateConsumer = jest.fn((onRecieve) => { 9 | onRecieve(); 10 | }); 11 | 12 | const declareExchange = jest.fn((exchangeName, type, options) => { 13 | return exchangeName; 14 | }); 15 | 16 | const declareQueue = jest.fn((exchangeName, type, options) => { 17 | return { 18 | bind, 19 | activateConsumer, 20 | }; 21 | }); 22 | 23 | const connection = jest.fn((url) => { 24 | return { 25 | connectionUrl: url, 26 | declareExchange, 27 | declareQueue, 28 | }; 29 | }); 30 | 31 | module.exports.Connection = connection; 32 | module.exports.activateConsumer = activateConsumer; 33 | module.exports.bind = bind; 34 | module.exports.declareExchange = declareExchange; 35 | module.exports.declareQueue = declareQueue; 36 | -------------------------------------------------------------------------------- /services/articles-management/src/message-bus/send/article.added.js: -------------------------------------------------------------------------------- 1 | const logger = require('winston'); 2 | const amqp = require('amqp-ts-async'); 3 | const config = require('../../environment/config'); 4 | 5 | const exchangeName = 'articles.added'; 6 | 7 | module.exports = { 8 | send: (article) => { 9 | try { 10 | if (!article) { 11 | throw new Error('Sould send a valid article to message queue'); 12 | } 13 | const connection = new amqp.Connection(config.messagebus); 14 | const exchange = connection.declareExchange(exchangeName, 'fanout', { durable: false }); 15 | const message = new amqp.Message(JSON.stringify(article)); 16 | exchange.send(message); 17 | setTimeout(() => { 18 | connection.close(); 19 | }, config.messageTimeout); 20 | } catch (err) { 21 | logger.error(`Error Sending Article Added Event to ${exchangeName}: ${err}`); 22 | } 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /services/user-management/src/models/__mocks__/user.model.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable arrow-body-style */ 2 | 3 | const userModel = {}; 4 | 5 | let data = []; 6 | 7 | userModel.find = jest.fn(() => data); 8 | 9 | userModel.findById = jest.fn((id) => { 10 | return data.find(user => user.id === id); 11 | }); 12 | 13 | userModel.create = jest.fn((user) => { 14 | data.push(user); 15 | return user; 16 | }); 17 | 18 | userModel.findByIdAndUpdate = jest.fn((id, updateduser) => { 19 | const index = data.findIndex(user => user.id === id); 20 | if (index >= 0) { 21 | data[index] = updateduser; 22 | } 23 | return updateduser; 24 | }); 25 | 26 | userModel.findByIdAndRemove = jest.fn((id) => { 27 | const index = data.findIndex(user => user.id === id); 28 | data.splice(index, 1); 29 | return id; 30 | }); 31 | 32 | userModel.count = () => data.length; 33 | userModel.reset = () => { data = []; }; 34 | 35 | module.exports = userModel; 36 | -------------------------------------------------------------------------------- /services/events-management/src/models/__mocks__/event.model.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable arrow-body-style */ 2 | 3 | const eventModel = {}; 4 | 5 | let data = []; 6 | 7 | eventModel.find = jest.fn(() => data); 8 | 9 | eventModel.findById = jest.fn((id) => { 10 | return data.find(event => event.id === id); 11 | }); 12 | 13 | eventModel.create = jest.fn((event) => { 14 | data.push(event); 15 | return event; 16 | }); 17 | 18 | eventModel.findByIdAndUpdate = jest.fn((id, updatedEvent) => { 19 | const index = data.findIndex(event => event.id === id); 20 | if (index >= 0) { 21 | data[index] = updatedEvent; 22 | } 23 | return updatedEvent; 24 | }); 25 | 26 | eventModel.findByIdAndRemove = jest.fn((id) => { 27 | const index = data.findIndex(event => event.id === id); 28 | data.splice(index, 1); 29 | return id; 30 | }); 31 | 32 | eventModel.count = () => data.length; 33 | eventModel.reset = () => { data = []; }; 34 | 35 | module.exports = eventModel; 36 | -------------------------------------------------------------------------------- /services/articles-management/src/models/__mocks__/article.model.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable arrow-body-style */ 2 | 3 | const articleModel = {}; 4 | 5 | let data = []; 6 | 7 | articleModel.find = jest.fn(() => data); 8 | 9 | articleModel.findById = jest.fn((id) => { 10 | return data.find(article => article.id === id); 11 | }); 12 | 13 | articleModel.create = jest.fn((article) => { 14 | data.push(article); 15 | return article; 16 | }); 17 | 18 | articleModel.findByIdAndUpdate = jest.fn((id, updatedArticle) => { 19 | const index = data.findIndex(article => article.id === id); 20 | if (index >= 0) { 21 | data[index] = updatedArticle; 22 | } 23 | return updatedArticle; 24 | }); 25 | 26 | articleModel.findByIdAndRemove = jest.fn((id) => { 27 | const index = data.findIndex(article => article.id === id); 28 | data.splice(index, 1); 29 | return id; 30 | }); 31 | 32 | articleModel.count = () => data.length; 33 | articleModel.reset = () => { data = []; }; 34 | 35 | module.exports = articleModel; 36 | -------------------------------------------------------------------------------- /services/notification/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "notification", 3 | "version": "1.0.0", 4 | "description": "Notifications Service (Internal) - Reposible to send out notifications to users regarding various topics", 5 | "main": "./src/server.js", 6 | "scripts": { 7 | "start": "node ./src/server.js", 8 | "test": "jest --coverage", 9 | "watch": "jest --coverage --watchAll", 10 | "lint": "eslint ./src && eslint ./tests", 11 | "testOnly" : "jest", 12 | "checkCodeQuality" : "eslint ./src && eslint ./tests && jest --coverage" 13 | }, 14 | "author": "Rithin Chalumuri", 15 | "license": "MIT", 16 | "dependencies": { 17 | "amqp-ts-async": "^1.3.7", 18 | "dotenv": "^6.2.0", 19 | "koa": "^2.6.2", 20 | "nodemailer": "^4.7.0", 21 | "winston": "^3.1.0" 22 | }, 23 | "devDependencies": { 24 | "eslint": "^5.9.0", 25 | "eslint-config-airbnb-base": "^13.1.0", 26 | "eslint-plugin-import": "^2.14.0", 27 | "jest": "^23.6.0", 28 | "supertest": "^3.3.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /services/notification/src/modules/email/email.js: -------------------------------------------------------------------------------- 1 | const nodemailer = require('nodemailer'); 2 | const logger = require('winston'); 3 | const config = require('../../environment/config'); 4 | const template = require('./email.templates'); 5 | 6 | const transporter = nodemailer.createTransport({ 7 | service: config.email.service, 8 | auth: { 9 | user: config.email.username, 10 | pass: config.email.password, 11 | }, 12 | }); 13 | 14 | const email = {}; 15 | 16 | email.sendArticleAddedEmail = async (message) => { 17 | try { 18 | const emailHtml = template.GetRenderedArticleAddedEmailHtml(message.title, message.description); 19 | 20 | const mailOptions = { 21 | from: config.email.username, 22 | to: config.email.adminEmailID, 23 | subject: 'LocalNewsApplication - New Article Added!', 24 | html: emailHtml, 25 | }; 26 | await transporter.sendMail(mailOptions); 27 | } catch (err) { 28 | logger.error(`Error sending email ${err}`); 29 | } 30 | }; 31 | 32 | module.exports = email; 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Rithin Chalumuri 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 | -------------------------------------------------------------------------------- /services/user-management/__mocks__/amqp-ts-async.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable arrow-body-style */ 2 | /* eslint-disable no-unused-vars */ 3 | 4 | 5 | const bind = jest.fn((exchange) => { 6 | }); 7 | 8 | const activateConsumer = jest.fn((onRecieve) => { 9 | onRecieve(); 10 | }); 11 | 12 | const send = jest.fn(msg => msg); 13 | 14 | const declareExchange = jest.fn((exchangeName, type, options) => { 15 | return { 16 | send, 17 | }; 18 | }); 19 | 20 | const declareQueue = jest.fn((exchangeName, type, options) => { 21 | return { 22 | bind, 23 | activateConsumer, 24 | }; 25 | }); 26 | 27 | const close = jest.fn(); 28 | 29 | const connection = jest.fn((url) => { 30 | return { 31 | connectionUrl: url, 32 | declareExchange, 33 | declareQueue, 34 | close, 35 | }; 36 | }); 37 | 38 | const message = jest.fn(msg => msg); 39 | 40 | module.exports.Connection = connection; 41 | module.exports.activateConsumer = activateConsumer; 42 | module.exports.bind = bind; 43 | module.exports.declareExchange = declareExchange; 44 | module.exports.declareQueue = declareQueue; 45 | module.exports.Message = message; 46 | module.exports.send = send; 47 | module.exports.close = close; 48 | -------------------------------------------------------------------------------- /services/articles-management/__mocks__/amqp-ts-async.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable arrow-body-style */ 2 | /* eslint-disable no-unused-vars */ 3 | 4 | 5 | const bind = jest.fn((exchange) => { 6 | }); 7 | 8 | const activateConsumer = jest.fn((onRecieve) => { 9 | onRecieve(); 10 | }); 11 | 12 | const send = jest.fn(msg => msg); 13 | 14 | const declareExchange = jest.fn((exchangeName, type, options) => { 15 | return { 16 | send, 17 | }; 18 | }); 19 | 20 | const declareQueue = jest.fn((exchangeName, type, options) => { 21 | return { 22 | bind, 23 | activateConsumer, 24 | }; 25 | }); 26 | 27 | const close = jest.fn(); 28 | 29 | const connection = jest.fn((url) => { 30 | return { 31 | connectionUrl: url, 32 | declareExchange, 33 | declareQueue, 34 | close, 35 | }; 36 | }); 37 | 38 | const message = jest.fn(msg => msg); 39 | 40 | module.exports.Connection = connection; 41 | module.exports.activateConsumer = activateConsumer; 42 | module.exports.bind = bind; 43 | module.exports.declareExchange = declareExchange; 44 | module.exports.declareQueue = declareQueue; 45 | module.exports.Message = message; 46 | module.exports.send = send; 47 | module.exports.close = close; 48 | -------------------------------------------------------------------------------- /services/user-management/tests/unit/user.model.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | const mongoose = require('mongoose'); 3 | 4 | const mockSchema = jest.fn(); 5 | const mockModel = jest.fn((modelName, schema) => ({ modelName, schema })); 6 | 7 | mongoose.model = mockModel; 8 | mongoose.Schema = mockSchema; 9 | 10 | describe('User Model', () => { 11 | test('Should have the correct user model name', () => { 12 | const model = require('../../src/models/user.model'); 13 | expect(model.modelName).toBe('User'); 14 | }); 15 | 16 | test('Schema should contain the required fields', () => { 17 | require('../../src/models/user.model'); 18 | expect(mockSchema).toBeCalledTimes(1); 19 | const schema = mockSchema.mock.calls[0][0]; 20 | expect(Object.keys(schema).length).toBe(10); 21 | expect(schema.createdDate).toBeDefined(); 22 | expect(schema.updatedDate).toBeDefined(); 23 | expect(schema.firstName).toBeDefined(); 24 | expect(schema.description).toBeDefined(); 25 | expect(schema.lastName).toBeDefined(); 26 | expect(schema.meta).toBeDefined(); 27 | expect(schema.emailAddress).toBeDefined(); 28 | expect(schema.imagesUID).toBeDefined(); 29 | expect(schema.tags).toBeDefined(); 30 | expect(schema.role).toBeDefined(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /services/articles-management/tests/unit/article.model.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | const mongoose = require('mongoose'); 3 | 4 | const mockSchema = jest.fn(); 5 | const mockModel = jest.fn((modelName, schema) => ({ modelName, schema })); 6 | 7 | mongoose.model = mockModel; 8 | mongoose.Schema = mockSchema; 9 | 10 | describe('Article Model', () => { 11 | test('Should have the correct article name', () => { 12 | const model = require('../../src/models/article.model'); 13 | expect(model.modelName).toBe('Article'); 14 | }); 15 | 16 | test('Schema should contain the required fields', () => { 17 | require('../../src/models/article.model'); 18 | expect(mockSchema).toBeCalledTimes(1); 19 | const schema = mockSchema.mock.calls[0][0]; 20 | expect(Object.keys(schema).length).toBe(10); 21 | expect(schema.authorUID).toBeDefined(); 22 | expect(schema.createdDate).toBeDefined(); 23 | expect(schema.updatedDate).toBeDefined(); 24 | expect(schema.title).toBeDefined(); 25 | expect(schema.description).toBeDefined(); 26 | expect(schema.body).toBeDefined(); 27 | expect(schema.meta).toBeDefined(); 28 | expect(schema.status).toBeDefined(); 29 | expect(schema.imagesUID).toBeDefined(); 30 | expect(schema.tags).toBeDefined(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /services/events-management/tests/unit/event.model.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | const mongoose = require('mongoose'); 3 | 4 | const mockSchema = jest.fn(); 5 | const mockModel = jest.fn((modelName, schema) => ({ modelName, schema })); 6 | 7 | mongoose.model = mockModel; 8 | mongoose.Schema = mockSchema; 9 | 10 | describe('Event Model', () => { 11 | test('Should have the correct event name', () => { 12 | const model = require('../../src/models/event.model'); 13 | expect(model.modelName).toBe('Event'); 14 | }); 15 | 16 | test('Schema should contain the required fields', () => { 17 | require('../../src/models/event.model'); 18 | expect(mockSchema).toBeCalledTimes(1); 19 | const schema = mockSchema.mock.calls[0][0]; 20 | expect(Object.keys(schema).length).toBe(11); 21 | expect(schema.authorUID).toBeDefined(); 22 | expect(schema.createdDate).toBeDefined(); 23 | expect(schema.updatedDate).toBeDefined(); 24 | expect(schema.title).toBeDefined(); 25 | expect(schema.description).toBeDefined(); 26 | expect(schema.body).toBeDefined(); 27 | expect(schema.meta).toBeDefined(); 28 | expect(schema.status).toBeDefined(); 29 | expect(schema.imagesUID).toBeDefined(); 30 | expect(schema.tags).toBeDefined(); 31 | expect(schema.eventDate).toBeDefined(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /services/events-management/tests/unit/server.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | 3 | const mongoose = require('mongoose'); 4 | const app = require('../../src/app'); 5 | const config = require('../../src/environment/config'); 6 | 7 | const consoleObj = console; 8 | const consoleLogMock = jest.fn(); 9 | 10 | const mockConnectDB = jest.fn(); 11 | const mockListen = jest.fn((port, startedMessage) => { 12 | startedMessage(); 13 | }); 14 | 15 | mongoose.connect = mockConnectDB; 16 | app.listen = mockListen; 17 | consoleObj.log = consoleLogMock; 18 | 19 | afterEach(() => { 20 | mockListen.mockReset(); 21 | mockConnectDB.mockReset(); 22 | consoleLogMock.mockReset(); 23 | }); 24 | 25 | test('Server works', async () => { 26 | require('../../src/server'); 27 | 28 | expect(mockConnectDB.mock.calls.length).toBe(1); 29 | expect(mockConnectDB.mock.calls[0][0]).toBe(config.db.uri); 30 | expect(mockConnectDB.mock.calls[0][1].user).toBe(config.db.username); 31 | expect(mockConnectDB.mock.calls[0][1].pass).toBe(config.db.password); 32 | 33 | expect(mockListen.mock.calls.length).toBe(1); 34 | expect(mockListen.mock.calls[0][0]).toBe(config.port); 35 | expect(mockListen.mock.calls[0][1]).not.toBeNull(); 36 | 37 | expect(consoleLogMock.mock.calls.length).toBe(1); 38 | expect(consoleLogMock.mock.calls[0][0]).toBe(config.startedMessage); 39 | }); 40 | -------------------------------------------------------------------------------- /services/user-management/tests/unit/server.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | 3 | const mongoose = require('mongoose'); 4 | const app = require('../../src/app'); 5 | const config = require('../../src/environment/config'); 6 | 7 | const consoleObj = console; 8 | const consoleLogMock = jest.fn(); 9 | 10 | const mockConnectDB = jest.fn(); 11 | const mockListen = jest.fn((port, startedMessage) => { 12 | startedMessage(); 13 | }); 14 | 15 | mongoose.connect = mockConnectDB; 16 | app.listen = mockListen; 17 | consoleObj.log = consoleLogMock; 18 | 19 | afterEach(() => { 20 | mockListen.mockReset(); 21 | mockConnectDB.mockReset(); 22 | consoleLogMock.mockReset(); 23 | }); 24 | 25 | test('Server works', async () => { 26 | require('../../src/server'); 27 | 28 | expect(mockConnectDB.mock.calls.length).toBe(1); 29 | expect(mockConnectDB.mock.calls[0][0]).toBe(config.db.uri); 30 | expect(mockConnectDB.mock.calls[0][1].user).toBe(config.db.username); 31 | expect(mockConnectDB.mock.calls[0][1].pass).toBe(config.db.password); 32 | 33 | expect(mockListen.mock.calls.length).toBe(1); 34 | expect(mockListen.mock.calls[0][0]).toBe(config.port); 35 | expect(mockListen.mock.calls[0][1]).not.toBeNull(); 36 | 37 | expect(consoleLogMock.mock.calls.length).toBe(1); 38 | expect(consoleLogMock.mock.calls[0][0]).toBe(config.startedMessage); 39 | }); 40 | -------------------------------------------------------------------------------- /services/articles-management/tests/unit/server.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | 3 | const mongoose = require('mongoose'); 4 | const app = require('../../src/app'); 5 | const config = require('../../src/environment/config'); 6 | 7 | const consoleObj = console; 8 | const consoleLogMock = jest.fn(); 9 | 10 | const mockConnectDB = jest.fn(); 11 | const mockListen = jest.fn((port, startedMessage) => { 12 | startedMessage(); 13 | }); 14 | 15 | mongoose.connect = mockConnectDB; 16 | app.listen = mockListen; 17 | consoleObj.log = consoleLogMock; 18 | 19 | afterEach(() => { 20 | mockListen.mockReset(); 21 | mockConnectDB.mockReset(); 22 | consoleLogMock.mockReset(); 23 | }); 24 | 25 | test('Server works', async () => { 26 | require('../../src/server'); 27 | 28 | expect(mockConnectDB.mock.calls.length).toBe(1); 29 | expect(mockConnectDB.mock.calls[0][0]).toBe(config.db.uri); 30 | expect(mockConnectDB.mock.calls[0][1].user).toBe(config.db.username); 31 | expect(mockConnectDB.mock.calls[0][1].pass).toBe(config.db.password); 32 | 33 | expect(mockListen.mock.calls.length).toBe(1); 34 | expect(mockListen.mock.calls[0][0]).toBe(config.port); 35 | expect(mockListen.mock.calls[0][1]).not.toBeNull(); 36 | 37 | expect(consoleLogMock.mock.calls.length).toBe(1); 38 | expect(consoleLogMock.mock.calls[0][0]).toBe(config.startedMessage); 39 | }); 40 | -------------------------------------------------------------------------------- /services/events-management/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "events-management", 3 | "version": "1.0.0", 4 | "description": "Events Management Service - reponsible for handling local events related operations", 5 | "main": "./src/server.js", 6 | "scripts": { 7 | "start": "node ./src/server.js", 8 | "test": "jest --coverage", 9 | "watch": "jest --coverage --watchAll", 10 | "lint": "eslint ./src && eslint ./tests", 11 | "testOnly": "jest", 12 | "checkCodeQuality": "eslint ./src && eslint ./tests && jest --coverage" 13 | }, 14 | "jest": { 15 | "testEnvironment": "node", 16 | "verbose": true, 17 | "coverageThreshold": { 18 | "global": { 19 | "branches": 100, 20 | "functions": 100, 21 | "lines": 100, 22 | "statements": -10 23 | } 24 | } 25 | }, 26 | "author": "Rithin Chalumuri", 27 | "license": "MIT", 28 | "dependencies": { 29 | "dotenv": "^6.1.0", 30 | "koa": "^2.6.2", 31 | "koa-bodyparser": "^4.2.1", 32 | "koa-helmet": "^4.0.0", 33 | "koa-jwt": "^3.5.1", 34 | "koa-logger": "^3.2.0", 35 | "koa-router": "^7.4.0", 36 | "mongoose": "^5.3.13" 37 | }, 38 | "devDependencies": { 39 | "eslint": "^5.9.0", 40 | "eslint-config-airbnb-base": "^13.1.0", 41 | "eslint-plugin-import": "^2.14.0", 42 | "jest": "^23.6.0", 43 | "supertest": "^3.3.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /services/articles-management/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "articles-management", 3 | "version": "1.0.0", 4 | "description": "Articles Management Service - responsible for handling news articles related operations such as creating, reading, updating, deleting, verifying, reporting etc.", 5 | "main": "./src/server.js", 6 | "scripts": { 7 | "start": "node ./src/server.js", 8 | "test": "jest --coverage", 9 | "watch": "jest --coverage --watchAll", 10 | "lint": "eslint ./src && eslint ./tests", 11 | "testOnly": "jest" 12 | }, 13 | "jest": { 14 | "testEnvironment": "node", 15 | "verbose": true, 16 | "coverageThreshold": { 17 | "global": { 18 | "branches": 100, 19 | "functions": 100, 20 | "lines": 100, 21 | "statements": -10 22 | } 23 | } 24 | }, 25 | "author": "Rithin Chalumuri", 26 | "license": "MIT", 27 | "dependencies": { 28 | "amqp-ts-async": "^1.3.7", 29 | "dotenv": "^6.1.0", 30 | "koa": "^2.6.2", 31 | "koa-bodyparser": "^4.2.1", 32 | "koa-helmet": "^4.0.0", 33 | "koa-jwt": "^3.5.1", 34 | "koa-logger": "^3.2.0", 35 | "koa-router": "^7.4.0", 36 | "mongoose": "^5.3.13", 37 | "winston": "^3.1.0" 38 | }, 39 | "devDependencies": { 40 | "eslint": "^5.9.0", 41 | "eslint-config-airbnb-base": "^13.1.0", 42 | "eslint-plugin-import": "^2.14.0", 43 | "jest": "^23.6.0", 44 | "supertest": "^3.3.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /services/user-management/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "user-management", 3 | "version": "1.0.0", 4 | "description": "User Management Service - Reposible for adding, getting, updating new users, their contact details, activity etc.", 5 | "main": "./src/server.js", 6 | "scripts": { 7 | "start": "node ./src/server.js", 8 | "test": "jest --coverage", 9 | "watch": "jest --coverage --watchAll", 10 | "lint": "eslint ./src && eslint ./tests", 11 | "testOnly": "jest", 12 | "checkCodeQuality": "eslint ./src && eslint ./tests && jest --coverage" 13 | }, 14 | "author": "Rithin Chalumuri", 15 | "license": "MIT", 16 | "jest": { 17 | "testEnvironment": "node", 18 | "verbose": true, 19 | "coverageThreshold": { 20 | "global": { 21 | "branches": 100, 22 | "functions": 100, 23 | "lines": 100, 24 | "statements": -10 25 | } 26 | } 27 | }, 28 | "dependencies": { 29 | "amqp-ts-async": "^1.3.7", 30 | "dotenv": "^6.1.0", 31 | "koa": "^2.6.2", 32 | "koa-bodyparser": "^4.2.1", 33 | "koa-helmet": "^4.0.0", 34 | "koa-jwt": "^3.5.1", 35 | "koa-logger": "^3.2.0", 36 | "koa-router": "^7.4.0", 37 | "mongoose": "^5.3.13", 38 | "winston": "^3.1.0" 39 | }, 40 | "devDependencies": { 41 | "eslint": "^5.9.0", 42 | "eslint-config-airbnb-base": "^13.1.0", 43 | "eslint-plugin-import": "^2.14.0", 44 | "jest": "^23.6.0", 45 | "supertest": "^3.3.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /services/authentication/src/controllers/auth.controller.js: -------------------------------------------------------------------------------- 1 | const logger = require('winston'); 2 | const jwt = require('jsonwebtoken'); 3 | const bcrypt = require('bcryptjs'); 4 | const Auth = require('../models/auth.model'); 5 | const config = require('../environment/config'); 6 | 7 | const authController = { 8 | authenticate: async (ctx) => { 9 | try { 10 | const user = await Auth.findOne({ emailAddress: ctx.request.body.emailAddress }); 11 | if (!user) ctx.throw(404); 12 | if (!(bcrypt.compareSync(ctx.request.body.password, user.password))) { 13 | ctx.body = { auth: false, token: null }; 14 | } else { 15 | const token = jwt.sign({ id: user.emailAddress, role: user.role }, config.jwtsecret, { 16 | expiresIn: 86400, // expires in 24 hours 17 | }); 18 | ctx.body = { auth: true, token }; 19 | } 20 | } catch (err) { 21 | ctx.throw(500); 22 | } 23 | }, 24 | 25 | add: async (message) => { 26 | let user; 27 | try { 28 | user = JSON.parse(message.content.toString()); 29 | const hashedPassword = bcrypt.hashSync(user.password, 8); 30 | await Auth.create({ 31 | role: user.role, 32 | emailAddress: user.emailAddress, 33 | password: hashedPassword, 34 | }); 35 | logger.info(`user auth record created - ${user.emailAddress}`); 36 | } catch (err) { 37 | logger.error(`Error creating auth record for user ${user.emailAddress} : ${err}`); 38 | } 39 | }, 40 | }; 41 | 42 | module.exports = authController; 43 | -------------------------------------------------------------------------------- /services/authentication/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "authentication", 3 | "version": "1.0.0", 4 | "description": "Authentication Service - Responsible for authenticating users and providing access tokens", 5 | "main": "./src/server.js", 6 | "scripts": { 7 | "start": "node ./src/server.js", 8 | "test": "jest --coverage", 9 | "watch": "jest --coverage --watchAll", 10 | "lint": "eslint ./src && eslint ./tests", 11 | "testOnly": "jest", 12 | "checkCodeQuality": "eslint ./src && eslint ./tests && jest --coverage", 13 | "snyk-protect": "snyk protect", 14 | "prepare": "npm run snyk-protect" 15 | }, 16 | "jest": { 17 | "testEnvironment": "node", 18 | "verbose": true, 19 | "coverageThreshold": { 20 | "global": { 21 | "branches": 100, 22 | "functions": 100, 23 | "lines": 100, 24 | "statements": -10 25 | } 26 | } 27 | }, 28 | "author": "Rithin Chalumuri", 29 | "license": "MIT", 30 | "dependencies": { 31 | "amqp-ts-async": "^1.3.7", 32 | "bcryptjs": "^2.4.3", 33 | "dotenv": "^6.1.0", 34 | "jsonwebtoken": "^8.5.1", 35 | "koa": "^2.13.0", 36 | "koa-bodyparser": "^4.3.0", 37 | "koa-helmet": "^4.2.1", 38 | "koa-logger": "^3.2.1", 39 | "koa-router": "^7.4.0", 40 | "mongoose": "^5.9.26", 41 | "winston": "^3.3.3", 42 | "snyk": "^1.369.3" 43 | }, 44 | "devDependencies": { 45 | "eslint": "^5.9.0", 46 | "eslint-config-airbnb-base": "^13.1.0", 47 | "eslint-plugin-import": "^2.14.0", 48 | "jest": "^23.6.0", 49 | "supertest": "^3.3.0" 50 | }, 51 | "snyk": true 52 | } 53 | -------------------------------------------------------------------------------- /services/notification/tests/unit/article.message.controller.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | jest.mock('winston'); 3 | jest.mock('../../src/modules/email/email'); 4 | 5 | const winston = require('winston'); 6 | const email = require('../../src/modules/email/email'); 7 | const articleMessageController = require('../../src/message-controllers/articles'); 8 | 9 | const message = { 10 | content: { 11 | title: 'Test Article', 12 | description: 'Test Text', 13 | }, 14 | }; 15 | 16 | describe('Article Added Message Controller', async () => { 17 | afterEach(() => { 18 | email.sendArticleAddedEmail.mockClear(); 19 | }); 20 | 21 | test('Should succeed when correct message content is passed', async () => { 22 | articleMessageController.added({ content: Buffer.from(JSON.stringify(message.content)) }); 23 | expect(winston.error).not.toBeCalled(); 24 | expect(email.sendArticleAddedEmail).toBeCalledTimes(1); 25 | expect(email.sendArticleAddedEmail).toBeCalledWith(message.content); 26 | }); 27 | 28 | test('Should throw an error when correct message content is null', async () => { 29 | articleMessageController.added(null); 30 | expect(winston.error).toBeCalled(); 31 | expect(email.sendArticleAddedEmail).not.toBeCalled(); 32 | }); 33 | 34 | test('Should throw an error when sending email fails', async () => { 35 | email.sendArticleAddedEmail = jest.fn((content) => { 36 | throw new Error(); 37 | }); 38 | articleMessageController.added({ content: Buffer.from(JSON.stringify(message.content)) }); 39 | expect(winston.error).toBeCalled(); 40 | expect(email.sendArticleAddedEmail).toBeCalledTimes(1); 41 | expect(email.sendArticleAddedEmail).toBeCalledWith(message.content); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /services/events-management/src/controllers/event.controller.js: -------------------------------------------------------------------------------- 1 | const Events = require('../models/event.model'); 2 | 3 | const eventController = { 4 | find: async (ctx) => { 5 | ctx.body = await Events.find(); 6 | }, 7 | 8 | findById: async (ctx) => { 9 | try { 10 | const result = await Events.findById(ctx.params.id); 11 | if (!result) { 12 | ctx.throw(404, 'Event Not Found'); 13 | } 14 | ctx.body = result; 15 | } catch (err) { 16 | if (err.name === 'CastError' || err.name === 'NotFoundError') { 17 | ctx.throw(404); 18 | } else { 19 | ctx.throw(500); 20 | } 21 | } 22 | }, 23 | 24 | add: async (ctx) => { 25 | try { 26 | const newEvent = await Events.create(ctx.request.body); 27 | ctx.body = newEvent; 28 | } catch (err) { 29 | ctx.throw(422); 30 | } 31 | }, 32 | 33 | update: async (ctx) => { 34 | try { 35 | const result = await Events.findByIdAndUpdate( 36 | ctx.params.id, 37 | ctx.request.body, 38 | ); 39 | if (!result) { 40 | ctx.throw(404); 41 | } 42 | ctx.body = result; 43 | } catch (err) { 44 | if (err.name === 'CastError' || err.name === 'NotFoundError') { 45 | ctx.throw(404); 46 | } else { 47 | ctx.throw(500); 48 | } 49 | } 50 | }, 51 | 52 | delete: async (ctx) => { 53 | try { 54 | const result = await Events.findByIdAndRemove(ctx.params.id); 55 | if (!result) { 56 | ctx.throw(404); 57 | } 58 | ctx.body = result; 59 | } catch (err) { 60 | if (err.name === 'CastError' || err.name === 'NotFoundError') { 61 | ctx.throw(404); 62 | } else { 63 | ctx.throw(500); 64 | } 65 | } 66 | }, 67 | }; 68 | 69 | module.exports = eventController; 70 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | articles-management: 5 | container_name: articles-management 6 | build: 7 | context: ./services/articles-management 8 | dockerfile: Dockerfile 9 | ports: 10 | - "3000:3000" 11 | environment: 12 | - PORT=3000 13 | - MESSAGE_BUS=amqp://rabbitmq 14 | links: 15 | - rabbitmq 16 | 17 | events-management: 18 | container_name: events-management 19 | build: 20 | context: ./services/events-management 21 | dockerfile: Dockerfile 22 | ports: 23 | - "3001:3000" 24 | environment: 25 | - PORT=3000 26 | - MESSAGE_BUS=amqp://rabbitmq 27 | links: 28 | - rabbitmq 29 | 30 | user-management: 31 | container_name: user-management 32 | build: 33 | context: ./services/user-management 34 | dockerfile: Dockerfile 35 | ports: 36 | - "3002:3000" 37 | environment: 38 | - PORT=3000 39 | - MESSAGE_BUS=amqp://rabbitmq 40 | links: 41 | - rabbitmq 42 | 43 | authentication: 44 | container_name: authentication 45 | build: 46 | context: ./services/authentication 47 | dockerfile: Dockerfile 48 | ports: 49 | - "3003:3000" 50 | environment: 51 | - PORT=3000 52 | - MESSAGE_BUS=amqp://rabbitmq 53 | links: 54 | - rabbitmq 55 | 56 | notification: 57 | container_name: notification 58 | build: 59 | context: ./services/notification 60 | dockerfile: Dockerfile 61 | environment: 62 | - MESSAGE_BUS=amqp://rabbitmq 63 | - EMAIL_SERVICE=gmail 64 | - EMAIL_ID=noreply.localnewsapplication@gmail.com 65 | - EMAIL_PASSWORD=Testing0* 66 | - ADMIN_EMAIL=localnewsapp340ct@gmail.com 67 | links: 68 | - rabbitmq 69 | 70 | rabbitmq: 71 | container_name: rabbitmq 72 | image: rabbitmq:3.7.4 -------------------------------------------------------------------------------- /services/user-management/src/controllers/user.controller.js: -------------------------------------------------------------------------------- 1 | const Users = require('../models/user.model'); 2 | const userAddedMessage = require('../message-bus/send/user.added'); 3 | 4 | const userController = { 5 | find: async (ctx) => { 6 | ctx.body = await Users.find(); 7 | }, 8 | 9 | findById: async (ctx) => { 10 | try { 11 | const result = await Users.findById(ctx.params.id); 12 | if (!result) { 13 | ctx.throw(404, 'User Not Found'); 14 | } 15 | ctx.body = result; 16 | } catch (err) { 17 | if (err.name === 'CastError' || err.name === 'NotFoundError') { 18 | ctx.throw(404); 19 | } else { 20 | ctx.throw(500); 21 | } 22 | } 23 | }, 24 | 25 | add: async (ctx) => { 26 | try { 27 | const newUser = await Users.create(ctx.request.body); 28 | userAddedMessage.send(ctx.request.body); 29 | ctx.body = newUser; 30 | } catch (err) { 31 | ctx.throw(422); 32 | } 33 | }, 34 | 35 | update: async (ctx) => { 36 | try { 37 | const result = await Users.findByIdAndUpdate( 38 | ctx.params.id, 39 | ctx.request.body, 40 | ); 41 | if (!result) { 42 | ctx.throw(404); 43 | } 44 | ctx.body = result; 45 | } catch (err) { 46 | if (err.name === 'CastError' || err.name === 'NotFoundError') { 47 | ctx.throw(404); 48 | } else { 49 | ctx.throw(500); 50 | } 51 | } 52 | }, 53 | 54 | delete: async (ctx) => { 55 | try { 56 | const result = await Users.findByIdAndRemove(ctx.params.id); 57 | if (!result) { 58 | ctx.throw(404); 59 | } 60 | ctx.body = result; 61 | } catch (err) { 62 | if (err.name === 'CastError' || err.name === 'NotFoundError') { 63 | ctx.throw(404); 64 | } else { 65 | ctx.throw(500); 66 | } 67 | } 68 | }, 69 | }; 70 | 71 | module.exports = userController; 72 | -------------------------------------------------------------------------------- /services/articles-management/src/controllers/article.controller.js: -------------------------------------------------------------------------------- 1 | const Article = require('../models/article.model'); 2 | const articleAddedMessage = require('../message-bus/send/article.added'); 3 | 4 | const articleController = { 5 | find: async (ctx) => { 6 | ctx.body = await Article.find(); 7 | }, 8 | 9 | findById: async (ctx) => { 10 | try { 11 | const result = await Article.findById(ctx.params.id); 12 | if (!result) { 13 | ctx.throw(404, 'Article Not Found'); 14 | } 15 | ctx.body = result; 16 | } catch (err) { 17 | if (err.name === 'CastError' || err.name === 'NotFoundError') { 18 | ctx.throw(404); 19 | } else { 20 | ctx.throw(500); 21 | } 22 | } 23 | }, 24 | 25 | add: async (ctx) => { 26 | try { 27 | const newArticle = await Article.create(ctx.request.body); 28 | ctx.body = newArticle; 29 | articleAddedMessage.send(newArticle); 30 | } catch (err) { 31 | ctx.throw(422); 32 | } 33 | }, 34 | 35 | update: async (ctx) => { 36 | try { 37 | const result = await Article.findByIdAndUpdate( 38 | ctx.params.id, 39 | ctx.request.body, 40 | ); 41 | if (!result) { 42 | ctx.throw(404); 43 | } 44 | ctx.body = result; 45 | } catch (err) { 46 | if (err.name === 'CastError' || err.name === 'NotFoundError') { 47 | ctx.throw(404); 48 | } else { 49 | ctx.throw(500); 50 | } 51 | } 52 | }, 53 | 54 | delete: async (ctx) => { 55 | try { 56 | const result = await Article.findByIdAndRemove(ctx.params.id); 57 | if (!result) { 58 | ctx.throw(404); 59 | } 60 | ctx.body = result; 61 | } catch (err) { 62 | if (err.name === 'CastError' || err.name === 'NotFoundError') { 63 | ctx.throw(404); 64 | } else { 65 | ctx.throw(500); 66 | } 67 | } 68 | }, 69 | }; 70 | 71 | module.exports = articleController; 72 | -------------------------------------------------------------------------------- /services/user-management/tests/unit/user.routes.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../src/controllers/user.controller'); 2 | jest.mock('../../src/middlewares/jwt'); 3 | 4 | const request = require('supertest'); 5 | const Koa = require('koa'); 6 | const userRoutes = require('../../src/routes/user.routes'); 7 | const userController = require('../../src/controllers/user.controller'); 8 | 9 | const app = new Koa().use(userRoutes.routes()); 10 | 11 | describe('GET /api/users', () => { 12 | test('Should sucessfully get status 200', async () => { 13 | const response = await request(app.callback()).get('/api/users'); 14 | 15 | expect(response.status).toBe(200); 16 | expect(response.text).toEqual(''); 17 | expect(userController.find).toBeCalledTimes(1); 18 | }); 19 | }); 20 | 21 | describe('GET /api/users/:id', () => { 22 | test('Should sucessfully get status 200', async () => { 23 | const response = await request(app.callback()).get('/api/users/123'); 24 | 25 | expect(userController.findById).toBeCalledTimes(1); 26 | expect(response.status).toBe(200); 27 | expect(response.text).toEqual('123'); 28 | }); 29 | }); 30 | 31 | describe('POST /api/users', () => { 32 | test('Should sucessfully get status 200', async (done) => { 33 | const response = await request(app.callback()).post('/api/users'); 34 | 35 | expect(userController.add).toBeCalledTimes(1); 36 | expect(response.status).toBe(200); 37 | expect(response.text).toEqual(''); 38 | done(); 39 | }); 40 | }); 41 | 42 | describe('PUT /api/users/:id', () => { 43 | test('Should sucessfully get status 200', async (done) => { 44 | const response = await request(app.callback()).put('/api/users/123'); 45 | 46 | expect(userController.update).toBeCalledTimes(1); 47 | expect(response.status).toBe(200); 48 | expect(response.text).toEqual('123'); 49 | done(); 50 | }); 51 | }); 52 | 53 | describe('DELETE /api/users/:id', () => { 54 | test('Should sucessfully get status 200', async (done) => { 55 | const response = await request(app.callback()).delete('/api/users/123'); 56 | 57 | expect(userController.delete).toBeCalledTimes(1); 58 | expect(response.status).toBe(200); 59 | expect(response.text).toEqual('123'); 60 | done(); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /services/events-management/tests/unit/event.routes.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../src/controllers/event.controller'); 2 | jest.mock('../../src/middlewares/jwt'); 3 | 4 | const request = require('supertest'); 5 | const Koa = require('koa'); 6 | const eventRoutes = require('../../src/routes/event.routes'); 7 | const eventController = require('../../src/controllers/event.controller'); 8 | 9 | const app = new Koa().use(eventRoutes.routes()); 10 | 11 | describe('GET /api/events', () => { 12 | test('Should sucessfully get status 200', async () => { 13 | const response = await request(app.callback()).get('/api/events'); 14 | 15 | expect(response.status).toBe(200); 16 | expect(response.text).toEqual(''); 17 | expect(eventController.find).toBeCalledTimes(1); 18 | }); 19 | }); 20 | 21 | describe('GET /api/events/:id', () => { 22 | test('Should sucessfully get status 200', async () => { 23 | const response = await request(app.callback()).get('/api/events/123'); 24 | 25 | expect(eventController.findById).toBeCalledTimes(1); 26 | expect(response.status).toBe(200); 27 | expect(response.text).toEqual('123'); 28 | }); 29 | }); 30 | 31 | describe('POST /api/events', () => { 32 | test('Should sucessfully get status 200', async (done) => { 33 | const response = await request(app.callback()).post('/api/events'); 34 | 35 | expect(eventController.add).toBeCalledTimes(1); 36 | expect(response.status).toBe(200); 37 | expect(response.text).toEqual(''); 38 | done(); 39 | }); 40 | }); 41 | 42 | describe('PUT /api/events/:id', () => { 43 | test('Should sucessfully get status 200', async (done) => { 44 | const response = await request(app.callback()).put('/api/events/123'); 45 | 46 | expect(eventController.update).toBeCalledTimes(1); 47 | expect(response.status).toBe(200); 48 | expect(response.text).toEqual('123'); 49 | done(); 50 | }); 51 | }); 52 | 53 | describe('DELETE /api/events/:id', () => { 54 | test('Should sucessfully get status 200', async (done) => { 55 | const response = await request(app.callback()).delete('/api/events/123'); 56 | 57 | expect(eventController.delete).toBeCalledTimes(1); 58 | expect(response.status).toBe(200); 59 | expect(response.text).toEqual('123'); 60 | done(); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /services/notification/tests/unit/email.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /* eslint-disable global-require */ 3 | jest.mock('nodemailer'); 4 | jest.mock('winston'); 5 | jest.mock('../../src/modules/email/email.templates'); 6 | 7 | const nodemailer = require('nodemailer'); 8 | const winston = require('winston'); 9 | const templates = require('../../src/modules/email/email.templates'); 10 | 11 | const message = { 12 | title: 'Test Article', 13 | description: 'Test Text', 14 | }; 15 | 16 | describe('Email Options', () => { 17 | test('Shoud set the correct email credentials', () => { 18 | require('../../src/modules/email/email'); 19 | expect(nodemailer.createTransport).toBeCalledTimes(1); 20 | expect(nodemailer.createTransport.mock.calls[0][0]).toMatchSnapshot(); 21 | expect(winston.error).not.toBeCalled(); 22 | }); 23 | }); 24 | 25 | describe('Send Article Added Email', () => { 26 | afterEach(() => { 27 | templates.GetRenderedArticleAddedEmailHtml.mockClear(); 28 | }); 29 | 30 | test('Shoud send email when correct details are provided', () => { 31 | const email = require('../../src/modules/email/email'); 32 | 33 | email.sendArticleAddedEmail(message); 34 | 35 | expect(templates.GetRenderedArticleAddedEmailHtml).toBeCalledTimes(1); 36 | expect(templates.GetRenderedArticleAddedEmailHtml) 37 | .toBeCalledWith(message.title, message.description); 38 | 39 | expect(nodemailer.sendMail.mock.calls.length).toBe(1); 40 | expect(nodemailer.sendMail.mock.calls[0][0]).toMatchSnapshot(); 41 | }); 42 | 43 | test('Shoud throw an error when an exception occurs', () => { 44 | templates.GetRenderedArticleAddedEmailHtml = jest.fn((title, description) => { 45 | throw new Error(); 46 | }); 47 | 48 | const email = require('../../src/modules/email/email'); 49 | 50 | email.sendArticleAddedEmail(message); 51 | 52 | expect(templates.GetRenderedArticleAddedEmailHtml).toBeCalledTimes(1); 53 | expect(templates.GetRenderedArticleAddedEmailHtml) 54 | .toBeCalledWith(message.title, message.description); 55 | 56 | expect(nodemailer.sendMail.mock.calls.length).toBe(1); 57 | expect(nodemailer.sendMail.mock.calls[0][0]).toMatchSnapshot(); 58 | 59 | expect(winston.error).toBeCalledTimes(1); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /services/articles-management/tests/unit/article.routes.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../src/controllers/article.controller'); 2 | jest.mock('../../src/middlewares/jwt'); 3 | 4 | const request = require('supertest'); 5 | const Koa = require('koa'); 6 | const articleRoutes = require('../../src/routes/article.routes'); 7 | const articleController = require('../../src/controllers/article.controller'); 8 | 9 | const app = new Koa().use(articleRoutes.routes()); 10 | 11 | describe('GET /api/articles', () => { 12 | test('Should sucessfully get status 200', async () => { 13 | const response = await request(app.callback()).get('/api/articles'); 14 | 15 | expect(response.status).toBe(200); 16 | expect(response.text).toEqual(''); 17 | expect(articleController.find).toBeCalledTimes(1); 18 | }); 19 | }); 20 | 21 | describe('GET /api/articles/:id', () => { 22 | test('Should sucessfully get status 200', async () => { 23 | const response = await request(app.callback()).get('/api/articles/123'); 24 | 25 | expect(articleController.findById).toBeCalledTimes(1); 26 | expect(response.status).toBe(200); 27 | expect(response.text).toEqual('123'); 28 | }); 29 | }); 30 | 31 | describe('POST /api/articles', () => { 32 | test('Should sucessfully get status 200', async (done) => { 33 | const response = await request(app.callback()).post('/api/articles'); 34 | 35 | expect(articleController.add).toBeCalledTimes(1); 36 | expect(response.status).toBe(200); 37 | expect(response.text).toEqual(''); 38 | done(); 39 | }); 40 | }); 41 | 42 | describe('PUT /api/articles/:id', () => { 43 | test('Should sucessfully get status 200', async (done) => { 44 | const response = await request(app.callback()).put('/api/articles/123'); 45 | 46 | expect(articleController.update).toBeCalledTimes(1); 47 | expect(response.status).toBe(200); 48 | expect(response.text).toEqual('123'); 49 | done(); 50 | }); 51 | }); 52 | 53 | describe('DELETE /api/articles/:id', () => { 54 | test('Should sucessfully get status 200', async (done) => { 55 | const response = await request(app.callback()).delete('/api/articles/123'); 56 | 57 | expect(articleController.delete).toBeCalledTimes(1); 58 | expect(response.status).toBe(200); 59 | expect(response.text).toEqual('123'); 60 | done(); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /services/notification/tests/unit/article.added.subscription.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /* eslint-disable global-require */ 3 | jest.mock('winston'); 4 | jest.mock('amqp-ts-async'); 5 | jest.mock('../../src/message-controllers/articles'); 6 | 7 | const winston = require('winston'); 8 | const amqp = require('amqp-ts-async'); 9 | const config = require('../../src/environment/config'); 10 | const articleMessageController = require('../../src/message-controllers/articles'); 11 | 12 | describe('Message Broker Connection', () => { 13 | afterEach(() => { 14 | jest.mock('amqp-ts-async'); 15 | }); 16 | 17 | test('Should connect to correct url', () => { 18 | require('../../src/subscriptions/article.added'); 19 | expect(amqp.Connection).toBeCalledTimes(1); 20 | expect(amqp.Connection).toBeCalledWith(config.messagebus); 21 | }); 22 | 23 | test('Should init with correct exchange details', () => { 24 | require('../../src/subscriptions/article.added'); 25 | expect(amqp.declareExchange.mock.calls.length).toBe(1); 26 | expect(amqp.declareExchange.mock.calls[0][0]).toBe('articles.added'); 27 | expect(amqp.declareExchange.mock.calls[0][1]).toBe('fanout'); 28 | }); 29 | 30 | test('Should init correct queue', () => { 31 | require('../../src/subscriptions/article.added'); 32 | expect(amqp.declareQueue.mock.calls.length).toBe(1); 33 | expect(amqp.declareQueue.mock.calls[0][0]).toBe(''); 34 | }); 35 | test('Should bind the queue to exchange', () => { 36 | require('../../src/subscriptions/article.added'); 37 | expect(amqp.bind.mock.calls.length).toBe(1); 38 | expect(amqp.bind.mock.calls[0][0]).toBe('articles.added'); 39 | }); 40 | }); 41 | 42 | describe('Listening', () => { 43 | afterEach(() => { 44 | jest.mock('amqp-ts-async'); 45 | amqp.activateConsumer.mockClear(); 46 | articleMessageController.added.mockClear(); 47 | }); 48 | 49 | test('Should start listening', () => { 50 | require('../../src/subscriptions/article.added').start(); 51 | expect(amqp.activateConsumer.mock.calls.length).toBe(1); 52 | expect(articleMessageController.added).toBeCalledTimes(1); 53 | expect(winston.error).not.toBeCalled(); 54 | }); 55 | 56 | test('Should log exception if there is an error when starting listening', () => { 57 | articleMessageController.added = jest.fn((msg) => { 58 | throw new Error(); 59 | }); 60 | require('../../src/subscriptions/article.added').start(); 61 | expect(amqp.activateConsumer.mock.calls.length).toBe(1); 62 | expect(articleMessageController.added).toBeCalledTimes(1); 63 | expect(winston.error).toBeCalled(); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /services/user-management/tests/unit/user.added.message.send.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /* eslint-disable global-require */ 3 | jest.mock('winston'); 4 | jest.mock('amqp-ts-async'); 5 | jest.useFakeTimers(); 6 | 7 | const winston = require('winston'); 8 | const amqp = require('amqp-ts-async'); 9 | const config = require('../../src/environment/config'); 10 | 11 | const message = { 12 | title: 'Test Article', 13 | description: 'Test Text', 14 | }; 15 | 16 | describe('Message Broker Connection', () => { 17 | afterEach(() => { 18 | amqp.close.mockClear(); 19 | amqp.declareExchange.mockClear(); 20 | jest.mock('amqp-ts-async'); 21 | }); 22 | 23 | test('Should connect to correct url', () => { 24 | require('../../src/message-bus/send/user.added').send(message); 25 | expect(amqp.Connection).toBeCalledTimes(1); 26 | expect(amqp.Connection).toBeCalledWith(config.messagebus); 27 | expect(winston.error).not.toBeCalled(); 28 | }); 29 | 30 | test('Should init with correct exchange details', () => { 31 | require('../../src/message-bus/send/user.added').send(message); 32 | expect(amqp.declareExchange.mock.calls.length).toBe(1); 33 | expect(amqp.declareExchange.mock.calls[0][0]).toBe('user.added'); 34 | expect(amqp.declareExchange.mock.calls[0][1]).toBe('fanout'); 35 | expect(winston.error).not.toBeCalled(); 36 | }); 37 | 38 | test('Should close connection after timeout', () => { 39 | jest.runAllTimers(); 40 | require('../../src/message-bus/send/user.added').send(message); 41 | expect(amqp.close.mock.calls.length).toBeGreaterThan(0); 42 | expect(winston.error).not.toBeCalled(); 43 | }); 44 | }); 45 | 46 | describe('Send', () => { 47 | afterEach(() => { 48 | amqp.send.mockClear(); 49 | jest.mock('amqp-ts-async'); 50 | }); 51 | 52 | test('Should send succesfully when valid user is passed', () => { 53 | require('../../src/message-bus/send/user.added').send(message); 54 | expect(amqp.send).toHaveBeenCalled(); 55 | expect(winston.error).not.toBeCalled(); 56 | }); 57 | 58 | test('Should correcly parse the json object', () => { 59 | require('../../src/message-bus/send/user.added').send(message); 60 | expect(amqp.send).toBeCalledTimes(1); 61 | expect(amqp.Message).toBeCalledWith(JSON.stringify(message)); 62 | expect(amqp.send.mock.calls[0][0]).toMatchSnapshot(); 63 | expect(winston.error).not.toBeCalled(); 64 | }); 65 | 66 | test('Should log exception if there null user is passed', () => { 67 | require('../../src/message-bus/send/user.added').send(null); 68 | expect(amqp.send).not.toBeCalled(); 69 | expect(winston.error).toBeCalled(); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /services/articles-management/tests/unit/article.added.message.send.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /* eslint-disable global-require */ 3 | jest.mock('winston'); 4 | jest.mock('amqp-ts-async'); 5 | jest.useFakeTimers(); 6 | 7 | const winston = require('winston'); 8 | const amqp = require('amqp-ts-async'); 9 | const config = require('../../src/environment/config'); 10 | 11 | const message = { 12 | title: 'Test Article', 13 | description: 'Test Text', 14 | }; 15 | 16 | describe('Message Broker Connection', () => { 17 | afterEach(() => { 18 | amqp.close.mockClear(); 19 | amqp.declareExchange.mockClear(); 20 | jest.mock('amqp-ts-async'); 21 | }); 22 | 23 | test('Should connect to correct url', () => { 24 | require('../../src/message-bus/send/article.added').send(message); 25 | expect(amqp.Connection).toBeCalledTimes(1); 26 | expect(amqp.Connection).toBeCalledWith(config.messagebus); 27 | expect(winston.error).not.toBeCalled(); 28 | }); 29 | 30 | test('Should init with correct exchange details', () => { 31 | require('../../src/message-bus/send/article.added').send(message); 32 | expect(amqp.declareExchange.mock.calls.length).toBe(1); 33 | expect(amqp.declareExchange.mock.calls[0][0]).toBe('articles.added'); 34 | expect(amqp.declareExchange.mock.calls[0][1]).toBe('fanout'); 35 | expect(winston.error).not.toBeCalled(); 36 | }); 37 | 38 | test('Should close connection after timeout', () => { 39 | jest.runAllTimers(); 40 | require('../../src/message-bus/send/article.added').send(message); 41 | expect(amqp.close.mock.calls.length).toBeGreaterThan(0); 42 | expect(winston.error).not.toBeCalled(); 43 | }); 44 | }); 45 | 46 | describe('Send', () => { 47 | afterEach(() => { 48 | amqp.send.mockClear(); 49 | jest.mock('amqp-ts-async'); 50 | }); 51 | 52 | test('Should send succesfully when valid article is passed', () => { 53 | require('../../src/message-bus/send/article.added').send(message); 54 | expect(amqp.send).toHaveBeenCalled(); 55 | expect(winston.error).not.toBeCalled(); 56 | }); 57 | 58 | test('Should correcly parse the json object', () => { 59 | require('../../src/message-bus/send/article.added').send(message); 60 | expect(amqp.send).toBeCalledTimes(1); 61 | expect(amqp.Message).toBeCalledWith(JSON.stringify(message)); 62 | expect(amqp.send.mock.calls[0][0]).toMatchSnapshot(); 63 | expect(winston.error).not.toBeCalled(); 64 | }); 65 | 66 | test('Should log exception if there null article is passed', () => { 67 | require('../../src/message-bus/send/article.added').send(null); 68 | expect(amqp.send).not.toBeCalled(); 69 | expect(winston.error).toBeCalled(); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /services/notification/src/modules/email/email.templates.js: -------------------------------------------------------------------------------- 1 | module.exports.GetRenderedArticleAddedEmailHtml = (title, description) => { 2 | const name = 'Admin'; 3 | const line1 = `A new article with the title '${title}' has been to local news web application. It is described as '${description}'`; 4 | const buttonText = 'View Article'; 5 | const buttonLink = ''; 6 | const unsubscribeLink = ''; 7 | 8 | return ` 9 | 10 | 11 | 12 | 13 | 14 | Simple Transactional Email 15 | 95 | 96 | 97 | 98 | 99 | 100 | 155 | 156 | 157 |
  101 |
102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 134 | 135 | 136 | 137 |
110 | 111 | 112 | 131 | 132 |
113 |

Hi ${name},

114 |

${line1}

115 | 116 | 117 | 118 | 127 | 128 | 129 |
119 | 120 | 121 | 122 | 123 | 124 | 125 |
${buttonText}
126 |
130 |
133 |
138 | 139 | 140 | 150 | 151 | 152 | 153 |
154 |
 
158 | 159 | `; 160 | }; 161 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Event-Driven Microservices Backend Sample 2 | 3 | Proof of Concept for a scalable Local News Application, based on simplified event-driven microservices architecture and Docker containers. :whale: 4 | 5 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=rithinch_event-driven-microservices-docker-example&metric=alert_status)](https://sonarcloud.io/dashboard?id=rithinch_event-driven-microservices-docker-example) 6 | [![MIT license](https://img.shields.io/badge/License-MIT-blue.svg)](https://lbesson.mit-license.org/) 7 | [![Say Thanks!](https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg)](https://saythanks.io/to/rithinch) 8 | [![HitCount](http://hits.dwyl.io/rithinch/Event-Driven-Microservices-Sample.svg)](http://hits.dwyl.io/rithinch/Event-Driven-Microservices-Sample) 9 | 10 | [![forthebadge](https://forthebadge.com/images/badges/made-with-javascript.svg)](https://forthebadge.com) 11 | [![forthebadge](https://forthebadge.com/images/badges/built-with-love.svg)](https://forthebadge.com) 12 | 13 | ## Introduction 14 | 15 | This repo presents a proof of concept of a highly scalable local news application backend. The application was developed keeping a local news domain in mind, but the principles used can easily be applied to design software solutions for any domain. One of the primary business requirements for a local news application domain is that it has to be blazing fast since news updates are requested very often by customers and it would largely benefit the business if the system architecture can support such scale. After evaluating several different system architectures, a hybrid event-based microservices architecture was designed to meet the requirements. This approach leverages RabbitMQ message broker for events communication between the microservices and all the services are containerized using Docker such that they can independently developed, deployed, monitored and scaled. 16 | 17 | ## Full Application Backend Demo 18 | 19 | [![Video](https://img.youtube.com/vi/F2uVu6hKZTc/0.jpg)](https://www.youtube.com/watch?v=F2uVu6hKZTc) 20 | 21 | The following video demo shows all the currently supported features for the proof of concept. It goes through how to run the application stack and perform operations requesting the api's. 22 | 23 | Event-based communication samples are highlighted in following two scenario's: 24 | * When a new article is added through articles-management service, the notification service picks up that event and sends an email to the admin with the article details. 25 | * When a new user is added through user-management service, the authentication service picks up that event and stores the login details of the user. Demonstrating Atomic Transactions in a Microservices Architecture. 26 | 27 | ## Running the entire application stack 28 | 29 | If you have docker-compose installed and docker running; it is really simple to spin up the entire application stack. 30 | 31 | Make sure you are in the root directory of the repository where the docker-compose file is. 32 | 33 | **docker-compose up** starts it and **docker-compose down** stops it 34 | 35 | Example: 36 | 37 | ``` 38 | docker-compose build --no-cache 39 | docker-compose up 40 | docker-compose down 41 | ``` 42 | 43 | All the environment variables for the application need to be specified in the docker compose file, each service has environment/config.js which can used in anywhere in it's service application to get the config files for that instance. This allows to seperate environment configurations concerns from our applicaiton code meaning it can easily spun up for local, development and production environments with different db credentials, ports etc. 44 | 45 | ## Working Features 46 | 47 | Once you run the entire application stack using docker compose, you should be able access the public routes below: 48 | 49 | Feature | Type | Route | Access 50 | ------------ | ------------- | ------------- | ------------- 51 | Get all articles | GET | http://localhost:3000/api/articles | Public 52 | Get a specific article | GET | http://localhost:3000/api/articles/:id | Public 53 | Add a new article | POST | http://localhost:3000/api/articles | Protected 54 | Update an article | PUT | http://localhost:3000/api/articles/:id | Protected 55 | Delete an article | DELETE | http://localhost:3000/api/articles/:id | Protected 56 | Get all events | GET | http://localhost:3001/api/events | Public 57 | Get a specific event | GET | http://localhost:3001/api/events/:id | Public 58 | Add a new event| POST | http://localhost:3001/api/events | Protected 59 | Update an event | PUT | http://localhost:3001/api/events/:id | Protected 60 | Delete an event | DELETE | http://localhost:3001/api/events/:id | Protected 61 | Get all users | GET | http://localhost:3002/api/users | Public 62 | Get a specific user | GET | http://localhost:3002/api/users/:id | Public 63 | Add a new user | POST | http://localhost:3002/api/users | Protected 64 | Update an user | PUT | http://localhost:3002/api/users/:id | Protected 65 | Delete an user | DELETE | http://localhost:3002/api/users/:id | Protected 66 | Authenticate a user | POST | http://localhost:3003/api/auth | Public 67 | 68 | For protected routes: you can post to http://localhost:3003/api/auth first with the following 'body' to get the admin token 69 | 70 | ```json 71 | { 72 | "emailAddress": "rithinch@gmail.com", 73 | "password": "Testing0*" 74 | } 75 | ``` 76 | 77 | Then put the recieved token in the authorization header for other protected routes. 78 | 79 | Ofcourse, now with that in place you can create new users and authenticate with their credentials next time to get a different token. :grimacing: 80 | 81 | To add a new user send a post request to http://localhost:3002/api/users with the following json body structure and its contents: 82 | 83 | ```json 84 | { 85 | "firstName": "New", 86 | "lastName": "User", 87 | "emailAddress": "newuser@new.com", 88 | "description": "New User", 89 | "password": "Testing0*" 90 | } 91 | ``` 92 | 93 | ## Event-Based Communication Between the Microservices - Example 94 | 95 | This is where things get interesting, our microservice ecosystem consists of 5 microservices. 4 of them are public facing exposed via an api i.e articles-management, events-management, users-management and authentication. We also have an internal notification microservice (no client apps have access to this). These 5 services form our application microservice ecosystem. 96 | 97 | None of the microservices talk to each other directly (using their api's) ... wait.. what.. then how is notification service sending an email when article-management service adds an article? :confused: 98 | 99 | Using a Pub/Sub pattern with RabbitMQ message broker. Which means that mean a client sends a post request to article-management service; the service processes the request and after it's done, it simply publishes a message with some payload to 'article.added' exchange and completes the request. 100 | 101 | Now within our ecosystem if any microservice is subscribed to that event it will be alerted and start to process the recieved message with payload. So in this case, our notification service is subscribed to the article.added' exchange. 102 | 103 | This also means that we can have multiple subscribers to that event, so article-management doesn't need to worry about who is subsrcibed it can simply publish the message finish the request. This makes our services loosely coupled and we can easily add more independent services to our ecosystem. 104 | 105 | All our services are can be run, developed and scaled independently. :sunglasses: 106 | 107 | ### Code example: 108 | 109 | **Publisher**: 110 | 111 | If you see the file 'article.added.js' in services/articles-management/src/message-bus/send folder; that is being used in add method of controllers/article.controller.js and is called when the adding finishes. 112 | 113 | **Subscriber**: 114 | 115 | If you see the file 'article.added.js' in services/notification/src/subscriptions folder; that is called in the server.js of the file, so telling the node application to start listening to that service. 116 | 117 | Another such event based communication is applied in this demo; when adding the user through user-management service. The responsibility of user-management service is to handle adding of users and user releated activity on the application (their likes, bookmarks) etc. The responsibility of the authentication service is to handle authentication related activites i.e assigning token if the password is valid, password reset routines etc. But users can be created only through user-management service... then how is that user record created in the authentication db? 118 | 119 | Simple. 120 | . 121 | . 122 | **Events**. 123 | 124 | This allows us to handle inserting of data in two microservices from one request. Atomic Transactions are crutial for complex business domain and can be challenging when dealt within a microservices architecture. Event Sourcing patterns play a large part in microservice architecture design patterns. 125 | 126 | **Microservices + Events + Docker = Awesome DevOps** :bowtie: 127 | 128 | Understanding the above concepts are the just foundations to get started with the modern trio (Microservices+Events+Docker), there is still a lot more to learn and explore when adapting such an architecture in an production environment. Especially handling issues disaster recovery challenges and monitoring. 129 | 130 | I found the book, [Microservices Architecture from O'Reilly](https://www.oreilly.com/library/view/microservice-architecture/9781491956328/) a good read for learning about microservices concepts and how to approach about building such systems. 131 | 132 | ## Running the Unit Tests 133 | 134 | Go to the respective service directory where the package.json is and run tests. 135 | 136 | Eg: to run the tests for the articles-management service 137 | 138 | ``` 139 | cd services/articles-management 140 | npm test 141 | ``` 142 | 143 | To run all the tests for all microservices, a script 'run_all_tests' has been created in the root directory. 144 | 145 | ``` 146 | ./run_all_tests 147 | ``` 148 | 149 | ## Running the linter 150 | 151 | Go to the respective service directory where the package.json is and run linter. 152 | 153 | Eg: to run the tests for the articles-management service 154 | 155 | ``` 156 | cd services/articles-management 157 | npm run lint 158 | ``` 159 | 160 | All services have adopted the eslint airbnb configuration. A strict linting policy has been followed to ensure consistent code is produced. 161 | -------------------------------------------------------------------------------- /services/user-management/tests/unit/user.controller.test.js: -------------------------------------------------------------------------------- 1 | // #region Disabled ESLint Rules... 2 | 3 | /* eslint-disable arrow-body-style */ 4 | /* eslint-disable no-unused-vars */ 5 | 6 | // #endregion 7 | 8 | // #region Setup Mocks... 9 | 10 | jest.mock('../../src/models/user.model'); 11 | 12 | // #endregion 13 | 14 | // #region Imports... 15 | 16 | const userModel = require('../../src/models/user.model'); 17 | const userController = require('../../src/controllers/user.controller'); 18 | 19 | // #endregion 20 | 21 | // #region Methods... 22 | const sampleUser = { 23 | id: 123, 24 | emailAddress: 'test@email.com', 25 | firstName: 'Test User', 26 | }; 27 | 28 | const createMockContext = (context = {}) => { 29 | const ctx = context; 30 | ctx.throw = jest.fn((errorCode) => {}); 31 | return ctx; 32 | }; 33 | 34 | // #endregion 35 | 36 | // #region Unit Tests... 37 | 38 | // #region userController_add 39 | 40 | describe('add', async () => { 41 | const createMockOriginalImplementation = userModel.create; 42 | 43 | beforeEach(() => { 44 | userModel.create.mockClear(); 45 | }); 46 | 47 | afterEach(() => { 48 | userModel.reset(); 49 | userModel.create = createMockOriginalImplementation; 50 | }); 51 | 52 | test('should add user when valid data is passed', async () => { 53 | const ctx = createMockContext({ request: { body: sampleUser } }); 54 | 55 | await userController.add(ctx); 56 | 57 | expect(userModel.create).toBeCalledTimes(1); 58 | expect(userModel.create).toBeCalledWith(sampleUser); 59 | expect(ctx.body).toMatchObject(sampleUser); 60 | expect(ctx.throw).not.toBeCalled(); 61 | expect(userModel.count()).toBe(1); 62 | }); 63 | 64 | test('should add user successfully with multiple tags', async () => { 65 | const newsampleUser = Object.assign({}, sampleUser); 66 | newsampleUser.tags = ['Sample', 'user']; 67 | 68 | const ctx = createMockContext({ request: { body: newsampleUser } }); 69 | 70 | await userController.add(ctx); 71 | 72 | expect(userModel.create).toBeCalledTimes(1); 73 | expect(userModel.create).toBeCalledWith(newsampleUser); 74 | expect(ctx.body).toMatchObject(newsampleUser); 75 | expect(ctx.throw).not.toBeCalled(); 76 | expect(userModel.count()).toBe(1); 77 | }); 78 | 79 | test('should add user successfully without images', async () => { 80 | const ctx = createMockContext({ request: { body: sampleUser } }); 81 | 82 | await userController.add(ctx); 83 | 84 | expect(userModel.create).toBeCalledTimes(1); 85 | expect(userModel.create).toBeCalledWith(sampleUser); 86 | expect(ctx.body).toMatchObject(sampleUser); 87 | expect(ctx.throw).not.toBeCalled(); 88 | expect(userModel.count()).toBe(1); 89 | }); 90 | 91 | test('should add user successfully with images', async () => { 92 | const newsampleUser = Object.assign({}, sampleUser); 93 | newsampleUser.images = ['imageLink1', 'imageLink2']; 94 | 95 | const ctx = createMockContext({ request: { body: newsampleUser } }); 96 | 97 | await userController.add(ctx); 98 | 99 | expect(userModel.create).toBeCalledTimes(1); 100 | expect(userModel.create).toBeCalledWith(newsampleUser); 101 | expect(ctx.body).toMatchObject(newsampleUser); 102 | expect(ctx.throw).not.toBeCalled(); 103 | expect(userModel.count()).toBe(1); 104 | }); 105 | 106 | test('should throw an error and not add user when invalid data is passed', async () => { 107 | userModel.create = jest.fn((user) => { 108 | throw new Error(); 109 | }); 110 | 111 | const ctx = createMockContext({ request: { body: null } }); 112 | 113 | await userController.add(ctx); 114 | 115 | expect(userModel.create).toBeCalledTimes(1); 116 | expect(userModel.create).toBeCalledWith(null); 117 | expect(ctx.body).toBeUndefined(); 118 | expect(ctx.throw).toBeCalledTimes(1); 119 | expect(ctx.throw).toBeCalledWith(422); 120 | expect(userModel.count()).toBe(0); 121 | }); 122 | 123 | test('should throw an error and not add user when email is missing', async () => { 124 | userModel.create = jest.fn((user) => { 125 | throw new Error(); 126 | }); 127 | 128 | const ctx = createMockContext({ request: { body: null } }); 129 | 130 | await userController.add(ctx); 131 | 132 | expect(userModel.create).toBeCalledTimes(1); 133 | expect(userModel.create).toBeCalledWith(null); 134 | expect(ctx.body).toBeUndefined(); 135 | expect(ctx.throw).toBeCalledTimes(1); 136 | expect(ctx.throw).toBeCalledWith(422); 137 | expect(userModel.count()).toBe(0); 138 | }); 139 | 140 | test('should throw an error and not add user when role is missing', async () => { 141 | userModel.create = jest.fn((user) => { 142 | throw new Error(); 143 | }); 144 | 145 | const ctx = createMockContext({ request: { body: null } }); 146 | 147 | await userController.add(ctx); 148 | 149 | expect(userModel.create).toBeCalledTimes(1); 150 | expect(userModel.create).toBeCalledWith(null); 151 | expect(ctx.body).toBeUndefined(); 152 | expect(ctx.throw).toBeCalledTimes(1); 153 | expect(ctx.throw).toBeCalledWith(422); 154 | expect(userModel.count()).toBe(0); 155 | }); 156 | }); 157 | 158 | // #endregion 159 | 160 | // #region userController_delete 161 | describe('delete', async () => { 162 | const deleteOriginalMockImplementation = userModel.findByIdAndRemove; 163 | 164 | beforeEach(() => { 165 | userModel.create(sampleUser); 166 | userModel.findByIdAndRemove.mockClear(); 167 | }); 168 | 169 | afterEach(() => { 170 | userModel.reset(); 171 | userModel.findByIdAndRemove = deleteOriginalMockImplementation; 172 | }); 173 | 174 | test('should delete user successfully when correct id is passed', async () => { 175 | const ctx = createMockContext({ params: { id: sampleUser.id } }); 176 | const beforeCount = userModel.count(); 177 | 178 | await userController.delete(ctx); 179 | 180 | expect(userModel.findByIdAndRemove).toBeCalledTimes(1); 181 | expect(userModel.findByIdAndRemove).toBeCalledWith(sampleUser.id); 182 | expect(ctx.body).toBe(sampleUser.id); 183 | expect(ctx.throw).not.toBeCalled(); 184 | expect(userModel.count()).toBe(beforeCount - 1); 185 | }); 186 | 187 | test('should throw an error when user to delete not found', async () => { 188 | userModel.findByIdAndRemove = jest.fn((id) => { 189 | const error = new Error(); 190 | error.name = 'NotFoundError'; 191 | throw error; 192 | }); 193 | 194 | const ctx = createMockContext({ params: { id: 555 } }); 195 | 196 | await userController.delete(ctx); 197 | 198 | expect(userModel.findByIdAndRemove).toBeCalledTimes(1); 199 | expect(userModel.findByIdAndRemove).toBeCalledWith(555); 200 | expect(ctx.body).toBeUndefined(); 201 | expect(ctx.throw).toBeCalledTimes(1); 202 | expect(ctx.throw).toBeCalledWith(404); 203 | }); 204 | 205 | test('should throw an error when user id passed is null', async () => { 206 | userModel.findByIdAndRemove = jest.fn((id) => { 207 | return null; 208 | }); 209 | 210 | const ctx = createMockContext({ params: { id: 555 } }); 211 | 212 | await userController.delete(ctx); 213 | 214 | expect(userModel.findByIdAndRemove).toBeCalledTimes(1); 215 | expect(userModel.findByIdAndRemove).toBeCalledWith(555); 216 | expect(ctx.body).toBeNull(); 217 | expect(ctx.throw).toBeCalledTimes(1); 218 | expect(ctx.throw).toBeCalledWith(404); 219 | }); 220 | 221 | test('should throw an error when id passed is of incorrect type', async () => { 222 | userModel.findByIdAndRemove = jest.fn((id) => { 223 | const error = new Error(); 224 | error.name = 'CastError'; 225 | throw error; 226 | }); 227 | 228 | const ctx = createMockContext({ params: { id: 0.9 } }); 229 | 230 | await userController.delete(ctx); 231 | 232 | expect(userModel.findByIdAndRemove).toBeCalledTimes(1); 233 | expect(userModel.findByIdAndRemove).toBeCalledWith(0.9); 234 | expect(ctx.body).toBeUndefined(); 235 | expect(ctx.throw).toBeCalledTimes(1); 236 | expect(ctx.throw).toBeCalledWith(404); 237 | }); 238 | 239 | test('should throw a 500 error and return null for any other errors caught', async () => { 240 | userModel.findByIdAndRemove = jest.fn((id) => { 241 | const error = new Error(); 242 | throw error; 243 | }); 244 | 245 | const ctx = createMockContext({ params: { id: 123 } }); 246 | 247 | await userController.delete(ctx); 248 | 249 | expect(userModel.findByIdAndRemove).toBeCalledTimes(1); 250 | expect(userModel.findByIdAndRemove).toBeCalledWith(123); 251 | expect(ctx.body).toBeUndefined(); 252 | expect(ctx.throw).toBeCalledTimes(1); 253 | expect(ctx.throw).toBeCalledWith(500); 254 | }); 255 | }); 256 | // #endregion 257 | 258 | // #region userController_find 259 | 260 | describe('find', async () => { 261 | afterEach(() => { 262 | userModel.find.mockClear(); 263 | userModel.reset(); 264 | }); 265 | 266 | test('should return empty list when no data present', async () => { 267 | const ctx = createMockContext(); 268 | await userController.find(ctx); 269 | 270 | expect(ctx.body).toMatchObject([]); 271 | expect(userModel.find).toBeCalledTimes(1); 272 | }); 273 | 274 | test('should return values when data present', async () => { 275 | userModel.create(sampleUser); 276 | 277 | const ctx = createMockContext(); 278 | await userController.find(ctx); 279 | 280 | expect(userModel.find).toBeCalledTimes(1); 281 | expect(ctx.body).toMatchObject([sampleUser]); 282 | }); 283 | }); 284 | 285 | // #endregion 286 | 287 | // #region userController_findById 288 | 289 | describe('findById', async () => { 290 | const findByIdOrignialMockImplementation = userModel.findById; 291 | 292 | beforeEach(() => { 293 | userModel.create(sampleUser); 294 | userModel.findById.mockClear(); 295 | }); 296 | 297 | afterEach(() => { 298 | userModel.reset(); 299 | userModel.findById = findByIdOrignialMockImplementation; 300 | }); 301 | 302 | test('should return user when correct id is passed', async () => { 303 | const ctx = createMockContext({ params: { id: sampleUser.id } }); 304 | 305 | await userController.findById(ctx); 306 | 307 | expect(userModel.findById).toBeCalledTimes(1); 308 | expect(userModel.findById).toBeCalledWith(sampleUser.id); 309 | expect(ctx.body).toMatchSnapshot(); 310 | expect(ctx.throw).not.toBeCalled(); 311 | }); 312 | 313 | test('should throw an error when id passed doesn\'t exist', async () => { 314 | const ctx = createMockContext({ params: { id: 555 } }); 315 | 316 | await userController.findById(ctx); 317 | 318 | expect(userModel.findById).toBeCalledTimes(1); 319 | expect(userModel.findById).toBeCalledWith(555); 320 | expect(ctx.body).toBeUndefined(); 321 | expect(ctx.throw).toBeCalledTimes(1); 322 | expect(ctx.throw).toBeCalledWith(404, 'User Not Found'); 323 | }); 324 | 325 | test('should throw an error when id passed is null', async () => { 326 | userModel.findById = jest.fn((id) => { 327 | const error = new Error(); 328 | error.name = 'NotFoundError'; 329 | throw error; 330 | }); 331 | 332 | const ctx = createMockContext({ params: { id: null } }); 333 | 334 | await userController.findById(ctx); 335 | 336 | expect(userModel.findById).toBeCalledTimes(1); 337 | expect(userModel.findById).toBeCalledWith(null); 338 | expect(ctx.body).toBeUndefined(); 339 | expect(ctx.throw).toBeCalledTimes(1); 340 | expect(ctx.throw).toBeCalledWith(404); 341 | }); 342 | 343 | test('should throw an error when id passed is of incorrect type', async () => { 344 | userModel.findById = jest.fn((id) => { 345 | const error = new Error(); 346 | error.name = 'CastError'; 347 | throw error; 348 | }); 349 | 350 | const ctx = createMockContext({ params: { id: 0.99 } }); 351 | 352 | await userController.findById(ctx); 353 | 354 | expect(userModel.findById).toBeCalledTimes(1); 355 | expect(userModel.findById).toBeCalledWith(0.99); 356 | expect(ctx.body).toBeUndefined(); 357 | expect(ctx.throw).toBeCalledTimes(1); 358 | expect(ctx.throw).toBeCalledWith(404); 359 | }); 360 | 361 | test('should throw a 500 error and return null for any other errors caught', async () => { 362 | userModel.findById = jest.fn((id) => { 363 | throw new Error(); 364 | }); 365 | 366 | const ctx = createMockContext({ params: { id: 123 } }); 367 | 368 | await userController.findById(ctx); 369 | 370 | expect(userModel.findById).toBeCalledTimes(1); 371 | expect(userModel.findById).toBeCalledWith(123); 372 | expect(ctx.body).toBeUndefined(); 373 | expect(ctx.throw).toBeCalledTimes(1); 374 | expect(ctx.throw).toBeCalledWith(500); 375 | }); 376 | }); 377 | 378 | // #endregion 379 | 380 | // #region userController_update 381 | describe('update', async () => { 382 | const updateOriginalMockImplemenation = userModel.findByIdAndUpdate; 383 | 384 | beforeEach(() => { 385 | userModel.findByIdAndUpdate.mockClear(); 386 | userModel.create(sampleUser); 387 | }); 388 | 389 | afterEach(() => { 390 | userModel.reset(); 391 | userModel.findByIdAndUpdate = updateOriginalMockImplemenation; 392 | }); 393 | 394 | test('should update user correctly when valid id and data are passed', async () => { 395 | const newsampleUser = Object.assign({}, sampleUser); 396 | newsampleUser.firstName = 'firstName Changed!'; 397 | 398 | const ctx = createMockContext( 399 | { 400 | params: { id: sampleUser.id }, 401 | request: { body: newsampleUser }, 402 | }, 403 | ); 404 | 405 | await userController.update(ctx); 406 | 407 | expect(userModel.findByIdAndUpdate).toBeCalledTimes(1); 408 | expect(userModel.findByIdAndUpdate).toBeCalledWith(sampleUser.id, newsampleUser); 409 | expect(userModel.findById(sampleUser.id).firstName).toBe(newsampleUser.firstName); 410 | expect(ctx.throw).not.toBeCalled(); 411 | expect(ctx.body).toBe(newsampleUser); 412 | expect(userModel.count()).toBe(1); 413 | }); 414 | 415 | test('should throw an error when user not found', async () => { 416 | userModel.findByIdAndUpdate = jest.fn((id) => { 417 | const error = new Error(); 418 | error.name = 'NotFoundError'; 419 | throw error; 420 | }); 421 | 422 | const newsampleUser = Object.assign({}, sampleUser); 423 | newsampleUser.firstName = 'firstName Changed!'; 424 | 425 | const ctx = createMockContext( 426 | { 427 | params: { id: 1234 }, 428 | request: { body: newsampleUser }, 429 | }, 430 | ); 431 | 432 | await userController.update(ctx); 433 | 434 | expect(userModel.findByIdAndUpdate).toBeCalledTimes(1); 435 | expect(userModel.findByIdAndUpdate).toBeCalledWith(1234, newsampleUser); 436 | expect(userModel.findById(sampleUser.id)).toBe(sampleUser); 437 | expect(ctx.throw).toBeCalledTimes(1); 438 | expect(ctx.throw).toBeCalledWith(404); 439 | expect(ctx.body).toBeUndefined(); 440 | expect(userModel.count()).toBe(1); 441 | }); 442 | 443 | test('should throw an error when model returns null', async () => { 444 | userModel.findByIdAndUpdate = jest.fn((id) => { 445 | return null; 446 | }); 447 | 448 | const newsampleUser = Object.assign({}, sampleUser); 449 | newsampleUser.firstName = 'firstName Changed!'; 450 | 451 | const ctx = createMockContext( 452 | { 453 | params: { id: 123 }, 454 | request: { body: newsampleUser }, 455 | }, 456 | ); 457 | 458 | await userController.update(ctx); 459 | 460 | expect(userModel.findByIdAndUpdate).toBeCalledTimes(1); 461 | expect(userModel.findByIdAndUpdate).toBeCalledWith(123, newsampleUser); 462 | expect(userModel.findById(sampleUser.id)).toBe(sampleUser); 463 | expect(ctx.throw).toBeCalledTimes(1); 464 | expect(ctx.throw).toBeCalledWith(404); 465 | expect(ctx.body).toBeNull(); 466 | expect(userModel.count()).toBe(1); 467 | }); 468 | 469 | test('should throw an error 500 for any other issues found', async () => { 470 | userModel.findByIdAndUpdate = jest.fn((id) => { 471 | const error = new Error(); 472 | throw error; 473 | }); 474 | 475 | const newsampleUser = Object.assign({}, sampleUser); 476 | newsampleUser.firstName = 'firstName Changed!'; 477 | 478 | const ctx = createMockContext( 479 | { 480 | params: { id: 1234 }, 481 | request: { body: newsampleUser }, 482 | }, 483 | ); 484 | 485 | await userController.update(ctx); 486 | 487 | expect(userModel.findByIdAndUpdate).toBeCalledTimes(1); 488 | expect(userModel.findByIdAndUpdate).toBeCalledWith(1234, newsampleUser); 489 | expect(userModel.findById(sampleUser.id)).toBe(sampleUser); 490 | expect(ctx.throw).toBeCalledTimes(1); 491 | expect(ctx.throw).toBeCalledWith(500); 492 | expect(ctx.body).toBeUndefined(); 493 | expect(userModel.count()).toBe(1); 494 | }); 495 | }); 496 | // #endregion 497 | 498 | // #endregion 499 | -------------------------------------------------------------------------------- /services/events-management/tests/unit/event.controller.test.js: -------------------------------------------------------------------------------- 1 | // #region Disabled ESLint Rules... 2 | 3 | /* eslint-disable arrow-body-style */ 4 | /* eslint-disable no-unused-vars */ 5 | 6 | // #endregion 7 | 8 | // #region Setup Mocks... 9 | 10 | jest.mock('../../src/models/event.model'); 11 | 12 | // #endregion 13 | 14 | // #region Imports... 15 | 16 | const eventModel = require('../../src/models/event.model'); 17 | const eventController = require('../../src/controllers/event.controller'); 18 | 19 | // #endregion 20 | 21 | // #region Methods... 22 | const sampleEvent = { 23 | id: 123, 24 | title: 'Test Event', 25 | }; 26 | 27 | const createMockContext = (context = {}) => { 28 | const ctx = context; 29 | ctx.throw = jest.fn((errorCode) => {}); 30 | return ctx; 31 | }; 32 | 33 | // #endregion 34 | 35 | // #region Unit Tests... 36 | 37 | // #region EventController_add 38 | 39 | describe('add', async () => { 40 | const createMockOriginalImplementation = eventModel.create; 41 | 42 | beforeEach(() => { 43 | eventModel.create.mockClear(); 44 | }); 45 | 46 | afterEach(() => { 47 | eventModel.reset(); 48 | eventModel.create = createMockOriginalImplementation; 49 | }); 50 | 51 | test('should add event when valid data is passed', async () => { 52 | const ctx = createMockContext({ request: { body: sampleEvent } }); 53 | 54 | await eventController.add(ctx); 55 | 56 | expect(eventModel.create).toBeCalledTimes(1); 57 | expect(eventModel.create).toBeCalledWith(sampleEvent); 58 | expect(ctx.body).toMatchObject(sampleEvent); 59 | expect(ctx.throw).not.toBeCalled(); 60 | expect(eventModel.count()).toBe(1); 61 | }); 62 | 63 | test('should add event successfully with multiple tags', async () => { 64 | const newsampleEvent = Object.assign({}, sampleEvent); 65 | newsampleEvent.tags = ['Sample', 'event']; 66 | 67 | const ctx = createMockContext({ request: { body: newsampleEvent } }); 68 | 69 | await eventController.add(ctx); 70 | 71 | expect(eventModel.create).toBeCalledTimes(1); 72 | expect(eventModel.create).toBeCalledWith(newsampleEvent); 73 | expect(ctx.body).toMatchObject(newsampleEvent); 74 | expect(ctx.throw).not.toBeCalled(); 75 | expect(eventModel.count()).toBe(1); 76 | }); 77 | 78 | test('should add event successfully without images', async () => { 79 | const ctx = createMockContext({ request: { body: sampleEvent } }); 80 | 81 | await eventController.add(ctx); 82 | 83 | expect(eventModel.create).toBeCalledTimes(1); 84 | expect(eventModel.create).toBeCalledWith(sampleEvent); 85 | expect(ctx.body).toMatchObject(sampleEvent); 86 | expect(ctx.throw).not.toBeCalled(); 87 | expect(eventModel.count()).toBe(1); 88 | }); 89 | 90 | test('should add event successfully with images', async () => { 91 | const newsampleEvent = Object.assign({}, sampleEvent); 92 | newsampleEvent.images = ['imageLink1', 'imageLink2']; 93 | 94 | const ctx = createMockContext({ request: { body: newsampleEvent } }); 95 | 96 | await eventController.add(ctx); 97 | 98 | expect(eventModel.create).toBeCalledTimes(1); 99 | expect(eventModel.create).toBeCalledWith(newsampleEvent); 100 | expect(ctx.body).toMatchObject(newsampleEvent); 101 | expect(ctx.throw).not.toBeCalled(); 102 | expect(eventModel.count()).toBe(1); 103 | }); 104 | 105 | test('should throw an error and not add event when invalid data is passed', async () => { 106 | eventModel.create = jest.fn((event) => { 107 | throw new Error(); 108 | }); 109 | 110 | const ctx = createMockContext({ request: { body: null } }); 111 | 112 | await eventController.add(ctx); 113 | 114 | expect(eventModel.create).toBeCalledTimes(1); 115 | expect(eventModel.create).toBeCalledWith(null); 116 | expect(ctx.body).toBeUndefined(); 117 | expect(ctx.throw).toBeCalledTimes(1); 118 | expect(ctx.throw).toBeCalledWith(422); 119 | expect(eventModel.count()).toBe(0); 120 | }); 121 | 122 | test('should throw an error and not add event when authorUID is missing', async () => { 123 | eventModel.create = jest.fn((event) => { 124 | throw new Error(); 125 | }); 126 | 127 | const ctx = createMockContext({ request: { body: null } }); 128 | 129 | await eventController.add(ctx); 130 | 131 | expect(eventModel.create).toBeCalledTimes(1); 132 | expect(eventModel.create).toBeCalledWith(null); 133 | expect(ctx.body).toBeUndefined(); 134 | expect(ctx.throw).toBeCalledTimes(1); 135 | expect(ctx.throw).toBeCalledWith(422); 136 | expect(eventModel.count()).toBe(0); 137 | }); 138 | 139 | test('should throw an error and not add event when title is missing', async () => { 140 | // eventModel.create = jest.fn(eventModel.create); 141 | eventModel.create = jest.fn((event) => { 142 | throw new Error(); 143 | }); 144 | 145 | const ctx = createMockContext({ request: { body: null } }); 146 | 147 | await eventController.add(ctx); 148 | 149 | expect(eventModel.create).toBeCalledTimes(1); 150 | expect(eventModel.create).toBeCalledWith(null); 151 | expect(ctx.body).toBeUndefined(); 152 | expect(ctx.throw).toBeCalledTimes(1); 153 | expect(ctx.throw).toBeCalledWith(422); 154 | expect(eventModel.count()).toBe(0); 155 | }); 156 | }); 157 | 158 | // #endregion 159 | 160 | // #region EventController_delete 161 | describe('delete', async () => { 162 | const deleteOriginalMockImplementation = eventModel.findByIdAndRemove; 163 | 164 | beforeEach(() => { 165 | eventModel.create(sampleEvent); 166 | eventModel.findByIdAndRemove.mockClear(); 167 | }); 168 | 169 | afterEach(() => { 170 | eventModel.reset(); 171 | eventModel.findByIdAndRemove = deleteOriginalMockImplementation; 172 | }); 173 | 174 | test('should delete event successfully when correct id is passed', async () => { 175 | const ctx = createMockContext({ params: { id: sampleEvent.id } }); 176 | const beforeCount = eventModel.count(); 177 | 178 | await eventController.delete(ctx); 179 | 180 | expect(eventModel.findByIdAndRemove).toBeCalledTimes(1); 181 | expect(eventModel.findByIdAndRemove).toBeCalledWith(sampleEvent.id); 182 | expect(ctx.body).toBe(sampleEvent.id); 183 | expect(ctx.throw).not.toBeCalled(); 184 | expect(eventModel.count()).toBe(beforeCount - 1); 185 | }); 186 | 187 | test('should throw an error when event to delete not found', async () => { 188 | eventModel.findByIdAndRemove = jest.fn((id) => { 189 | const error = new Error(); 190 | error.name = 'NotFoundError'; 191 | throw error; 192 | }); 193 | 194 | const ctx = createMockContext({ params: { id: 555 } }); 195 | 196 | await eventController.delete(ctx); 197 | 198 | expect(eventModel.findByIdAndRemove).toBeCalledTimes(1); 199 | expect(eventModel.findByIdAndRemove).toBeCalledWith(555); 200 | expect(ctx.body).toBeUndefined(); 201 | expect(ctx.throw).toBeCalledTimes(1); 202 | expect(ctx.throw).toBeCalledWith(404); 203 | }); 204 | 205 | test('should throw an error when event id passed is null', async () => { 206 | eventModel.findByIdAndRemove = jest.fn((id) => { 207 | return null; 208 | }); 209 | 210 | const ctx = createMockContext({ params: { id: 555 } }); 211 | 212 | await eventController.delete(ctx); 213 | 214 | expect(eventModel.findByIdAndRemove).toBeCalledTimes(1); 215 | expect(eventModel.findByIdAndRemove).toBeCalledWith(555); 216 | expect(ctx.body).toBeNull(); 217 | expect(ctx.throw).toBeCalledTimes(1); 218 | expect(ctx.throw).toBeCalledWith(404); 219 | }); 220 | 221 | test('should throw an error when id passed is of incorrect type', async () => { 222 | eventModel.findByIdAndRemove = jest.fn((id) => { 223 | const error = new Error(); 224 | error.name = 'CastError'; 225 | throw error; 226 | }); 227 | 228 | const ctx = createMockContext({ params: { id: 0.9 } }); 229 | 230 | await eventController.delete(ctx); 231 | 232 | expect(eventModel.findByIdAndRemove).toBeCalledTimes(1); 233 | expect(eventModel.findByIdAndRemove).toBeCalledWith(0.9); 234 | expect(ctx.body).toBeUndefined(); 235 | expect(ctx.throw).toBeCalledTimes(1); 236 | expect(ctx.throw).toBeCalledWith(404); 237 | }); 238 | 239 | test('should throw a 500 error and return null for any other errors caught', async () => { 240 | eventModel.findByIdAndRemove = jest.fn((id) => { 241 | const error = new Error(); 242 | throw error; 243 | }); 244 | 245 | const ctx = createMockContext({ params: { id: 123 } }); 246 | 247 | await eventController.delete(ctx); 248 | 249 | expect(eventModel.findByIdAndRemove).toBeCalledTimes(1); 250 | expect(eventModel.findByIdAndRemove).toBeCalledWith(123); 251 | expect(ctx.body).toBeUndefined(); 252 | expect(ctx.throw).toBeCalledTimes(1); 253 | expect(ctx.throw).toBeCalledWith(500); 254 | }); 255 | }); 256 | // #endregion 257 | 258 | // #region EventController_find 259 | 260 | describe('find', async () => { 261 | afterEach(() => { 262 | eventModel.find.mockClear(); 263 | eventModel.reset(); 264 | }); 265 | 266 | test('should return empty list when no data present', async () => { 267 | const ctx = createMockContext(); 268 | await eventController.find(ctx); 269 | 270 | expect(ctx.body).toMatchObject([]); 271 | expect(eventModel.find).toBeCalledTimes(1); 272 | }); 273 | 274 | test('should return values when data present', async () => { 275 | eventModel.create(sampleEvent); 276 | 277 | const ctx = createMockContext(); 278 | await eventController.find(ctx); 279 | 280 | expect(eventModel.find).toBeCalledTimes(1); 281 | expect(ctx.body).toMatchObject([sampleEvent]); 282 | }); 283 | }); 284 | 285 | // #endregion 286 | 287 | // #region EventController_findById 288 | 289 | describe('findById', async () => { 290 | const findByIdOrignialMockImplementation = eventModel.findById; 291 | 292 | beforeEach(() => { 293 | eventModel.create(sampleEvent); 294 | eventModel.findById.mockClear(); 295 | }); 296 | 297 | afterEach(() => { 298 | eventModel.reset(); 299 | eventModel.findById = findByIdOrignialMockImplementation; 300 | }); 301 | 302 | test('should return event when correct id is passed', async () => { 303 | const ctx = createMockContext({ params: { id: sampleEvent.id } }); 304 | 305 | await eventController.findById(ctx); 306 | 307 | expect(eventModel.findById).toBeCalledTimes(1); 308 | expect(eventModel.findById).toBeCalledWith(sampleEvent.id); 309 | expect(ctx.body).toMatchObject(sampleEvent); 310 | expect(ctx.throw).not.toBeCalled(); 311 | }); 312 | 313 | test('should throw an error when id passed doesn\'t exist', async () => { 314 | const ctx = createMockContext({ params: { id: 555 } }); 315 | 316 | await eventController.findById(ctx); 317 | 318 | expect(eventModel.findById).toBeCalledTimes(1); 319 | expect(eventModel.findById).toBeCalledWith(555); 320 | expect(ctx.body).toBeUndefined(); 321 | expect(ctx.throw).toBeCalledTimes(1); 322 | expect(ctx.throw).toBeCalledWith(404, 'Event Not Found'); 323 | }); 324 | 325 | test('should throw an error when id passed is null', async () => { 326 | eventModel.findById = jest.fn((id) => { 327 | const error = new Error(); 328 | error.name = 'NotFoundError'; 329 | throw error; 330 | }); 331 | 332 | const ctx = createMockContext({ params: { id: null } }); 333 | 334 | await eventController.findById(ctx); 335 | 336 | expect(eventModel.findById).toBeCalledTimes(1); 337 | expect(eventModel.findById).toBeCalledWith(null); 338 | expect(ctx.body).toBeUndefined(); 339 | expect(ctx.throw).toBeCalledTimes(1); 340 | expect(ctx.throw).toBeCalledWith(404); 341 | }); 342 | 343 | test('should throw an error when id passed is of incorrect type', async () => { 344 | eventModel.findById = jest.fn((id) => { 345 | const error = new Error(); 346 | error.name = 'CastError'; 347 | throw error; 348 | }); 349 | 350 | const ctx = createMockContext({ params: { id: 0.99 } }); 351 | 352 | await eventController.findById(ctx); 353 | 354 | expect(eventModel.findById).toBeCalledTimes(1); 355 | expect(eventModel.findById).toBeCalledWith(0.99); 356 | expect(ctx.body).toBeUndefined(); 357 | expect(ctx.throw).toBeCalledTimes(1); 358 | expect(ctx.throw).toBeCalledWith(404); 359 | }); 360 | 361 | test('should throw a 500 error and return null for any other errors caught', async () => { 362 | eventModel.findById = jest.fn((id) => { 363 | throw new Error(); 364 | }); 365 | 366 | const ctx = createMockContext({ params: { id: 123 } }); 367 | 368 | await eventController.findById(ctx); 369 | 370 | expect(eventModel.findById).toBeCalledTimes(1); 371 | expect(eventModel.findById).toBeCalledWith(123); 372 | expect(ctx.body).toBeUndefined(); 373 | expect(ctx.throw).toBeCalledTimes(1); 374 | expect(ctx.throw).toBeCalledWith(500); 375 | }); 376 | }); 377 | 378 | // #endregion 379 | 380 | // #region eventController_update 381 | describe('update', async () => { 382 | const updateOriginalMockImplemenation = eventModel.findByIdAndUpdate; 383 | 384 | beforeEach(() => { 385 | eventModel.findByIdAndUpdate.mockClear(); 386 | eventModel.create(sampleEvent); 387 | }); 388 | 389 | afterEach(() => { 390 | eventModel.reset(); 391 | eventModel.findByIdAndUpdate = updateOriginalMockImplemenation; 392 | }); 393 | 394 | test('should update event correctly when valid id and data are passed', async () => { 395 | const newsampleEvent = Object.assign({}, sampleEvent); 396 | newsampleEvent.title = 'Title Changed!'; 397 | 398 | const ctx = createMockContext( 399 | { 400 | params: { id: sampleEvent.id }, 401 | request: { body: newsampleEvent }, 402 | }, 403 | ); 404 | 405 | await eventController.update(ctx); 406 | 407 | expect(eventModel.findByIdAndUpdate).toBeCalledTimes(1); 408 | expect(eventModel.findByIdAndUpdate).toBeCalledWith(sampleEvent.id, newsampleEvent); 409 | expect(eventModel.findById(sampleEvent.id).title).toBe(newsampleEvent.title); 410 | expect(ctx.throw).not.toBeCalled(); 411 | expect(ctx.body).toBe(newsampleEvent); 412 | expect(eventModel.count()).toBe(1); 413 | }); 414 | 415 | test('should throw an error when event not found', async () => { 416 | eventModel.findByIdAndUpdate = jest.fn((id) => { 417 | const error = new Error(); 418 | error.name = 'NotFoundError'; 419 | throw error; 420 | }); 421 | 422 | const newsampleEvent = Object.assign({}, sampleEvent); 423 | newsampleEvent.title = 'Title Changed!'; 424 | 425 | const ctx = createMockContext( 426 | { 427 | params: { id: 1234 }, 428 | request: { body: newsampleEvent }, 429 | }, 430 | ); 431 | 432 | await eventController.update(ctx); 433 | 434 | expect(eventModel.findByIdAndUpdate).toBeCalledTimes(1); 435 | expect(eventModel.findByIdAndUpdate).toBeCalledWith(1234, newsampleEvent); 436 | expect(eventModel.findById(sampleEvent.id)).toBe(sampleEvent); 437 | expect(ctx.throw).toBeCalledTimes(1); 438 | expect(ctx.throw).toBeCalledWith(404); 439 | expect(ctx.body).toBeUndefined(); 440 | expect(eventModel.count()).toBe(1); 441 | }); 442 | 443 | test('should throw an error when model returns null', async () => { 444 | eventModel.findByIdAndUpdate = jest.fn((id) => { 445 | return null; 446 | }); 447 | 448 | const newsampleEvent = Object.assign({}, sampleEvent); 449 | newsampleEvent.title = 'Title Changed!'; 450 | 451 | const ctx = createMockContext( 452 | { 453 | params: { id: 123 }, 454 | request: { body: newsampleEvent }, 455 | }, 456 | ); 457 | 458 | await eventController.update(ctx); 459 | 460 | expect(eventModel.findByIdAndUpdate).toBeCalledTimes(1); 461 | expect(eventModel.findByIdAndUpdate).toBeCalledWith(123, newsampleEvent); 462 | expect(eventModel.findById(sampleEvent.id)).toBe(sampleEvent); 463 | expect(ctx.throw).toBeCalledTimes(1); 464 | expect(ctx.throw).toBeCalledWith(404); 465 | expect(ctx.body).toBeNull(); 466 | expect(eventModel.count()).toBe(1); 467 | }); 468 | 469 | test('should throw an error 500 for any other issues found', async () => { 470 | eventModel.findByIdAndUpdate = jest.fn((id) => { 471 | const error = new Error(); 472 | throw error; 473 | }); 474 | 475 | const newsampleEvent = Object.assign({}, sampleEvent); 476 | newsampleEvent.title = 'Title Changed!'; 477 | 478 | const ctx = createMockContext( 479 | { 480 | params: { id: 1234 }, 481 | request: { body: newsampleEvent }, 482 | }, 483 | ); 484 | 485 | await eventController.update(ctx); 486 | 487 | expect(eventModel.findByIdAndUpdate).toBeCalledTimes(1); 488 | expect(eventModel.findByIdAndUpdate).toBeCalledWith(1234, newsampleEvent); 489 | expect(eventModel.findById(sampleEvent.id)).toBe(sampleEvent); 490 | expect(ctx.throw).toBeCalledTimes(1); 491 | expect(ctx.throw).toBeCalledWith(500); 492 | expect(ctx.body).toBeUndefined(); 493 | expect(eventModel.count()).toBe(1); 494 | }); 495 | }); 496 | // #endregion 497 | 498 | // #endregion 499 | -------------------------------------------------------------------------------- /services/articles-management/tests/unit/article.controller.test.js: -------------------------------------------------------------------------------- 1 | // #region Disabled ESLint Rules... 2 | 3 | /* eslint-disable arrow-body-style */ 4 | /* eslint-disable no-unused-vars */ 5 | 6 | // #endregion 7 | 8 | // #region Setup Mocks... 9 | 10 | jest.mock('../../src/models/article.model'); 11 | jest.mock('../../src/message-bus/send/article.added'); 12 | 13 | // #endregion 14 | 15 | // #region Imports... 16 | 17 | const articleModel = require('../../src/models/article.model'); 18 | const articleController = require('../../src/controllers/article.controller'); 19 | 20 | // #endregion 21 | 22 | // #region Methods... 23 | const sampleArticle = { 24 | id: 123, 25 | title: 'Test Article', 26 | }; 27 | 28 | const createMockContext = (context = {}) => { 29 | const ctx = context; 30 | ctx.throw = jest.fn((errorCode) => {}); 31 | return ctx; 32 | }; 33 | 34 | // #endregion 35 | 36 | // #region Unit Tests... 37 | 38 | // #region ArticleController_add 39 | 40 | describe('add', async () => { 41 | const createMockOriginalImplementation = articleModel.create; 42 | 43 | beforeEach(() => { 44 | articleModel.create.mockClear(); 45 | }); 46 | 47 | afterEach(() => { 48 | articleModel.reset(); 49 | articleModel.create = createMockOriginalImplementation; 50 | }); 51 | 52 | test('should add article when valid data is passed', async () => { 53 | const ctx = createMockContext({ request: { body: sampleArticle } }); 54 | 55 | await articleController.add(ctx); 56 | 57 | expect(articleModel.create).toBeCalledTimes(1); 58 | expect(articleModel.create).toBeCalledWith(sampleArticle); 59 | expect(ctx.body).toMatchObject(sampleArticle); 60 | expect(ctx.throw).not.toBeCalled(); 61 | expect(articleModel.count()).toBe(1); 62 | }); 63 | 64 | test('should add article successfully with multiple tags', async () => { 65 | const newSampleArticle = Object.assign({}, sampleArticle); 66 | newSampleArticle.tags = ['Sample', 'Article']; 67 | 68 | const ctx = createMockContext({ request: { body: newSampleArticle } }); 69 | 70 | await articleController.add(ctx); 71 | 72 | expect(articleModel.create).toBeCalledTimes(1); 73 | expect(articleModel.create).toBeCalledWith(newSampleArticle); 74 | expect(ctx.body).toMatchObject(newSampleArticle); 75 | expect(ctx.throw).not.toBeCalled(); 76 | expect(articleModel.count()).toBe(1); 77 | }); 78 | 79 | test('should add article successfully without images', async () => { 80 | const ctx = createMockContext({ request: { body: sampleArticle } }); 81 | 82 | await articleController.add(ctx); 83 | 84 | expect(articleModel.create).toBeCalledTimes(1); 85 | expect(articleModel.create).toBeCalledWith(sampleArticle); 86 | expect(ctx.body).toMatchObject(sampleArticle); 87 | expect(ctx.throw).not.toBeCalled(); 88 | expect(articleModel.count()).toBe(1); 89 | }); 90 | 91 | test('should add article successfully with images', async () => { 92 | const newSampleArticle = Object.assign({}, sampleArticle); 93 | newSampleArticle.images = ['imageLink1', 'imageLink2']; 94 | 95 | const ctx = createMockContext({ request: { body: newSampleArticle } }); 96 | 97 | await articleController.add(ctx); 98 | 99 | expect(articleModel.create).toBeCalledTimes(1); 100 | expect(articleModel.create).toBeCalledWith(newSampleArticle); 101 | expect(ctx.body).toMatchObject(newSampleArticle); 102 | expect(ctx.throw).not.toBeCalled(); 103 | expect(articleModel.count()).toBe(1); 104 | }); 105 | 106 | test('should throw an error and not add article when invalid data is passed', async () => { 107 | articleModel.create = jest.fn((article) => { 108 | throw new Error(); 109 | }); 110 | 111 | const ctx = createMockContext({ request: { body: null } }); 112 | 113 | await articleController.add(ctx); 114 | 115 | expect(articleModel.create).toBeCalledTimes(1); 116 | expect(articleModel.create).toBeCalledWith(null); 117 | expect(ctx.body).toBeUndefined(); 118 | expect(ctx.throw).toBeCalledTimes(1); 119 | expect(ctx.throw).toBeCalledWith(422); 120 | expect(articleModel.count()).toBe(0); 121 | }); 122 | 123 | test('should throw an error and not add article when authorUID is missing', async () => { 124 | articleModel.create = jest.fn((article) => { 125 | throw new Error(); 126 | }); 127 | 128 | const ctx = createMockContext({ request: { body: null } }); 129 | 130 | await articleController.add(ctx); 131 | 132 | expect(articleModel.create).toBeCalledTimes(1); 133 | expect(articleModel.create).toBeCalledWith(null); 134 | expect(ctx.body).toBeUndefined(); 135 | expect(ctx.throw).toBeCalledTimes(1); 136 | expect(ctx.throw).toBeCalledWith(422); 137 | expect(articleModel.count()).toBe(0); 138 | }); 139 | 140 | test('should throw an error and not add article when title is missing', async () => { 141 | // articleModel.create = jest.fn(articleModel.create); 142 | articleModel.create = jest.fn((article) => { 143 | throw new Error(); 144 | }); 145 | 146 | const ctx = createMockContext({ request: { body: null } }); 147 | 148 | await articleController.add(ctx); 149 | 150 | expect(articleModel.create).toBeCalledTimes(1); 151 | expect(articleModel.create).toBeCalledWith(null); 152 | expect(ctx.body).toBeUndefined(); 153 | expect(ctx.throw).toBeCalledTimes(1); 154 | expect(ctx.throw).toBeCalledWith(422); 155 | expect(articleModel.count()).toBe(0); 156 | }); 157 | }); 158 | 159 | // #endregion 160 | 161 | // #region ArticleController_delete 162 | describe('delete', async () => { 163 | const deleteOriginalMockImplementation = articleModel.findByIdAndRemove; 164 | 165 | beforeEach(() => { 166 | articleModel.create(sampleArticle); 167 | articleModel.findByIdAndRemove.mockClear(); 168 | }); 169 | 170 | afterEach(() => { 171 | articleModel.reset(); 172 | articleModel.findByIdAndRemove = deleteOriginalMockImplementation; 173 | }); 174 | 175 | test('should delete article successfully when correct id is passed', async () => { 176 | const ctx = createMockContext({ params: { id: sampleArticle.id } }); 177 | const beforeCount = articleModel.count(); 178 | 179 | await articleController.delete(ctx); 180 | 181 | expect(articleModel.findByIdAndRemove).toBeCalledTimes(1); 182 | expect(articleModel.findByIdAndRemove).toBeCalledWith(sampleArticle.id); 183 | expect(ctx.body).toBe(sampleArticle.id); 184 | expect(ctx.throw).not.toBeCalled(); 185 | expect(articleModel.count()).toBe(beforeCount - 1); 186 | }); 187 | 188 | test('should throw an error when article to delete not found', async () => { 189 | articleModel.findByIdAndRemove = jest.fn((id) => { 190 | const error = new Error(); 191 | error.name = 'NotFoundError'; 192 | throw error; 193 | }); 194 | 195 | const ctx = createMockContext({ params: { id: 555 } }); 196 | 197 | await articleController.delete(ctx); 198 | 199 | expect(articleModel.findByIdAndRemove).toBeCalledTimes(1); 200 | expect(articleModel.findByIdAndRemove).toBeCalledWith(555); 201 | expect(ctx.body).toBeUndefined(); 202 | expect(ctx.throw).toBeCalledTimes(1); 203 | expect(ctx.throw).toBeCalledWith(404); 204 | }); 205 | 206 | test('should throw an error when article id passed is null', async () => { 207 | articleModel.findByIdAndRemove = jest.fn((id) => { 208 | return null; 209 | }); 210 | 211 | const ctx = createMockContext({ params: { id: 555 } }); 212 | 213 | await articleController.delete(ctx); 214 | 215 | expect(articleModel.findByIdAndRemove).toBeCalledTimes(1); 216 | expect(articleModel.findByIdAndRemove).toBeCalledWith(555); 217 | expect(ctx.body).toBeNull(); 218 | expect(ctx.throw).toBeCalledTimes(1); 219 | expect(ctx.throw).toBeCalledWith(404); 220 | }); 221 | 222 | test('should throw an error when id passed is of incorrect type', async () => { 223 | articleModel.findByIdAndRemove = jest.fn((id) => { 224 | const error = new Error(); 225 | error.name = 'CastError'; 226 | throw error; 227 | }); 228 | 229 | const ctx = createMockContext({ params: { id: 0.9 } }); 230 | 231 | await articleController.delete(ctx); 232 | 233 | expect(articleModel.findByIdAndRemove).toBeCalledTimes(1); 234 | expect(articleModel.findByIdAndRemove).toBeCalledWith(0.9); 235 | expect(ctx.body).toBeUndefined(); 236 | expect(ctx.throw).toBeCalledTimes(1); 237 | expect(ctx.throw).toBeCalledWith(404); 238 | }); 239 | 240 | test('should throw a 500 error and return null for any other errors caught', async () => { 241 | articleModel.findByIdAndRemove = jest.fn((id) => { 242 | const error = new Error(); 243 | throw error; 244 | }); 245 | 246 | const ctx = createMockContext({ params: { id: 123 } }); 247 | 248 | await articleController.delete(ctx); 249 | 250 | expect(articleModel.findByIdAndRemove).toBeCalledTimes(1); 251 | expect(articleModel.findByIdAndRemove).toBeCalledWith(123); 252 | expect(ctx.body).toBeUndefined(); 253 | expect(ctx.throw).toBeCalledTimes(1); 254 | expect(ctx.throw).toBeCalledWith(500); 255 | }); 256 | }); 257 | // #endregion 258 | 259 | // #region ArticleController_find 260 | 261 | describe('find', async () => { 262 | afterEach(() => { 263 | articleModel.find.mockClear(); 264 | articleModel.reset(); 265 | }); 266 | 267 | test('should return empty list when no data present', async () => { 268 | const ctx = createMockContext(); 269 | await articleController.find(ctx); 270 | 271 | expect(ctx.body).toMatchObject([]); 272 | expect(articleModel.find).toBeCalledTimes(1); 273 | }); 274 | 275 | test('should return values when data present', async () => { 276 | articleModel.create(sampleArticle); 277 | 278 | const ctx = createMockContext(); 279 | await articleController.find(ctx); 280 | 281 | expect(articleModel.find).toBeCalledTimes(1); 282 | expect(ctx.body).toMatchObject([sampleArticle]); 283 | }); 284 | }); 285 | 286 | // #endregion 287 | 288 | // #region ArticleController_findById 289 | 290 | describe('findById', async () => { 291 | const findByIdOrignialMockImplementation = articleModel.findById; 292 | 293 | beforeEach(() => { 294 | articleModel.create(sampleArticle); 295 | articleModel.findById.mockClear(); 296 | }); 297 | 298 | afterEach(() => { 299 | articleModel.reset(); 300 | articleModel.findById = findByIdOrignialMockImplementation; 301 | }); 302 | 303 | test('should return article when correct id is passed', async () => { 304 | const ctx = createMockContext({ params: { id: sampleArticle.id } }); 305 | 306 | await articleController.findById(ctx); 307 | 308 | expect(articleModel.findById).toBeCalledTimes(1); 309 | expect(articleModel.findById).toBeCalledWith(sampleArticle.id); 310 | expect(ctx.body).toMatchObject(sampleArticle); 311 | expect(ctx.throw).not.toBeCalled(); 312 | }); 313 | 314 | test('should throw an error when id passed doesn\'t exist', async () => { 315 | const ctx = createMockContext({ params: { id: 555 } }); 316 | 317 | await articleController.findById(ctx); 318 | 319 | expect(articleModel.findById).toBeCalledTimes(1); 320 | expect(articleModel.findById).toBeCalledWith(555); 321 | expect(ctx.body).toBeUndefined(); 322 | expect(ctx.throw).toBeCalledTimes(1); 323 | expect(ctx.throw).toBeCalledWith(404, 'Article Not Found'); 324 | }); 325 | 326 | test('should throw an error when id passed is null', async () => { 327 | articleModel.findById = jest.fn((id) => { 328 | const error = new Error(); 329 | error.name = 'NotFoundError'; 330 | throw error; 331 | }); 332 | 333 | const ctx = createMockContext({ params: { id: null } }); 334 | 335 | await articleController.findById(ctx); 336 | 337 | expect(articleModel.findById).toBeCalledTimes(1); 338 | expect(articleModel.findById).toBeCalledWith(null); 339 | expect(ctx.body).toBeUndefined(); 340 | expect(ctx.throw).toBeCalledTimes(1); 341 | expect(ctx.throw).toBeCalledWith(404); 342 | }); 343 | 344 | test('should throw an error when id passed is of incorrect type', async () => { 345 | articleModel.findById = jest.fn((id) => { 346 | const error = new Error(); 347 | error.name = 'CastError'; 348 | throw error; 349 | }); 350 | 351 | const ctx = createMockContext({ params: { id: 0.99 } }); 352 | 353 | await articleController.findById(ctx); 354 | 355 | expect(articleModel.findById).toBeCalledTimes(1); 356 | expect(articleModel.findById).toBeCalledWith(0.99); 357 | expect(ctx.body).toBeUndefined(); 358 | expect(ctx.throw).toBeCalledTimes(1); 359 | expect(ctx.throw).toBeCalledWith(404); 360 | }); 361 | 362 | test('should throw a 500 error and return null for any other errors caught', async () => { 363 | articleModel.findById = jest.fn((id) => { 364 | throw new Error(); 365 | }); 366 | 367 | const ctx = createMockContext({ params: { id: 123 } }); 368 | 369 | await articleController.findById(ctx); 370 | 371 | expect(articleModel.findById).toBeCalledTimes(1); 372 | expect(articleModel.findById).toBeCalledWith(123); 373 | expect(ctx.body).toBeUndefined(); 374 | expect(ctx.throw).toBeCalledTimes(1); 375 | expect(ctx.throw).toBeCalledWith(500); 376 | }); 377 | }); 378 | 379 | // #endregion 380 | 381 | // #region ArticleController_update 382 | describe('update', async () => { 383 | const updateOriginalMockImplemenation = articleModel.findByIdAndUpdate; 384 | 385 | beforeEach(() => { 386 | articleModel.findByIdAndUpdate.mockClear(); 387 | articleModel.create(sampleArticle); 388 | }); 389 | 390 | afterEach(() => { 391 | articleModel.reset(); 392 | articleModel.findByIdAndUpdate = updateOriginalMockImplemenation; 393 | }); 394 | 395 | test('should update article correctly when valid id and data are passed', async () => { 396 | const newSampleArticle = Object.assign({}, sampleArticle); 397 | newSampleArticle.title = 'Title Changed!'; 398 | 399 | const ctx = createMockContext( 400 | { 401 | params: { id: sampleArticle.id }, 402 | request: { body: newSampleArticle }, 403 | }, 404 | ); 405 | 406 | await articleController.update(ctx); 407 | 408 | expect(articleModel.findByIdAndUpdate).toBeCalledTimes(1); 409 | expect(articleModel.findByIdAndUpdate).toBeCalledWith(sampleArticle.id, newSampleArticle); 410 | expect(articleModel.findById(sampleArticle.id).title).toBe(newSampleArticle.title); 411 | expect(ctx.throw).not.toBeCalled(); 412 | expect(ctx.body).toBe(newSampleArticle); 413 | expect(articleModel.count()).toBe(1); 414 | }); 415 | 416 | test('should throw an error when article not found', async () => { 417 | articleModel.findByIdAndUpdate = jest.fn((id) => { 418 | const error = new Error(); 419 | error.name = 'NotFoundError'; 420 | throw error; 421 | }); 422 | 423 | const newSampleArticle = Object.assign({}, sampleArticle); 424 | newSampleArticle.title = 'Title Changed!'; 425 | 426 | const ctx = createMockContext( 427 | { 428 | params: { id: 1234 }, 429 | request: { body: newSampleArticle }, 430 | }, 431 | ); 432 | 433 | await articleController.update(ctx); 434 | 435 | expect(articleModel.findByIdAndUpdate).toBeCalledTimes(1); 436 | expect(articleModel.findByIdAndUpdate).toBeCalledWith(1234, newSampleArticle); 437 | expect(articleModel.findById(sampleArticle.id)).toBe(sampleArticle); 438 | expect(ctx.throw).toBeCalledTimes(1); 439 | expect(ctx.throw).toBeCalledWith(404); 440 | expect(ctx.body).toBeUndefined(); 441 | expect(articleModel.count()).toBe(1); 442 | }); 443 | 444 | test('should throw an error when model returns null', async () => { 445 | articleModel.findByIdAndUpdate = jest.fn((id) => { 446 | return null; 447 | }); 448 | 449 | const newSampleArticle = Object.assign({}, sampleArticle); 450 | newSampleArticle.title = 'Title Changed!'; 451 | 452 | const ctx = createMockContext( 453 | { 454 | params: { id: 123 }, 455 | request: { body: newSampleArticle }, 456 | }, 457 | ); 458 | 459 | await articleController.update(ctx); 460 | 461 | expect(articleModel.findByIdAndUpdate).toBeCalledTimes(1); 462 | expect(articleModel.findByIdAndUpdate).toBeCalledWith(123, newSampleArticle); 463 | expect(articleModel.findById(sampleArticle.id)).toBe(sampleArticle); 464 | expect(ctx.throw).toBeCalledTimes(1); 465 | expect(ctx.throw).toBeCalledWith(404); 466 | expect(ctx.body).toBeNull(); 467 | expect(articleModel.count()).toBe(1); 468 | }); 469 | 470 | test('should throw an error 500 for any other issues found', async () => { 471 | articleModel.findByIdAndUpdate = jest.fn((id) => { 472 | const error = new Error(); 473 | throw error; 474 | }); 475 | 476 | const newSampleArticle = Object.assign({}, sampleArticle); 477 | newSampleArticle.title = 'Title Changed!'; 478 | 479 | const ctx = createMockContext( 480 | { 481 | params: { id: 1234 }, 482 | request: { body: newSampleArticle }, 483 | }, 484 | ); 485 | 486 | await articleController.update(ctx); 487 | 488 | expect(articleModel.findByIdAndUpdate).toBeCalledTimes(1); 489 | expect(articleModel.findByIdAndUpdate).toBeCalledWith(1234, newSampleArticle); 490 | expect(articleModel.findById(sampleArticle.id)).toBe(sampleArticle); 491 | expect(ctx.throw).toBeCalledTimes(1); 492 | expect(ctx.throw).toBeCalledWith(500); 493 | expect(ctx.body).toBeUndefined(); 494 | expect(articleModel.count()).toBe(1); 495 | }); 496 | }); 497 | // #endregion 498 | 499 | // #endregion 500 | -------------------------------------------------------------------------------- /services/notification/tests/unit/__snapshots__/email.templates.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Article Added Email Template Should return correct email template 1`] = ` 4 | " 5 | 6 | 7 | 8 | 9 | 10 | Simple Transactional Email 11 | 91 | 92 | 93 | 94 | 95 | 96 | 151 | 152 | 153 |
  97 |
98 | 99 | 100 | Local News Application - A new article has been added to the website 101 | 102 | 103 | 104 | 105 | 130 | 131 | 132 | 133 |
106 | 107 | 108 | 127 | 128 |
109 |

Hi Admin,

110 |

A new article with the title 'Test Name' has been to local news web application. It is described as 'Test Text'

111 | 112 | 113 | 114 | 123 | 124 | 125 |
115 | 116 | 117 | 118 | 119 | 120 | 121 |
View Article
122 |
126 |
129 |
134 | 135 | 136 |
137 | 138 | 139 | 143 | 144 |
140 | Local News Application, Coventry, United Kingdom CV1 5GA 141 |
Don't like these emails? Unsubscribe. 142 |
145 |
146 | 147 | 148 | 149 |
150 |
 
154 | 155 | " 156 | `; 157 | 158 | exports[`Article Added Email Template Should return correct email template when no data is passed 1`] = ` 159 | " 160 | 161 | 162 | 163 | 164 | 165 | Simple Transactional Email 166 | 246 | 247 | 248 | 249 | 250 | 251 | 306 | 307 | 308 |
  252 |
253 | 254 | 255 | Local News Application - A new article has been added to the website 256 | 257 | 258 | 259 | 260 | 285 | 286 | 287 | 288 |
261 | 262 | 263 | 282 | 283 |
264 |

Hi Admin,

265 |

A new article with the title 'undefined' has been to local news web application. It is described as 'undefined'

266 | 267 | 268 | 269 | 278 | 279 | 280 |
270 | 271 | 272 | 273 | 274 | 275 | 276 |
View Article
277 |
281 |
284 |
289 | 290 | 291 |
292 | 293 | 294 | 298 | 299 |
295 | Local News Application, Coventry, United Kingdom CV1 5GA 296 |
Don't like these emails? Unsubscribe. 297 |
300 |
301 | 302 | 303 | 304 |
305 |
 
309 | 310 | " 311 | `; 312 | --------------------------------------------------------------------------------