├── 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 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 | |
113 | Hi ${name},
114 | ${line1}
115 |
116 |
117 |
118 | |
119 |
126 | |
127 |
128 |
129 |
130 | |
131 |
132 |
133 | |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
150 |
151 |
152 |
153 |
154 | |
155 | |
156 |
157 |
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 | [](https://sonarcloud.io/dashboard?id=rithinch_event-driven-microservices-docker-example)
6 | [](https://lbesson.mit-license.org/)
7 | [](https://saythanks.io/to/rithinch)
8 | [](http://hits.dwyl.io/rithinch/Event-Driven-Microservices-Sample)
9 |
10 | [](https://forthebadge.com)
11 | [](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 | [](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 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 | |
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 | |
115 |
122 | |
123 |
124 |
125 |
126 | |
127 |
128 |
129 | |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
146 |
147 |
148 |
149 |
150 | |
151 | |
152 |
153 |
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 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 | |
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 | |
270 |
277 | |
278 |
279 |
280 |
281 | |
282 |
283 |
284 | |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
301 |
302 |
303 |
304 |
305 | |
306 | |
307 |
308 |
309 |
310 | "
311 | `;
312 |
--------------------------------------------------------------------------------